여러분의 CS 교육에서 누락된 학기 - 쉘 스크립팅 연습문제

Hyuno Choi·2021년 8월 13일
post-thumbnail

이 포스팅은 MIT 공개 강의를 바탕으로 작성되었습니다. https://missing.csail.mit.edu/


2021년 8월 13일

이번 포스팅에서는 2주차 강의였던 <쉘 툴과 스트립팅> 연습문제를 풀어보는 시간을 갖도록 하겠습니다. 실습 환경은 다음과 같습니다.

  • 운영체제: macOS
  • 터미널: iTerm2
  • 쉘: zsh

1번 문제

$ man ls 명령으로 메뉴얼을 읽고 다음과 같은 방식으로 파일을 나열하는 ls 명령을 작성합니다.

  • 모든 파일 포함, 모든 숨겨진 파일 포함
  • 사이즈는 사람이 읽을 수 있을 법한 형식으로 (e.g. 454M instead of 454279954)
  • 최신순 파일 정렬
  • 색상화 되어 출력

문제에서 제시한 요구사항들은 모두 ls 명령어의 옵션으로 해결할 수 있을 것 같습니다. $ man ls 명령어를 통해 요구사항에 해당되는 옵션들을 찾아보겠습니다.

우선 첫 번째 요구사항은 "모든 (숨겨진) 파일 포함"입니다. 숨겨진 파일이라고 하면 파일명이 . 으로 시작하는 파일을 말합니다. 메뉴얼의 옵션 부분을 보면 -a 옵션이 여기에 해당하는 것을 알 수 있습니다.

-a      Include directory entries whose names begin with a dot (.).

두 번째 요구사항은 파일의 사이즈를 사람이 읽을 수 있는 단위로 출력하라는 것입니다. 파일 사이즈를 비롯한 자세한 정보는 자주 사용한 -l 옵션을 사용해서 출력할 수 있습니다.

문제는 사이즈의 단위인데요, 메뉴얼을 넘기다보면 -h 라는 옵션을 발견할 수 있습니다.

-h 옵션은 -l 옵션과 함께 사용하면 바이트, 킬로바이트, 메가바이트 등의 단위로 파일 사이즈를 출력한다고 합니다. $ ls -l -h 와 같이 사용할 수 있고, $ ls -lh 처럼 붙여서 쓸 수도 있습니다.

이제 파일 사이즈가 긴 숫자가 아니라 흔히 사용하는 단위로 표시됩니다.

다음은 최신순 파일 정렬입니다. 이 옵션을 찾기 전에 메뉴얼에서 옵션을 조금 더 쉽게 찾는 방법을 생각해봅시다. 예를 들어 지금 찾으려는 '최신순 파일 정렬' 옵션의 경우 설명에 sort 라는 단어가 포함되어 있을 확률이 높습니다. 따라서 명령 파이프라인을 사용해 grep 명령어로 검색을 해보겠습니다.

$ man ls | grep sort 명령어를 통해 man 명령어의 출력 중에 sort를 포함하고 있는 줄만 출력하도록 했습니다. 설명을 읽어보면 -t 옵션이 최신순 파일 정렬 기능을 하는 것을 알 수 있습니다.

마지막은 색상 출력입니다. 이 옵션 역시 아까와 똑같은 논리로 찾겠습니다. 색상 출력과 관련된 옵션이라면 설명에 반드시 color 가 포함되어 있을 것입니다.

첫 번째 줄에 원하는 옵션이 바로 나왔습니다. -G 옵션을 주면 색상 출력을 해준다고 합니다. 지금까지 찾았던 옵션들을 모두 사용하여 명령어를 만들면 이렇게 됩니다.

$ ls -a -l -h -t -G

출력 결과입니다. 숨겨진 파일이 보이고, 용량이 익숙한 단위로 보이고, 수정 시간 기준으로 파일이 정렬되어 있으며, 생상 출력을 지원합니다.

2번 문제

다음을 수행하는 bash 함수 marco 및 polo를 작성합니다. marco를 실행할 때마다 현재 작업 디렉토리가 어떤 방식으로 저장되어야합니다. 그러면 polo를 실행할 때 어떤 디렉토리에 있든 상관없이 polo가 cd를 수행해서 marco를 실행한 디렉토리로 돌아갑니다. 디버깅을 쉽게하기 위해 marco.sh 파일에 코드를 작성하고source marco.sh를 실행하여 쉘에 정의를 (재)로드 할 수 있습니다.

