본문 바로가기

Academy I/Tech Academy

Linux Shell Programming

1. What's Shell

쉘은 사용자와 리눅스 사이의 인터페이스로 작동하는 프로그램이다. 사용자는 쉘을 통하여 리눅스가 실행할 명령을 입력한다. 쉘은 Windows의 명령 프롬프트와 비슷하지만 더욱 강력한 기능을 갖추고 있다.


리눅스 쉘은 인터프리트로 실행되는 쉘 스크립트 언어를 지원하며 이를 이용해 빠르고 간단하게 프로그래밍 할 수 있다.


쉘에는 여러 종류가 있지만 리눅스의 기본 쉘 프로그램은 BASH(GNU Bourne-Again SHell)이다. 쉘의 위치는 보통 “/bin/sh”이며 “/bin/bash"에 대한 링크이다.


2. 리디렉션(Redirection)과 파이프(Pipe)


2.1. Redirection

표준 입력, 출력, 에러 출력 장치를 재지정(Redirection) 하는 기능. 재지정에는 “>”를 사용하며 파일로 재지정할 경우 기존 파일을 덮어쓴다. 기존 파일에 추가하기 위해서는 “>>”를 사용한다. “&”을 이용하여 다른 장치와 동일하게 재지정 할 수 있다.


$ ls -l > output.txt

표준 출력 장치로 출력되는 결과를 output.txt에 저장한다.

$ ls -l 1> output.txt

표준 출력 장치로 출력되는 결과를 output.txt에 저장한다.

$ ls -l 2> output.txt

표준 에러 출력 장치로 출력되는 결과를 output.txt에 저장한다.

$ ls -l 3> input.txt

표준 입력 장치로 input.txt의 내용을 입력시킨다.

$ kill -9 1234 >o.txt 2>&1

표준 출력 장치와, 표준 에러 출력 장치를 o.txt로 재지정한다.

<Redirection Examples>


2.2. Pipe

Pipe 연산자 “|”를 이용해 프로세스끼리 연결시킨다. Pipe를 이용하면 Redirection을 이용해 여러 단계에 걸쳐 수행해야 하는 일을 한 번에 수행할 수 있다.


Redirection을 사용한 경우

Pipe를 사용한 경우

$ ps > ps.txt

$ sort psout.txt > sort.txt

$ ps | sort > sort.txt

$ ps | sort > sort.txt

$ more sort.txt

$ ps | sort | more

<Redirection과 Pipe의 비교>


3. 쉘 프로그래밍


3.1. 스크립트 파일 생성 및 실행

텍스트 파일의 첫 부분에 “#!쉘경로”가 명시되어있다면 그 파일은 쉘 스크립트 파일로 간주되며 실행될 수 있다. 보통 “#!/bin/sh"로 설정하면 된다. 환경변수 ”PATH“에 “.”이 추가되어 있지 않다면 스크립트 실행시 “./스크립트파일명”과 같은 방법으로 실행해야 한다.


스크립트 파일은 확장자에 관계없이 실행될 수 있으나 파일에 Execution 속성이 설정되어 있어야 한다. ”chmod +x 파일명“을 실행하면 파일에 Execution 속성을 부여할 수 있다. 또는 "sh 파일명”과 같은 방법으로 스크립트를 실행할 수도 있는데 이 경우에는 파일의 속성에 관계없이 실행할 수 있다.


스크립트 파일의 끝에서는 “exit 종료코드” 구문을 이용해 종료코드를 명시한다. 생략해도 실행에는 지장이 없으나 다른 프로그램과 연동되어 동작하는 경우를 생각해 명시해 주는 것이 좋다. “exit 0"은 정상적인 종료를 의미한다.


3.2. 변수

쉘에서는 변수를 사용하기 전에 선언하지 않아도 된다. “set 변수명”으로 선언할 수 있으며 “unset 변수명”으로 회수할 수 있다. 기본적으로 모든 변수는 문자열로 간주되며 대소문자가 구별된다. 변수의 내용을 참조할 경우에는 변수명 앞에 “$”를 붙인다.


변수에 값을 대입할 경우에는 “변수명=값”의 형식을 취하면 된다. 이 때 “=”의 사이에 공백이 있어서는 안 된다. 변수에 대입되는 값(문자열)이 공백을 포함하고 있는 경우에는 값의 앞뒤에 큰따옴표(”)를 추가해 준다.


