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

# 프로세스 치환을 이용하는 방법
$ read rows cols < <(stty size)

$ echo $rows $cols
20 95

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

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

라인의 앞, 뒤 공백은 제거됩니다.

라인의 앞, 뒤 공백을 유지하려면 IFS 값을 null 로 설정합니다.

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

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

Xempty    spaceX
Xempty    spaceX

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

Xempty    space    X
X    empty    spaceX

Options

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

$ echo "$line"
xxxtnyyy                    # 출력

$ read line <<\EOF
1111111111\
2222222222
EOF

$ echo "$line"
11111111112222222222        # backslash-newline 에의해 하나의 라인이 된다.
-------------------------------------------------

$ read -r line <<< 'xxx\t\nyyy'    # -r 옵션 사용

$ echo "$line"
xxx\t\nyyy                  # 입력된 그대로 출력

$ read -r line <<\EOF
1111111111\
2222222222
EOF

$ echo "$line"              # 입력된 그대로 첫번째 라인만 출력
1111111111\
  • -d delim : 라인 구분자를 의미하며 기본적으로 newline 입니다.
# find 명령에서 -print0 을 이용해 출력했으므로 -d 값을 null 로 설정
find * -print0 | while read -r -d '' name; do
    echo "$name"
done

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

# -d 값을 null 로 설정하면 파일 전체 라인을 읽어 들입니다.
# -d '' 는 실제 -d $'\0' 와 같고 -d 와 '' 사이에 공백이 있어야 됩니다.
# ( -d '' 는 두 개의 인수가 되지만 -d'' 일 경우는 quotes 이 제거되고 나면 -d 하나의 인수가 되므로 )
$ read -r -d '' whole < datafile

$ echo "$whole"
20081010 1123 xxx
20081011 1234 def
20081012 0933 xyz
...
$ echo $?            # $'\0' 값을 만나지 못했으므로
1
  • -a array : 원소를 분리해서 array 에 입력합니다.
$ IFS=, read -a arr <<< "100,200,300,400,500"

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

