read

read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

read 명령은 stdin 로부터 라인을 읽어들여서 IFS 에 따라 값을 분리한 다음 지정한 [name ...] 에 할당합니다. awk 의 용어를 빌려보면 라인은 record 에 해당하고 분리된 값은 field 에 해당하며 -d delim 옵션으로 지정하는 값은 RS (record seperator) , IFS 값은 FS (field seperator) 와 같은 의미가 됩니다. name 값을 주지 않으면 읽어들인 라인은 REPLY 변수에 할당됩니다.

[name ...] 에 값을 할당하는 방법

# name 이 하나면 단어 전체를 할당
$ read v1 <<< "1 2 3 4 5"
$ echo $v1
1 2 3 4 5

-------------------------------

# name 이 단어 개수보다 적을경우 마지막 name 에 나머지를 할당
$ read v1 v2 v3 <<< "1 2 3 4 5"
$ echo $v1
1
$ echo $v2
2
$ echo $v3
3 4 5

----------------------------------

$ read _ v2 _ v4 _ <<< "1 2 3 4 5"
$ echo $v2
2
$ echo $v4
4

read 명령은 IFS 값에 따라 단어를 분리 합니다.

$ IFS=':|@' read -r c1 c2 c3 c4 <<< "red:green|blue@white"
$ printf "c1: %s, c2: %s, c3: %s, c4: %s\n" "$c1" "$c2" "$c3" "$c4"
c1: red, c2: green, c3: blue, c4: white

---------------------------------------------------------------------------

datetime="2008:07:04 00:34:45"
IFS=': ' read -r year month day hour minute second <<< "$datetime"

파이프에 연결된 명령은 subshell 에서 실행되므로 다음과 같이 할 수 없습니다.

$ echo 1 2 3 4 5 | read v1 v2 v3
$ echo $v1 $v2 $v3
$
................................

# 명령 group 을 이용하면 값을 표시할 수 있습니다.
$ echo 1 2 3 4 5 | { read v1 v2 v3; echo $v1 $v2 $v3 ;}
1 2 3 4 5
-----------------------------------------------------------

# here document 를 이용하는 방법
$ read v1 v2 v3 <<END
$( echo 1 2 3 4 5 )
END

$ echo $v1 $v2 $v3
1 2 3 4 5

파일에서 라인을 읽어 들일때 라인의 앞,뒤 공백을 유지하려면 IFS 값을 NUL 로 설정합니다.

$ cat test.txt               # 라인 앞,뒤에 공백이 있다.
empty    space    
    empty    space

$ while read -r  line; do    # 라인 앞,뒤 공백이 없어진다.
    echo "X${line}X"
done < test.txt

Xempty    spaceX
Xempty    spaceX

# IFS 값을 NUL 로 설정해야 라인 앞,뒤 공백이 유지된다.
$ while IFS= read -r  line; do
    echo "X${line}X"
done < test.txt

Xempty    space    X
X    empty    spaceX

Options

  • -r : 읽어 들이는 데이터에서 \ 문자를 이용한 escape 을 disable 합니다 (raw read).
$ read v       # -r 옵션 미사용
xxx\t\nyyy     # 입력

$ echo "$v"
xxxtnyyy       # 출력
---------------------------------------

$ read -r v    # -r 옵션 사용
xxx\t\nyyy     # 입력

$ echo "$v"
xxx\t\nyyy     # 출력( 입력된 그대로 출력 )

$ echo -e "$v"
xxx
yyy
  • -d delim : 라인 구분자를 의미하며 기본적으로 newline 입니다.
# -d 값을 null 로 설정하면 파일 전체 라인을 읽어 들입니다.
$ read -r -d '' whole < datafile

$ echo "$whole"
20081010 1123 xxx
20081011 1234 def
20081012 0933 xyz
...
------------------------------------------------

# find 명령에서 -print0 을 이용해 출력했으므로 -d 값을 null 로 설정
find * -print0 | while read -r -d '' file; do
    echo "$file"
