IT/기초 지식

[Linux] 좋은 쉘 스크립트 쓰는 팁

개발자 두더지 2022. 5. 26. 01:20
728x90

일본의 글을 번역한 포스팅입니다. 오역 및 직역, 의역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 더 좋은 쉘 스크립트 쓰는 방법에 대해 정리해보았다.

 

아무것도 하지 않는 : (콜론) 커맨드


 쉘을 작성했을 때에 매우 필요했던 커맨드가 바로 이거 아무것도 하지 않는 커맨드이다. : 이라는 커맨드를 이용하면, 아무것도 하지 않고 종료 상태 0 (즉, 정상 종료)가 반환된다. 

 이것은 언제든지 쓸 수 있는 만능 커맨드로, 이 커맨드를 이용하여 귀찮은 에러 처리를 간소화하거나, 입력이나 출력의 리다이렉트할 곳/하는 곳으로 사용하는 등에 사용할 수 있다. 커맨드이므로 인수도 받을 수 있다. 하지만 물론 아무것도 하지 않는다.

 예를 들어 표준 출력에도 아무것도 반환하지 않으므로, 이것을 이용하여 파일의 초기화도 간단히 할 수 있다.

: > foo.log # 파일을 0바이트로 덮어쓰기

 : 에 대해서는 이 포스팅의 후반부에도 자주 등장할 것이다.

 

 

삼항 연산자같은 것


 삼항연산자 편리하다. 그러나 쉘 스크립트에는 삼항 연산자가 없다. 그러나 거의 비슷한 기능을 하도록 작성할 수 있는데, 그게 바로 아래의 작성법이다.

# $foo가 $bar보다 크면 0 , 그렇지 않으면 1를 $baz에 대입한다.
[ $foo -ge $bar ] && baz=0 || baz=1

 쉘에 사용된  [] 이라는 조건식을 이용해 if등을 사용하지 않아도 간단히 실행되도록 할 수 있다. 이 때에는 평가 결과가 True인가 False인가에 따라 종료 상태가 0 혹은 1이 반환되게 된다. 따라서 [] 안의 처리가 True인 경우에도 False인 경우에도 위와 같이 처리를 분리하는 것이 가능하다. 

 동일한 처리를 if로 작성하고자 하면 아래와 같이 된다. 

# $foo가 $bar보다 크면 0, 그렇지 않으면 1를 $baz에 대입한다.
if [ $foo -ge $bar ]; then
  baz=0
else
  baz=1
fi

 

 

변수의 기본값을 지정


 변수를 참고했을 때 혹은 그 변수가 미정의이거나, 빈 문자열인 경우에 어떤 무언가를 기본 값으로 설정하고 싶을 때가 있을 것이다. 그럴 경우 다음과 같이 쓸 수 있다.

# $foo에 $bar를 대입. 그러나 $bar 값이 정의되있지 않다면 0을 대입
foo=${bar:-0}

 한편, -의 부분을 =으로 변환하는 것으로, $bar자체에 덮어쓰는 것도 가능하다.

# $bar가 미정의인 경우, $foo에도 $bar에도 0을 대입
foo=${bar:=0}

 또한, 미정의 경우만 기본값 설정 대상으로 하고 싶은 경우 중간에 : 를 끼우면 좋다.

 

 

실행되고 있는 디렉토리의 절대 패스를 동적으로 구하기


 쉘 스크립트는 원래 실행됐을 때의 현재 디렉토리가 그대로 쉘 스크립트 안의 현재 디렉토리로써 다뤄진다. 그러므로 예를 들어 쉘에서 쉘을 호출하고 싶은 경우 등에서는 상대 경로가 사용하기 어렵다.

 그러나 그렇다고 절대 패스로 작성하면, 완전히 환경에 의존하기 때문에 git의 리포지토리 등에 의해 자신 주변의 디렉토리/파일 구조가 결정되는 경우에서는 스크립트 자신의 절대 패스를, 동적으로 취득하고 싶어진다.

 그러한 상황에서 항상 사용하는 커맨드가 아래의 커맨드이다. 어떤 쉘에서도 어떤 생각도 하지않고 단순히 복사-붙여 넣기해서 쓸 수 있다.

script_dir_path=$(dirname $(readlink -f $0))

 그러나 BSD나 OSX 등에서는 readlink의 구현이 다르므로, 대신에 coreutils의 greadlink(GNU readlink)를 이용하는 것도 좋을 것이다.

