具有别名命令的函数可与 eval 一起使用,但不能与 shell 扩展一起使用

具有别名命令的函数可与 eval 一起使用,但不能与 shell 扩展一起使用

下面是我试图执行的函数。该问题似乎与main_cmd另一个命令的别名有关。我认为这只是生成子 shell 时的问题,那么为什么简单的数组扩展会出现这种情况呢? IIUC 不会生成子 shell。

my_function() {
    ... code to set opt1 based on $1 ...
    cmd=(
        main_cmd hard_coded_arg_1 hard_coded_arg_2
        -a "$opt1"
        -b hard_coded_opt
        "'$2'"
    )

    # Doesn't work.
    "$cmd[@]"
    # Works
    eval "$cmd[@]"
}

我只想使用,eval但这似乎是一种反模式(尽管我不明白为什么它仅对表达式很重要将要执行直接地,但那是另一个话题了)。

编辑:澄清这是使用zsh

答案1

别名很特殊。它们在解析完全完成之前在命令行处理的早期进行处理,并且它们的工作方式更像是直接文本替换,而不是调用单独的函数。

使用别名,您可以执行以下操作:

% alias maybe=if
% maybe true; then echo yes; fi
yes

或者

% alias foo='echo hi; '
% foo echo bar
hi
bar

甚至

% alias xyz='echo "foo'
% xyz bar"
foo bar

在第二种情况下,foo echo bar几乎只是被翻译成echo hi; echo bar,然后以通常的方式进行解析。对于函数,它将被解析为命令名称和两个参数,然后 shell 稍后需要确定它foo是函数还是内置命令或外部命令。在最后一种情况下,xyz bar"如果别名不存在,则该命令将具有未闭合的引号。

上面的例子都不适用于函数。(好吧,无论如何,它们可以说是“工作”的。有人可能会说它们更像是休息shell 语法。)

但这也意味着在"${cmd[@]}"解析和扩展之后,别名处理早已不复存在。事实上你可以这样做:

% alias runcmd='"${cmd[@]}"'
% cmd=(printf "'%s'\n" "foo bar" abc)
% runcmd
'foo bar'
'abc'

这应该可以表明在数组扩展完成之前已经处理了别名。

使用eval,它“有效”,因为eval强制进行另一轮命令行处理,包括别名扩展。但它还涉及处理命令参数中的任何引号,而您可能并不真正想要这样做。看Stéphane 的详细回答关于这一点。

使用函数main_cmd而不是别名可能不会那么混乱。

答案2

您想要:

cmd=(
  main_cmd hard_coded_arg_1 hard_coded_arg_2
  -a "$opt1"
  -b hard_coded_opt
  "$2"
)
"${cmd[@]}"

我们定义一个包含以下元素的数组:

  1. main_cmd
  2. hard_coded_arg_1
  3. hard_coded_arg_2
  4. -a
  5. value-of-opt1-variable
  6. -b
  7. hard_coded_opt
  8. value-of-second-positional-parameter

然后"${cmd[@]}"(或"$cmd[@]"在 zsh 中)分别扩展为每个参数,因此我们最终运行从数组的第一个元素 ( ) 查找的命令,$cmd[1]并将数组的所有元素作为参数。

在 zsh 中,使用 just$cmd也可以在这里工作,但因为它删除了空元素,所以不能用于任意命令。

或者(尽管使用数组没有什么意义):

cmd=(
  'main_cmd hard_coded_arg_1 hard_coded_arg_2'
  '-a "$opt1"'
  '-b hard_coded_opt'
  '"$2"'
)
eval " ${cmd[@]}"

其中数组有:

  1. main_cmd hard_coded_arg_1 hard_coded_arg_2
  2. -a "$opt1"
  3. -b hard_coded_opt
  4. "$2"

我们将它们eval作为单独的参数传递给(在第一个参数前面添加一个空格,以防它包含可以作为选项的 a -eval,它与中间的空格连接起来给出:main_cmd hard_coded_arg_1 hard_coded_arg_2 -a "$opt1" -b hard_coded_opt "$2"然后被解释为 shell 代码(因为那是做什么eval)。

正在做:

cmd=(
    main_cmd hard_coded_arg_1 hard_coded_arg_2
    -a "$opt1"
    -b hard_coded_opt
    "'$2'"
)

eval "$cmd[@]"

是错误的,因为您得到的数组的元素如下:

  1. main_cmd
  2. hard_coded_arg_1
  3. hard_coded_arg_2
  4. -a
  5. value-of-opt1-variable
  6. -b
  7. hard_coded_opt
  8. 'value-of-second-positional-parameter'

所以你最终会eval解释:

main_cmd hard_coded_arg_1 hard_coded_arg_2 -a value-of-opt1-variable -b hard_coded_opt 'value-of-second-positional-parameter'

如果value-of-opt1-variable碰巧存在$(reboot)$2恰好包含,'; rm -rf ~ #你可以想象会产生灾难性的后果。

请注意, in zsh 是使用其间的第一个字符连接数组元素的eval " $cmd"缩写。这对于以空格开头的默认值来说没问题,但在已修改的上下文中使用时会中断(尽管在 zsh 中不常见,因为 zsh 具有适当的显式拆分和连接运算符,因此很少需要对其进行修改) )。eval " ${cmd[*]}"$IFS$IFS$IFS$IFS

如果这里的目的是通过将命令分成几行来使命令更容易阅读,那么几乎在每个 shell 中都有效的传统方法是使用行延续通过在行尾插入反斜杠:

my_function() {
  main_cmd hard_coded_arg_1 hard_coded_arg_2 \
    -a "$opt1" \
    -b hard_coded_opt \
    "$2"
}

通常会缩进行的延续部分,以使其更清楚地表明它们是延续部分,而不是单独的行。有些人更喜欢这样写:

my_function() {
  main_cmd hard_coded_arg_1 hard_coded_arg_2 \
    -a "$opt1"                               \
    -b hard_coded_opt                        \
    "$2"
}

这使得更容易发现 a\缺失的情况。

无论如何,您可能不想在这里使用别名。别名有点像 C 预处理器宏,它们很早就被扩展了(在 C 中的预处理阶段,在 shell 中读取后、标记代码并部分解析其语法之后),但与 C 不同,只有在以下情况下才扩展:在发现指挥位置² 并且不要接受争论。

这就解释了为什么它没有扩展,因为在命令位置中找不到array=( alias_name ... )它。alias_name

使用main_cmd() the command "$@"代替alias main_cmd='the command'没有问题,因为您实际上main_cmd在那里定义了一个真正的命令,而不是一个令牌,该令牌意味着被一些文本替换,以便在某种条件下重新评估为代码。


1 在别名扩展之后,结果中的语法将被重新解析,这甚至可能完全改变命令的含义,并且可能会经历更多轮别名扩展。

² 尽管看到alias -g全局zsh别名,这些别名在全局范围内扩展,而不仅仅是在指挥位置

相关内容