$()에서 반환된 문자열을 다른 명령의 여러 인수로 사용하려면 어떻게 해야 합니까?

$()에서 반환된 문자열을 다른 명령의 여러 인수로 사용하려면 어떻게 해야 합니까?

내 사용 사례:

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"왜 이런 일이 발생하는지 이해할 수 없습니다. 아마도 작곡가는 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-nice별도 projecty/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공백이나 탭이 포함되어서는 안 되며, 다른 두 문자는 $IFS기본값 in 이므로 그대로 bash둘 수 있습니다 $IFS.

그러나 글로브가 포함될 수 있습니다. 예를 들어, composer show -s출력 인 경우 etc/p* blah blah파이프라인은 출력되고 etc/p*, 내부에서 실행되지 /않으면 파이프라인은 , , ... 로 확장됩니다 set -o noglob.etc/p*etc/pam.confetc/passwdetc/profile

분할+glob을 방지하고 출력 cmd(명령 대체에 의해 제거된 후행 개행 제외)을 명령에 대한 단 하나의 인수로 전달하려면 큰따옴표를 사용하십시오.

composer remove --dev "$(cmd)"

cmd(패키지가 하나만 출력되는 경우에만 의미가 있습니다.)

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))

관련 정보