변수에 저장된 명령을 어떻게 실행할 수 있나요?

변수에 저장된 명령을 어떻게 실행할 수 있나요?
$ ls -l /tmp/test/my\ dir/
total 0

위 명령을 실행하는 다음 방법이 실패하거나 성공하는 이유를 알고 싶습니다.

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0

답변1

이것은 unix.SE에 대한 많은 질문에서 논의되었으며 여기서 질문할 수 있는 모든 질문을 수집하려고 노력할 것입니다. 아래는

  • 다양한 시도가 실패한 이유와 방법에 대한 설명,
  • 기능(고정 명령의 경우)을 사용하여 이를 올바르게 수행하는 방법 또는
  • 쉘 배열(Bash/ksh/zsh) 또는 $@의사 배열(POSIX sh)을 사용하십시오. 둘 다 몇 가지 옵션만 변경해야 하는 경우 명령줄 조각을 작성할 수도 있습니다.
  • eval이 작업 사용에 대한 참고 사항 .

마지막에 일부 참고자료가 있습니다.

여기서는 명령 매개변수나 명령 이름을 변수에 저장하는 것이 중요하지 않습니다. 명령이 실행될 때까지 유사하게 처리됩니다. 여기서 쉘은 첫 번째 단어만 실행될 명령의 이름으로 사용합니다.


왜 실패했는가

이러한 문제에 직면한 이유는 다음과 같습니다.분사매우 단순하여 복잡한 경우에는 적합하지 않으며, 변수에서 확장된 따옴표는 따옴표 역할을 하지 않고 일반 문자로만 작동합니다.

(따옴표에 관한 부분은 다른 모든 프로그래밍 언어와 유사합니다. 예를 들어 C의 함수는 char *s = "foo()"; printf("%s\n", s)호출되지 않지만 문자열은 인쇄될 뿐입니다 . 이는 m4, C 전처리기 또는 Make(To)와 같은 매크로 프로세서에서는 다릅니다. 쉘이 매크로 프로세서가 아닌 프로그래밍 언어인 정도).foo()foo()

Unix 계열 시스템에서 셸은 명령줄에서 따옴표와 변수 확장을 처리하고 이를 단일 문자열에서 기본 시스템 호출에 의해 시작 명령에 전달된 문자열 목록으로 변환하는 역할을 합니다. 프로그램 자체는 쉘이 처리하는 따옴표를 볼 수 없습니다. 예를 들어, 명령이 주어지면 ls -l "foo bar"쉘은 이를 세 개의 문자열 ls, -lfoo bar(따옴표 제거)로 변환하고 에 전달합니다 ls. (모든 프로그램에서 사용하는 것은 아니지만 명령 이름도 전달됩니다.)

질문에 제시된 사례:

여기서 할당은 단일 문자열을 ls -l "/tmp/test/my dir"다음에 할당합니다 abc.

$ abc='ls -l "/tmp/test/my dir"'

아래에서는 $abc공백으로 나누고 ls세 개의 매개변수 sum 을 -l얻 습니다 . 여기의 따옴표는 단지 ​​데이터이므로 두 번째 매개변수 앞에 하나, 세 번째 매개변수 뒤에 하나가 있습니다. 이 옵션은 작동하지만 따옴표가 파일 이름의 일부로 처리되므로 경로가 잘못 처리됩니다 ."/tmp/test/mydir"ls

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

여기서는 확장자를 인용하여 단일 단어로 유지합니다. 쉘은 ls -l "/tmp/test/my dir"공백과 따옴표가 포함된 문자 그대로 이름이 지정된 프로그램을 찾으려고 시도합니다 .

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

여기서는 $abc분할되어 첫 번째 결과 단어만 인수로 사용되므로 -cBash는 ls현재 디렉터리에서만 실행됩니다. 다른 단어는 패딩 등에 사용되는 bash의 매개변수입니다 $0.$1

$ bash -c $abc
'my dir'

bash -c "$abc", 및 의 경우 eval "$abc"따옴표가 작동하도록 하는 추가 쉘 처리 단계가 있지만또한 모든 쉘 확장이 다시 처리됩니다., 따라서 인용할 때 매우 주의하지 않는 한 사용자가 제공한 데이터를 명령으로 대체하는 등 실수로 실행할 위험이 있습니다.


