构造包含空格、竖线和进程替换的参数的 Bash 命令

构造包含空格、竖线和进程替换的参数的 Bash 命令

我正在编写一个小型实用程序脚本,该脚本可以参数化以附加地time执行它并将其标准输出写入另一个文件。

我们假设基本脚本很简单:

ls "$@"

那么以下将是一个简单的实现,这里使用cat虚拟函数来对输出进行更复杂的处理:

if [[ "$TIMEIT" -eq 1 ]]; then
  if [[ "$LOGIT" -eq 1 ]]; then
    time ls "$@" | tee >(cat)
  else
    time ls "$@"
  fi
else
  if [[ "$LOGIT" -eq 1 ]]; then
    ls "$@" | tee >(cat)
  else
    ls "$@"
  fi
fi

您可以看到问题所在,特别是如果需要添加新参数。

更灵活的重写如下:


cmd="ls $@"

if [[ "$TIMEIT" -eq 1 ]]; then
  cmd="time $cmd"
fi

if [[ "$LOGIT" -eq 1 ]]; then
  cmd="$cmd | tee >(cat)"
fi

eval $cmd 

这是可行的,但是一旦你这样调用它,它就会中断:

./script.sh "file 1" file2

因为文件名将不会被正确处理。

当我申请时此解决方案,在命令前面添加以下内容时,我可以正确地标记参数time

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

cmd=(ls "$@")

if [[ "$TIMEIT" -eq 1 ]]; then
  cmd=(time "${cmd[@]}")
fi

eval "$(token_quote "${cmd[@]}")"

但同样的处理管道的方法不起作用。当我使用此解决方案

function eval_args {
  local quoted=''
  while (( $# )); do
    if [[ $1 = '|' ]]; then
      quoted+="| "
    else
      printf -v quoted '%s%q ' "$quoted" "$1"
    fi
    shift
  done
  eval "$quoted"
}

cmd=(ls "$@")

# prepend to array
if [[ "$TIMEIT" -eq 1 ]]; then
  cmd=(time "${cmd[@]}")
fi

# append to array?
if [[ "$LOGIT" -eq 1 ]]; then
  cmd=("${cmd[@]}" "|" "tee" ">(cat)")
fi

eval_args "${cmd[@]}"

它创建了一个文件>(cat)...所以我在这里走进兔子洞,但似乎这不是解决问题的办法。

我该怎么做才能构造命令并处理文件名中的空格/特殊字符,包括管道/进程替换?

答案1

评估

您的最后一种方法可能会有效,只要您跳过自定义添加并仅将其用于生成原始命令即可。也就是说,将 eval_args 更改为仅建造命令字符串:

build_args() {
    local arg quoted=""
    for arg; do
        printf -v quoted '%s%q ' "$quoted" "$1"
    done
    echo "$quoted"
}

cmd=$(build_args ls "$@")
if (( TIMEIT )); then
    cmd="time $cmd"
fi
if (( LOGIT )); then
    cmd="$cmd | tee >(foo)"
fi

eval "$cmd"

实际上循环是不必要的,因为 bash 的 printf 会自动使用额外的参数重复相同的格式字符串:

argv=(ls -la "$@")
cmd=$(printf '%q ' "${argv[@]}")
eval "time $cmd | tee >(foo)"

Bash 5.1 有一个${foo@Q}修饰符,它是一个更方便的替代方案:

argv=(ls -la "$@")
cmd=${argv[*]@Q}
eval "time $cmd | tee >(foo)"

eval 的替代方案

假设你的脚本在一个函数中有其主要代码:

run() { ls "$@"; }

您可以有条件地为其定义一个包装函数:

if (( LOGIT )); then
    run_logged() { run "$@" | tee >(cat); }
else
    run_logged() { run "$@"; }
fi

if (( TIMEIT )); then
    run_timed() { time run_logged "$@"; }
else
    run_timed() { run_logged "$@"; }
fi

run_timed "$@"

(函数定义本身可以生成字符串并进行 eval。)

重新执行

在某些情况下,在新环境下重新执行整个脚本可能是有意义的:

if (( UID > 0 )); then
    exec sudo "$0" "$@" || exit
fi

if (( ! _LOGIT_ACTIVE )); then
    exec _LOGIT_ACTIVE=1 "$0" "$@" | tee >(foo) || exit
fi

ls "$@"

杂项

至于重定向具体来说,您可以对使用进行硬编码tee并仅交换文件名(效率不高,因为它仍然会导致 tee 运行并重复写入,但是......它可以完成工作):

if (( LOGIT )); then
    out=/var/log/foo
else
    out=/dev/null
fi

ls "$@" | tee $out

对于直接重定向到文件也是一样:

if (( LOGIT )); then
    exec {fd}>/var/log/foo
else
    fd=1     # stdout
fi

ls "$@" >&$fd

if (( fd != 1 )); then
    exec {fd}>&-    # close
fi

或者,您可以通过管道传入一个条件,或者传入一个包装该条件的函数(由于是强制性的,效率也不是 100% cat):

ls "$@" | if (( LOGIT )); then tee >(whatever); else cat; fi

相关内容