우선 marco.sh 프로그램부터 만들겠습니다. bash 쉘로 실행할 것이므로 env를 사용한 shebang을 맨 첫 줄에 작성해줍니다. 그리고 marco.sh 프로그램이 현재 실행중인 디렉토리는 다음과 같이 저장할 수 있을 것입니다.

#!/usr/bin/env bash

echo $(pwd) > ~/.marco_path

echo 명령의 인자로 pwd의 명령의 출력이 들어가게 됩니다. 그리고 에코 명령어의 출력 스트림을 홈 디렉토리의 .marco_path 파일로 변경합니다. 이 파일이 사용자에게 보여야 할 이유는 없으므로 . 으로 파일명을 시작해 숨김 파일로 만들어줍니다.

그리고 $ bash march.sh 와 같이 프로그램을 쉘의 인자로 주는 방식으로 실행하지 않고도 $ ./march.sh 처럼 바로 실행이 가능하게 실행 권한을 수정하겠습니다. $ ls -l | grep marco.sh 명령어를 통해 현재 marco.sh 의 권한을 살펴보면 실행권한이 없습니다.

$ chmod 744 marco.sh 명령어를 통해 파일 소유자에게 모든 권한을 부여합니다. 744의 뜻은 저번 포스팅에서 다루었습니다.

  • 7: 소유자에게 모든 권한을
  • 4: 그룹에게 읽기 권한을
  • 4: 기타 사용자에게 읽기 권한을

이제 $ ./marco.sh 명령어를 통해 프로그램을 실행하면 정상적으로 실행됩니다. 실행 이후 $ cat .marco_patah 를 통해 생성된 파일을 확인해보면 marco.sh를 실행한 디렉토리 경로가 저장되어 있습니다.

이제 polo.sh 프로그램을 만들겠습니다. polo.sh 는 홈 디렉토리에서 .marco_path 파일의 내용을 가져온 다음, 그 경로로 이동할 수 있어야 합니다.

#!/usr/bin/env bash

path=$(cat ~/.marco_path 2> /dev/null)

if [[ $? -ne 1 ]]
then
	cd "$path"
else
	echo "Failed to find .marco_path file."
fi

$SHELL

스크립트를 보면서 설명하겠습니다. 우선 환경 변수를 사용해 shebang을 만들어줍니다. 그리고 cat ~/.marco_path 명령어를 통해 홈 디렉토리의 .marco_path 파일에서 marco.sh가 실행된 디렉토리 경로를 얻어 path 변수에 저장합니다. 혹시 해당 파일이 존재하지 않을 경우 별도로 오류 메시지를 띄우기 위해 STDERR 출력을 터미널에 띄우지 않고 버리겠습니다. 2> /dev/null을 사용해 STDERR를 null 파일에 버립니다.

cd 명령을 실행하기 전에 경로를 잘 가져왔는지를 조건문으로 판단합니다. 특수변수인 $?에는 직전에 실행한 명령의 실행 결과가 담깁니다. 만약 이전 출력이 정상적으로 실행되었다면(STDOUT 이 1 이 아니라면) 받아온 경로로 cd 를 수행합니다. 만약 이전 명령이 제대로 실행되지 않았다면 에러메시지를 띄웁니다.

스크립트 마지막 줄에는 $SHELL이라는 특수변수를 사용했습니다. 기본적으로 쉘 스크립트는 현재 실행중인 환경의 자식 환경에서 실행됩니다. 즉, 스크립트에서 cd 명령어를 통해 작업 디렉토리를 변경해도 부모 환경의 디렉토리에는 전혀 영향을 미칠 수 없습니다. 프로세스가 끝나도 변경한 디렉토리를 유지하고 싶으면 스크립트 끝에 $SHELL 을 적어주면 됩니다.

polo.sh 스크립트도 직접 실행할 것이기 때문에 chmod 를 사용해 실행권한을 부여합니다. 이제 홈 디렉토리에서 marco.sh를 실행하고 다른 디렉토리에서 polo.sh 를 실행했을 때 홈 디렉토리로 돌아가는지 실험해보겠습니다.