큰 따옴표 내부에서도 “$변수명”과 같은 부분은 변수의 값으로 변환되어 출력된다. 작은따옴표(‘)를 사용하면 변수명 그 자체가 출력된다. 또는 큰따옴표 내부에서 “$”앞에 “\”를 추가하여 변수의 값 대신 “$변수명” 자체를 그대로 출력할 수도 있다.


#!/bin/sh

variable="value of variable"

echo $variable

echo "$variable"

echo '$variable'

echo \$variable

echo Input some text

read variable

echo 'variable' is $variable

exit 0

<변수 예제 코드>

 

<실행 결과>


3.3. 환경 변수 및 매개변수

어떤 변수들은 실행 환경으로부터 얻은 값으로 초기화된다. 이런 환경변수들은 대문자를 사용한다. 매개변수는 스크립트 실행 시 주어진 값으로 초기화 된다.


환경 변수

매개 변수

$HOME

현재 사용자 홈 디렉터리

$1, $1, ...

각각의 매개변수

$PATH

명령 검색 경로

$*

모든 매개 변수 목록. IFS에 지정된

첫 번째 문자를 구분자로 사용.

$PS1

명령 프롬프트 모양 설정

$PS2

추가 입력 프롬프트 모양 설정

$IFS

입력 필드 구분자

$@

모든 매개변수 목록. IFS 미사용.

$0

쉘 스크립트 이름

$#

전달된 매개변수의 수

$$

쉘 스크립트의 프로세스 ID

<환경변수 및 매개변수>


#!/bin/sh

mes=Welcome

IFS=^

echo $welcome

echo "I'm $0"

echo "1st Parameter is $1"

echo "2nd Parameter is $2"

echo "Parameter list with IFS is $*"

echo "Parameter list without IFS is $@"

echo "User's home is $HOME"

exit 0

<환경변수 및 매개변수 예제 코드>

 

<실행 결과>


3.4. if / elif 문

C의 if / else if 문과 유사하다. 맨 마지막에는 "fi"로 끝을 알린다. if / elif 문 다음 줄에는 then이 온다. 한 줄에 사용하기 위해서는 “;”을 사용한다. C와 마찬가지로 else 문을 사용할 수 있는데 else문 다음에는 then이 오지 않는다.


조건식 검사를 위해 “test 조건” 또는 “[ 조건 ]”을 사용할 수 있다. 조건에 따라 참 또는 거짓이 된다.


str1 = str2

두 문자열이 같으면 참

e1 -lt e2

산술적으로 e1이 작으면 참

str1 != str2

두 문자열이 다르면 참

e1 -le e2

산술적으로 e1이 작거만 같으면 참

-n str

널 문자열이 아니면 참

! exp

exp가 참이면 거짓, 거짓이면 참

-z str

널 문자열이면 참

-d file

파일이 디렉터리이면 참

e1 -eq e2

산술연산 결과가 같으면 참

-f file

파일이 보통 파일이면 참

e1 -ne e2

산술연산 결과가 다르면 참

-r file

파일을 읽을 수 있으면 참

e1 -gt e2

산술적으로 e1이 크면 참

-w file

파일에 쓸 수 있으면 참

e1 -ge e2

산술적으로 e1이 크거나 같으면 참

-x file

파일을 실행할 수 있으면 참

<조건식>


#!/bin/sh

echo Is it morning?

read yesorno

if [ "$yesorno" = "yes" ]; then

echo "good morning"

elif [ "$yesorno" = "no" ]; then

echo "good afternoon"

else

echo "$yesorno not recognized."

exit 1

fi

exit 0

<if / elif 문 예제 코드>

 

<실행 결과>


3.5. for 문

주어진 값의 범위 내에서 반복한다. 주어지는 값은 문자열이 될 수도 있고 쉘 확장을 사용할 수도 있다. 문자열 값에 대해 와일드카드를 사용하면 쉘이 모든 변수의 값을 채워넣는다.


#!/bin/sh

for file in $(ls *.c); do

echo $file

done

exit 0

<for 문 예제 코드>


<실행 결과>