이 작업을 수행하는 더 좋은 방법

명령을 저장하는 두 가지 더 좋은 방법은 a) 함수를 사용하는 것, b) 배열 변수(또는 위치 매개변수)를 사용하는 것입니다.

사용 기능:

명령이 포함된 함수를 선언하고 명령과 같은 함수를 실행하면 됩니다. 함수 내의 명령 확장은 명령이 정의될 때가 아니라 명령이 실행될 때만 처리되며 개별 명령을 참조할 필요가 없습니다. 이는 미리 준비된 명령(또는 여러 개의 미리 준비된 명령)을 저장해야 하는 경우에만 도움이 됩니다.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

여러 함수를 정의하고 변수를 사용하여 최종적으로 실행될 함수의 이름을 저장하는 것도 가능합니다.

배열을 사용하십시오:

배열을 사용하면 개별 단어에 공백이 포함된 다중 단어 변수를 생성할 수 있습니다. 여기서 개별 단어는 별개의 배열 요소로 저장되며 확장은 "${array[@]}"각 요소를 별도의 셸 단어로 확장합니다.

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# expand the array, run the command
"${mycmd[@]}"

명령은 명령을 실행할 때 작성된 것과 동일하게 괄호 안에 작성됩니다. 셸에서 수행되는 처리는 두 경우 모두 동일합니다. 단, 한 경우에서는 프로그램을 실행하는 데 사용하지 않고 결과 문자열 목록을 저장하기만 한다는 점만 다릅니다.

하지만 나중에 배열을 확장하기 위한 구문은 약간 나쁘고 그 주위의 따옴표가 중요합니다.

배열을 사용하면 명령줄을 하나씩 작성할 수도 있습니다. 예를 들어:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag, append to array
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

또는 명령줄의 일부를 변경하지 않고 그대로 두고 배열을 사용하여 옵션이나 파일 이름과 같은 일부를 채웁니다.

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

somecommand "${options[@]}" "${files[@]}" "$target"

( somecommand이것은 실제 명령이 아닌 일반적인 자리 표시자 이름입니다.)

배열의 단점은 표준 기능이 아니기 때문에 일반적인 POSIX 셸(예: Debian/Ubuntu의 dash기본값 /bin/sh)에서는 배열을 지원하지 않는다는 것입니다(아래 참조). 그러나 Bash, ksh 및 zsh는 이를 지원하므로 시스템에 배열을 지원하는 일부 셸이 있을 수 있습니다.

사용"$@"

명명된 배열을 지원하지 않는 쉘에서도 위치 매개변수(의사 배열 "$@")를 사용하여 명령 매개변수를 보유할 수 있습니다.

다음은 이전 섹션의 코드 비트와 동일한 작업을 수행하는 이식 가능한 스크립트 비트여야 합니다. 배열은 "$@"위치 인수 목록 으로 대체됩니다 . 설정은 "$@"으로 완료되며 set, 큰따옴표를 둘러싸는 "$@"것이 중요합니다(이렇게 하면 목록의 요소가 개별적으로 인용됩니다).

먼저, 명령을 인수와 함께 저장 "$@"하고 실행하세요.

set -- ls -l "/tmp/test/my dir"
"$@"

명령에 대한 일부 명령줄 옵션을 조건부로 설정합니다.

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

"$@"옵션 및 피연산자 에만 해당 :

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

somecommand "$@"

물론 "$@"일반적으로 스크립트 자체의 매개변수로 채워져 있으므로 재사용하기 전에 어딘가에 저장해야 합니다 "$@".

${var:+word}단일 매개변수를 조건부로 전달하려면 대체 값 확장 및 일부 주의 깊은 인용을 사용할 수도 있습니다 . 여기서는 -f 파일 이름이 비어 있지 않은 경우에만 합계 파일 이름을 포함합니다.

file="foo bar"
somecommand ${file:+-f "$file"}

사용 eval(여기서 주의하세요!)

eval문자열을 승인하고 마치 쉘 명령줄에 입력된 것처럼 명령으로 실행합니다. 여기에는 유용하면서도 위험한 모든 참조 및 확장 처리가 포함됩니다.

간단한 경우에는 우리가 원하는 것을 할 수 있습니다:

cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"

