여러분의 CS 교육에서 누락된 학기 - 셸 스크립팅 1부

Hyuno Choi·2021년 7월 10일
0
post-thumbnail

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


2021년 7월 10일

첫 번째 챕터에서는 기본적인 셸 명령에 대해 알아보고 데이터를 간단한 파이프로 처리하는 시간을 가졌습니다. 두 번째 챕터에서는 셸 스크립팅에 대해 공부해보겠습니다.

스크립트📝

셸 스크립팅은 지금까지 셸에게 내렸던 개별 명령들을 묶어 큰 단위로 만든 것입니다. 예를 들어,

  1. 특정 디렉토리를 찾고
  2. 그 디렉토리로 이동하고
  3. 새 파일을 만들기

를 수행하는 세 명령을 순서대로 실행하는 스크립트를 짤 수 있습니다. 다른 프로그래밍 언어와 마찬가지로 셸 스크립팅에서도 제어 구문과 변수 할당을 사용할 수 있습니다.

실습 환경은 지난번과 동일합니다.

  • OS: MacOS
  • Terminal: iTerm
  • Shell: bash

변수

변수 할당

bash에서 변수에 값을 할당하는 방법은 다음과 같습니다.

variable=A

variable = A처럼 공백을 넣으면 안 됩니다. 다른 프로그래밍 언어에서는 가독성을 위해 공백을 사용하는 경우도 있지만, 셸에서 공백은 인수분할의 의미를 가집니다.

저장된 변수의 값을 불러오고 싶다면 $변수명으로 값에 접근합니다.

bash-3.2$ variable=1
bash-3.2$ echo $variable
1

'와 "의 차이

bash에서 문자열을 감싸는 '"의 기능은 명확히 구분됩니다. '로 둘러싸인 문자열은 문자열 자체를 뜻하고, "로 둘러싸인 문자열은 해당 변수값을 반환합니다.

bash-3.2$ echo '$varibale'
$varibale
bash-3.2$ echo "$variable"
1

echo의 출력 결과가 다른 것을 볼 수 있습니다.

특수 변수

bash에서는 셸 작업에 특화된 특수한 변수를 사용합니다.

$0

$0는 스크립트의 이름을 나타냅니다. 예를 들어 test.sh라는 파일에 밑의 코드를 작성해보겠습니다.

#!/bin/bash
echo "$0"

이 스크립트의 실행 결과는 다음과 같습니다.

bash-3.2$ ./test.sh
./test.sh

스크립트의 이름인 ./test.sh가 출력된 것을 볼 수 있습니다.

$1 ~ $9

$1부터 $9는 1번부터 9번까지의 인자를 나타냅니다. test.sh파일의 내용은 다음과 같습니다.

#!/bin/bash
echo "$1 $2 $3 $4 $5"

이 프로그램을 5개의 인자와 함께 실행해보겠습니다.

bash-3.2$ ./test.sh hello im fine thank you
hello im fine thank you

test.sh를 실행하면서 hello, im, fine, thank, you 다섯 개의 인수를 주었고, 각각의 변수 자리에서 출력되었습니다.

$@

$@는 인자로 전달받은 모든 값을 나타냅니다.

#!/bin/bash
echo "$@"

다음 파일을 아까와 같은 명령으로 출력해도 같은 결과가 나옵니다.

bash-3.2$ ./test.sh hello im fine thank you
hello im fine thank you

$#

$#는 인자의 개수를 나타냅니다.

#!/bin/bash
echo "$#"

이 파일을 역시 아까와 같은 명령으로 실행해보겠습니다.

bash-3.2$ ./test.sh hello im fine thank you
5

인자의 개수인 5를 출력합니다.

$?

$?는 이전 명령의 STDOUT 값을 나타냅니다. 여기서 STDOUT이란 standard output을 의미합니다. 이 값은 일반적으로 프로그램이 정상적으로 종료되었다면 0이며, 오류가 발생하면 0 이외의 값을 갖습니다.

bash-3.2$ cat test
Hello world
bash-3.2$ echo "$?"
0

cat 프로그램이 정상 작동하였고, STDOUT 값은 0입니다.

bash-3.2$ cat none
cat: none: No such file or directory
bash-3.2$ echo "$?"
1

이번에는 cat에게 없는 파일을 인자로 주었습니다. STDOUT 값이 0이 아닌 것을 볼 수 있습니다.

$$

$$는 현재 스크립트의 프로세스 식별 번호 값을 가집니다. 프로세스 식별 번호(PID)란 운영체제 상에서 동작하고 있는 프로세스들을 구분하기 위해 붙여진 고유한 번호입니다.

bash-3.2$ echo "$$"
2185

이 프로세스의 PID는 2185인 것을 확인할 수 있습니다.

!!

!!는 직전에 실행한 명령어를 인수까지 포함하여 불러옵니다.

