4

我有数百个 Google Apps 脚本项目和各种 Bash 脚本,用于使用clasp工具(一个 Node.js 应用程序)管理项目。许多脚本需要clasp pull先在本地拉取项目,然后再对本地文件执行一些操作,所以我有一个脚本,它遍历本地 clasp 项目文件夹并clasp pull在每个文件夹上运行。该循环按顺序遍历目录,因此如果拉取一个项目需要 3-4 秒,那么每 100 个项目运行它最终需要 5-6 分钟。

我的目标是能够clasp pull并行运行命令,使它们同时启动,并且能够知道哪些项目被成功拉取,哪些项目被拉取失败。

给定这样的目录结构:

├── project-1
│   ├── .clasp.json
│   ├── .claspignore
│   ├── _main.js
│   └── appsscript.json
├── project-2
│   ├── .clasp.json
│   ├── .claspignore
│   ├── _main.js
│   └── appsscript.json
├── project-3
│   ├── .clasp.json
│   ├── .claspignore
│   ├── _main.js
│   └── appsscript.json
└── pull_all.sh

这个pull_all.shBash 脚本:

#!/bin/bash

# use Node 14.17.5 to prevent "Error: Looks like you are offline." errors
# (see https://github.com/google/clasp/issues/872)
[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
nvm install 14.17.5
nvm use 14.17.5

find . -name '.clasp.json' | 
while read file; do
    (
        cd "$(dirname "$file")"
        project_dir_name="$(basename "$(pwd)")"
        echo "Pulling project ($project_dir_name)"
        clasp pull
    ) &
done

运行此脚本时,它会为每个目录输出“Pulling project”行,然后给出 shell 提示,暗示脚本已完成执行。但是在没有用户做任何事情的情况下,3-4 秒后它显示了所有clasp pull命令的输出(显然是并行运行,因为命令的一些输出是乱序/重叠的),然后挂起,并且没有给出新的外壳提示符。此时我必须按 ctrl+c 来终止脚本。

完整的输出最终看起来像这样:

$ ./pull_all.sh
v14.17.5 is already installed.
Now using node v14.17.5 (npm v6.14.14)
Now using node v14.17.5 (npm v6.14.14)
Pulling project (project-3)
Pulling project (project-2)
Pulling project (project-1)
$
Cloned 2 files.
⠙ Pulling files…└─ appsscript.json
└─ _main.js
Cloned 2 files.
└─ _main.js
└─ appsscript.json
Cloned 2 files.
└─ _main.js

要强制其中一个脚本失败,我可以在任何文件中将其更改scriptId为无效的脚本 ID 。.clasp.json在这种情况下,我确实看到了预期的输出:

Could not find script.
Did you provide the correct scriptId?
Are you logged in to the correct account with the script?

...但它仍然与其他输出混合在一起,并且不清楚来自哪个项目。

我怎样才能做到这一点:

  1. 该脚本在执行脚本期间不会导致出现新的 shell 提示。
  2. 该脚本输出一行指示每个clasp pull操作的成功或失败,由项目的目录名称(.clasp.json找到文件的位置)引用。
  3. 奖励:抑制输出,clasp pull因此脚本仅显示每个项目的成功或失败结果(由目录名称引用)。

注意:我已经提到clasp pull了一个示例命令,但一个有效的解决方案将允许我在 bash while 循环中将任何 clasp 命令作为后台进程运行,包括但不限于clasp push,clasp deploy等。

4

5 回答 5

3

我建议以下解决方案:

#!/usr/bin/env bash

# use Node 14.17.5 to prevent "Error: Looks like you are offline." errors
# (see https://github.com/google/clasp/issues/872)
[ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"
nvm install 14.17.5
nvm use 14.17.5

# Check and process command line
if (( $# < 1 )); then
    echo "Usage: $(basename "$0") ACTION [ARG]..."
    exit 2
fi
action="$1"
args=("${@:2}")

# Define cleanup handler, create temporary log directory
trap '[[ -n "$(jobs -p)" ]] && kill -- -$$; [[ -n "${logdir}" ]] && rm -rf "${logdir}"' EXIT
logdir=$(mktemp -d)

# Start specified action for each project
declare -A pid_pro_map=() pid_log_map=()
readarray -t files < <(find . -name '.clasp.json' -printf "%P\n" | sort -V)
for file in "${files[@]}"; do
    project=$(dirname "${file}")
    logfile=$(mktemp -p "${logdir}")
    ( cd "${project}" && clasp "${action}" "${args[@]}" ) &>"${logfile}" &
    pid=$!; pid_pro_map[${pid}]="${project}"; pid_log_map[${pid}]="${logfile}"
    echo -e "Started action '\e[1m${action}\e[0m' for project '\e[1m${project}\e[0m' (pid ${pid})"
done

# Wait for background jobs to finish and report results
echo -e "\nWaiting for background jobs to finish...\n"
jobs_done=0; jobs_total=${#files[@]}
while true; do
    wait -n -p pid; result=$?
    [[ -z "${pid}" ]] && break
    jobs_done=$((jobs_done + 1))
    if (( ${result} == 0 )); then
        echo -e "Action '\e[1m${action}\e[0m' for project '\e[1m${pid_pro_map[${pid}]}\e[0m' (pid ${pid}) (${jobs_done}/${jobs_total}): \e[1;32mSUCCESS\e[0m"
    else
        echo -e "Action '\e[1m${action}\e[0m' for project '\e[1m${pid_pro_map[${pid}]}\e[0m' (pid ${pid}) (${jobs_done}/${jobs_total}): \e[1;31mFAILURE\e[0m"
        cat "${pid_log_map[${pid}]}"
    fi
done

特征:

  • 允许运行clasp(例如pull, push, deploy)支持的任何操作
  • 在后台并行执行每个项目的指定操作
  • 产生的输出clasp被抑制(但在失败的情况下被捕获以打印)
  • 等待后台任务完成并在结果可用时立即报告
  • 提供有关每个项目的成功/失败的信息(包括clasp在失败情况下为进一步分析而产生的输出)
  • 显示当前进度(以 的形式<projects-done>/<projects-total>
  • 彩色输出以提高可读性

要求:

  • Bash >= 5.1(详细信息:Bash >= 5.1 for wait -p,Bash >= 4.3 for wait -n,Bash >= 4.0 for 关联数组)
  • GNU find ( findutils的一部分) for find ... -printf "%P\n"; 可能的解决方法:
    readarray -t files < <(find . -name '.clasp.json' | sort -V)
    for file in "${files[@]}"; do
        project=$(dirname "${file#'./'}")
    

样本输出:

样本输出


作为对此评论的回应,这里有一个可能的调整来限制产生的并发后台作业的数量:

# Start specified action for each project
max_jobs=25; poll_delay="0.1s"
declare -A pid_pro_map=() pid_log_map=()
readarray -t files < <(find . -name '.clasp.json' -printf "%P\n" | sort -V)
for file in "${files[@]}"; do
    if (( ${max_jobs} > 0 )); then
        while jobs=$(jobs -r -p | wc -l) && (( ${jobs} >= ${max_jobs} )); do
            sleep "${poll_delay}"
        done
    fi
    project=$(dirname "${file}")
    logfile=$(mktemp -p "${logdir}")
    ( cd "${project}" && clasp "${action}" "${args[@]}" ) &>"${logfile}" &
    pid=$!; pid_pro_map[${pid}]="${project}"; pid_log_map[${pid}]="${logfile}"
    echo -e "Started action '\e[1m${action}\e[0m' for project '\e[1m${project}\e[0m' (pid ${pid})"
done

此外,这可以用来将产生的后台进程数量减少一半:

( cd "${project}" && exec clasp "${action}" "${args[@]}" ) &>"${logfile}" &

这将用 替换子shell 的进程clasp,这应该很好,因为子shell 在执行后立即失去了它的用处cd

于 2021-10-25T11:44:49.857 回答
1
  1. 该脚本在执行脚本期间不会导致出现新的 shell 提示。

出现新的 shell 提示是因为您正在while循环中创建一个新的子 shell(有关子 shell 如何在 bash 中工作的进一步指导,请参考 tldp.org 中的此页面:链接)。为防止这种情况发生,请直接调用命令,不要将它们放在括号内。

  1. 该脚本输出一行指示每个 clasp pull 操作的成功或失败,由项目的目录名称引用(找到 .clasp.json 文件的位置)。

您通常可以通过||在命令后添加一个命令(例如grep "foobar" file.txt || echo "Error: 'foobar' not found in file.txt")来捕获命令是否失败。您还可以将命令放在if/中else并为每个命令回显相应的状态消息。

  1. 奖励:抑制 clasp pull 的输出,因此脚本仅显示每个项目的成功或失败结果(由目录名称引用)。

注意:此响应使用第二个问题中的上述解决方案。您可以创建 2 个数组——1 个代表成功,1 个代表失败,然后在 if/else 语句中,将当前迭代元素添加到正确的数组中。

如果上述任何部分不清楚,请随时要求澄清!

于 2021-10-23T06:57:03.537 回答
1

您应该强制脚本在完成之前等待输出:

{ 
    while IFS= read -d $'\0' -ru $find file; do
        (
            cd "$(dirname "$file")"
            project_dir_name="$(basename "$(pwd)")"
            echo "Pulling project ($project_dir_name)"
            if clasp pull </dev/null 2>&1 ;then
                printf '\nExeClaspResult: %s Success\n' "$project_dir_name"
            else
                printf '\nExeClaspResult: %s Failed\n' "$project_dir_name"
            fi
        ) &
    done {find}< <(find . -name '.clasp.json' -print0)
    wait
} |
    sed -une 's/^ExeClaspResult: //p'

在哪里:

  1. 无交互
  2. 所有输出都将被删除(按sed
  3. 只会显示结果。

如果你想做一些调试

{ 
    while IFS= read -d $'\0' -ru $find file; do
        (
            cd "$(dirname "$file")"
            project_dir_name="$(basename "$(pwd)")"
            echo "Pulling project ($project_dir_name)"
            if clasp pull </dev/null > >(
                  sed "s/^/OUT $project_dir_name: /") 2> >(
                  sed "s/^/ERR $project_dir_name: /"
                );then
                printf '\nExeClaspResult: %s Success\n' "$project_dir_name"
            else
                printf '\nExeClaspResult: %s Failed\n' "$project_dir_name"
            fi
        ) &
    done {find}< <(find . -name '.clasp.json' -print0)
    wait
} |
    sed -ue '
        s/^ExeClaspResult: \(.* Failed\)$/\o33[31m** \1 **\o33[0m/;
        s/^ExeClaspResult: \(.*\)$/\o33[32m** \1 **\o33[0m/;'

将显示所有以OUT $projector为前缀的输出ERR project,如果成功,颜色结果为绿色,失败时为红色。

于 2021-10-23T14:26:54.833 回答
1

我的方法是首先记录所有异步调用。这些会很快发生。然后开始打印任务失败/成功日志。

该脚本使用命名管道来实现这一点。异步命令发生在子shell中,子shell继承了成功/失败消息也可以打印的打开文件描述符。我们可以等到所有调用都记录下来,然后再将它们发送到终端。

的输出pull all被隐藏。它可以用另一个命令或 shell 函数替换。

stdout并且stderr可以正常重定向(例如pull-all 2>err-log,或pull-all >/dev/null仅查看错误)。

该脚本等待拉取命令完成,然后正常退出。

如果它被中断(ctrl+c),所有的子进程都会被杀死。否则它们会在脚本退出后继续运行。我不确定是否有更好的方法来处理它。

解决方案:

#!/bin/bash

cleanup () {
    exec 3>&-
    exec 4>&-

    rm .pull-all-log.fifo .pull-all-log-err.fifo

    kill $(jobs -p)
    kill -9 $(jobs -p) &>/dev/null
}

# use Node 14.17.5 to prevent "Error: Looks like you are offline." errors
# (see https://github.com/google/clasp/issues/872)
[[ -s "/usr/local/opt/nvm/nvm.sh" ]] && . "/usr/local/opt/nvm/nvm.sh"
nvm install 14.17.5
nvm use 14.17.5

# you can use /tmp or mktemp -u if you're worried about clobbering
rm -f .pull-all-log.fifo .pull-all-log-err.fifo
mkfifo .pull-all-log.fifo .pull-all-log-err.fifo
trap cleanup EXIT

exec 3<> .pull-all-log.fifo
exec 4<> .pull-all-log-err.fifo

for file in ./*/.clasp.json; do
    [[ -d "$file" ]] && continue

    parent=$(dirname "$file")
    proj=${parent##*/}

    echo "Pulling $proj..."

    {
        cd "$parent"

        if clasp pull &>/dev/null; then
            echo "Pulling $proj succeeded" >&3
        else
            echo "Pulling $proj FAILED" >&4
        fi
    } &
done

running_pids=$(jobs -p)

cat <&3 &
cat <&4 >&2 &

[[ "$running_pids" ]] && wait $running_pids

示例输出:

Pulling project-1...
Pulling project-2...
Pulling project-3...
Pulling project-4...
Pulling project-5...
Pulling project-6...
Pulling project-7...
Pulling project-8...
Pulling project-9...
Pulling project-1 succeeded
Pulling project-9 succeeded
Pulling project-3 succeeded
Pulling project-4 FAILED
Pulling project-5 succeeded
Pulling project-2 succeeded
Pulling project-6 FAILED
Pulling project-7 FAILED
Pulling project-8 FAILED

FAILEDsucceeded为每次调用打印,只要它完成(即使它在调用循环结束之前)。

示例目录已命名project-1等。我重新创建了您的示例树来测试它。

于 2021-10-26T16:11:45.600 回答
0

你可以做:

parallel -j100 --tag 'cd {//} && clasp pull || echo Fail' ::: */.clasp.json

如果您只想clasp在失败时输出:

parallel -j100 --tag 'cd {//} && neno clasp pull || echo Fail' ::: */.clasp.json

nenohttps://gitlab.com/ole.tange/tangetools/-/tree/master/neno

于 2021-11-20T08:54:29.943 回答