$()から返された文字列を他のコマンドの複数の引数として使用するにはどうすればよいですか?

$()から返された文字列を他のコマンドの複数の引数として使用するにはどうすればよいですか?

マイユースケース:

composer.jsonファイルにリストされているすべての開発パッケージを削除する必要があります。projectx/package-niceという2つのパッケージがあるとしましょう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"なぜこれが起こるのか理解できません。おそらく作曲家は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)部分は参照されず、リストコンテキストで分割+globの影響を受けるからです(ここでは単純なコマンドの引数から)。

変更しない限り、$IFS分ける部分)に空白文字が含まれているため、cmd出力の場合はprojectx/package-nice projecty/package-good\n合計に分割され、projectx/package-niceprojecty/package-goodの引数としてに渡されますcomposer

ところで、改行文字もデフォルト値なので、$IFSあなたのものxargs(改行文字を空白に変換する意図のようです)は意味がありません。

シェルを使用する場合は、分割+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スペースやタブを含めないでください。他の2つの文字は$IFSデフォルト値inであるため、そのままbash残すことができます$IFS

ただし、地球を含めることができます。たとえば、composer show -s出力の場合はetc/p* blah blahパイプラインが出力されetc/p*、内部で実行されない場合/はパイプラインは、、...に拡張されますset -o noglobetc/p*etc/pam.confetc/passwdetc/profile

分割+globを防止し、出力cmd(コマンド置換によって削除された末尾の改行を除く)をコマンドの引数として1つだけ渡すには、二重引用符を使用します。

composer remove --dev "$(cmd)"

cmd(パッケージが1つだけ出力される場合にのみ意味があります。)

Linuxでは、次のコマンドを使用して引数がコマンドに渡されるのを見ることができます。

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

(またはstraceシェルでコマンドを実行して、シェルexecve()で実行されたすべてのシステムコールまたは生成されたすべてのプロセスを追跡します。)

たとえば、

スプリット+グローブ、デフォルトは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

参照で分割+グローブを無効にします。

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

では、zshglob部分ではなく引用符なしでコマンドを置き換えると、IFS分割部分のみが実行されます(kshは分割+globに加えて中括弧拡張も実行します)。また、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))

関連情報