bash-3.2$ echo 'Hello'
Hello
bash-3.2$ !!
echo 'Hello'
Hello

이 특수 변수는 sudo 권한을 얻는 것을 깜빡했을 때 유용하게 쓰일 수 있습니다. 명령이 거부되었을 때, sudo !!를 입력하면 명령어를 다시 입력할 필요가 없습니다.

$_

$_는 직전 명령어의 마지막 인수를 나타냅니다.

bash-3.2$ echo hello im fine thank you
hello im fine thank you
bash-3.2$ echo "$_"
you

직전 명령어의 마지막 인수인 you가 출력되었습니다.

short-circuiting 연산자

short-circuiting단축 판단으로 번역할 수 있을 것입니다. 논리연산을 하는 과정에서 결과가 정해지면 남아있는 부분의 판단은 하지 않는 방법입니다.

예를 들어보겠습니다.
true and false
and 연산이 참이 되려면 두 값이 모두 참이어야 합니다. 즉, 앞의 true만 봐서는 논리 연산의 결과가 정해지지 않으므로 뒤에 있는 값까지 확인해야 하는 판단입니다.

false and true
이번에는 앞의 값만으로도 논리 연산의 결과가 false로 정해졌습니다. 앞의 값이 false인 이상 뒤에 어떤 값이 오더라도 이 연산은 false가 되기 때문입니다. 이때 앞의 값만으로 논리 연산의 결과를 판단하는 것을 단축 판단이라고 합니다.

true or false
이번에도 마찬가지로 단축 판단이 가능합니다. or 연산이 참이 되기 위해서는 둘 중 하나의 값만 참이면 되기 때문에 앞의 값만으로 논리 연산의 결과가 정해졌기 때문입니다.

일반적으로 프로그램에서 true0, false1로 표현됩니다.

또한 bash의 논리 연산자는 다음과 같습니다.

  • AND: &&
  • OR: ||
bash-3.2$ true || echo 'Hello world'
bash-3.2$ false && echo 'Hello world'
bash-3.2$ false || echo 'Hello world'
Hello world

위의 예를 통해 단축 판단이 일어났는지의 여부를 알 수 있습니다. 처음 두 명령의 경우 단축 판단이 가능하므로 연산자 뒤의 명령이 실행되지 않았습니다. 그러나 마지막 명령은 앞의 값만 보고 논리 연산의 결과를 알 수 없으므로 연산자 뒤의 명령까지 실행한 것을 볼 수 있습니다.

명령어 출력 변수로 가져오기

명령어 대체

$(CMD) 변수를 사용하면 명령어의 출력을 바로 변수 자리에 넣을 수 있습니다.

bash-3.2$ variable=$(echo 'Hello')
bash-3.2$ echo "$variable"
Hello

variable이라는 변수에 echo 'Hello'라는 명령의 출력을 값으로 할당했습니다.

절차 대체

<(CMD)는 괄호 안의 명령어의 출력을 임시 파일로 만들고, 그 임시 파일의 이름이 <() 대신 들어가게 됩니다.

bash-3.2$ diff <(echo 'Hello World') <(echo 'Hello world')
1c1
< Hello World
---
> Hello world

diff 명령어는 비교할 파일명을 인자로 받습니다. 위에서는 <()을 사용하여 echo의 출력을 파일의 형태로 diff에 인자로 전달하였습니다.

셸 스크립팅 예제

지금까지 알아본 내용과 새로운 내용을 곁들여서 다음 스트립트를 분석해보겠습니다.

#!/bin/bash

echo "Starting program at $(date)"

echo "Running program $0 with $# arguments with pid $$"

for file in $@; do
    grep foobar $file > /dev/null 2> /dev/null

    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

echo "Starting program at $(date)"
첫 번째 줄입니다. $(date)에는 date의 실행 결과인 현재 시각이 들어갈 것입니다.

echo "Running program $0 with $# arguments with pid $$"
복습하면 $0는 스크립트 명, $#는 인자의 개수, $$는 프로세스 식별 번호 값을 가집니다.

스크립트의 나머지 부분을 살펴보기 전에 bash에서 반복문조건문을 작성하는 방법에 대해 간략하게 알아보겠습니다.

반복문

for VARIABLE in 1 2 3 4 5 .. N
do
    command1
    command2
    commandN
done

bash에서 반복문은 일반적으로 위와 같이 작성합니다. 다른 프로그래밍 언어와의 차이점은 반복문을 {}로 감싸지 않고 시작과 끝을 dodone으로 명시한다는 것입니다.

1부터 5까지의 숫자를 순서대로 출력하는 반복문은 이렇게 작성할 수 있습니다.

for i in 1 2 3 4 5
do
    echo "$i"
done

혹은 C 언어와 같이 Three-expression을 사용할 수도 있습니다.

for (( i=1; i<=10; i++))
do
    echo "$1"
done

