이 포스팅은 MIT 공개 강의를 바탕으로 작성되었습니다. https://missing.csail.mit.edu/
2021년 8월 13일
이번 포스팅에서는 2주차 강의였던 <쉘 툴과 스트립팅> 연습문제를 풀어보는 시간을 갖도록 하겠습니다. 실습 환경은 다음과 같습니다.
$ 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
출력 결과입니다. 숨겨진 파일이 보이고, 용량이 익숙한 단위로 보이고, 수정 시간 기준으로 파일이 정렬되어 있으며, 생상 출력을 지원합니다.
다음을 수행하는 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의 뜻은 저번 포스팅에서 다루었습니다.
이제 $ ./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
가 있다는 가정 하에)
cd ~
./marco .sh
cd ..
~/polo.sh
명령어를 순서대로 실행하면 마지막에 다시 홈 디렉토리로 돌아오는 것을 확인할 수 있습니다.
물론 marco.sh
를 어떤 디렉토리에서 실행해도 polo.sh
를 실행하면 그 디렉토리로 이동합니다.
거의 실패하지 않는 명령이 있다고 가정 해보십시오. 그것을 디버그하려고 출력을 캡처해야하지만, 실패를 실행하는 데 시간이 오래 걸릴 수 있습니다. 실패 할 때까지 표준 출력 및 오류 스트림을 파일로 캡처하고 마지막에 앞의 모든 것을 출력하는 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
명령으로 파일에 로그가 잘 찍혔는지 확인합니다.
성공 메시지와 마지막 오류 메시지가 잘 기록되었습니다.
강의에서 다루었듯이
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주차 강의의 연습문제를 풀었습니다. 다음 포스팅에서는 빔 편집기를 주제로 공부하겠습니다.📚
<참고 문서>