运行 多个 Google Apps Script clasp 命令并行使用 Bash 脚本

Run multiple Google Apps Script clasp commands in parallel using a Bash script

我有数百个 Google Apps 脚本项目,并且有各种 Bash 脚本用于使用 clasp 工具(Node.js 应用程序)管理项目。许多脚本需要使用 clasp pull 在对本地文件采取一些操作之前首先将项目拉到本地,所以我有一个循环遍历本地 clasp 项目文件夹和 运行s 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.sh Bash 脚本:

#!/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 命令的输出(显然 运行ning 是并行的,因为一些命令的输出不在 order/overlapping),然后挂起,并且不给出新的 shell 提示。此时我必须按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

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

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 作为示例命令,但有效的解决方案允许我 运行 任何 clasp 命令作为 bash while 循环中的后台进程,包括但不限于clasp pushclasp deploy

  1. The script does not cause a new shell prompt to appear during the execution of the script.

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

  1. The script outputs a line indicating the success or failure of each clasp pull operation, referenced by the directory name of the project (where the .clasp.json file was found).

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

  1. Bonus: suppress the output of clasp pull so the script only shows the success or failure result of each project (referenced by the directory name).

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

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

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

{ 
    while IFS= read -d $'[=10=]' -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 $'[=11=]' -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**  **\o33[0m/;
        s/^ExeClaspResult: \(.*\)$/\o33[32m**  **\o33[0m/;'

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

我建议采用以下解决方案:

#!/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 "[=10=]") ACTION [ARG]..."
    exit 2
fi
action=""
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 支持的任何操作(例如 pullpushdeploy
  • 在后台为每个项目并行执行指定的操作
  • clasp 产生的输出被抑制(但捕获以在失败的情况下打印)
  • 等待后台任务完成并在结果可用时立即报告结果
  • 为每个项目提供有关 success/failure 的信息(包括 clasp 产生的输出,以便在失败时进行进一步分析)
  • 显示当前进度(以<projects-done>/<projects-total>的形式)
  • 彩色输出以提高可读性

要求:

  • Bash >= 5.1(详情:Bash >= 5.1 for wait -p, Bash >= 4.3 for wait -n, Bash >= 4.0 对于关联数组)
  • GNU 查找(findutils 的一部分)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}" &

这将用 clasp 替换子 shell 的进程,这应该是完美的,因为子 shell 在执行 cd 之后就失去了它的用处。

我的方法是首先记录所有异步调用。这些会很快发生。然后开始打印任务failed/succeeded log.

此脚本使用命名管道来实现此目的。异步命令发生在 subshells 中,它继承了 success/fail 消息也可以打印的打开文件描述符。我们可以等到所有调用都被记录下来,然后再将它们发送到终端。

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

stdoutstderr 可以正常重定向(如 pull-all 2>err-logpull-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 等。我重新创建了您的示例树来测试它。

你能做到吗:

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