我正在使用简单的函数来使我的脚本更加详细。
cmd() {
echo "[#] $*" >&2
"$@"
}
当使用选项调用脚本时,-v
所有命令都会通过cmd
函数:
build() {
cmd yarn webpack --config webpack.prod.js
}
copy_to_server() {
cmd tar -cf - -C ./dist . | ssh [email protected] tar -xf - -C /path/to/desirable/dir
}
main() {
build
copy_to_server
}
main
除了带管道的命令外,一切都运行良好。它甚至只回显到管道符号前的点。有没有可能让它正常工作?
答案1
解释
在这段代码中:
cmd tar -cf - -C ./dist . | ssh [email protected] tar -xf - -C /path/to/desirable/dir
cmd
看不到管道符号及其后面的内容。如果cmd
没有,tar
也看不到管道符号。
开箱即用的解决方案
不要重新发明轮子。shell(不仅是 Bash,还有任何类似 POSIX 的 shell)至少会为您提供两个类似于您cmd
所做的选项:
-v
当读取输入时,shell 会将其写入标准错误。
-x
在扩展命令之后、执行命令之前,shell 应将每个命令的跟踪写入标准错误。[…]
(来源)
set -v
您可以使用和启用选项set -x
;使用 和 禁用set +v
选项。您可以使用或set +x
一次启用或禁用两者。set -vx
set +vx
如果是set -v
,您的管道将按原样打印。set -x
将分别打印tar
和ssh
,但它会先将其展开;另一方面,iw 不会向您显示重定向。以下脚本显示了这些差异(可能还有更多):
#!/bin/sh
set -vx
foo=bar
echo "$foo" | wc -c 2>/dev/null
输出将是(# comments
我的):
foo=bar # from set -v, to stderr
+ foo=bar # from set -x, to stderr
echo "$foo" | wc -c 2>/dev/null # from set -v, to stderr
+ echo bar # from set -x, to stderr
+ wc -c # from set -x, to stderr
4 # actual output, to stdout
另请注意set -v
打印 shell 代码读起来,这导致了至少两个怪现象:
某些片段必须一次读取,例如
while … done
或(因为或if … fi
之后可能有重定向),因此将把每个这样的片段作为一个整体显示一次。done
fi
set -v
类似这样的命令
sh -c 'set -v; echo foo'
将被“忽略”,set -v
因为在执行时set -v
所有输入都已被读取。如果您使用换行符代替;
; 或使用 ,也会发生这种情况sh -vc 'echo foo'
。在脚本中set -v
应该可以正常工作。
一个优点是您可以在脚本的早期(可能有条件地)运行set -v
或set -x
(或set -vx
),而无需修改其余代码。缺点是整个后续代码(直到set +v
左右)都会受到影响,而使用您的cmd
(如果它完全按照您的意愿工作)您可以cmd
仅应用于特定行。
定制解决方案
我可以想象,既不是set -v
也不是set -x
(也不是set -vx
)并不是你想要的。有一种方法可以使您的cmd
打印管道像tar … | ssh …
。以这个函数为例:
cmd() {
[ "$debug" = 1 ] && (IFS=' '; printf '%s\n' "$*" >&2)
eval "$@"
}
像这样使用它:
debug=1
cmd 'tar -cf - -C ./dist . | ssh [email protected] tar -xf - -C /path/to/desirable/dir'
使用debug=1
和,debug=something_else
您可以打开或关闭打印cmd
。
但你需要非常小心。我特意将cmd
它设计成接受多个参数。对多个参数的支持使你只需运行:
cmd echo foo
或者
cmd echo foo \| wc -c
只|
需要转义(或加引号)。不过,你需要小心扩展和引用(即使是简单案件)。代码在cmd
运行前进行解析,再次运行时eval
。因此:
foo='$(echo 123456789)'
cmd echo "$foo"
将$foo
扩大cmd
进而 eval
将展开$(echo 123456789)
,输出为123456789
。请注意,它可能是foo='$(reboot)'
,而cmd echo "$foo"
您显然不想重新启动。
虽然echo ">very-important-file"
无害,但cmd echo ">very-important-file"
会截断very-important-file
为零大小。引号将在cmd
运行前被删除,并且不会出现在eval
求值中,因此命令将变为echo >very-important-file
。
避免此类事故的一个可靠方法是使用cmd
一个单引号参数。如果某些部分(如变量foo
)不完全受您控制(例如,它来自环境或来自read foo
),这一点尤其重要。安全用法如下:
cmd 'echo "$foo"'
或者
cmd 'echo ">very-important-file"'
将所有内容作为单引号参数传递会使引用变得复杂,因为在引入之前您确实需要单引号cmd
,但由于您应该在此阶段防止扩展,因此一些额外的引用和/或转义是不可避免的;否则您可能会执行错误的代码。
在 shell 中对任何字符串进行单引号括起来的一般步骤(即使字符串包含单引号)是:
- 将所有替换
'
为'"'"'
或'\''
, - 用单引号括住整个结果字符串。
最后在前面添加cmd
。该过程完全是“机械的”,没有怪癖,没有例外,不需要进一步的洞察,因此可以轻松实现自动化(Bash 本身可以做到这一点,但请注意,链接的问题是针对交互式 Bash 的命令行,它在文本编辑器中编写脚本时不会帮助您;不过,足够强大的编辑器应该允许您以自己的方式自动执行此操作)。
现在您可以将任何代码片段传递给cmd
,无论它是否包含|
、、、整个子句、换行符、重定向或其他内容。示例&&
:[[
if
cmd 'echo foo && echo bar; echo "$PATH" | dd conv=ucase 2>/dev/null'
cmd '
echo '\''$PATH'\'' is
echo "$PATH"
'
在里面cmd
我们使用$*
和$@
。它确实$*
使用了 的第一个字符$IFS
,因此我们在使用 的地方进行了修复;IFS
但是使用而类似地修复是错误的,因为它会影响我们要执行的 shell 代码;幸运的是 可以很好地与 配合使用。我们的应该可以与任何 配合使用。示例:$*
printf
$*
eval
IFS
IFS
eval
"$@"
cmd
IFS
IFS=.
cmd read a b c
提供1.2.3
Enter,然后使用 检查echo "$a"
是否$a
为1
。 (请注意,如果在交互式 shell 中执行此操作,则修改后的内容IFS
将保留。)
IFS=. cmd read a b c
和cmd IFS=. read a b c
也应该能按预期工作。在任何情况下,修改IFS
都不会破坏我们的cmd
。请注意,在这些示例中,我故意将多个参数传递给 ,以cmd
真正测试 和 是否$*
整个$@
函数表现良好。
概括
尝试set -v
和/或set -x
,也许它们对你来说就足够了。如果没有,请使用我们的自定义cmd
,但你真的需要引用和/或转义权。