script_dir_path=$(dirname $(greadlink -f $0))

 커맨드에 대해서 해설하면 길어지나, $0를 사용하여 실행된 패스에서 자신의 파일까지의 상대 패스를 습득하고, 이것을 dirname으로 나눠서 디렉토리명만을 추출한 뒤 readlink -f를 실행하는 것으로, 쉘 스크립트 자신이 위치해있는 디렉토리까지의 절대 패스가 시스템에 의존하지 않고 동적으로 절대 경로를 얻게 해준다.

 

 

스크립트가 종료/중단되면 자동으로 뒤처리를 해주는 trap


 쉘 스크립트의 특성상 파일을 작성하거나 삭제하거나 어떠한 성과를 만드는 커맨드를 실행하는 경우가 많다. 이러한 경우 귀찮은 것이 뒤처리이다.

 도중에 종료되는 처리를 쓴다고해도 컨트롤+C 등으로 종료됐을 때에 제대로 실행되지 않으면, 작업도중의 쓰레기 파일이 남거나, 일시적으로 대피시켜 놓으려고 했던 파일이 원래 장소에 돌아가지 않는 슬픈 사고 등이 일어난다.

 이럴 때 도움받을 수 있는 것이 trap 커맨드이다. 사전에 trap 처리를 작성해 놓으면 처리가 도중에 중단됐을 경우에도 반드시 안에 적혀있는 커맨드를 뒤처리로써 실행해준다.

 더욱이 종료때의 시그널을 분리해 지정하면 에러시에는 이 동작, 정상 종료시에는 이 동작과 같은 처리를 분리하는 것도 가능하다. 즉, 컨트롤+C등으로 도중에 종료되는 경우에도 제대로 뒤처리를 해준다.

trap "rm /tmp/temporary-file" 0

 잠시 실험해보면, trap은 동일한 스크립트상에서는 동일 시그널에 대해서 2개 이상 적어도 1개만 실행되는 듯한 느낌을 받았으므로 설정할 때에는 뒤처리를 1개의 커맨드 안에 작성하는 것이 좋다는 생각이 들었다. 이 때 함수를 만들어 이것을 호출하면 좋지만, 개인적으로는 문자열내에 아래와 같이 쓰는 것이 간단하다고 생각한다.

trap "
  mv /tmp/swap-file original-file
  rm /tmp/target-file
" 0

 trap 커맨드에 대해서는 제대로 시그널과 아울러서 이해해두는 것이 좋을 것 같으므로 그 내용에 대해서는 기회가 되면 다루도록 하겠다.

 

 

당장 지금만 필요한 임시 파일/디렉토리를 만드는 mktemp


 mktemp 커맨드를 이용하면 적당한 이름의 파일/디렉토리를 만들 수 있다. 만들어진 디렉토리도 기본적으로는 /tmp이하인듯하므로, 일시적인 파일이나 디렉토리를 다루는데 최적의 커맨드이다.

 mktemp 커맨드는 랜덤한 이름으로 파일/디렉토리를 만들어주지만, 표준 출력에 그 결과를 반환해주므로, 기본적으로는 서브쉘에서 실행했던 것을 변수에 저장해서 사용하는 듯하다.

temp_file=$(mktemp) # /tmp 아래에 랜덤한 이름의 파일을 만든다. 예: /tmp/tmp.C3N9Ng6IaU
temp_dir=$(mktemp -d) # -d 옵션을 사용하면 디렉토리가 생성된다. 예: /tmp/tmp.wDOVMXVcio

 그리고 trap 커맨드와 함께 병용해서 사용하는 것으로 그 쉘 스크립트가 동작하고 있는 때에 생성되는 파일/디렉토리를 작성하는 것이 가능하다.

# 일시 디렉토리/파일을 만든다.
temp_file=$(mktemp)
temp_dir=$(mktemp -d)

# 뒤처리를 정의한다.
trap "
rm -f $temp_file
rm -rf $temp_dir
" 0

 

 

Bash 옵션을 활용하기


 bash에는 편리한 옵션들이 이미 준비되어 있다. 이러한 옵션을 스크립트의 선두에 set 커맨드와 함께 기재해두면, 그 스크립트를 실행중에는 항상 그 옵션이 유효 실행 상태가 되므로, 하나하나 처리를 쓰거나 이러한 처리를 생략되도록 하는 등의 수고가 덜어진다.

 개인적으로 자주 쓰는 것은 set -eux이다. 각각의 옵션 의미에 대해 설명하도록 하겠다.

 

