Bash 函数参数列表中的管道

Bash 函数参数列表中的管道

我正在使用简单的函数来使我的脚本更加详细。

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 -vxset +vx

如果是set -v,您的管道将按原样打印。set -x将分别打印tarssh,但它会先将其展开;另一方面,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 代码读起来,这导致了至少两个怪现象:

  1. 某些片段必须一次读取,例如while … done或(因为或if … fi之后可能有重定向),因此将把每个这样的片段作为一个整体显示一次。donefiset -v

  2. 类似这样的命令sh -c 'set -v; echo foo'将被“忽略”,set -v因为在执行时set -v所有输入都已被读取。如果您使用换行符代替;; 或使用 ,也会发生这种情况sh -vc 'echo foo'。在脚本中set -v应该可以正常工作。

一个优点是您可以在脚本的早期(可能有条件地)运行set -vset -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$*evalIFSIFSeval"$@"cmdIFS

IFS=.
cmd read a b c

提供1.2.3Enter,然后使用 检查echo "$a"是否$a1。 (请注意,如果在交互式 shell 中执行此操作,则修改后的内容IFS将保留。)

IFS=. cmd read a b ccmd IFS=. read a b c也应该能按预期工作。在任何情况下,修改IFS都不会破坏我们的cmd。请注意,在这些示例中,我故意将多个参数传递给 ,以cmd真正测试 和 是否$*整个$@函数表现良好。


概括

尝试set -v和/或set -x,也许它们对你来说就足够了。如果没有,请使用我们的自定义cmd,但你真的需要引用和/或转义权。

相关内容