3.6. while 문

주어진 조건이 참일 경우 반복한다.

#/bin/sh

echo -n "Input Password: "

read pass

while [ "$pass" != "1234" ]; do

echo -n "Incorrect Password! Input Password: "

read pass

done

exit 0

<while 문 예제 코드>


<실행 결과>


3.7. until 문

while과 반대로 주어진 조건이 거짓일 경우 반복한다.


#!/bin/sh

loop=10

until [ "$loop" -eq 0 ]; do

echo "loop $loop"

loop=$(($loop-1))

done

exit 0

<until 문 예제>


<실행 결과>


3.8. case 문

변수의 값에 따라 다른 처리를 한다. “|”를 이용해 값을 조합할 수 있다. “*”와 “?”를 사용할 수 있다. “[]”를 사용해 글자 단위로 조합할 수 있다. 각 패턴 줄은 “;;”으로 끝난다.


#!/bin/sh

echo Is it morning?

read var

case "$var" in

[yY][eE][sS] ) echo "Goor Morning!";;

[nN]* ) echo "Good Afternoon!";;

* ) echo "Answer not recognized!";;

esac

exit 0

<case 문 예제>


<실행 결과>



1. 리스트

1.1. AND(&&) 리스트

AND 리스트 구조를 사용하면 일련의 명령을 실행할 수 있다. 이 때 반드시 이전의 모든 명령이 성공해야 다음 명령을 실행한다.


아래 코드는 file_one이라는 파일은 만들고 file_two라는 파일을 삭제한 후 각 파일의 존재 유무에 따라 다른 출력을 내보낸다.


#!/bin/sh

touch file_one #파일 존재를 확인하고 없으면 생성한다

rm -f file_two #파일을 삭제한다

if [ -f file_one ] && echo "hello" && [ -f file_two ] && echo " there"; then

echo "in if"

else

echo "in else"

fi

exit 0

<And 리스트 예제 코드>


이 코드를 실행하면 if문에 조건으로 주어진 구문이 하나씩 실행되어진다. file_one이 존재하므로 "hello"가 화면이 출력되지만 그 다음 조건에서 file_two가 존재하지 않으므로 “there"는 찍히지 않는다. 전체가 true가 아니므로 전체 if문은 flase가 되어 ”in else“가 출력된다.


<실행 결과>


1.2. OR(||) 리스트

OR 리스트 구조를 사용하면 어느 하나가 성공할 때까지 일련의 명령들을 실행할 수 있다. 어느 하나가 성공하고 나면 이후의 것은 더 이상 실행하지 않는다.


아래 코드는 file_one이라는 파일을 지운 뒤 if안의 조건을 하나씩 검사해 나가며 if문의 전체 결과에 따라 다른 문구를 출력한다.


#!/bin/sh

rm -f file_one #file_one이라는 파일을 삭제함

if [ -f file_one ] || echo "hello" || echo " there"; then

echo "in if"

else

echo "in else"

fi

exit 0

<OR 리스트 예제 코드>


이 코드를 실행하면 일단 file_one이 지워진다. 그 후 if문의 첫 구문은 파일이 없어 실패한다. 다음 구문 으로 “hello"를 출력하는데, 성공적으로 출력 되었으므로 구문은 true를 반환하고 세 번째 조건은 실행되지 않는다. OR 리스트를 사용했으므로 전체 if문은 true가 되어 "in if”가 출력된다.


<실행 결과>


2. 함수


2.1. 함수 선언

함수 이름 다음에 빈 괄호를 쓰고 구문들을 중괄호로 묶으면 함수가 선언된다. 함수는 항상 호출 이전에 선언되어야 한다. 쉘에서는 전방 선언이 없다. 함수가 호출되면, 위치 매개변수(Positional Parameter), $*, $@, $#, $1, $2 등이 함수에 대한 매개변수로 대체된다.


함수가 종료되면 이전 값으로 복구된다. "local" 키워드를 이용해 함수 내부에서 지역 변수를 선언할 수 있다. 함수 내부에서 변수 사용 시 지역 변수가 전역 변수에 우선한다. 반환 값을 지정하는 return 명령이 없을 경우 실행된 마지막 명령의 종료 상태를 반환한다.


#!/bin/sh