-e

 보통 쉘 스크립트를 1번 실행하면 도중에 에러가 있어도 멈추지 않고 마지막 처리까지 진행된다. 이때에 실행시에 -e 옵션을 정의해두면 그 쉘 스크립트 내에서 어떠한 에러가 발생했을 시점에 그 이후의 처리는 중지시켜준다.

 파일의 작성이나 삭제등 주의해서 다뤄야할 커맨드를 수행하는 일이 많은 쉘이기 때문이 그러한 돌이킬 수 없는 처리를 하기 전에 조건식 등으로 사전에 확인할 때에 좋을 것이다.

set -e
ls /path/to/file # ls 커맨드에서 파일을 찾을 수 없을 시점에 에러가 발생한다.
cat /path/to/file # 위 에러가 발생했을 경우 이 커맨드가 실행되기 전에 스크립트가 중단된다.

 아까 봤던 삼항 연산자에서 설명했듯, 조건식의 참, 거짓에 따라 성공/실패를 경우에 따라 다룰 수 있으므로 아래와 같이 스크립트 내의 요소마다 아래와 같은 조건식을 걸어두는 것으로, 사전 확인 => 거짓이라면 중지이라는 처리를 구현할 수 있다.

set -e

# 〜 중략 〜

[ $hoge -ge $fuga ]

 또한 스크립트의 마지막에 위와 같은 조건식으로 작성하는 것으로 특정 조건의 만족 여부에 따라 그 스크립트 자체의 종료 상태를 조정할 수 있다. 

 

-u

 -u는 미정의 변수에 대해서 읽어들이기 등을 했을 때에 에러로써 다뤄준다. java의 NullPointerException과 비슷한 느낌이다.  이 옵션을 아까의 : 의 커맨드나 -e 옵션과 함께 사용하면 매우 간략하게 인수의 체크 등이 가능해진다. 

set -eu
# 인수가 3개 전달되지 않았으면 스크립트가 실행 중지된다.
: $1 $2 $3

 이것만으로 미정의 인수가 있는 경우에 발생한 에러에 따라 즉시 중지시켜준다. 스크립의 선두부근에 어떠한 생각도 하지 않고 일단 적어두는 것으로도 충분히 효과적이다.

 단순히 스크립트 상에 $1 $2 $3으로 작성하면 $1이 shell 커맨드라고 인식되어버려, 에러가 발생하므로, : 의 인수로써 전달되도록 하여 제대로 변수 혹은 문자열로 인식하도록 한다. 스페이스를 포함한 문자열이 전달될 경우 각각의 인수를 "로 감싸주면 문제 없이 처리가 이뤄진다. 

 중지됐을 때 에러 메시지도 bash가 자동적으로 아래와 같은 보기 쉽게 출력해준다.

./foo.sh: 행 2: $1: 미할당된 변수이다.

 

-x

 마지막으로 -x이다. 하지만 이것은 다른 옵션과 조금 방향이 다른다. 실행한 커맨드를 모두 표준 에러 출력으로 해주는 옵션이다. 바꿔 말하자면 실행 로그를 출력해준다고 생각하면 된다. 하지만 어디까지나 표준 에러 출력이므로, 표준 출력으로 무언가 실행되도록 할 스크립트와 제대로 병용할 수 있다.

 이 옵션을 붙여주는 것만으로 디버그나 개발시에 매우 도움이 된다. 아무튼 이 옵션이 제대로 진가를 발휘할 때는 Jenkins를 이용한 Job을 만들 때에, 내부에서 호출된 쉘 스크립트 등을 사용할 때이다. 

 Jenkins는 Job의 콘솔로그로써 -x 상당하는 것을 제공하고 있으나, 외부 스크립트의 안에서는 그러한 기능을 제공해주지 않는다. 외부 스크립의 실행시에는 bash -x foo.sh 등으로 실행하면 되지만, 미리 스크립트 선두에 set -x를 써두면 사용하는 쪽에서 의식하지 않도 항상 알기 쉽게 로그를 확인할 수 있다. 

 

일시적으로 옵션의 효과를 무효로 하기

 set을 이용한 옵션을 편리하지만, 이것을 스크립트 안에서 일시적으로 무효화하고 싶은 경우가 있다. 이 때에 set +x나 set +eu 등과 같이 -가 아닌 +로 지정하면 된다. 무효화하고 싶은 처리가 끝나면 잊지말고 set  -eu와 같이 다시 적어주면 다시 유효화된다.

set -eux

# 중략

set +u # 일시적으로 -u 옵션을 무효화

foo=$1 # 이 $1은 미정의일지 모르지만, 미정의라도 에러가 발생하지 않는다.
echo $foo

set -u # -u 옵션을 다시 유효화한다.

# 그 이후의 처리

참고자료

https://qiita.com/m-yamashita/items/889c116b92dc0bf4ea7d

728x90