var=$( 뭐가 문제야?

var=$( 뭐가 문제야?

우리는 최근에 이것을 사용한 몇 가지 게시물을 보았습니다.

var=$(</dev/stdin)

쉘의 표준 입력을 변수로 읽어보십시오.

그러나 적어도 Linux 기반 시스템과 Cygwin에서는 이것이 올바른 접근 방식이 아닙니다.

왜? 올바른 방법은 무엇입니까?

답변1

(아니, 이번에는 그것과 관련이 있어주변에 따옴표가 없습니다.$(...)1).

$(<file)운영자

Korn 쉘 연산자( zsh및 에서도 지원됨 bash)는 다음에 자세히 설명되어 있습니다.Bash의 파일 읽기 명령 대체 이해.

간단히 말해서, 이는 $(cat < file)파일 읽기가 cat실행이 필요하지 않고 셸에서 내부적으로 수행된다는 점과 를 제외 bash하거나 추가 프로세스²가 필요하다는 점을 제외하면 기능적으로 동일합니다².

에서는 실제로 내장²과 동일 bash합니다 .$(cat < file)catcat

bash다른 제한 사항도 있습니다. 이는 stdin 입력 파일 리디렉션에만 적용되며 $(<&3)또는 와 같은 다른 형태의 리디렉션에는 적용되지 않습니다 $(<<<foo).

/dev/표준 입력

/dev/stdin, 프로세스의 파일 설명자를 이름으로 참조할 수 있도록 1980년대 다양한 Unices에 추가된 특수 파일입니다 ./dev/stdout/dev/stderr/dev/fd/x

이러한 Unices에서 /dev/stdin(문자 장치 파일)을 열면 stdin(fd 0)과 중복되는 파일 설명자가 생성되므로 3를 실행하는 것과 동일합니다 dup(0).

1990년대 Linux에 유사한 기능이 추가되었을 때 구현 방식은 상당히 달랐고 호환되지 않았습니다.

Linux에서 이러한 파일 /dev/std.../dev/fd/x특수 문자 장치 파일이 아니라 에 대한 기호 링크입니다 /proc/self/fd/x.마법의 심볼릭 링크fd에서 열린 파일에엑스.

그래서 /dev/stdin다른 것으로 열어보세요. dup(0)그렇게 할 수 있는 권한이 있다고 가정하면 처음부터 시작하여(현재 stdin이 가리키는 파일 내의 오프셋이 아님) 요청된 모드로 원본 파일을 다시 엽니다. 이는 또한 fd 0과 독립적인 fd에서 읽기/쓰기/탐색을 수행하는 경우 파일의 stdin 오프셋이 업데이트되지 않음을 의미합니다.

Cygwin은 Linux의 접근 방식을 복사하여 2000년대에 유사한 기능을 추가했습니다. 전부는 아니지만 대부분의 다른 Unices는 원시 방식으로 실행됩니다( /dev/fd/x완전히 지원하는 경우).

그렇다면 왜 잘못됩니까?

Linux와 Cygwin에서는 stdin에서 직접 읽는 것이 아니라 결과 파일 설명자에서 읽기 위해 $(</dev/stdin)열고 /dev/stdin읽는 것이 동일하지 않기 때문에 쉽게 올바른 내용을 읽지 못하거나 전혀 읽지 못하고 내용을 알려주지 못할 수 있습니다. 표준 입력을 읽은 나머지 스크립트.

다음 예를 고려하십시오.

$ cat wrong
#! /bin/bash -
var=$(</dev/stdin)
printf 'I got: "%s"\n' "$var"
printf "This is how many bytes are left to read on stdin: "
wc -c
$ cat right
#! /bin/bash -
var=$(cat)
printf 'I got: "%s"\n' "$var"
printf "This is how many bytes are left to read on stdin: "
wc -c
$ cat file
1
2
3
4
5
$
$ ./wrong < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 10
$ ./right < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0

이 경우에도 wrong모든 stdin 라인을 읽는 것처럼 보이지만 실제로는 그것을 소비하는 것처럼 보이지는 않습니다. wc -c여전히 10바이트를 읽을 수 있습니다.

$ { read var; ./wrong; } < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 8
$ { read var; ./right } < file
I got: "2
3
4
5"
This is how many bytes are left to read on stdin: 0

스크립트의 표준 입력이 첫 번째 줄을 초과할 때 호출되더라도 wrong첫 번째 줄을 가져오는 방법을 알아보세요 .file

$ socat -u file:file exec:./wrong
./wrong: line 2: /dev/stdin: No such device or address
I got: ""
This is how many bytes are left to read on stdin: 10
$ socat -u file:file exec:./right
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0

wrong/dev/stdin소켓이기 때문에 열 수 없으며 열 수 없습니다.열려 있는()콘센트.

$ chmod 600 file
$ sudo -u other_user ./wrong < file
./wrong: line 2: /dev/stdin: Permission denied
I got: ""
This is how many bytes are left to read on stdin: 10
$ sudo -u other_user ./right < file
I got: "1
2
3
4
5"
This is how many bytes are left to read on stdin: 0

right내가 열었던 fd 0에서 읽는 중이지만 다음 과 같이 wrong다시 열려고 합니다 .file다른 사용자누가 이런 일을 할 권리가 없습니다.

Linux/Cygwin에서는 파이프 및 tty와 같은 일부 문자 장치와 같이 열려 있고(소켓이 아니며 읽기 권한이 있는) 검색할 수 없는 파일을 열 $(</dev/stdin)때와 같은 몇 가지 간단한 경우에만 작동합니다. /dev/stdin열 수 있는 권한이 있는 검색 가능한 파일의 시작 부분에서 stdin을 여는 것과 같은 일부 다른 경우에는 다음이 발생할 수 있습니다.나타나다작동하지만 입력을 사용할 수 없습니다.

올바른 방법

위에 표시된 대로:

var=$(cat)

올바른 방법입니다⁴. catfd 0(stdin)에서 읽고 파이프인 fd 1에 씁니다. 그리고 쉘은 다른 쪽 끝의 출력을 읽어서 채웁니다 $var.

cat이 작업을 수행하는 유일한 명령은 아니지만 가장 간단하고 옵션이 전달되지 않으면 입력을 텍스트로 해석하지도 수정하지도 않습니다.

ksh93 또는 zsh에서는 var=$(<&0)이 작업을 수행할 수 있지만( <&0no-op로, 하나 이상의 리디렉션이 필요함) ksh93에서는 zsh기본적으로 이 작업을 수행하므로 최적화가 아닙니다 var=$($NULLCMD <&0).$NULLCMDcat

텍스트 입력(텍스트에 NUL 문자가 포함되지 않음)의 경우 zsh또는 를 사용하여 bash다음을 수행할 수 있습니다.

{ ! IFS= read -rd '' var; } < file

read첫 번째 NUL 구분 기호를 읽고 구분 기호가 발견되면 성공을 반환합니다. 여기서는 구분 기호를 찾는 것을 원하지 않으므로 종료 상태를 무효화합니다. file이는 열 수 있지만 읽을 수 없는 경우 올바른 종료 상태를 얻을 수 없음 을 의미합니다 .

추가 고려 사항

명령어 대체( $(cat)) 및 $(<file)연산자 삭제모두입력의 후행 개행 문자입니다. 따라서 기술적으로, 이후에는 var=$(cat)전체 $var입력이 포함되지 않고 전체 입력에서 후행 개행 문자를 뺀 값이 포함됩니다.

전체 입력에 대해 다음을 수행할 수 있습니다.

var=$(cat; ret=$?; echo . && exit "$ret")
ret=$? var=${var%.}

(종료 상태는 cat그대로 유지됩니다 $ret).

zsh, 입력에 NUL 바이트가 있는 경우 $var다른 쉘이 이러한 바이트를 변수에 저장하는 것을 지원하지 않기 때문에 보존되지 않습니다.

$ printf 'a\0b' | ksh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <a>$
$ printf 'a\0b' | mksh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <ab>$
$ printf 'a\0b' | bash -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
bash: line 1: warning: command substitution: ignored null byte in input
Got: <ab>$
$ printf 'a\0b' | dash -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <ab>$
$ printf 'a\0b' | zsh -c 'var=$(cat); printf "Got: <%s>\n" "$var"' | sed -n l
Got: <a\000b>$

여기서 1은 $(...)목록 컨텍스트가 아닌 스칼라(배열 아님) 변수 할당에 사용되므로 확장 시 분할+glob이 발생하지 않습니다. 따라서 문제가 되지는 않지만 주위에 따옴표를 추가해도 $(<...)아무런 차이가 없습니다.

² 또 다른 차이점은 최신 버전의 zsh를 제외한 모든 버전에서는 읽기 오류가 자동으로 무시된다는 것입니다. var=$(</); echo "$? <$var>"예를 들어 오류는 보고되지 않지만 bash(ksh93 또는 mksh와 반대)는 0이 아닌 종료 상태를 반환합니다.

³ 최소한 fd가 열린 모드와 호환되는 모드에서 파일이 열리는 한. exec >/dev/stdin예를 들어 stdin(fd 0)은 읽기 전용 모드로 열면 일반적으로 작동하지 않습니다.

⁴ 표준이지만 $(<file)ksh/zsh/bash에서만 발견되며 /dev/stdin모든 Unices에서는 발견되지 않습니다.

관련 정보