(홈 디렉토리에 marco.sh 가 있다는 가정 하에)

  1. cd ~
  2. ./marco .sh
  3. cd ..
  4. ~/polo.sh

명령어를 순서대로 실행하면 마지막에 다시 홈 디렉토리로 돌아오는 것을 확인할 수 있습니다.

물론 marco.sh 를 어떤 디렉토리에서 실행해도 polo.sh를 실행하면 그 디렉토리로 이동합니다.

3번 문제

거의 실패하지 않는 명령이 있다고 가정 해보십시오. 그것을 디버그하려고 출력을 캡처해야하지만, 실패를 실행하는 데 시간이 오래 걸릴 수 있습니다. 실패 할 때까지 표준 출력 및 오류 스트림을 파일로 캡처하고 마지막에 앞의 모든 것을 출력하는 bash 스크립트를 작성하십시오. 스크립트가 실패하는 데 걸린 실행 횟수도 보고 할 수 있다면 보너스 포인트입니다.

'거의 실패하지 않는 명령' 스트립트:

 #!/usr/bin/env bash
 
 n=$(( RANDOM % 100 ))
 
 if [[ n -eq 42 ]]; then
    echo "Something went wrong"
    >&2 echo "The error was using magic numbers"
    exit 1
 fi
 
 echo "Everything went according to plan"

스크립트 작성에 앞서, '거의 실패하지 않는 명령'을 대신할 스크립트를 분석하겠습니다. 우선 n 변수에 0부터 99까지의 숫자가 랜덤으로 들어갑니다. bash 쉘에서 $RANDOM 은 0에서 32767까지의 숫자 중 하나를 랜덤으로 출력하는 함수입니다. 이 값에 모듈러 연산(%)을 사용해서 100으로 나눈 나머지 값을 구하면 0부터 99까지 범위의 값을 나오게 할 수 있습니다.

만약 n이 42와 같다면 오류로 판단합니다. 오류가 발생할 확률이 1/100인 셈입니다. -eq 는 지난 포스팅에서 정리한 것처럼 정수 비교에서 같다는 의미입니다. "Something went wrong"을 먼저 출력한 후, 에러 메시지를 STDERR로 출력합니다.

원래 echo "The error was using magic numbers"의 출력은 STDOUT입니다. 여기서 >&2를 사용해 STDERR로 출력을 리디렉트 합니다. 지난 번에도 다룬 적이 있는데, >&2 는 1>&2를 축약한 표현입니다. 즉, 1에 해당하는 STDOUT 스트림을 2에 해당하는 STDERR로 바꾸는 역할을 합니다. 중간의 &는 > 뒤에 오는 것이 파일명이 아니라 파일 디스크립터라는 것을 의미합니다. 출력을 마친 뒤에는 exit 1 을 사용해 에러로 스크립트를 종료합니다.

만약 n이 42가 아닌 다른 숫자라면 정상적으로 스크립트를 종료합니다.

위의 스크립트를 test.sh 라는 이름으로 저장했습니다. 이제 이 스크립트를 오류가 날 때까지 실행하는 tester.sh라는 스크립트를 만들겠습니다.

#!/usr/bin/env bash

n=0

for (( ; ; ))
do
    ((n++))
    ./test.sh >> log.txt 2>> log.txt
    if [[ $? -eq 1 ]]
    then
        echo "$n"
        break
    fi
done

스크립트를 보면서 설명하겠습니다. 우선 실행 횟수를 저장할 변수인 n을 초기화합니다. 그리고 오류가 날 때까지 실행할 것이므로 무한 반복문을 만들어줍니다. bash 스크립트에서 무한 반복문은 for (( ; ; )) 로 만들 수 있습니다.

반복문 안에서는 실행 횟수를 세기 위해 n에 1을 더합니다. 그리고 ./test.sh 를 통해 test.sh 스크립트를 실행합니다. 물론 tester.sh 와 같은 디렉토리 상에 있어야 실행이 가능합니다. test.sh를 실행한 후 STDOUT과 STDERR 모두 터미널 창 대신 log.txt 파일에 기록합니다. > 대신 >> 을 사용하면 파일을 덮어쓰는 대신 맨 마지막 줄에 추가할 수 있습니다.

