我的用例:
我需要删除composer.json
文件中列出的所有开发包。假设我有两个包:projectx/package-nice
和projecty/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-nice
和projecty/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
不应包含空格或制表符,其他两个字符位于$IFS
in的默认值中bash
,因此您可以保持$IFS
原样。
但它可能包含 glob。例如,如果composer show -s
输出etc/p* blah blah
,您的管道将输出etc/p*
,如果从内部运行/
,没有set -o noglob
,etc/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
在命令替换之上应用显式拆分。例如,在换行符上拆分输出,因此在这里,您可以这样做:s
f
0
${(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))