done
  • -a array : 단어를 분리해서 array 에 입력합니다.
$ IFS=, read -r -a arr <<< "1,2,3,4,5"

$ echo ${#arr[@]}
5
$ echo ${arr[1]}
2

# IFS 가 데이타 마지막에 올 경우 마지막 필드는 항목에서 제외됩니다.
$ IFS=, read -a arr <<< "1,2,3,4," 
$ echo ${#arr[@]}
4
  • -p prompt : 사용자에게 값을 입력받을때 prompt 를 설정할 수 있습니다.
    ( 프롬프트가 표시될 때는 stderr 로 출력됩니다. )

  • -e : 사용자에게 값을 입력받을때 readline 을 사용합니다.

  • -i text : -e 옵션과 같이 사용하며, 초기 입력값을 설정할 수 있습니다.

$ read -p "Enter the path to the file: " -ei "/usr/local/" reply
Enter the path to the file: /usr/local/bin

$ echo "$reply"
/usr/local/bin
  • -s : 사용자에게 값을 입력받을때 타입 한 값을 화면에 표시하지 않습니다.

  • -n nchars : nchars 만큼 문자를 읽어 들입니다. 중간에 라인 구분자를 만나면 중단합니다.

  • -N nchars : 라인 구분자를 상관하지 않고 무조건 nchars 만큼 읽어 들입니다.

$ read -n 8 v1 <<END
12345
6789  
END

# 중간에 라인구분자 newline 을 만나므로 5 까지만 표시됩니다.
$ echo "$v1"
12345
--------------------

$ read -N 8 v1 <<END
12345
6789
END

# newline 이 포함되므로 7 까지 표시됩니다.
$ echo "$v1"
12345
67
-----------------------------------------

asksure() {
    echo -n "Are you sure (Y/N)? "
    while read -n 1 answer; do
        echo
        case $answer in
            [Yy]) return 0 ;;
            [Nn]) return 1 ;;
        esac
    done
}
-----------------------------------------

pause() {
    read -s -n 1 -p "Press any key to continue..." 
}
  • -t timeout : 사용자에게 입력을 받을 때 timeout 값을 설정할 수 있습니다.

이 외에도 FD 를 named pipe 에 연결해 사용할 때 유용한 기능입니다. FD 를 파일에 연결해 사용할 경우 읽어들일 라인이 없으면 바로 리턴하고 오류 값이 반환되지만 named pipe 같은 경우는 더 이상 진행하지 못하고 block 됩니다. 이때 timeout 값을 설정하면 block 상태를 벗어날 수 있습니다.

timeout 값을 0 으로 설정하면 실제 라인을 읽어들이지 않습니다. 그러나 읽어들일 라인이 있을 경우는 0 을, 그 외는 오류를 반환하므로 읽어들일 라인이 있는지 없는지 테스트하는데 사용할 수 있습니다.

timeout 값은 소수로 입력할 수 있습니다.

$ mkfifo pipe

$ exec 3<> pipe

$ echo -e "111\n222" > pipe

$ read -r v <&3; echo status: $?, value: $v
status: 0, value: 111

$ read -r v <&3; echo status: $?, value: $v
status: 0, value: 222

# 읽어들일 라인이 없으므로 block 된다.
$ read -r v <&3; echo status: $?, value: $v
^C

--------------------------------------------
# 다음은 timeout 값을 0.1 초로 설정합니다.

$ echo -e "111\n222" > pipe

$ read -r -t.1  v <&3; echo status: $?, value: $v
status: 0, value: 111

$ read -r -t.1  v <&3; echo status: $?, value: $v
status: 0, value: 222

# 읽어들일 라인이 없을경우 0.1 초 후에 block 상태에서 리턴합니다.
$ read -r -t.1  v <&3; echo status: $?, value: $v
status: 142, value:

