如何使用 $() 返回的字符串在另一命令中用作多个参数?

如何使用 $() 返回的字符串在另一命令中用作多个参数?

我的用例:

我需要删除composer.json文件中列出的所有开发包。假设我有两个包:projectx/package-niceprojecty/package-good。要删除它们,我需要运行:

$ composer remove --dev projectx/package-nice projecty/package-good

所以我构建这个命令来提取包列表:

echo $(composer show -s | grep -E "^[a-z]+/[0-9a-z_-]+" | awk '{print $1}' | xargs)

这将返回包列表,如下所示:projectx/package-nice projecty/package-good

所以我尝试运行下面的命令,但没有成功,因为 bash 将返回解释为用引号引起来的单个字符串:

$ composer remove --dev $(composer show -s | grep -E "^[a-z]+/[0-9a-z_-]+" | awk '{print $1}' | xargs)

它等同于:

$ composer remove --dev "projectx/package-nice projecty/package-good"

那么,我做错了什么?

编辑:

请注意,问题不在于解析。返回$()由空格分隔的预期值。问题在于为什么 bash 将此返回解释为唯一值。

正如@MarcusMüller 指出的,这个问题不应该发生。我在里面/etc跑:

$ ls $(ls | head -n 2)

并且执行的命令是ls file1 file2而不是ls "file1 file2",所以我不明白为什么会发生这种情况。也许是因为composer只是一个由php运行的脚本,而这会干扰某些东西?

谢谢。

答案1

composer remove --dev $(composer show -s | grep -E "^[a-z]+/[0-9a-z_-]+" | awk '{print $1}' | xargs)

不等于:

composer remove --dev "projectx/package-nice projecty/package-good"

$(cmd)部分,因为它没有被引用并且在列表上下文中(在此处的简单命令的参数中)受到 split+glob 的影响。

除非你修改了它,$IFS(用于分裂部分)恰好包含空格字符,因此如果cmd输出projectx/package-nice projecty/package-good\n,它将被拆分为projectx/package-niceprojecty/package-good并作为单独的参数传递给composer.

顺便说一句,换行符也是默认值$IFS,所以你的xargs(我想它是为了将换行符转换为空格)是毫无意义的。

如果使用 shell,而不是使用 split+glob,bash使用将readarray某个文件的行读入数组的各个元素会更有意义:

readarray -t packages < <(
  composer show -s | grep -Po '^\p{Ll}+/[\p{Ll}\d_-]\H*'
)
(( ${#packages[@]} == 0 )) ||
  composer remove --dev "${packages[@]}"

使用 split+glob 也是一种选择,但与往常一样,使用它时最好根据您的具体需要进行调整:

IFS=$'\n' # split on newline only
set -o noglob # disable the glob part which we don't want
packages=( $(cmd...) ) # split+glob, result assigned to an array

在您的情况下, 的输出cmd不应包含空格或制表符,其他两个字符位于$IFSin的默认值中bash,因此您可以保持$IFS原样。

但它可能包含 glob。例如,如果composer show -s输出etc/p* blah blah,您的管道将输出etc/p*,如果从内部运行/,没有set -o noglobetc/p*则会扩展到etc/pam.conf, etc/passwd, etc/profile...

为了防止 split+glob,为了将输出cmd(减去由命令替换删除的尾随换行符)作为一个且唯一的一个参数传递给命令,请使用双引号:

composer remove --dev "$(cmd)"

cmd(仅当仅输出一个包时才有意义)。

在 Linux 上,您可以使用以下命令查看正在传递给命令的参数

strace -s999999 -qqfe execve the-command and its args

(或者strace在 shell 上运行该命令来跟踪execve()它或其生成的任何进程进行的所有系统调用)

例如:

split+glob,默认值为 IFS:

bash-5.0$ strace -s999999 -qqfe execve true $(echo foo; echo foo bar)
execve("/usr/bin/true", ["true", "foo", "foo", "bar"], 0x7ffe1374cc50 /* 66 vars */) = 0

仅在换行符上分割:

bash-5.0$ (IFS=$'\n'; strace -s999999 -qqfe execve true $(echo foo; echo; echo foo bar))
execve("/usr/bin/true", ["true", "foo", "foo bar"], 0x7ffd16c547f8 /* 66 vars */) = 0

(请注意,空行已被删除)。

glob部分的作用:

bash-5.0$ (strace -s999999 -qqfe execve true $(echo 'etc/p*'))
execve("/usr/bin/true", ["true", "etc/pam.conf", "etc/pam.d", "etc/papersize", "etc/parallel", "etc/passwd", "etc/passwd-", "etc/pcmcia", "etc/perl", "etc/php", "etc/pki", "etc/pm", "etc/pnm2ppa.conf", "etc/polkit-1", "etc/popularity-contest.conf", "etc/ppp", "etc/printcap", "etc/profile", "etc/profile.d", "etc/protocols", "etc/pulse", "etc/python2.7", "etc/python3", "etc/python3.8"], 0x7ffdc911f8a0 /* 66 vars */) = 0

固定为set -o noglob

bash-5.0$ (set -o noglob; strace -s999999 -qqfe execve true $(echo 'etc/p*'))
execve("/usr/bin/true", ["true", "etc/p*"], 0x7ffe9c278a50 /* 66 vars */) = 0

通过引用禁用 split+glob:

bash-5.0$ (IFS=$'\n'; strace -s999999 -qqfe execve true "$(echo foo; echo; echo foo bar; echo 'etc/p*')")
execve("/usr/bin/true", ["true", "foo\n\nfoo bar\netc/p*"], 0x7ffcf0e70d20 /* 66 vars */) = 0

在 中zsh,只有 IFS 分割部分是在不带引号的命令替换时完成的,而不是 glob 部分(除了 split+glob 之外,ksh 还进行大括号扩展)。在 中,您还可以使用、、参数扩展标志zsh在命令替换之上应用显式拆分。例如,在换行符上拆分输出,因此在这里,您可以这样做:sf0${(f)"$(cmd)"}cmd

packages=( ${(f)"$(composer show -s | grep -Po '^\p{Ll}+/[\p{Ll}\d_-]\H*')"} )
(( $#packages == 0 )) || composer remove --dev $packages

无需$IFS全局修改或禁用通配符。


¹ 这样做的方法是错误的,因为有很多方法可能因任意输入而失败并且效率很低。

答案2

您手头上有解决方案。您的echo命令返回 2 个由空格分隔的字符串。将其包含在$()命令替换中(是的,它们可以嵌套),您将获得所需的内容:

composer remove --dev $(echo $(composer show -s | grep -E "^[a-z]+/[0-9a-z_-]+" | awk '{print $1}' | xargs))

相关内容