이 포스팅은 MIT 공개 강의를 바탕으로 작성되었습니다. https://missing.csail.mit.edu/
2021년 7월 10일
첫 번째 챕터에서는 기본적인 셸 명령에 대해 알아보고 데이터를 간단한 파이프로 처리하는 시간을 가졌습니다. 두 번째 챕터에서는 셸 스크립팅에 대해 공부해보겠습니다.
셸 스크립팅은 지금까지 셸에게 내렸던 개별 명령들을 묶어 큰 단위로 만든 것입니다. 예를 들어,
를 수행하는 세 명령을 순서대로 실행하는 스크립트를 짤 수 있습니다. 다른 프로그래밍 언어와 마찬가지로 셸 스크립팅에서도 제어 구문과 변수 할당을 사용할 수 있습니다.
실습 환경은 지난번과 동일합니다.
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
는 스크립트의 이름을 나타냅니다. 예를 들어 test.sh
라는 파일에 밑의 코드를 작성해보겠습니다.
#!/bin/bash
echo "$0"
이 스크립트의 실행 결과는 다음과 같습니다.
bash-3.2$ ./test.sh
./test.sh
스크립트의 이름인 ./test.sh
가 출력된 것을 볼 수 있습니다.
$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은 단축 판단으로 번역할 수 있을 것입니다. 논리연산을 하는 과정에서 결과가 정해지면 남아있는 부분의 판단은 하지 않는 방법입니다.
예를 들어보겠습니다.
true and false
and
연산이 참이 되려면 두 값이 모두 참이어야 합니다. 즉, 앞의 true
만 봐서는 논리 연산의 결과가 정해지지 않으므로 뒤에 있는 값까지 확인해야 하는 판단입니다.
false and true
이번에는 앞의 값만으로도 논리 연산의 결과가 false
로 정해졌습니다. 앞의 값이 false
인 이상 뒤에 어떤 값이 오더라도 이 연산은 false
가 되기 때문입니다. 이때 앞의 값만으로 논리 연산의 결과를 판단하는 것을 단축 판단이라고 합니다.
true or false
이번에도 마찬가지로 단축 판단이 가능합니다. or
연산이 참이 되기 위해서는 둘 중 하나의 값만 참이면 되기 때문에 앞의 값만으로 논리 연산의 결과가 정해졌기 때문입니다.
일반적으로 프로그램에서 true
는 0
, false
는 1
로 표현됩니다.
또한 bash의 논리 연산자는 다음과 같습니다.
&&
||
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에서 반복문은 일반적으로 위와 같이 작성합니다. 다른 프로그래밍 언어와의 차이점은 반복문을 {}
로 감싸지 않고 시작과 끝을 do
와 done
으로 명시한다는 것입니다.
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
()
안의 값이 true
냐 false
냐에 따라 조건문이 실행됩니다.
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 2
나 4 -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
를 쓰게 됩니다.
강의에서 배우는 내용에 더하여 모르는 내용을 찾아본 것까지 합치니 포스팅이 꽤 길어진 것 같습니다.📚 다음 포스팅에서 이어서 작성하겠습니다.
<참고한 문서>