4 분 소요

쉘스크립트 템플릿 분석


동기

리눅스 쉘 스크립트를 공부하다가, 쉘 스크립트에 공통적으로 쓰이는 boiler-plate 같은 게 있을것 같았습니다. 이 곳에 원문이 있고, 이 분이 한국어로 번역을 해 두었으니 전체적인 설명을 보려면 링크타고 가면 됩니다.

저는 쉘 스크립트 문법을 공부하려는 의도라서, 스크립트의 전체적인 구조나 의도는 생략하겠습니다.

Shell script template 원문

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#!/usr/bin/env bash

set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

usage() {
  cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-f] -p param_value arg1 [arg2...]

Script description here.

Available options:

-h, --help      Print this help and exit
-v, --verbose   Print script debug info
-f, --flag      Some flag description
-p, --param     Some param description
EOF
  exit
}

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup here
}

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
    NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo >&2 -e "${1-}"
}

die() {
  local msg=$1
  local code=${2-1} # default exit status 1
  msg "$msg"
  exit "$code"
}

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

parse_params "$@"
setup_colors

# script logic here

msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"

테스트

1
2
3
4
5
$ ./test.sh -p param argument
Read parameters:
- flag: 0
- param: param
- arguments: argument

사용된 문법들

#! (SheBang, 사용할 쉘 선언)

보통 인터넷 자료를 보면 #!/bin/sh 혹은 #! /bin/sh 같은 식인데, 템플릿에선 #!/usr/bin/env bash 를 사용하고 있습니다. 찾아보니 원래는 #!/bin/bash 처럼 bash 의 절대경로를 넣어줘야 하는데, #!/usr/bin/env 는 뭘까요?


사실 원래 엄밀하겐 사용할 쉘을 선언하는게 아니라, 가장 먼저 실행될 프로그램을 지정하는 거라고 해요. 찾아보면 이런 예제도 있습니다.

#!bin/rm

  :
  :

아이러니하게도 이렇게 쓰여진 쉘 스크립트는 자기 자신을 지우는 스크립트가 됩니다. 어쨋든 #!/usr/bin/env bashenv bash 명령어를 먼저 실행시킨다는 의미가 되겠네요. 쉘에서 env 명령어는 단독으로 쓰면 환경변수를 출력하지만, argument 를 주면 내 환경에서 해당 프로그램을 찾아 실행시키는 명령어가 됩니다. 이런 기능 덕분에 #!/usr/bin/env python 처럼 사용되곤 합니다.

trap

특정 시그널을 잡아채서, 지정한 명령어를 실행합니다.

trap [실행될 명령어] [잡아챌 시그널(들)]

이런 식으로 사용하고, 간단한 스크립트에선 one-liner 처럼 사용될 수 있습니다.

trap `rm -f $TEMP; exit $USER_INTERRUPT` SIGINT

… 이런 식으로요.

템플릿에선 SIGINT, SIGTERM, ERR, EXIT 네가지 시그널에 대해 반응하도록 만들었는데, 필요하다면 여기서 가감해도 될 것입니다. 참고로 SIGINT 는 Ctrl+C 입력으로 종료시, SIGTERMkill 명령어로 종료시 발생합니다.

/dev/null

script_dir 에 쉘 스크립트가 실행되고 있는 경로를 할당하는데, 중간에 /dev/null 이라는 파일이 있습니다. /dev 에 있으니까 장치에 관련된 파일인데, /dev/null 은 그냥 비워져 있는 파일이라고 하고, 표준 출력을 버리고 싶을 때 사용한다고 합니다.

EOM, here doc

이전 포스팅 에도 비슷한 내용이 있었습니다. 거기선 이런 식으로 작성되어있었습니다.

if [ $# != 2 ]; then cat << EOM

    usage: $0 ENDPOINT INDEX

    where ENDPOINT is the path to the qbox.io endpoint (or http://localhost:9200)
      and INDEX is the name of the endpoint, e.g. 'sagan-production'

EOM
    exit
fi

위 스크립트 같은 경우 인자를 잘못 넣어줬을 경우 해당 출력을 보여줍니다. 템플릿에선 usage() 함수를 만들어줬는데, 크게 특별한 것은 없습니다. 다만 나중에 -h 같은 옵션에서 호출되기 좋아서 템플릿의 convention 이 더 괜찮아 보이긴 합니다.

if - then - else - fi

쉘 스크립트의 if 문은 다른 언어의 if 문과 비슷하긴 한데, 좀 다른 점이 있습니다. 조건 안에 명령문이 정상종료가 되면 0 를 리턴하는데, 이걸 true 처럼 봅니다. (python 하고 어떻게 보면 반대로 작동) (()) 혹은 [[]]로 감싸면 python 같은 언어에서 하는 것처럼 사용할 수 있습니다만 이런 헷갈리는 부분은 확실히 골치아픕니다… 참고로 [[]] 에서 사용하는 옵션은 test 가 사용하는 옵션과 같습니다. (그냥 동등하다고 생각하면 됨)

”${1-}”

첫 번째 argument 를 뜻하는데, 없을 경우 아무것도 안쓴다는 뜻입니다. 원래는 "${var-default}" 같은 식으로 사용합니다.

@, *

둘 다 전부를 뜻하긴 합니다. @ 는 각각 분리해서 표현되고, * 는 공백을 포함한 하나의 텍스트로 표현됩니다. 템플릿에서 $@ 로 표현된 경우 arguments 를 list 로 가져옵니다 ($0 는 안 가져옴)

참고로 for loop 에 arguments 를 집어넣고 싶을 땐 단순히 in 을 생략하면 됩니다.

case 문

처음 본 case 문이라서 따로 적습니다. 아래와 같은 꼴로 사용합니다. if-fi 를 처음 봤을땐 눈치챘는데 이번엔 눈치 못챘네요…

case "$var" in
  "$regex1" ) command1 ;;
  "$regex2" ) command2 ;;
    :
    :
esac

마치며

개인적으로 python script 로 만드는 쪽이 훨씬 생산적이지 않나 생각이 들긴 하지만, 호환성 이슈 하나 때문이라도 shell script 를 읽고 쓸 줄 알아야 한다… 라는 생각이 들었습니다.