sample_text="global variable" #전역 변수 sample_text를 선언한다

foo()

{

local sample_text="local variable" #지역 변수 sample_text를 선언한다

echo "Function foo is executing"

echo $sample_text #지역 변수가 출력된다

}

echo "Script starting"

echo $sample_text #전역 변수가 출력된다

foo

echo "Script ended"

echo $sample_text #전역 변수가 출력된다

exit 0

<지역 변수 예제 코드>


코드가 실행되면 일단 전역 변수로 sample_text가 생성된다. 이 후 이 전역 변수를 출력한 후 함수 foo를 호출한다. foo는 내부에서 지역 변수로 sample_text를 출력한다. 함수 내부에서 전역 변수와 지역 변수의 이름이 충돌할 경우 지역 변수가 우선하므로 지역 변수 sample_text의 내용이 출력된다.


함수가 종료된 후 다시 전역변수 sample_text가 출력된다. foo함수 내부에서 sample_text를 할당했지만 함수 밖에서 sample_text를 쓰면 전역 변수가 나오게 된다.


<실행 결과>


2.2. 값 반환

return을 이용해 숫자 값을 반환할 수 있다. 문자열을 반환하는 일반적인 방법은 변수에 문자열 값을 저장한 뒤 사용하는 것이다.


아래 코드는 스크립트 실행 시 넘어온 파라메터를 함수에 넘겨 yes, no에 대한 처리를 하고 결과를 리턴으로 받아 결과에 따른 출력을 하는 코드이다.


#!/bin/sh

yes_or_no()

{

echo "Is your name $* ?" #함수가 호출되면 $*s는 함수 호출시 뒤따르는 파라메터가 된다

while true; do # 무한루프

echo -n "Enter yes or no: "

read x

case "$x" in

y | yes ) return 0;; #참에 해당하는 값이 0임에 주의

n | no ) return 1;; #0이 아닌 값들은 false가 된다

esac

done

}

echo Original Parameters are $*

if yes_or_no "$1"; then

echo "Hi $1, nice name" #리턴 값이 참(0)인 경우

else

echo Never mind #리턴 값이 거짓인 경우

fi

exit 0

<retrun 예제 코드>


위 코드가 실행되면, 첫 번째 파라메터를 이용해 함수 yes_or_no를 호출한다. 함수 내부에서는 무한루프를 돌며 사용자의 입력 yes(y)또는 no(n)를 받는다. y이면 0을 리턴 n이면 1을 리턴한다. 함수를 호출한 if문은 리턴값에 따라 다른 메시지를 출력하게 된다.


<실행 결과>


3. 쉘에 내장된 명령

쉘 스크립트에서는 두 가지 형식의 명령을 실행할 수 있다. 명령 프롬프트에서도 실행할 수 있는 “외부 명령”과 외부 프로그램으로 호출될 수 없는 “내부 명령”으로 나눌 수 있다.


3.1. break

이 명령을 사용하여, 제어 조건이 충족되기 전에 for, while, until 문에서 빠져 나올 수 있다. 숫자를 매개변수로 전달하면 그 만큼 반복한 후에 빠져 나온다. 전달하지 않으면 한 번에 빠져 나온다.


다음 코드는 4번 반복 후 5번째가 되면 강제로 종료하는 코드이다.


#!/bin/sh

for var in 1 2 3 4 5 6 7 8 9 10; do

echo "var is $var"

if [ "$var" = "5" ]; then #var의 값이 5가 되면 (5번째 루프)

echo "Break!"

break; #강제로 루프를 빠져나온다

fi

done

exit 0

<break 예제 코드>


코드가 실행되면 var값이 차례로 바뀌면서 루프가 수행된다. 루프 안의 if문에서 조건을 검사해 var의 값이 5가 되면 “Break"를 출력하고 break문으로 루프를 빠져나온다.


<실행 결과>


3.2. :

“:”은 Null 명령이다. True에 대한 별칭으로 사용되기도 한다. “while :”는 "while true"와 같지만 내장 명령이기 때문에 더 빨리 실행된다. 다음과 같은 코드는 “var"이 존재하면 기존 값을 보존하고 없다면 새로운 값으로 선언한다.


: ${var:=새값}


3.3. continue