그리고 test.sh 실행 결과를 조건문으로 확인합니다. 만약 이전 명령의 실행 결과가 1 이라면, 즉 오류라면 echo 명령을 통해 터미널 창에 실행 횟수를 표시하고 반복문을 탈출합니다. 만약 test.sh 스크립트가 정상적으로 종료되었다면 반복문을 다시 돌 것입니다.

이제 두 스크립트 모두 실행 권한을 부여합니다.

  • $ chmod 744 test.sh
  • $ chmod 744 tester.sh

이제 $ ./tester.sh 를 실행하면 test.sh 의 실행 횟수가 조금 있다가 출력됩니다.

139번째 실행에 오류가 발생했습니다. $ cat log.txt 명령으로 파일에 로그가 잘 찍혔는지 확인합니다.

성공 메시지와 마지막 오류 메시지가 잘 기록되었습니다.

4번 문제

강의에서 다루었듯이 find의 -exec는 검색하는 파일에 대한 작업을 수행하는데 매우 강력합니다. 그러나 zip 파일을 만드는 것과 같이 모든 파일로 작업을 수행하려면 어떻게 해야합니까? 지금까지 본 것처럼 명령은 인수와 STDIN 모두에서 입력을 받습니다. 명령을 타이핑 할 때 STDOUT을 STDIN에 연결하지만 ‘tar’와 같은 일부 명령은 인수에서 입력을받습니다. 이러한 문제를 해결하기 위해 STDIN을 인수로 사용하여 명령을 실행하는 xargs 명령이 있습니다. 예를 들어 ls | xargs rm은 현재 디렉토리의 파일을 삭제합니다.
당신의 임무는 폴더에서 모든 HTML 파일을 재귀적으로 찾아서 zip 파일을 만드는 명령을 작성하는 것입니다. 파일에 공백이 있어도 명령은 작동되어야 합니다. (hint: check -d flag for xargs)

명령어를 만들기 전에 xargs 의 기능에 대해 알아보겠습니다. $ cat test.py | grep import 라는 명령어를 실행하면 test.py 파일의 내용 중 import 를 포함하고 있는 줄만 출력합니다. 이런 파이프라인을 만드는 것이 가능한 이유는 grep 이 STDIN으로 입력을 받기 때문입니다. 이렇게 하면 cat 명령어의 STDOUT 이 grep 명령어의 STDIN 으로 연결됩니다.

그러나 $ fd test.py | rm 과 같은 명령어는 동작하지 않습니다. rm 명령어는 파일 이름을 인자를 통해서만 받기 때문입니다. 따라서 rm 과 같이 인자로만 입력을 받는 명령어를 사용해 파이프라인을 만들기 위해서는 이전 명령어의 STDOUT 을 인자로 연결해주는 무언가가 필요한데, 그것이 xargs 의 역할입니다. 위의 명령어를 $ fd test.py | xargs rm 과 같이 쓰면 정상적으로 test.py 파일이 지워집니다. xargs가 중간에서 fd 의 출력을 rm의 인자로 전달했기 때문입니다.

이제 xargs 를 사용해 문제에서 요구하는 명령어를 만들겠습니다. 우선 폴더에서 모든 html 파일을 찾아야 합니다. $ fd -e html 명령어로 확장자가 html 인 파일을 현재 폴더 및 현재 폴더의 자식 폴더에서 모두 찾겠습니다. fd 명령어는 따로 탐색 공간을 지정해주지 않으면 현재 디렉토리를 시작점으로 해서 모든 자식 디렉토리를 찾습니다.

두 번째는 찾은 html 파일들을 하나의 zip 파일로 만드는 것입니다. 리눅스에서는 $ zip [zip 파일 이름] [file1] [file2] ... 와 같이 zip 명령어를 사용해서 파일을 압축할 수 있습니다. 두 명령어를 xargs 로 이어주면 완성입니다.

$ fd -e html | xargs zip html.zip

명령어를 실행하면 압축 파일이 정상적으로 만들어집니다.

이렇게 2주차 강의의 연습문제를 풀었습니다. 다음 포스팅에서는 빔 편집기를 주제로 공부하겠습니다.📚


<참고 문서>

profile
프론트엔드 웹 개발자를 목표로 하고 있습니다.

0개의 댓글