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
옵션에 사용된 유효하지 않은 FDend-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 [==================================================>]