continue는 반복문의 실행을 건너뛴다. 건너 뛸 루프 수를 매개변수로 넘길 수 있고 생략하면 바로 다음 단계로 건너뛴다. 이 때 반복문의 매개변수는 다음 값으로 설정된다.


다음 코드는 파일 네 개와 한 개의 디렉터리를 만든 후 루프를 돌며 파일과 디렉터리를 판별하여 각기 다른 메시지를 출력하는 코드이다.


#!/bin/sh

touch fred1 fred2 fred3 fred4 #파일을 네 개 만든다

mkdir fredd #디렉터리를 한 개 만든다

for file in fred*; do #fred로 시작하는 모든 파일이름에 대해 루프를 돈다

if [ -d "$file" ]; then #디렉토리라면

echo "Skipping directory $file" #메시지를 출력하고

continue #나머지 내용을 생략하고 다음 단계 루프를 돈다

fi

echo "file is $file"

done

rm -rf fred1 fred2 fred3 fred4 fredd #파일과 디렉토리를 지운다

exit 0

<continue 예제 코드>


코드가 실행되면 네 개의 파일과 한 개의 디렉토리를 만든다. 이 때 이름은 모두 fred로 시작한다. 루프에서 fred로 시작하는 모든 파일(디렉터리 포함)에 대해 반복한다. 루프 안에서 if로 현재 file 변수에 들어간 파일 이름을 검사해 디렉토리하면 메시지를 출력한 후, continue문을 이용해 루프를 빠져나온다.


<실행 결과>


3.4. eval

인자를 연산할 수 있다. 따라서 실행 중에 코드를 생성할 수도 있다.


다음 코드는 eval을 사용한 경우와 그렇지 않은 경우가 어떻게 다른지 보여준다.


#!/bin/sh

foo=10

x=foo

y='$'$x

echo $y


foo=10

x=foo

eval y='$'$x

echo $y

<eval 예제 코드>


첫 번째 네 줄에서는 eval을 사용하지 않았다. 다라서 y의 값은 ‘$’와 변수 x의 내용인 "foo"의 결합이 된다. 두 번째 네 줄에서는 eval을 사용했다. 여기서는 y의 내용인 $foo을 변수로 생각해고 y에 변수 foo의 값을 넣는다.


<실행 결과>


3.5. exit n

종료 코드 n으로 종료한다.


종료코드

의미

0

성공

1 ~ 125

스크립트에서 사용할 수 있는 에러 코드

126

파일이 실행 가능이 아님

127

명령을 찾을 수 없음

128 이상

신호(Signal)가 발생함


3.6. export

변수를 다른 하위 쉘에서 사용할 수 있도록 한다.


다음 코드는 export1.sh에서 선언된 변수가 export2.sh에서도 사용될 수 있음을 보여준다.


#!/bin/sh

foo="The first meta-syntatic variable"

export bar="The second meta-syntatic variable" #export를 사용해 선언한다

sh export2.sh

exit 0

<export1.sh>


#!/bin/sh

echo "foo = $foo"

echo "bar = $bar"

exit 0

<export2.sh>


export1.sh에서 foo와 bar 두 개의 변수를 선언했다. 하지만 bar에만 export 키워드를 사용했다. 그 후 export2.sh를 호출한다. export2.sh에서는 변수 foo와 bar를 출력하는데, 두 변수 모두 export2.sh에서는 선언되지 않았다. 따라서 foo는 선언되지 않은 변수이므로 값이 없어 출력되지 않는다. 하지만 bar는 export1.sh에서 export로 선언했으므로 export2.sh에서 유효하며 따라서 export1.sh bar의 값이 출력된다.


<실행 결과>


3.7. expr

자신의 인자를 표현식으로 연산한다. x=x+1을 수행하려면 다음과 같이 한다.


x=`expr $x + 1` 또는 x=$(expr $x + 1)


최근의 스크립트에서는 expr대신 더욱 효율적인 $((...))을 사용한다. 이를 이용해 위 식을 표현하면 다음과 같다.


x=$(($x + 1))


expr이 처리할 수 있는 연산은 다음과 같다.


| & = > >= < <= != + - * / %


이 중 “|”는 OR 연산 “&”는 AND 연산이다. 그 외의 연산은 C와 같다.


