The linux command line - 36. exotica
36. Exotica
책의 마지막 장에서는 자주 사용하지는 않지만 특정 상황에서 도움이 되는 bash 명령어들을 살펴볼 것.
Group commands and subshells
아래 문법으로 사용한다.
# group command
{ command1; command2; [command3; ...] }
# subshell
(command1; command2; [command3;...])
둘 다 redirection을 관리하는데서 사용된다. 여러 명령에서 redirection을 수행하는 스크립트를 생각해보자.
ls -l > output.txt
echo "foo.txt" >> output.txt
cat foo.txt >> output.txt
위 명령어는 각 명령어의 output들을 output.txt
에 적는 간단한 명령어들이다. 이를 group command로 적으면 다음과 같다.
{ ls -l; echo "Listing of foo.txt"; cat foo.txt; } > output.txt
subshell로는 아래와 같이 적는다.
(ls -l; echo "Listing of foo.txt"; cat foo.txt) > output.txt
이 기술로 한 줄로 명령어를 만들었지만, 실제로 group command와 subshell은 파이프라인에서 잘 활용된다.
command pipeline을 구성할 때 여러 명령의 결과를 단일 stream으로 결합하는 경우에 이를 쉽게 수행할 수 있다.
아래는 이를 활용한 예시 스크립트이다. array-2
라는 스크립트를 만들어서 디렉토리 이름을 받아 해당 파일 소유자, 그룹 소유자의 이름과 함께
목록의 끝에서 각 소유자/그룹에 속하는 파일 수를 집계한다.
[me@linuxbox ~]$ array-2 /usr/bin
/usr/bin/2to3-2.6 root root
/usr/bin/2to3 root root
/usr/bin/a2p root root
/usr/bin/abrowser root root
/usr/bin/aconnect root root
/usr/bin/acpi_fakekey root root
/usr/bin/acpi_listen root root
/usr/bin/add-apt-repository root root
--snip--
/usr/bin/zipgrep root root
/usr/bin/zipinfo root root
/usr/bin/zipnote root root
/usr/bin/zip root root
/usr/bin/zipsplit root root
/usr/bin/zjsdecode root root
/usr/bin/zsoelim root root
File owners:
daemon : 1 file(s)
root : 1394 file(s)
File group owners:
crontab : 1 file(s)
daemon : 1 file(s)
lpadmin : 1 file(s)
mail : 4 file(s)
mlocate : 1 file(s)
root : 1380 file(s)
shadow : 2 file(s)
ssh : 1 file(s)
tty : 2 file(s)
utmp : 2 file(s)
아래는 실제 스크립트.
1 #!/bin/bash
2
3 # array-2: Use arrays to tally file owners
4
5 declare -A files file_group file_owner groups owners
6
7 if [[ ! -d "$1" ]]; then
8 echo "Usage: array-2 dir" >&2
9 exit 1
10 fi
11
12 for i in "$1"/*; do
13 owner="$(stat -c %U "$i")"
14 group="$(stat -c %G "$i")"
15 files["$i"]="$i"
16 file_owner["$i"]="$owner"
17 file_group["$i"]="$group"
18 ((++owners[$owner]))
19 ((++groups[$group]))
20 done
21
22 # List the collected files
23 { for i in "${files[@]}"; do
24 printf "%-40s %-10s %-10s\n" \
25 "$i" "${file_owner["$i"]}" "${file_group["$i"]}"
26 done } | sort
27 echo
28
29 # List owners
30 echo "File owners:"
31 { for i in "${!owners[@]}"; do
32 printf "%-10s: %5d file(s)\n" "$i" "${owners["$i"]}"
33 done } | sort
34 echo
35
36 # List groups
37 echo "File group owners:"
38 { for i in "${!groups[@]}"; do
39 printf "%-10s: %5d file(s)\n" "$i" "${groups["$i"]}"
40 done } | sort
각 스크립트를 살펴보자.
- 5번쨰 줄: declare를 사용해서 이후에 사용할 array를 만듦
- 7-10번째 줄: 유효한 디렉토리 명인지 검증.
- 12-20번째 줄: 디렉토리의 파일을 반복하면서 stat 명령어로 필요한 정보를 각각의 배열에 추가
- 22-27번째 줄: 파일 목록을 출력함. group command를 사용해서 전체 출력을 pipeline으로
sort
할 수 있음. - 29-40번째 줄: owner/group 목록을 출력. 위와 흡사하게 사용
process substitution
group command와 subshell은 거의 동일해 보이지만 중요한 차이점이 있다.
- group command 은 현재 쉘에서 모든 명령을 실행하는 반면
- subshell은 현재 shell의 자식 복사본에서 명령을 실행: 환경이 복사되어 쉘의 새 인스턴스에 제공
- subshell이 종료되면 환경에 대한 변경 사항 (변수 할당 등)이 모두 손실
그래서 subshell이 꼭 필요한 경우가 아니라면 group command가 더 선호됨. 실제로 group command가 더 적은 메모리를 필요로 한다.
이전 장에서 subshell의 문제를 다뤘는데 이를 보여주는 간단한 스크립트
echo "foo" | read
echo $REPLY
REPLY
변수는 read pipeline이 subshell에서 실행되고, subshell이 종료될 때 변수 할당이 파괴되기 때문에 항상 비어있게 됨.
pipeline은 항상 subshell에서 실행되기 때문에 변수를 할당하는 모든 명령에서 이 문제가 발생.
이를 해결하기 위해 shell에서 process substitution
이라는 확장 형식을 제공함.
이는 두 가지 방식으로 표현된다.
표준 출력을 생성하는 프로세스의 경우
<(list)
표준 입력을 받는 프로세스의 경우
>(list)
위 read pipeline 스크립트의 문제를 해결하기 위해서 process substitution을 사용해 보자.
read < <(echo "foo")
echo $REPLY
process substitution을 사용하면 redirection을 위헤 subshell의 출력을 일반 파일로 취급할 수 있다.
이를 실제로 확인해 보면 특정 파일이 subshell의 출력을 제공하고 있는걸 볼 수 있음.
[me@linuxbox ~]$ echo <(echo "foo")
/dev/fd/63
Trap
이전에 프로그램이 신호에 응답할 수 있는 기능을 추가할 수 있었다. 크고 복잡한 스크립트는 신호 처리 루틴을 사용하는 것이 좋다.
스크립트가 실행되는 동안 사용자가 로그오프 하거나 컴퓨터가 종료되는 등의 이벤트가 발생하면, 영향을 받는 프로세스에 신호가 전송되고 이에 대한 조치를 실행시킬 수 있음.
예를 들어 실행중에 임시 파일을 생성하는 스크립트가 있으면, 스크립트 실행 중 특정 이벤트 신호가 수신되면 파일을 삭제하게 할 수 있다.
bash는 이를 위해 trap
을 제공한다.
trap argument signal [signal...]
간단한 예시
#!/bin/bash
# trap-demo: simple signal handling demo
trap "echo 'I am ignoring you.'" SIGINT SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
이 스크립트는 5초마다 echo 명령어가 실행되는 간단한 스크립트. SIGINT
나 SIGTERM
이 수신될 때마다 I am ignoring you
메세지를 출력하게 한다.
실제로 스크립트를 실행시키고 Ctrl-C
를 눌러 스크립트를 중지하려고 할 때 마다 해당 메세지를 볼 수 있다.
$ ./trap-demo.sh
Iteration 1 of 5
Iteration 2 of 5
d^CI am ignoring you.
Iteration 3 of 5
^CI am ignoring you.
Iteration 4 of 5
^CI am ignoring you.
Iteration 5 of 5
이를 문자열로 적ㅇ 때문에 보통은 shell function으로 만들어서 사용한다.
#!/bin/bash
# trap-demo2: simple signal handling demo
exit_on_signal_SIGINT () {
echo "Script interrupted." 2>&1
exit 0
}
exit_on_signal_SIGTERM () {
echo "Script terminated." 2>&1
exit 0
}
trap exit_on_signal_SIGINT SIGINT
trap exit_on_signal_SIGTERM SIGTERM
for i in {1..5}; do
echo "Iteration $i of 5"
sleep 5
done
Asynchronous execution with wait
가끔은 동시에 둘 이상의 작업을 수행하는 것이 좋을 때가 있다.
스크립트를 멀티태스킹 방식으로 작동하게 할 수 있는데, bash에서는 wait
명령어로 지정된 프로세스 (하위 스크립트)가 완료 될 때까지 부모 스크립트를 일시 정지 시킬 수 있다.
이를 위해서는 부모 스크립트와 자식 스크립트가 필요한다.
아래는 부모 스크립트
#!/bin/bash
# async-parent: Asynchronous execution demo (parent)
echo "Parent: starting..."
echo "Parent: launching child script..."
async-child &
pid=$!
echo "Parent: child (PID= $pid) launched."
echo "Parent: continuing..."
sleep 2
echo "Parent: pausing to wait for child to finish..."
wait "$pid"
echo "Parent: child is finished. Continuing..."
echo "Parent: parent is done. Exiting."
다음은 자식 스크립트 예시
#!/bin/bash
# async-child: Asynchronous execution demo (child)
echo "Child: child is running..."
sleep 5
echo "Child: child is done. Exiting."
위 예시에서 볼 수 있듯이 자식 스크립트는 아무런 역할을 하지 않고 실제 작업은 부모 스크립트가 진행한다.
상위 스크립트는 하위 스크립트의 PID를 사용하여 wait
명령을 싱행하고, 하위 스크립트가 종료될 때 까지 상위 스크립트가 일시 중지된다.
[me@linuxbox ~]$ async-parent
Parent: starting...
Parent: launching child script...
Parent: child (PID= 6741) launched.
Parent: continuing...
Child: child is running...
Parent: pausing to wait for child to finish...
Child: child is done. Exiting.
Parent: child is finished. Continuing...
Parent: parent is done. Exiting.