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

Hyuno Choi·2021년 8월 13일
0
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 함수 marcopolo를 작성합니다. marco를 실행할 때마다 현재 작업 디렉토리가 어떤 방식으로 저장되어야합니다. 그러면 polo를 실행할 때 어떤 디렉토리에 있든 상관없이 polocd를 수행해서 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을 사용해 STDERRnull 파일에 버립니다.

cd 명령을 실행하기 전에 경로를 잘 가져왔는지를 조건문으로 판단합니다. 특수변수인 $?에는 직전에 실행한 명령의 실행 결과가 담깁니다. 만약 이전 출력이 정상적으로 실행되었다면(STDOUT1 이 아니라면) 받아온 경로로 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로 출력을 리디렉트 합니다. 지난 번에도 다룬 적이 있는데, >&21>&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를 실행한 후 STDOUTSTDERR 모두 터미널 창 대신 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 모두에서 입력을 받습니다. 명령을 타이핑 할 때 STDOUTSTDIN에 연결하지만 ‘tar’와 같은 일부 명령은 인수에서 입력을받습니다. 이러한 문제를 해결하기 위해 STDIN을 인수로 사용하여 명령을 실행하는 xargs 명령이 있습니다. 예를 들어 ls | xargs rm은 현재 디렉토리의 파일을 삭제합니다.
당신의 임무는 폴더에서 모든 HTML 파일을 재귀적으로 찾아서 zip 파일을 만드는 명령을 작성하는 것입니다. 파일에 공백이 있어도 명령은 작동되어야 합니다. (hint: check -d flag for xargs)

명령어를 만들기 전에 xargs 의 기능에 대해 알아보겠습니다. $ cat test.py | grep import 라는 명령어를 실행하면 test.py 파일의 내용 중 import 를 포함하고 있는 줄만 출력합니다. 이런 파이프라인을 만드는 것이 가능한 이유는 grepSTDIN으로 입력을 받기 때문입니다. 이렇게 하면 cat 명령어의 STDOUTgrep 명령어의 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개의 댓글