backend/ubuntu

The linux command line - 36. exotica

seul chan 2021. 8. 1. 19:01

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 명령어가 실행되는 간단한 스크립트. SIGINTSIGTERM이 수신될 때마다 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.