# timeout 값을 0 으로 설정하면 읽어들일 라인이 있는지 테스트할 수 있습니다.
$ read -r -t0  v <&3; echo status: $?
status: 1

sh 에서는 -t 옵션을 사용할 수 없으므로 다음과 같이 timeout 명령을 이용합니다.

# 10 초 동안 사용자로부터 입력을 받음
sh$ AA=$( timeout --foreground 10 sh -c 'read -r v; echo "$v"' )
hello     # 사용자 입력

sh$ echo "$AA"       
hello 
---------------------------------------------------------------

sh$ exec 3<> pipe
sh$ echo "111\n222" > pipe

sh$ AA=$( timeout --foreground .1 sh -c 'read -r v <&3; echo "$v"' ); echo status: $?, value: "$AA"
status: 0, value: 111

sh$ AA=$( timeout --foreground .1 sh -c 'read -r v <&3; echo "$v"' ); echo status: $?, value: "$AA"
status: 0, value: 222

sh$ AA=$( timeout --foreground .1 sh -c 'read -r v <&3; echo "$v"' ); echo status: $?, value: "$AA"
status: 124, value:
  • -u fd : stdin 대신에 file descriptor 로 부터 데이터를 읽어 들입니다.
exec 3< infile

while read -r -u3 line; do
    echo "$line"
done

exec 3>&-
............................

$ while read -r -u3 line; do echo "$line"; done 3<<END
> 111
> 222
> END
111
222

# sh 에서는 다음과 같이 하면 됩니다.
read -r line <&3

종료 상태 값

다음의 경우는 오류에 해당하고 0 이 아닌값을 리턴합니다.

  • end-of-file 상태를 만났을때
$ cat infile
111
222

$ exec 3< infile

$ read -r v <&3; echo status: $?, value: $v
status: 0, value: 111

$ read -r v <&3; echo status: $?, value: $v
status: 0, value: 222

# 읽어들일 라인이 없을 경우 종료상태값 1 을 리턴
$ read -r v <&3; echo status: $?, value: $v
status: 1, value:
  • read times out (이때는 128 이상의 값을 리턴합니다.)
  • 변수에 값을 할당할때 오류발생
  • -u 옵션에 사용된 유효하지 않은 FD (file descriptor)

여기서 첫 번째 경우 end-of-file 상태를 만났을 때를 살펴볼 필요가 있는데요. 파일 마지막 라인에 newline 이 없으면 value 값은 정상적으로 설정되는데도 종료 상태 값은 1 이 될 수 있습니다. 이것은 -d 옵션 값으로 null 을 사용할 때도 해당됩니다.

$ echo -en "111\n222" > file

$ od -a file
0000000   1   1   1  nl   2   2   2        # 마지막 라인에 newline 이 없다.
0000007

$ exec 3<> file

$ read -r v <&3; echo status: $?, value: "$v"              
status: 0, value: 111

# value 값은 정상적으로 설정되지만 종료 상태 값은 1 이 된다.
$ read -r v <&3; echo status: $?, value: "$v"              
status: 1, value: 222

Quiz

파일을 while 문으로 읽어들일 때 파일 끝에 newline 이 없으면 마지막 라인이 오류로 인식이 되어 value 값은 설정되지만 프린트가 되지 않습니다. 또한 기본적으로 각 라인의 앞, 뒤에 있는 공백이 제거되는데요. 어떻게 하면 파일을 원본 그대로 출력할 수 있을까요?

# 1. IFS='' 로 설정하여 라인의 앞, 뒤에 존재하는 공백을 유지합니다.
# 2. -r 옵션을 설정하여 '\' 문자의 escape 이 처리되지 않게 합니다.
# 3. || [ -n "$line" ] 을 추가하여 파일 끝에 newline 이 존재하지 않아
#    오류가 발생할시 처리될수 있게 합니다.

while IFS= read -r line || [ -n "$line" ]
do 
    echo "$line"
done < file.txt