The Linux Command Line: 28. Reading keyboard input - 2. IFS, validation, menu
IFS
보통 shell은 read의 input을 쪼개서 전달한다.
이는 IFS
(Internal Field Seperator)라는 shell variable로 설정된다.
IFS는 기본값은 스페이스, 탭, newline characher이다.
이를 변경하여 read의 input을 나누는 필드를 변경할 수 있다.
예를들면 /etc/passwd
파일은 colon (:
)을 필드 seperator로 사용한다. IFS를 single colon으로 변경하여 /etc/passwd
의 내용을 read로 전달할 수 있다.
아래는 이를 구현한 스크립트.
#!/bin/bash
# read-ifs: read fields from a file
FILE=/etc/passwd
read -p "Enter a username > " user_name
file_info="$(grep "^$user_name:" $FILE)"
if [ -n "$file_info" ]; then
IFS=":" read user pw uid gid name home shell <<< "$file_info"
echo "User = '$user'"
echo "UID = '$uid'"
echo "GID = '$gid'"
echo "Full Name = '$name'"
echo "Home Dir. = '$home'"
echo "Shell = '$shell'"
else
echo "No such user '$user_name'" >&2
exit 1
fi
file_info를 구하는 줄은 grep 명령 결과를 변수에 할당함.
IFS를 정의하는 줄에서는 변수할당, 읽기 명령, 리디렉션 연산자 세 부분으로 구성됨.
변수 할당
쉘은 하나 이상의 변수 할당이 명령 직전에 발생할 수 있게 해줌.
이는 일시적으로 명령이 실행되는 동안에만 환경이 변경되 게 함. 위 예시에서는 IFS 값을 콜론으로 변경.
한 줄이 아니라 아래처럼 해도 무방함
OLD_IFS = "$IFS"
IFS = ":"
read user pw uid gid name home shell <<< "$file_info"
IFS="$OLD_IFS"
<<<
operator는 here string
임. here string은 앞서 다룬 here document와 비슷하지만 single string만으로 구성되어있다.
위 예시에서는 /etc/passwd
파일이 read command의 표준 입력으로 제공.
굳이 here string을 사용한 이유는 read 명령어는 pipe로 사용할 수 없기 때문.
echo "foo" | read
위 명령어를 실행시켜보면 read가 pipe의 input을 받지 못해 실제로 아무것도 나오지 않는 것을 볼 수 있다.
이유는 쉘이 파이프라인을 처리하는 방식과 관련있는데, bash나 sh 등의 쉘에서 파이프라인은 subshell
을 생성하기 때문.
unix-like 시스템에서 subshell은 실행중인 프로세스의 환경의 복사본을 만들어주고, 프로세스가 종료되면 이 환경의 복사본은 파괴됨. 이는 subshell이 부모 프로세스의 환경을 변경할 수 없음을 의미함.
Validating Input
아래는 input을 validate 해주는 예시. 지금까지 다룬 쉘 함수, [[ ]]
, (( ))
, control operator (&&
), if 등을 모두 다룬 예시이기 때문에 한 번 따라 작성해보면 도움이 많이 된다.
#!/bin/bash
# read-validate: validate input
invalid_input () {
echo "Invalid input '$REPLY'" >&2
exit 1
}
read -p "Enter a single item > "
# input is empty (invalid)
[[ -z "$REPLY" ]] && invalid_input
# input is multiple items (invalid)
(( "$(echo "$REPLY" | wc -w)" > 1 )) && invalid_input
# is input a valid filename?
if [[ "$REPLY" =~ ^[-[:alnum:]\._]+$ ]]; then
echo "'$REPLY' is a valid filename."
if [[ -e "$REPLY" ]]; then
echo "And file '$REPLY' exists."
else
echo "However, file '$REPLY' does not exist."
fi
# is input a floating point number?
if [[ "$REPLY" =~ ^-?[[:digit:]]*\.[[:digit:]]+$ ]]; then
echo "'$REPLY' is a floating point number."
else
echo "'$REPLY' is not a floating point number."
fi
# is input an integer?
if [[ "$REPLY" =~ ^-?[[:digit:]]+$ ]]; then
echo "'$REPLY' is an integer."
else
echo "'$REPLY' is not an integer."
fi
else
echo "The string '$REPLY' is not a valid filename."
fi
Menu
menu-driven은 interactivity의 흔한 방식으로, 사용자는 여러 선택지중에 하나를 고를 수 있다.
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
Enter selection [0-3] >
이전에 만든 sys_info_page
프로그램에 이 방식을 도입해서 menu-driven program으로 만들어보자.
#!/bin/bash
# read-menu: a menu driven system information program
clear
echo "
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
"
read -p "Enter selection [0-3] > "
if [[ "$REPLY" =~ ^[0-3]$ ]]; then
if [[ "$REPLY" == 0 ]]; then
echo "Program terminated."
exit
fi
if [[ "$REPLY" == 1 ]]; then
echo "Hostname: $HOSTNAME"
uptime
exit
fi
if [[ "$REPLY" == 2 ]]; then
df -h
exit
fi
if [[ "$REPLY" == 3 ]]; then
if [[ "$(id -u)" -eq 0 ]]; then
echo "Home Space Utilization (All Users)"
du -sh /home/*
else
echo "Home Space Utilization ($USER)"
du -sh "$HOME"
fi
exit
fi
else
echo "Invalid entry." >&2
exit 1
fi
이를 실행시키면 user는 menu를 입력할 수 있게 되고, 입력받은 번호대로 스크립트가 실행됨.
$ ./read_menu
Please Select:
1. Display System Information
2. Display Disk Space
3. Display Home Space Utilization
0. Quit
Enter selection [0-3] > 1
Hostname: xxxx
22:42 up 4 days, 13:03, 3 users, load averages: 3.99 4.19 4.10