공백이나 기타 특수 문자 때문에 쉘 스크립트가 멈추는 이유는 무엇입니까?

공백이나 기타 특수 문자 때문에 쉘 스크립트가 멈추는 이유는 무엇입니까?

...또는 강력한 파일 이름 처리 및 쉘 스크립트에서 전달되는 기타 문자열에 대한 소개 가이드입니다.

나는 쉘 스크립트를 작성했고 대부분의 경우 잘 작동합니다. 그러나 특정 입력(예: 특정 파일 이름)에서는 차단됩니다.

다음과 같은 문제가 발생했습니다.

  • 공백이 포함된 파일 이름이 있는데 두 개의 별도 파일 hello world로 처리됩니다 .helloworld
  • 입력에서 하나로 축소되는 두 개의 연속 공백이 있는 입력 줄이 있습니다.
  • 입력 줄의 앞뒤 공백이 사라집니다.
  • 때때로 입력에 이러한 문자 중 하나가 포함되어 있으면 \[*?실제로 일부 파일의 이름인 일부 텍스트로 대체됩니다.
  • '입력에 아포스트로피(또는 큰따옴표)가 있고 그 이후에는 상황이 이상해집니다."
  • 입력에 백슬래시가 있습니다(대안: 저는 Cygwin을 사용하고 있으며 일부 파일 이름에는 Windows 스타일 \구분 기호가 있습니다).

어떻게 되어가나요? 이 문제를 어떻게 해결할 수 있나요?

답변1

변수 대체 및 명령 대체에는 항상 큰따옴표를 사용하십시오 "$foo"."$(foo)"

따옴표 없이 사용하면 스크립트는 $foo공백 또는 가 포함된 입력이나 인수(또는 명령 출력, )를 차단합니다.$(foo)\[*?

거기에서 읽기를 중단할 수 있습니다. 음, 여기에 몇 가지가 더 있습니다:

  • read내장 함수를 사용하여 입력을 한 줄씩 읽으려면 다음 read을 사용하십시오.while IFS= read -r line; do …
    일반은 read백슬래시와 공백을 특별히 처리합니다.
  • xargs피하다xargs. 반드시 사용해야 한다면 xargs그렇게 하십시오 xargs -0. 바꾸다 find … | xargs,선호하다find … -exec ….
    xargs공백과 문자를 특별히 취급하십시오 \"'.

이 답변은 Bourne/POSIX 스타일 쉘( sh, ash, dash, bash, ksh, mksh, yash...)에 적용됩니다. Zsh 사용자는 건너뛰고 끝 부분을 읽어야 합니다.언제 큰따옴표가 필요합니까?대신에. 자세한 내용을 원하시면,읽기 기준또는 쉘 매뉴얼.


아래 설명에는 몇 가지 대략적인 내용이 포함되어 있습니다(대부분의 경우 정확하지만 주변 상황이나 구성에 따라 영향을 받을 수 있음).

왜 글을 써야 하나요 "$foo"? 따옴표가 없으면 어떻게 되나요?

$foo"변수의 값을 얻습니다 foo"를 의미하지 않습니다. 이는 더 복잡한 것을 의미합니다.

  • 먼저 변수의 값을 가져옵니다.
  • 필드 분할: 값을 공백으로 구분된 필드 목록으로 처리하고 결과 목록을 작성합니다. 예를 들어 변수에 다음이 포함된 경우 foo * bar ​이 단계의 결과는 3개 요소 목록 foo, *, 입니다 bar.
  • 파일 이름 생성: 각 필드를 전역 변수, 즉 와일드카드 패턴으로 처리하고 해당 패턴과 일치하는 파일 이름 목록으로 바꿉니다. 패턴이 어떤 파일과도 일치하지 않으면 수정되지 않습니다. 이 예에서는 가 포함된 목록이 생성되고 foo그 뒤에 현재 디렉터리의 파일 목록이 오고 마지막으로 bar. 현재 디렉터리가 비어 있으면 결과는 foo, *, 입니다 bar.

결과는 문자열 목록입니다. 쉘 구문에는 목록 컨텍스트와 문자열 컨텍스트라는 두 가지 종류의 컨텍스트가 있습니다. 필드 분할 및 파일 이름 생성은 목록 컨텍스트에서만 발생하지만 대부분의 경우에 해당됩니다. 큰따옴표로 구분된 문자열 컨텍스트: 큰따옴표로 묶인 전체 문자열은 단일 문자열이므로 분할할 수 없습니다. (예외: "$@"위치 인수 목록으로 확장됩니다. 예를 들어 위치 인수가 세 개인 경우 "$@"와 동일합니다 . 참조"$1" "$2" "$3"$*와 $@의 차이점은 무엇인가요?)

$(foo)교체 명령을 사용하거나 사용하는 경우에도 마찬가지입니다 `foo`. 그런데 사용하지 마십시오 `foo`. 인용 규칙은 이상하고 이식성이 없으며 모든 최신 쉘은 $(foo)직관적인 인용 규칙 외에 완전히 동일한 인용 규칙을 지원합니다.

산술 대체의 출력도 동일한 확장을 거치지만 확장할 수 없는 문자만 포함하므로 일반적으로 문제가 되지 않습니다( IFS숫자 또는 가 없다고 가정 -).

바라보다언제 큰따옴표가 필요합니까?따옴표를 생략할 수 있는 상황에 대한 자세한 내용입니다.

이 모든 소란이 일어나는 것을 원하지 않는 한, 변수와 명령 대체에는 항상 큰따옴표를 사용하는 것을 기억하십시오. 참고: 따옴표를 생략하면 오류가 발생할 뿐만 아니라보안 취약점.

파일 이름 목록을 처리하는 방법은 무엇입니까?

공백을 사용하여 파일을 구분하여 작성하는 경우 myfiles="file1 file2"공백이 포함된 파일 이름에는 작동하지 않습니다. Unix 파일 이름에는 /(항상 디렉토리 구분 기호) 및 널 바이트(대부분의 쉘의 쉘 스크립트에서 사용할 수 없음)를 제외한 모든 문자가 포함될 수 있습니다.

같은 질문 myfiles=*.txt; … process $myfiles입니다. 이렇게 하면 변수에 myfiles5자 문자열이 포함되고 와일드카드 문자는 *.txt작성 시 확장됩니다. $myfiles이 예는 스크립트를 로 변경할 때까지 실제로 작동합니다 myfiles="$someprefix*.txt"; … process $myfiles. someprefix로 설정 하면 final report작동하지 않습니다.

모든 유형의 목록(예: 파일 이름)을 처리하려면 목록을 배열에 넣으세요. 이를 위해서는 mksh, ksh93, yash 또는 bash(또는 이러한 인용 문제가 없는 zsh)가 필요합니다. 일반 POSIX 셸(예: ash 또는 dash)에는 배열 변수가 없습니다.

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88에는 할당 구문이 다른 배열 변수가 있습니다 set -A myfiles "someprefix"*.txt(참조:다양한 ksh 환경의 할당 변수ksh88/bash 이식성이 필요한 경우). Bourne/POSIX 스타일 쉘에는 "$@"사용자가 설정 set하고 함수에 대한 로컬 인수인 위치 인수 배열인 배열이 있습니다.

set -- "$someprefix"*.txt
process -- "$@"

로 시작하는 파일 이름은 어떻습니까 -?

관련 참고 사항에서 파일 이름은 -(대시/빼기 기호)로 시작할 수 있으며 대부분의 명령은 옵션을 나타내는 것으로 해석됩니다. 일부 명령(예 sh: set또는 sort)에는 로 시작하는 옵션도 허용됩니다 +. 파일 이름이 변수 부분으로 시작하는 경우 --위의 스니펫에 표시된 대로 변수 부분 앞에 전달해야 합니다. 이는 옵션의 끝에 도달했음을 명령에 나타내므로 그 이후의 모든 내용은 -또는 로 시작 하더라도 파일 이름입니다 +.

또는 파일 이름이 이외의 문자로 시작하는지 확인할 수 있습니다 -. 절대 파일 이름은 으로 시작하며 상대 이름의 시작 부분에 추가 /할 수 있습니다 . ./다음 코드 조각은 변수의 내용을 f동일한 파일을 참조하는 "안전한" 방식으로 변환하며, -nor 로 시작 하지 않도록 보장합니다 +.

case "$f" in -* | +*) "f=./$f";; esac

이 주제에 대한 마지막 참고 사항은 이라는 실제 파일을 참조해야 하거나 이와 같은 프로그램을 호출하고 해당 파일을 읽지 않으려는 경우 -에도 일부 명령은 stdin 또는 stdout으로 해석된다는 점입니다. stdin stdout을 읽거나 쓰려면 반드시 위와 같이 덮어써야 합니다. 바라보다----"du -sh *"와 "du -sh ./*"의 차이점은 무엇입니까?추가 논의를 위해.

명령을 변수에 저장하는 방법은 무엇입니까?

"명령"은 세 가지를 의미할 수 있습니다: 명령 이름(전체 경로가 있거나 없는 실행 파일의 이름, 내장 또는 별칭이 지정된 함수 이름), 매개변수가 있는 명령 이름 또는 셸 코드 조각 . 따라서 변수에 저장하는 방법에는 여러 가지가 있습니다.

명령 이름이 있으면 저장하고 평소처럼 큰따옴표가 포함된 변수를 사용하세요.

command_path="$1"
"$command_path" --option --message="hello world"

인수를 취하는 명령이 있는 경우 문제는 위의 파일 이름 목록과 동일합니다. 즉, 문자열이 아닌 문자열 목록입니다. 중간에 공백이 있는 문자열에는 매개변수를 채울 수 없습니다. 그렇게 하면 매개변수의 일부인 공백과 매개변수를 구분하는 공백 간의 차이를 알 수 없기 때문입니다. 쉘에 배열이 있으면 이를 사용할 수 있습니다.

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

사용 중인 셸에 배열이 없으면 어떻게 되나요? 위치 매개변수를 수정해도 괜찮다면 계속 사용할 수 있습니다.

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

복잡한 셸 명령(예: 리디렉션, 파이프 등)을 저장해야 하는 경우 어떻게 해야 합니까? 아니면 위치 매개변수를 수정하고 싶지 않다면? 그런 다음 해당 명령이 포함된 문자열을 작성하고 eval내장 명령을 사용할 수 있습니다.

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

정의에서 중첩된 따옴표를 참고하세요 code. 작은 따옴표는 '…'문자열 리터럴을 구분하므로 변수 값은 codestring 입니다 /path/to/executable --option --message="hello world" -- /path/to/file1. 내장 함수는 eval인수로 전달된 문자열을 스크립트에 나타난 것처럼 구문 분석하도록 쉘에 지시하므로 이때 따옴표, 파이프 등이 구문 분석됩니다.

사용하기 eval까다롭습니다 . 언제 무엇을 분석해야 할지 신중하게 생각해 보세요. 특히 파일 이름을 코드에 넣을 수는 없습니다. 소스 코드 파일에 있는 것처럼 참조해야 합니다. 이를 수행할 수 있는 직접적인 방법은 없습니다. code="$code $filename"파일 이름에 쉘 특수 문자(공백,,,,,,,등)가 포함되어 있으면 $이와 같은 내용이 깨집니다 . 여전히 켜져 있고 꺼져 있습니다. 파일 이름에 .;|<>code="$code \"$filename\"""$\`code="$code '$filename'"'

  • 파일 이름 주위에 따옴표를 추가하십시오. 가장 쉬운 방법은 주위에 작은따옴표를 추가하고 작은따옴표를 '\''.

     quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
     code="$code '${quoted_filename%.}'"
    
  • 코드 조각이 빌드될 때가 아니라 코드가 평가될 때 조회되도록 코드 내부에 변수 확장을 유지합니다. 이는 더 간단하지만 코드가 실행될 때 변수가 여전히 동일한 값을 갖는 경우에만 작동합니다. 예를 들어 코드가 루프에 내장되어 있으면 작동하지 않습니다.

     code="$code \"\$filename\""
    

마지막으로, 코드가 포함된 변수가 정말 필요합니까? 코드 블록의 이름을 지정하는 가장 자연스러운 방법은 함수를 정의하는 것입니다.

뭐가 문제 야 read?

아니요 -r. read연속된 줄은 허용됩니다. 이는 입력의 단일 논리적 줄입니다.

hello \
world

read입력 행을 문자로 구분된 필드로 분할합니다 $IFS(그렇지 않은 경우 -r백슬래시는 이러한 필드도 이스케이프합니다). 예를 들어 입력이 세 단어를 포함하는 줄인 경우 read first second third설정은 입력의 first첫 번째 단어, second두 번째 단어, 세 번째 단어입니다. third단어가 더 많은 경우 마지막 변수에는 이전 단어를 설정한 후 남은 내용이 포함됩니다. 선행 및 후행 공백이 잘립니다.

IFS정리를 방지하려면 빈 문자열로 설정하세요 . 바라보다"IFS=;" 대신 "IFS= 읽기"가 자주 사용되는 이유는 무엇입니까?더 긴 설명을 위해.

질문이 있으신가요 xargs?

입력 형식 xargs은 공백으로 구분된 문자열이며 작은따옴표 또는 큰따옴표를 선택할 수 있습니다. 이 형식을 출력하는 표준 도구는 없습니다.

xargs -L1아니면 xargs -l입력을 분할하지 마세요.철사, 그러나 입력 라인당 하나의 명령을 실행합니다(라인은 여전히 ​​분할되어 인수를 형성하고 공백으로 끝나는 경우 다음 라인으로 계속됩니다).

xargs -I PLACEHOLDER바꾸기 위해 한 줄의 입력을 사용 PLACEHOLDER하지만 따옴표와 백슬래시는 계속 처리되고 선행 공백은 잘립니다.

xargs -r0해당되는 경우(사용 가능한 경우: GNU(Linux, Cygwin), BusyBox, BSD, OSX를 사용할 수 있지만 POSIX에서는 사용할 수 없음 ) 이는 대부분의 데이터, 특히 파일 이름과 외부 명령 인수에 null 바이트가 나타날 수 없기 때문에 안전합니다. 널로 구분된 파일 이름 목록을 생성하려면 다음을 사용하십시오 find … -print0(또는 find … -exec …아래 설명된 대로 사용할 수 있음).

발견된 파일로 무엇을 해야 합니까 find?

find … -exec some_command a_parameter another_parameter {} +

some_command쉘 함수나 별칭이 아닌 외부 명령이어야 합니다. 파일을 처리하기 위해 셸을 호출해야 하는 경우 sh명시적으로 호출하세요.

find … -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

다른 질문이 있습니다

검색이 사이트에 태그를 추가하거나또는. ("자세히 알아보기..."를 클릭하면 몇 가지 일반적인 팁과 직접 선택한 자주 묻는 질문 목록을 볼 수 있습니다.) 검색했지만 답변을 찾을 수 없는 경우,나가 줄 것을 요청한다.

답변2

Giles의 답변은 매우 훌륭하지만 그의 주요 요점에 문제가 있습니다.

변수 대체 및 명령 대체에는 항상 큰따옴표를 사용하십시오: "$foo", "$(foo)"

단어 분할을 위해 Bash와 같은 셸을 사용하기 시작할 때 안전한 조언은 물론 항상 따옴표를 사용하는 것입니다. 그러나 단어 분할이 항상 수행되는 것은 아닙니다.

§ 분사

이 명령은 잘 작동합니다

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

사용자에게 이 동작을 채택하도록 권장하는 것은 아니지만, 단어 분리가 발생하는 시점을 확실히 이해하고 있는 사람이라면 언제 인용문을 사용할지 스스로 결정할 수 있어야 합니다.

답변3

내가 아는 한, 큰따옴표 확장이 필요한 경우는 두 가지뿐입니다. 여기에는 두 개의 특수 쉘 매개변수 "$@""$*"- 가 포함되며, 이는 큰따옴표로 묶을 때 다르게 확장되도록 지정됩니다. 다른 모든 경우에는(아마도 쉘별 배열 구현은 제외)확장의 동작은 구성 가능하며 몇 가지 옵션이 있습니다.

물론 이것이 큰따옴표를 피해야 한다는 의미는 아닙니다. 반대로 큰따옴표는 아마도 쉘이 제공해야 하는 확장을 구분하는 가장 편리하고 신뢰할 수 있는 방법일 것입니다. 그러나 대안이 전문적으로 제시되었기 때문에 이곳은 쉘이 값을 확장할 때 어떤 일이 발생하는지 논의하기에 좋은 장소라고 생각합니다.

쉘, 그 마음과 영혼 속에(이런 분들을 위해)sed는 명령 해석기입니다. 쉘 문이 다음과 같은 경우 큰 대화형 구문 분석기입니다 .기절존재하다공백또는 이와 유사한 경우 쉘의 해석 프로세스, 특히 입력 명령문을 실행 가능한 명령으로 변환하는 방법과 이유를 완전히 이해하지 못할 가능성이 높습니다. 쉘의 역할은 다음과 같습니다.

  1. 입력을 수락

  2. 설명하고나뉘다토큰화된 입력으로 올바르게 들어갑니다.성격

    • 입력하다성격$word또는와 같은 셸 구문 항목입니다.echo $words 3 4* 5

    • 성격항상 공백으로 분할합니다. 이는 단지 구문일 뿐이지만 입력 파일의 셸에 리터럴 공백 문자만 제공됩니다.

  3. 필요한 경우 이를 여러 개로 확장합니다.필드

    • 필드다음의 결과단어확장 - 최종 실행 가능 명령을 형성합니다.

    • 와는 별개로 "$@",$IFS 필드 분할, 그리고경로명 확장입력단어항상 단일로 평가되어야 합니다.대지.

  4. 그런 다음 결과 명령을 실행하십시오.

    • 대부분의 경우 이는 해석 결과를 어떤 형식으로 전달하는 것을 포함합니다.

사람들은 종종 껍질이 다음과 같다고 말합니다.접착제, 그리고 이것이 사실이라면 그것은 무엇입니까?부착매개변수 목록입니다 - 또는필드- 둘 중 하나의 프로세스(해당 exec하는 경우). 대부분의 쉘은 NUL이미 바이트 단위로 분할되어 있기 때문에 바이트를 잘 처리 하지 못합니다 . 쉘은 반드시exec 많은그리고 NUL이때 시스템 커널에 전달된 구분된 인수 배열을 사용하여 이를 수행 해야 합니다 exec. 셸의 구분 기호를 구분된 데이터와 혼합하면 셸이 이를 망칠 수 있습니다. 대부분의 프로그램과 마찬가지로 내부 데이터 구조는 이 구분 기호에 의존합니다. zsh이것이 문제를 일으키지 않는다는 점은 주목할 가치가 있습니다.

이것이 $IFS들어오는 곳입니다. $IFS쉘이 쉘을 확장하는 방법을 정의하는 항상 존재하고 동일하게 설정 가능한 쉘 매개변수입니다.단어도착하다대지- 특히 이것이 어떤 가치가 있는지에 관해서필드기술해야 합니다. - $IFS이외의 구분 기호로 셸 확장을 분할합니다. 즉, 셸은 NUL내부 데이터 배열의 $IFS값과 일치하는 확장에 의해 생성된 바이트를 대체합니다. NUL이런 식으로 보면, 모든 것이 눈에 띄기 시작할 것입니다.필드 분할쉘 확장은 $IFS-로 구분된 데이터 배열입니다.

단지$IFS경계를 그리다확장자는아니요이미 다른 방법으로 구분되어 있습니다. "큰따옴표를 사용하여 구분할 수 있습니다. 확장자를 참조할 때 헤드에서 구분하고적어도그 가치의 끝까지. $IFS분리 가능한 필드가 없기 때문에 이러한 경우에는 적용되지 않습니다. 실제로 큰따옴표 확장은 동일한 효과를 나타냅니다.필드 분할IFS=NULL로 설정하면 동작은 따옴표가 없는 확장입니다.

인용하지 않는 한, $IFS그 자체는 $IFS구분된 쉘 확장입니다. 기본값은 지정된 값입니다 <space><tab><newline>. 세 가지 값 모두 포함되면 특별한 속성을 나타냅니다 $IFS. 지정된 다른 값은 $IFS단일로 평가됩니다.대지확장 당발생하다,$IFS 공백- 다음 세 가지 중 하나 - 각 확장의 단일 필드를 삭제하도록 지정됨주문하다그리고 선행/후행 시퀀스는 완전히 생략됩니다. 이는 아마도 예를 들어 이해하는 것이 가장 쉬울 것입니다.

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

하지만 그건 단지 $IFS- 단지 분사이거나공백그럼 질문대로특수 문자?

셸은 기본적으로 인용되지 않은 특정 토큰도 확장합니다.( ?*[여기 다른 곳에서 언급한 바와 같이)여러개로 나누어필드목록에 나타날 때. 이것은 ... 불리운다경로명 확장, 또는와일드카드. 매우 유용한 도구이며, 그런 일이 발생하기 때문에필드 분할쉘의 구문 분석 순서는 다음의 영향을 받지 않습니다.$IFS-필드경로 이름 확장으로 생성된 파일 이름은 해당 내용에 현재 파일 이름이 포함되어 있는지 여부에 관계없이 파일 이름 자체의 시작/끝으로 구분됩니다 $IFS. 이 동작은 기본적으로 on으로 설정되지만 그렇지 않은 경우 쉽게 구성할 수 있습니다.

set -f

이는 쉘을 나타냅니다.아니요도착하다전반적인 상황. 최소한 이 설정이 어떤 식으로든 실행 취소될 때까지 경로 이름 확장은 발생하지 않습니다. 예를 들어 현재 쉘이 다른 새로운 쉘 프로세스로 교체되거나...

set +f

...쉘로 전송됩니다. 큰따옴표 - 그들과 똑같습니다$IFS 필드 분할- 이 전역 설정은 모든 확장에 필요하지 않습니다. 그래서:

echo "*" *

...경로 이름 확장이 현재 활성화된 경우 각 인수는 매우 다른 결과를 생성할 수 있습니다. 첫 번째 인수는 리터럴 값으로만 ​​확장되기 때문입니다.(단일 별표 문자, 즉 전혀 없음)두 번째 것은 현재 작업 디렉토리에 잠재적으로 일치하는 파일 이름이 없는 경우에만 동일합니다.(거의 모두 일치함). 그러나 이렇게 하면:

set -f; echo "*" *

...두 매개변수의 결과는 동일합니다. *이 경우에는 확장이 없습니다.

답변4

위에서 언급한 모든 보안 관련 사항을 고려하고 확장한 변수를 신뢰하고 제어한다고 가정하면 을 사용해도 됩니다 eval.

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory

관련 정보