在另一个脚本的调用中包装“time”(和类似的关键字)

在另一个脚本的调用中包装“time”(和类似的关键字)

我有一个 Bash 脚本(我们称之为clock),它应该作为类似于timeBash 中的关键字的包装器工作,例如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 需要很早就识别它,大约在它识别管道的时候。timetime

您无法在变量扩展期间让|、或从变量(或参数)中弹出,从而将有效的&&、 或注入到 shell 代码中。在发生变量扩展时,shell 已经知道该行的逻辑是什么。同样,弹出并被解释为关键字也为时已晚。;time

这意味着传递变量(或参数)并将其解释为关键字的唯一方法time是对其进行评估(以及整个命令)从一开始就变量扩展之后。这就是evalorbash -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在脚本中。请注意,扩展分为三个阶段:

  1. 当你传递clock whatever给 shell 时,shell 会像往常一样解析命令:标记识别、括号扩展、波浪号扩展等等。您可以通过引用和/或转义来避免(在此列表的上下文中:可能延迟)各种扩展。

  2. 当脚本到达"$@""$*"或 时"$1",会发生参数扩展。如果参数未用双引号引起来,结果将进行分词和文件名扩展。如果您使用 ,您很可能在此阶段不想要这些eval;如果您使用 ,您肯定不想要这些bash -c

  3. 最后,当evalorbash -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;甚至clockls -l will runls` 也不会因为分词而导致参数丢失。


我发现的一个部分解决方案是使用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'(好吧);
  • 无法将timein识别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 可能遇到的任何歧义。

相关内容