bash 3.0 이상부터는 {시작값..끝값}을 사용해 range를 사용할 수 있습니다.

for i in {1..5}
do
    echo "$1"
done

bash 버전이 이보다 낮다면 다음 링크에서 자신의 환경에 맞는 업그레이드 방법을 참고해주세요. https://itnext.io/upgrading-bash-on-macos-7138bd1066ba

저는 MacOS 환경이므로 brew install bash 명령을 통해 bash 업그레이드를 진행했습니다.

bash 반복문에 대한 더 자세한 내용은 다음 링크를 참고해주세요. https://www.cyberciti.biz/faq/bash-for-loop/

조건문

if [[ EXPRESSION ]]
then
    command
fi

bash 조건문의 일반적 구조입니다. 이중 대괄호 [[]] 안에 조건에 해당하는 표현식이 들어갑니다. 이중 대괄호는 일반적인 대괄호 []보다 발전된 기능을 사용할 수 있습니다.

then 다음 줄에 조건문에서 실행한 명령어를 작성하고, fi로 조건문이 끝났다는 것을 명시합니다.

EXPRESSION에 들어갈 표현식의 리스트는 여기에서 확인할 수 있습니다. https://www.man7.org/linux/man-pages/man1/test.1.html

포스팅에서는 간단한 예제 몇 개만 살펴보겠습니다.

if [[ (true) ]]
then
    echo "It's ture"
fi

()안의 값이 truefalse냐에 따라 조건문이 실행됩니다.

if [[ 'world' = 'world' ]]
then
    echo "It's true"
fi

bash 조건문에서 =은 문자열끼리의 비교를 수행합니다. 같지 않음을 나타내는 연산자는 !=입니다.

if [[ 2 -ne 3 ]]
then
    echo "It's true"
fi

bash 조건문에서 정수 비교는 =<, >가 아니라 옵션을 통해 이루어집니다. 정수 비교에 쓰이는 옵션의 의미는 다음과 같습니다.

옵션의미
-eq=
-ge>=
-gt>
-le<=
-lt<
-ne!=

즉, 3 -ge 24 -eq 4와 같이 사용할 수 있습니다.

이제 셸 스트립팅 예제로 돌아가 나머지 부분을 분석해보겠습니다.

for file in $@; do
	# 인자로 받은 모든 파일에 대하여
    grep foobar $file > /dev/null 2> /dev/null
    # foobar 검색. STDOUT, STDERR 모두 버린다.

    if [[ $? -ne 0 ]]; then
    # foobar 검색이 실패했다면(STDOUT이 0이 아니라면)
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

for문을 보면 반복의 대상이 되는 것은 $@인 인자로 받은 모든 파일입니다. 모든 파일에 대해 grep 명령으로 foobar라는 문자열을 포함하고 있는지 검사합니다.

여기서 눈여겨볼 것은 > /dev/null 2> /dev/null 부분입니다. 이전 포스팅에서 >는 출력 스트림을 바꾸는 기호라고 했습니다. 여기서 더 나아가 1>STDOUT 스트림을 바꾸고, 2>STDERR 스트림을 바꾸는 데 사용됩니다.

이를 바탕으로 명령을 해석하면 grep의 실행 결과로 정상적인 출력이 나오든, 오류가 나오든 그것을 모두 /dev/null에 저장하라는 뜻이 됩니다. /dev/null이란 파일은 모든 유닉스 시스템에 존재하며, 기록하는 내용을 모두 삭제하는 블랙홀 같은 파일입니다. 즉, 필요 없는 출력의 스트림을 /dev/null로 설정함으로써 눈에 보이지 않게 만들 수 있습니다.

이 스크립트에서 grep 명령은 해당하는 문자열의 유무를 판정하는 데 사용되었을 뿐, 그 결과를 일일히 출력할 필요는 없습니다. 따라서 해당 명령의 모든 출력을 제거해줍니다.

다음은 조건문입니다. if [[ $? -ne 0 ]]에서 $?는 이전 명령의 STDOUT 값을 가지며, -ne 옵션은 정수1과 정수2가 같지 않을 때 true, 같을 때 false를 반환하는 식을 만들어줍니다. 즉, 이전 명령이 성공적으로 끝나지 않았다면(문자열 찾기에 실패했다면) 조건을 만족시키게 됩니다.

조건문 안의 명령어에서 주의해야 할 것은 echo "# foobar" >> "$file"입니다. 여기서 >>는 오른쪽 파일 맨 밑의 행에 출력을 저장한다는 의미입니다. >를 사용하면 파일의 모든 내용을 지우고 첫 줄에 # foobar를 쓰게 됩니다.

강의에서 배우는 내용에 더하여 모르는 내용을 찾아본 것까지 합치니 포스팅이 꽤 길어진 것 같습니다.📚 다음 포스팅에서 이어서 작성하겠습니다.


<참고한 문서>

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

0개의 댓글