# IFS 가 데이타 마지막에 올 경우 마지막 필드는 항목에서 제외됩니다.
$ IFS=, read -a arr <<< "100,200,300,400," 
$ echo ${#arr[@]}
4

# -d '' 이므로 전체 라인을 읽어들이고 기본 IFS 값에 따라 원소가 분리됩니다.
$ read -d '' -a arr < <( seq 100 100 900 )
$ echo ${#arr[@]}
9
$ echo ${arr[1]}
200
  • -p prompt : 사용자에게 값을 입력받을 때 prompt 를 설정할 수 있습니다.
    1 . 프롬프트가 표시될 때는 stderr 로 출력됩니다.
    2 . 파이프나 파일로부터 읽어들이기를 하여 stdin 이 터미널이 아닐 경우는 표시되지 않습니다.

  • -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 "exit: $?, value: $v"
exit: 0, value: 111

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

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

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

$ echo -e "111\n222" > pipe

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

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

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

# timeout 값을 0 으로 설정하면 읽어들일 라인이 있는지 테스트할 수 있습니다.
$ read -t 0  v <&3; echo "exit: $?"
exit: 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 "exit: $?, value: $AA"
exit: 0, value: 111

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

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

# 이것은 read -t 0 와 같은 효과로 stdin 에서 읽기에 성공했을 경우 if 문에 진입합니다.
if AA=$( dd iflag=nonblock 2> /dev/null ); then
    . . .
fi
  • -u fd : stdin 대신에 file descriptor 로 부터 데이터를 읽어 들입니다.
# item 은 FD 3번 으로부터 읽어 들이고, 사용자 입력은 stdin 으로부터 읽어들인다.
while read -u 3 item1 item2 item3       # fd 3
do
    . . .
    read -p "choose wisely: " choice    # stdin
    . . .
done 3< items.txt
......................................................

# sh 에서는 다음과 같이 하면 됩니다.
while read <&3 item1 item2 item3
do
    . . .
    read -p "choose wisely: " choice
    . . .
done 3< items.txt
......................................................

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

binary 파일은 다룰수 없다

$ stat -c %s /bin/date
108920

$ read -r -N 108920 whole < /bin/date

$ printf %s "$whole" > tmp

$ ls -l tmp
-rw-rw-r-- 1 mug896 mug896 232 2020-04-13 19:40 tmp
....................................................

$ dd bs=108920 count=1 status=none < /bin/date > tmp

$ ls -l tmp
-rw-rw-r-- 1 mug896 mug896 108920 2020-04-13 19:42 tmp

$ cmp /bin/date tmp

$ echo $?
0

종료 상태 값

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

  • read times out ( 이때는 128 이상의 값을 리턴합니다. )

  • 변수에 값을 할당할 때 오류 발생

  • -u 옵션에 사용된 유효하지 않은 FD

  • end-of-file 상태를 만났을 때

$ cat infile
111
222

$ exec 3< infile

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

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

# 읽어들일 라인이 없을 경우 종료상태값 1 을 리턴 (EOF)
$ read -r v <&3; echo "exit: $?, value: $v"
exit: 1, value:

파일 마지막에 newline 이 없으면 value 값은 정상적으로 설정되는데도 종료 상태 값은 1 이 될 수 있습니다. 따라서 while 문에서 사용된다면 마지막 라인은 출력되지 않게 됩니다.

$ 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 "exit: $?, value: $v"
exit: 0, value: 111

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

-d 옵션 값으로 null 을 사용해 파일 전체를 읽어들일 때도 종료 상태 값이 1 이 된다.

$ echo -en "111\n222\n" > file

$ od -a file
0000000   1   1   1  nl   2   2   2  nl       # 파일 마지막에 newline 이 있는데도
0000007                                       # -d '' 를 사용하면 종료 상태 값이 1 이 된다.

$ read -r -d '' v < file; echo "exit: $?, value: $v"  
exit: 1, value: 111
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

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

$ echo -en ' 111\n  222\n   333\n    444\n     555' > file

# 공백도 유지되지 않고 마지막 라인 555 도 표시되지 않는다.
$ while read -r line; do echo "$line"; done < file     
111
222
333
444

# IFS= 를 추가하여 출력에 공백이 유지된다.
$ while IFS= read -r line; do echo "$line"; done < file
 111
  222
   333
    444

# || [ -n "$line" ] 를 추가하여 마지막 라인 555 도 출력된다.
$ while IFS= read -r line || [ -n "$line" ]; do echo "$line"; done < file
 111
  222
   333
    444
     555     <--- 555

2 .

sleep 명령은 builtin 명령이 아니라 외부 명령인데요. 따라서 sleep 을 자주 반복해서 사용하게 될때는 스크립트 성능에 좋지않겠죠. 외부 명령을 사용하지 않고 sleep 을 구현하려면 어떻게 할까요?

read 명령의 -t (timeout) 옵션을 이용해 sleep 을 구현할 수 있습니다.

# stdin 이 아니라 stdout 에서 읽어들인다.
$ read -t 3 <&1       # 정상적으로 동작하는것 같지만

$ echo 111 | { read -t 3 <&1; cat ;} | cat       # 다음과 같은 경우에 문제가 있다.
bash: read: read error: 0: Bad file descriptor
111

$ echo 123 | { read -t 3 <&2; cat ;} | cat       # FD 를 stderr 로 변경하면 된다.  
123

# 기본적으로 timeout 이 되면 오류를 반환하므로 반복문에서 사용하려면
# 다음과 같이 종료 상태 값을 true 로 변경해 주어야 합니다.
$ while read -t .2 <&2 || true; do       
      echo $(( i++ ))
done
0
1
2
3
. . .
$ i=0
$ while read -t .2 <&2 || (( i < 5 )); do     # i < 5 까지만 출력
      echo $(( i++ ))
done
0
1
2
3
4

sleep 을 이용해 progress bar 를 출력

$ for ((i = 0; i <= 100; i++)); do
    read -t .02 <&2                        # .02 초 sleep
    ((elapsed = $i * 50 / 100))
    printf -v prog  "%${elapsed}s"
    printf -v total "%$((50-elapsed))s"
    printf '%s\r' "progress [${prog// /=}>${total}]"
done; echo

progress [==================================================>]