首页 > 解决方案 > 在 bash 中使用循环将进程替换添加到命令行

问题描述

在命令行中,我可以使用 gsutil cat 将输入通过管道传输到 sox。完美运行。这是从命令行工作的代码:

sox <(gsutil -q cat gs://some-bucket/201808130800.wav) ./test-show.wav -t 
wav trim 0 10

但是,在我的 bash 脚本中,它失败了。我尝试了许多配置。归根结底,sox 抱怨文件名错误。相同的代码从命令行运行。

这是代码行。所有变量都是正确的。如果我使用 sox 处理本地文件,效果很好。

sox $(for ((i=0; i<file_count; i++)); do
file_time=$(date "+%s" --date="$(date -d @$base_time) +$i hours")
file_name=$(date --date="@$file_time" "+%Y%m%d%H%M")
echo -e "<(gsutil -q cat gs://somebucket/$file_name.wav)"
done) "$target/$show_name.wav -t wav trim $f_offset $run_time"

如果我在命令前面加上一个回显,那么屏幕上打印的内容正是命令行中的内容。我可以将输出复制并粘贴到命令行,它可以工作。

这是输出:

sox FAIL formats: can't open input file `gs://somebucket/201808130800.wav)': No such file or directory

任何帮助,将不胜感激。

附录:

我几乎可以使用它:

gsutil cat "$(for ((i=0; i<file_count; i++)); do
  file_time=$(date "+%s" --date="$(date -d @$base_time) +$i hours")
  file_name=$(date --date="@$file_time" "+%Y%m%d%H%M")
  echo "gs://somebucket/$file_name.wav"
done)" | sox -V4 - $target/$show_name.wav trim $f_offset $run_time

标签: bash

解决方案


背景故事:怎么了

命令替换的输出($(...)封装for循环的语法)不会被 shell 解析为代码;相反,它只经过字符串拆分(在空格上拆分,除非IFS已更改)和全局扩展(用*.txt匹配列表替换字符串),然后直接放在命令行上sox

但是,<(...)不是sox;的指令 这是一个进程替换,指示shell放置一个可以读取的文件名,以便在调用sox.

您可以通过几种方式动态生成与命令输出相关的文件名,如下所述。


方法 1:改用命名管道

这里更容易做的事情之一是构建显式(命名的)FIFO,而不是依赖进程替换来创建匿名的:

#!/usr/bin/env bash

tempdir=$(mktemp -d "${TMPDIR:-/tmp}/sox-pipes.XXXXXX") || exit

declare -a fifos=( )

cleanup() {
  rm -rf "$tempdir"
  kill "${!fifos[@]}"
}
trap cleanup EXIT

for ((i=0; i<file_count; i++)); do
  file_time=$(date "+%s" --date="$(date -d @"$base_time") +$i hours")
  file_name=$(date --date="@$file_time" "+%Y%m%d%H%M")
  mkfifo "$tempdir/$file_name.fifo"
  gsutil -q cat gs://some-bucket/$file_name.wav >"$tempdir/$file_name.fifo" &
  fifos[$!]="$tempdir/$file_name.fifo"
done

sox "${fifos[@]}" ./test-show.wav -t

这种方法可以改进以在任何符合 POSIX 的 shell 上工作——数组的使用不是严格强制的——这意味着它也可以在根本不支持<(...)语法的 shell 上工作。


方法 2:生成一个eval-safe 命令

棘手的是,必须非常小心地防止数据(如文件名)被用于 shell 注入攻击。注意使用printf %q来转义被替换到字符串中的数据。

#!/usr/bin/env bash

cmdline=''
for ((i=0; i<file_count; i++)); do
  file_time=$(date "+%s" --date="$(date -d @"$base_time") +$i hours")
  file_name=$(date --date="@$file_time" "+%Y%m%d%H%M")
  printf -v piece ' <(gsutil -q cat gs://some-bucket/%q.wav) ' "$file_name"
  cmdline+="$piece"
done

eval "sox ${cmdline} ./test-show.wav -t"

方法 3:收集文件描述符数组

这里有一些棘手的警告:实例在其 stdout 描述符的所有副本gsutil都关闭之前不会退出,这意味着它们在完成后仍将运行,直到 shell 关闭它自己的副本。sox

#!/usr/bin/env bash
case $BASH_VERSION in ''|[123].*|4.0.*) echo "ERROR: Bash 4.1 required" >&2; exit 1;; esac

gsutil_fds=( )
for ((i=0; i<file_count; i++)); do
  file_time=$(date "+%s" --date="$(date -d @"$base_time") +$i hours")
  file_name=$(date --date="@$file_time" "+%Y%m%d%H%M")
  exec {gsutil_fd}< <(gsutil -q cat gs://some-bucket/"$file_name".wav)
  gsutil_fds+=( /dev/fd/"$gsutil_fd" )
done

sox "${gsutil_fds[@]}" ./test-show.wav -t

for fd in "${gsutil_fds[@]#/dev/fd/}"; do
  exec {fd}>&-                     # close the fifo so this copy of gsutil can exit
done

方法 4:使用递归

...如每个数组条目的进程替换中所述,没有 Eval


推荐阅读