3.8. printf

ANSI C의 표준 출력 함수인 printf와 유사하다. 사용법은 다음과 같다.


printf "형식 문자열“ 매개변수1 매개변수2 ...


형식 문자열에 쓰이는 값 역시 C의 printf와 유사하다.


다음 코드는 printf를 사용해 문자열과 변수를 출력하는 예이다.


#!/bin/sh

printf "%s\n" hello

printf "%s %d\t%s\n" "Hi There" 15 people

exit 0

<printf 예제>


첫 줄에서는 인자로 문자열을 받아 출력하고 “\n"의 영향으로 줄을 바꾼다. 두 번째 줄에서는 문자열 하나, 숫자 하나, 문자열 하나를 출력하고 줄을 바꾼다. 중간에 ”\t"에 의해 탭 크기만큼의 공백을 출력하게 된다.


<실행 결과>


3.9. shift

모든 매개변수를 하나씩 앞으로 옮긴다. 즉 $2는 $1이 되고 $3은 $2가 되는 식이다. $1는 없어지고 $0은 변하지 않는다.


다음 코드는 파라메터를 하나씩 이동시키면서 모든 파라메터를 출력하는 코드이다.


#/bin/sh

while [ "$1" != "" ]; do #파라메터가 있으면 루프를 계속 돈다

echo "$1"

shift

done

<Shift 예제 코드>


매개변수로 a b c를 주고 실행했다. while을 돌며 첫 번째 파라메터가 없을 때까지 출력한다. 매 루프마다 shift를 한 번씩 호출하기 때문에 파라메터가 한 단계식 밀려난다.

<실행 결과>


3.10. trap

Signal이 발생했을 경우 수행할 동작을 설정한다.


신 호

설 명

HUP(1)

Hang Up, 대개 사용자가 로그아웃 할 때

INT(2)

인터럽트, 대개 Ctrl+C 를 눌러 전송한다

QUIT(3)

끝내기, 대개 Ctrl+\ 를 눌러 전송한다

ABRT(6)

중단, 대개 어떤 심각한 실행 에러일 때

ALRM(14)

알람, 대개 시간 제한을 처리할 때

TERM(15)

종료, 대개 시스템이 종료 될 때


다음 코드는 임시 파일을 하나 만든 후 파일이 없어 질 때까지 무한 루프를 도는 코드이다.


#!/bin/sh

touch /tmp/my_tmp_file_$$ #파일을 만든다

trap "rm -f /tmp/my_tmp_file_$$" INT #INT 신호가 들어오면 파일을 지운다

echo creating file /tmp/my_tmp_file_$$

echo "press <CTRL-C> to interrupt.."

while [ -f /tmp/my_tmp_file_$$ ]; do #파일이 존재하는 한 무한루프

echo File exists

sleep 1

done

echo The file no longer exists

exit 0

<trap 예제 코드>


코드를 실행하면 우선 임시파일을 만든다. 임시파일 이름 뒷 부분은 프로세스 아이디로 지정해 파일이 겹치지 않도록 한다. trap 명령어로 INT(2)신호가 들어오면 임시 파일을 지우도록 지정한다. 그 뒤 임시 파일이 존재하면 계속 무한 루프를 돌게 된다. 중간에 CTRL-C를 누르면 INT신호가 발생되며 임시 파일이 지워진다. 따라서 루프가 끝나게 된다.


<실행 결과>


3.11. Here Document

실제로는 명령이 스크립트로부터 입력을 받고 있지만, 파일이나 키보드로부터 입력을 받아들이는 것처럼 실행한다. “<<”뒤에 따라오는 문자열이 나올 때까지 계속 입력 처리를 한다.


다음 코드는 Here Document를 이용해 cat에 입력을 주어 입력하는 내용을 하나로 연결해 출력하는 코드이다.


#!/bin/sh

cat << !END!

hello

this is a here

document

!END!

exit 0

<Here Document 예제 코드>


첫 줄에서 cat을 호출하며 Here Document의 종료 문자열로 “!END"를 주었다. 이후에 있는 줄들은 모두 cat의 인자로 들어가게 된다. 마지막에서 ”!END"를 만나면 입력을 종료한다.


<실행 결과>