를 사용하면 eval따옴표가 처리되어 우리가 원하는 대로 두 개의 인수 및 ls만 표시됩니다 . 또한 얻은 모든 인수를 연결할 수 있을 만큼 똑똑하므로 어떤 경우에는 작동할 수도 있지만 예를 들어 모든 공백은 단일 공백으로 변경됩니다. 변수가 로 수정되지 않도록 여기에서 변수를 인용하는 것이 가장 좋습니다 .-l/tmp/test/my direvaleval $cmdeval

하지만,명령 문자열에 사용자 입력을 포함하는 것은 위험합니다.eval. 예를 들어 다음과 같이 작동하는 것 같습니다.

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

그러나 사용자가 작은따옴표가 포함된 입력을 제공하면 따옴표를 분리하여 임의의 명령을 실행할 수 있습니다!예를 들어, input 을 사용하면 '$(whatever)'.txt스크립트가 명령 대체를 행복하게 실행합니다. 진실은 그럴 수도 rm -rf있고 더 나쁠 수도 있습니다.

$filename문제는 실행되는 명령줄에 값이 포함되어 있다는 것입니다 eval. eval명령과 같이 이전에 확장되었습니다 ls -l ''$(whatever)'.txt'. 안전을 위해 입력을 전처리해야 합니다.

다른 방법으로 파일 이름을 변수에 유지하고 eval명령이 확장하도록 하면 다시 더 안전해집니다.

read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";

외부 따옴표는 이제 작은 따옴표이므로 내부에서는 확장이 발생하지 않습니다. 따라서 eval명령을 확인 ls -l "$filename"하고 파일 이름을 직접 안전하게 확장하십시오.

그러나 이는 단순히 명령을 함수나 배열에 저장하는 것과 크게 다르지 않습니다. 함수나 배열의 경우 단어가 항상 분리되어 있고 내용이 인용되거나 달리 처리되지 않기 때문에 그러한 문제가 없습니다 filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

사용하는 거의 유일한 이유 eval는 변경 사항이 변수(파이프, 리디렉션 등)를 통해 도입될 수 없는 셸 구문 요소와 관련된 경우입니다. 그러나 모든 것을 인용/이스케이프해야 합니다.기타추가 구문 분석 단계로부터 보호가 필요한 명령줄에서(아래 링크 참조) 아무튼 최고피하다사용자 입력을 eval명령에 포함시키세요!


인용하다

답변2

(중요한) 명령을 실행하는 가장 안전한 방법은 입니다 eval. 그런 다음 명령줄에 있는 것처럼 명령을 작성할 수 있으며 방금 입력한 것과 동일하게 실행됩니다. 하지만 모든 것을 인용해야 합니다.

간단한 경우:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

상황은 그렇게 간단하지 않습니다.

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

답변3

두 번째 인용문은 명령을 중단합니다.

내가 실행할 때 :

abc="ls -l '/home/wattana/Desktop'"
$abc

오류가 발생합니다.

하지만 내가 달릴 때

abc="ls -l /home/wattana/Desktop"
$abc

전혀 오류가 없습니다

당시에는 이 문제에 대한 해결책이 없었지만 디렉터리 이름에 공백을 넣지 않으면 오류를 피할 수 있습니다.

이 답변 eval 명령을 사용하여 이 문제를 해결할 수 있지만 나에게는 작동하지 않습니다.

답변4

@ilkkachu가 언급했지만배쉬의 분사, IFS 쉘 변수의 중요성을 명시적으로 지적하면 좋겠다고 생각했습니다. 예를 들어 Bash에서는 다음과 같습니다.

OLD_IFS="$IFS"
IFS=$'\x1a'
my_command=$'ls\x1a-l\x1a-a\x1a/tmp/test/my dir'
$my_command
IFS="$OLD_IFS"

my_command에 저장된 명령은 예상대로 실행됩니다. \x1a는 CTRL-Z이고 a는 ASCII입니다.좋은 분리기 선택. 이는 실행되는 명령에 CTRL+Z 문자가 포함되어 있지 않은 한 작동합니다(공백보다 가능성이 높음). $'...'를 인용하는 ANSI-C 스타일이 아직 POSIX가 아니기 때문에 bash에 대해서도 언급했습니다.

이 기술은 명령을 하드 코딩했거나 명령을 구성할 때 활용됩니다.IFS를 이전 값으로 재설정하는 것을 잊지 마십시오.

관련 정보