我有以下 bash 函数:
function exe {
echo -e "Execute: $1"
# Loops every 3s, outputting '...' until command finished executing
LOOP=0
while true;
do
if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
sleep 3;
LOOP=$LOOP+1
done & ERROR="$($2 2>&1)" # Execute the command and capture output to variable
status=$?
kill $!; trap 'kill $!' SIGTERM
if [ $status -ne 0 ];
then
echo -e "✖ Error" >&2
echo -e "$ERROR" >&2
else
echo -e "✔ Success"
fi
return $status
}
其目的是这样称呼它:
exe "Update apt indexes" \
"sudo apt-get update"
哪个输出:
Execute: Update apt indexes
...
...
...
...
✔ Success
除非在传递的命令中使用带引号的字符串作为参数,否则它可以正常工作。
例如,以下内容不起作用:
exe "Create self signed certificate" \
"sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""
set -x 显示上面的命令被转换为以下命令来执行:
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '"/C=GB/ST=London/L=London/O=Confetti' Celebrations Ltd/OU=IT 'Department/CN=dev.sign-in.confetti.local"'
这似乎拾起了许多单引号并使命令无效。
我想要一个没有这个限制的版本。有任何想法吗?
===============
在建议和其他一些错误修复之后,我的最终代码是:
exe () {
echo -e "Execute: $1"
LOOP=0
while true;
do
if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
sleep 3;
LOOP=$((LOOP+1))
done & ERROR=$("${@:2}" 2>&1)
status=$?
kill $!; trap 'kill $!' SIGTERM
if [ $status -ne 0 ];
then
echo -e "✖ Error" >&2
echo -e "$ERROR" >&2
else
echo -e "✔ Success"
fi
return $status
}
该函数旨在作为 vagrant 配置 shell 脚本的“美化器”,并且可以调用为
exe "Update apt indexes" sudo apt-get update
输出显示为
Execute: Update apt indexes
...
...
...
...
✔ Success
持续时间少于 3 秒的命令看不到进度点输出
除非出现错误,否则您将收到错误状态和命令的完整输出。
主要目的是消除 vagrant 配置脚本在向 stderr 输出消息时显示的红线。许多命令正确地将信息输出到 stderr,因为它的目的是不应该通过管道传输到其他命令的消息。 Vagrant 将消息打印到标准输出为 这会留下许多看似错误但实际上并非错误的配置消息。
除非执行的命令返回非零状态,否则此函数不会打印到 stderr。这意味着除非命令指示失败,否则您将不会看到红色消息。当命令指示失败并显示非零消息时,我们将命令的完整输出输出到 stderr,并给出红线。
使用 shell 脚本使 vagrant 配置更加整洁,并且意味着我们确实可以留意红色消息,知道它们意味着什么。
与 vagrant 一起使用的完整函数,包括我在上面的代码片段中遗漏的一些视觉效果,可以在这里看到:https://gist.github.com/michaelward82/c1903f2b37a76975740e
答案1
您可能不想将整个命令作为字符串传递。我们在 shell 中有列表,作为参数列表,并且将列表作为列表传递要简单得多。
不用写exe blah "blahh cmd"
,而是直接写命令,就像exe blah blahh cmd
.然后,当你需要直接使用整个命令时,使用切片扩展获取第一个参数之后的所有内容:ERROR=$("${@:1}" 2>&1)
.
传统上,人们可能会使用shift
“向左”移动整个参数列表(请参阅help shift
):
f(){
local j="$1"
shift
echo "$j,$3"
shift 50
echo "$1" # guess what "$@" is now?
}
f {1..100}
但这对于 bash 显然不是必需的。
说到切片,您可能还想看看数组在bash中。
呃,仍然..您可以用来eval
直接运行字符串,但这通常被认为是一件坏事,因为您允许的不仅仅是简单的命令。
作为风格提示,与 和 相比,更喜欢更短且更(POSIX-)xxx()
便携function xxx
的function xxx()
。在 bash 中它们是相同的。
答案2
您问题的核心问题是“如何拆分字符串” $var
。
“邪恶”(因为它容易出错和代码执行)的方式是使用 eval:
eval set -- $var ### Dangerous, not recommended, do not use.
这会在位置参数中设置分割字符串(数组稍微复杂一些)。但是变量$var
不加引号(除非您真的知道自己在做什么,否则要不惜一切代价避免)使其受到“分词”(我们想要的)的影响,但这也允许“路径名扩展”发生。您可以尝试此命令(使用包含少量文件的目录)
$ var='hello * world'
$ eval set -- $var
$ echo "$@"
执行是安全的,没有外部设置值,并且扩展*
只会设置位置参数中的值。
为了避免“路径名扩展”,set -f
使用了 a,在这种情况下,很容易将 is 集成到命令中:
$ var='hello * world'
$ set -f
$ eval set -- $var
$ echo "$@"
hello * world
其默认 IFS 为spaceTabNew Line。
如果 IFS 可以在外部设置,事情可能会变得复杂。
使用以下方法可以解决几个问题read
:
$ IFS=' ' read -ra arr <<<"$var"
$ echo "${arr[@]}"
hello * world
这为命令设置了 IFS(避免从外部设置 IFS),读取时不处理反斜杠(-r 选项),将所有内容放入数组变量中(-a 选项),并使用引用的变量"$var"
。唯一需要注意的是单词之间重复的空格将被删除(因为 IFS 是一个空格)。这对于可执行命令行来说不是问题。
但尝试执行需要带空格的参数的命令将会失败:
$ var='date -d "-1 day" +"%Y.%m.%d-%H:%M:%S"'
$ IFS=' ' read -ra arr <<<"$var"
$ "${arr[@]}"
date: extra operand `+"%Y.%m.%d-%H:%M:%S"'
唯一真正的解决方案是从一开始就正确构建命令数组:
$ arr=( date -d "-1 day" +"%Y.%m.%d-%H:%M:%S" )
$ "${arr[@]}"
2016.03.05-00:25:17
将此解决方案视为 CSV“逗号(空格)分隔值”。
该脚本将起作用:
#!/bin/bash
function exe {
echo "Execute: $1"
# Loops every 3s, outputting '...' until command finished executing
LOOP=0
while true; do
if [ $LOOP -gt 0 ]; then echo -e "..."; fi;
sleep 3;
(( LOOP++ ))
done &
ERROR="$("${@:2}" 2>&1)" # Execute command and capture output.
status=$?
kill $!; trap 'kill $!' SIGTERM
if [ $status -ne 0 ];
then
echo "✖ Error" >&2
echo "$ERROR" >&2
else
echo "✔ Success"
fi
return $status
}
cmd=( date -d '-1 day' +'%Y.%m.%d-%H:%M:%S' )
exe "give me yesterday date" "${cmd[@]}"
cmd=( sudo apt-get update )
exe "update package list" "${cmd[@]}"
答案3
如果参数字符串内有引号作为代码执行,则可以将参数字符串重新解析为数组,例如位置参数数组$@
。这可以通过使用来实现——至少对于给定的示例... & ERROR="$( printf "%s" "$2" | xargs sh -c 'exec "$0" "$@" 2>&1' ) ...
。 (在已经引用的字符串中存在额外双引号的情况,可能会导致xargs: unterminated quote
消息)。
有关一些进一步的建议,请参阅:Linux/Bash:如何取消引号?。
# test cases
# help :
#set -- '' "ls -ld / 'a bc'"
set -- '' ": sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""
printf "%s" "$2" |
xargs sh -c '
echo "arg 0: ${0}"
for ((i=1; i<=$#; i++)); do
echo "arg $i: ${@:i:1}"
done
set -xv
"$0" "$@"
'
# output
arg 0: :
arg 1: sudo
arg 2: openssl
arg 3: req
arg 4: -x509
arg 5: -nodes
arg 6: -days
arg 7: 365
arg 8: -newkey
arg 9: rsa:2048
arg 10: -keyout
arg 11: /etc/apache2/ssl/apache.key
arg 12: -out
arg 13: /etc/apache2/ssl/apache.crt
arg 14: -subj
arg 15: /C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local
"$0" "$@"
+ : sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local'
(顺便说一句LOOP=$LOOP+1
,在你上面的代码中应该是LOOP=$((LOOP+1))
。)