我正在编写一个小型实用程序脚本,该脚本可以参数化以附加地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