我有一个 Bash 脚本(我们称之为clock
),它应该作为类似于time
Bash 中的关键字的包装器工作,例如clock ls
应该执行某些操作然后运行ls
。以下是此脚本的一个示例:
#!/bin/bash
echo "do something"
$@
请注意,它不使用exec
, 来允许包装内置函数。
但是,当 wrap 的参数是time
关键字时,它不能按预期工作:输出显示它运行/usr/bin/time
命令,而不是 shell 关键字。
我怎样才能让我的包装脚本将关键字(如time
)完全当做是在 shell 中直接输入的一样?
笔记: 在我的相关问题clock
,我学会了如何在同一个脚本中是 Bash 函数时使其工作,但在我的实际用例中,clock
它本身就是一个 Bash 脚本,因此先前的解决方案不起作用。此外,相关问题中提到的解决方案($@
直接使用或运行exec bash -c ""$@""
)在这种情况下不起作用。
我发现的一个部分解决方案是使用eval $@
,但它非常不可靠。它在这种简单情况下有效time
,但在许多情况下会失败,例如在 中clock ls '~$Document1'
。
答案1
分析
问题是您想要使用的命令是为处理 Bash 中的整个管道而设计的。如果像任何常规命令(例如外部可执行文件)或甚至内置命令一样被识别和“执行”,time
那是不可能的。它必须是一个关键字。shell 需要很早就识别它,大约在它识别管道的时候。time
time
您无法在变量扩展期间让|
、或从变量(或参数)中弹出,从而将有效的&&
、 或注入到 shell 代码中。在发生变量扩展时,shell 已经知道该行的逻辑是什么。同样,弹出并被解释为关键字也为时已晚。;
time
这意味着传递变量(或参数)并将其解释为关键字的唯一方法time
是对其进行评估(以及整个命令)从一开始就变量扩展之后。这就是eval
orbash -c
可以做的事情。你无法避免它。
基本解决方案
最简单的方法是要求clock
(你的脚本)只接受一个参数。你可以像这样使用它:
clock ls
clock 'ls -l'
clock 'time ls -l'
clock 'time ls -l | wc -l'
脚本中的关键命令应该是:
eval "$1"
# or
exec bash -c "$1" "$0"
(如果你对此感到疑惑,"$0"
那么请阅读这。重点是使$0
新 shell 中的 和当前 shell 中的 相同。其值很可能是clock
。)
我猜您希望能够方便地运行clock time ls -l
而不是clock 'time ls -l'
。如果是这样,脚本中的关键命令应该是:
eval "$@"
# or
IFS=$' \t\n'; eval "$*"
# or
IFS=$' \t\n'; exec bash -c "$*" "$0"
如果我是你,我会更喜欢它,eval
因为它不是从头开始bash
(性能)并且它保持未导出的变量可用(如果echo "do something"
你的脚本设置了一些变量,这些变量可能是相关的)。
eval "$@"
我更喜欢而不是eval "$*"
,因为前者不依赖于IFS
。在收到多个参数时( 可能是这种情况"$@"
)eval
将它们连接在一起,用空格分隔,然后计算结果。如果变量以空格开头,则这相当于传递"$*"
(始终是单个参数)IFS
。无论我在哪里使用,"$*"
我都确保IFS
以空格开头,以防您的脚本出于某种原因先前更改了变量。默认值是空格+制表符+换行符。
我的选择:
#!/bin/bash
echo "do something"
eval "$@"
引用
无论您选择什么,双引号$@
,$*
或者$1
在脚本中。请注意,扩展分为三个阶段:
当你传递
clock whatever
给 shell 时,shell 会像往常一样解析命令:标记识别、括号扩展、波浪号扩展等等。您可以通过引用和/或转义来避免(在此列表的上下文中:可能延迟)各种扩展。当脚本到达
"$@"
、"$*"
或 时"$1"
,会发生参数扩展。如果参数未用双引号引起来,结果将进行分词和文件名扩展。如果您使用 ,您很可能在此阶段不想要这些eval
;如果您使用 ,您肯定不想要这些bash -c
。最后,当
eval
orbash -c
执行其工作时,它会从头开始解析作为参数传递的字符串。同样,您可以通过适当的引用或转义来避免各种扩展。请注意应抑制某些扩展的引号和/或反斜杠,或应在此阶段扩展的字符(如*
or 或片段{a,b,c}
)$foo
——它们最初应该被引用或转义,以便它们在第一阶段中存活下来,而不是过早地“用完”。
您应该在第一阶段仔细引用和/或退出,了解并计划命令在最后阶段将如何显示。
"$@"
如果您选择使用(或"$*"
)而不是 的解决方案"$1"
,则以下两个命令将是等效的:
clock 'ls -l'
clock ls -l
(除非你的脚本的自定义部分将它们区分开来)。但不包括以下两个:
clock 'ls -l | wc -l'
clock ls -l | wc -l
请注意,这与命令watch 'ls -l'
或的ssh user@host 'ls -l'
行为非常相似。您可以省略引号并获得相同的结果。但watch 'ls -l | wc -l'
和watch ls -l | wc -l
并不等价;和也不ssh user@host 'ls -l > foo.txt'
等价ssh user@host ls -l > foo.txt
。
你的尝试
$@
直接使用
Sole$@
在变量扩展后不提供任何额外的求值。当time
它出现时,已经来不及将它解释为关键字了。
如果time
不是问题,那么$@
在这种情况下exec
$@ might be a good idea, but think twice if you want
$@` 就不会被引用。
跑步
exec bash -c ""$@""
这是错误的,我已通知了您从中得到答案的作者(答案已得到改进)。这些相邻的双引号相互抵消。实际上,$@
如上所述,未加引号,容易出现单词拆分和文件名生成。但即使"$@"
在这里也是错误的,因为bash -c
只接受一个参数作为代码。以下参数(如果有)定义位置参数(从 0 开始,这是有原因的)。如果您的脚本使用这个有缺陷的代码,那么例如clock ls -l
将运行ls
,而不是ls -l
;甚至clock
ls -l will run
ls` 也不会因为分词而导致参数丢失。
我发现的一个部分解决方案是使用
eval $@
,但它非常不可靠。它在这种简单情况下有效time
,但在许多情况下会失败,例如在 中clock ls '~$Document1'
。
通过单引号,您可以防止$Document
在第一阶段被扩展(作为变量),但在最后阶段则不会。使用稍微不同的字符串~
也可能有问题。不加引号$@
可能会在中间出现问题,尽管在本例中不是。您需要保护$
两次:
clock ls '~\$Document1'
我的基本解决方案$
在这种情况下也需要两次保护。要按time
您的意愿工作,您需要这个额外的扩展阶段,因此您只需处理这个问题即可。
比较watch ls '~$Document1'
和watch ls '~\$Document1'
。同样的情况。
有一个技巧。见下文。
诀窍
watch
在或的情况下,选择在哪个阶段扩展某些子字符串的能力很有用ssh
。
例如,您可能希望监视已存在*.tmp
文件的大小,而不关注新文件。在这种情况下,您需要*
扩展一次:watch ls -l *.tmp
。或者您可能希望包含与模式匹配的新文件。在这种情况下,您需要*
反复扩展:watch 'ls -l *.tmp'
。ssh
您可能希望在本地或远程服务器上扩展变量。对于这两种工具,延迟扩展有时很有用。
但是,您的脚本应该与time
关键字类似。关键字不会引入额外的扩展阶段,您的示例~$Document1
表明您不想引入它。不过,根据我的分析,您需要它,但只能将像time
(作为参数传递)这样的词解释为关键字。
有一种方法可以在最后阶段抑制这些不必要的扩展。您可以Q
更早地使用运算符:
${parameter@operator}
扩展是 值的变换
parameter
或有关其自身的信息parameter
,具体取决于 的值operator
。每个operator
都是一个字母:
Q
parameter
扩展名是一个字符串,它是用可以重复用作输入的格式引用 的值。
(来源)
这为扩展的字符串添加了一层单引号/转义。现在我们的想法是在第 2 阶段使用它,因此在第 3 阶段,这些额外的引号将阻止各种扩展(并被删除)。
只需更改eval "$@"
为eval "${@@Q}"
将导致:
clock ls '~$Document1'
能够像那样奔跑(很棒!);- 无法奔跑
clock 'time ls -l | wc -l'
(好吧); - 无法将
time
in识别clock time ls -l
为关键字(哎呀!);在第 3 阶段time
将被单引号引用并且'time'
不是关键字。
解决方案是不使用Q
第一个命令行参数:
#!/bin/bash
echo "do something"
cmnd="$1"
shift
eval "$cmnd" "${@@Q}"
第一个命令行参数clock
在第 3 阶段不受扩展保护,但其他参数受到保护。结果:
- 你就可以
clock ls '~$Document1'
像那样跑了(真棒!); - 你可以跑
clock 'time ls -l | wc -l'
(很好),不过你需要注意第一阶段的引号和第 3 阶段(这个问题在某些情况下可能会有帮助); time
或者clock time …
是clock 'time …'
您time
想要的(耶!)。
您是否应该担心第一个命令行参数clock
在第 3 阶段不受扩展保护?其实不必。它要么是一个完整的长命令(如管道),作为一个整体引用,那么您应该将其视为传递给watch
或作为一个参数的长命令;或者它将是一个关键字/内置/命令,由于命令名称刻意简单且安全(没有或等) ssh
,因此在第 3 阶段不会触发任何不必要的扩展。如果您想运行或,情况会有所不同。我相信你没有理由这样做。$
~
clock '*' …
clock './~$Document1' …
答案2
据我所记得,bash 有一个builtin
命令,它可以强制它运行,你猜对了,内置命令,即使有一个PATH
同名的文件。
我通过编写此脚本进行了测试/usr/bin
:
#!/bin/bash
echo "This is /usr/bin/cd, and it does nothing"
结果如下:
jarmund@jarmint/etc$ /usr/bin/cd ~
This is /usr/bin/cd, and it does nothing
jarmund@jarmint/etc$ builtin cd ~
jarmund@jarmint~$
结论:在命令前加上前缀builtin
可以消除 shell 可能遇到的任何歧义。