콘텐츠로 건너뛰기

[bash] command line option parsing하기 (옵션 받기)

Positional parameter

실험을 하다 보면 처음에는 각 과정을 하나하나 직접 실행하면서 결과를 보지만 결국에는 전체 과정을 한 번에 수행할 수 있는 자동화 스크립트를 작성하기 마련이다. 나는 이때 본 실험이 어떤 방식으로 수행되든 간에 최상단의 스크립트는 bash로 작성하는 편이다.

얼마 전까지는 어차피 나 혼자 실행하는 스크립트라는 생각으로 옵션의 순서를 정해놓고 positional parameter를 순서대로 받았다. 예를 들면 parameter 순서를 <입력 파일> <옵션1(Int)> <옵션2(Boolean)> 이렇게 정해두고 실행할 때에는 정확히 그 순서대로 옵션을 넣어주는 식이다. 혹시 나중에 까먹을 수도 있으니 적절한 help message 역시 작성해 두긴 했지만, 마음 한편으로는 옵션을 제대로 parsing할 수 있도록 수정해야겠다고 생각하고 있었다.

#!/bin/bash
# FILE: run.sh

if [ $# -eq 0 ]
then
    echo "Usage: bash run.sh <input> [options]"
    echo "    Options"
    echo "        option1(Int)          - (option1 설명)"
    echo "        option2(Boolean)  - (option2 설명)"
    echo "    Description"
    echo "        (스크립트 설명....)"
fi

INPUT_FILE=$1
OPT_INT=${2:-0}
OPT_BOOL=${3:-False}

echo "===parsed command line option==="
echo " - input: ${INPUT_FILE}"
echo " - option1: ${OPT_INT}"
echo " - option2: ${OPT_BOOL}"

위와 같이 작성하면 빠르게 작성할 수 있고 실행할 때에도 ./run.sh input/in1 1 False 이런 식으로 간편하게 실행할 수 있어서 은근히 편하긴 하다. 하지만 몇 달 정도 후에 코드를 다시 돌리려고 하면 옵션들을 까먹어서 코드를 다시 봐야 하고, 매번 실행을 하면서도 지금 내가 옵션을 맞게 주고 있는 건가 헷갈린다. 다른 사람에게 실험 코드를 넘겨주기도 좀 그렇다.

Command line option parsing

getopts 없이 option parsing하기

며칠 전에 살짝 남는 시간에 getopt와 비슷하게 command line option을 제대로 parsing할 수 있는 bash script를 작성했다. bash의 built-in command 중 하나인 getopts를 이용할 수도 있는데, 찾아보니 getopts는 long option name (eg. -i 대신 --input)을 지원하지 않는다고 해서 그냥 직접 작성했다.

#!/bin/bash
# FILE: run.sh

# options:
INPUT_FILE=""
OPT_INT=0
OPT_BOOL=False

# get options:
while (( "$#" )); do
    case "$1" in
        -i|--input)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                INPUT_FILE=$2
                shift 2
            else
                echo "Error: Argument for $1 is missing" >&2
                exit 1
            fi
            ;;
        -l|--limit)
            if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then
                LIMIT=$2
                shift 2
            else
                echo "Error: Argument for $1 is missing" >&2
                exit 1
            fi
            ;;
        -d|--debug)
            DEBUG=True
            shift
            ;;
        -h|--help)
            echo "Usage:  $0 -i <input> [options]" >&2
            echo "        -i | --input  %  (set input to ...)" >&2
            echo "        -l | --limit  %  (set limit of ...)" >&2
            echo "        -d | --debug     (debug mode)" >&2
            exit 0
            ;;
        -*|--*) # unsupported flags
            echo "Error: Unsupported flag: $1" >&2
            echo "$0 -h for help message" >&2
            exit 1
            ;;
        *)
            echo "Error: Arguments with not proper flag: $1" >&2
            echo "$0 -h for help message" >&2
            exit 1
            ;;
    esac
done

echo "===parsed command line option==="
echo " - input: ${INPUT_FILE}"
echo " - limit: ${LIMIT}"
echo " - debug: ${DEBUG}"

코드 자체는 parsing을 전혀 하지 않던 원래 스크립트보다 길어졌지만 사용하기에는 더 편해졌다. 적절한 예시를 들기 위해 옵션 이름을 살짝 수정했다. 이제 ./run.sh --input input/in1 -limit 2 --debug 또는 ./run.sh -i input/in2 -l 3 와 같이 일반적으로 command line option을 넘겨주는 것과 동일하게 스크립트를 실행할 수 있다.

Parsing한 옵션에 다른 스크립트로부터 접근

만약 같은 옵션을 받아 실행되는 스크립트가 여러 개 존재하거나, 또는 옵션의 개수가 많아서 메인 스크립트가 길어지는 문제가 있다면 메인 스크립트와 옵션 parsing을 하는 스크립트를 서로 분리할 수 있다. 예를 들어 위에서 작성한 코드를 getopt.sh로 저장하면 메인 스크립트에서는 아래와 같은 방법으로 parsing된 옵션에 접근할 수 있다.

#!/bin/bash
# File: run.sh
# 옵션을 parsing하는 스크립트는 getopt.sh에 작성되어 있다고 가정

arguments=$@
. ./getopt.sh $arguments

echo "parsed options: input=${INPUT_FILE} / limit=${LIMIT} / debug=${DEBUG}"

getopt.sh를 바로 source 하지 않고 굳이 arguments 변수에 먼저 모든 parameter를 넣어주는 이유는 getopt.sh를 실행하면 command line으로 받은 parameter들이 모두 삭제되기 때문이다. 메인 스크립트에서 다른 sub 스크립트들을 실행하고, 해당 스크립트들에서 조금씩 다른 방식의 옵션 parsing이 각각 필요하다면 이렇게 parameter를 따로 저장해 두는 것이 좋다.

전체 스크립트 예시

내 실험 스크립트는 대충 아래와 같이 작성되어 있다. 물론 각 옵션들은 포스트 작성을 위해 임의로 지정한 것들이고 중간중간 생략하면서 대략적인 흐름만 적었으니 참고. run script -> Makefile -> external tool (example: ICC)로 이어진다.

#!/bin/bash
# FILE: getopt.sh
# command line option parsing script (생략)
#     syntax: getopt.sh <cmd> [options]
#     examples
#        - getopt.sh step1
#        - getopt.sh step2 -debug
#        - getopt.sh all -limit 3
# parsing 결과 $CMD, $LIMIT, $DEBUG 변수 생성
#!/bin/bash
# File: run.sh
# script for environment setting & make
. ./getopt.sh

# option 검증
if [[ $LIMIT -gt 3 ]]; 
then
    echo "LIMIT should be not greater than 3 (given: $LIMIT)
    exit 1
fi

# environment setting
# 1. create working directory ($working_dir 이미 존재한다고 가정)
mkdir $working_dir/input
# 2. copy required files from previous experiment directory and input directory ($previous_out 이미 존재한다고 가정)
cp $previous_output/* $working_dir/input/
cp $INPUT $working_dir/input/
# 3. set variables and export them
OPTION_TO_PASS1=...
OPTION_TO_PASS2=...
export $OPTION_TO_PASS1
export $OPTION_TO_PASSS2

# make
make $CMD
# Example Makefile
# invoke tool ICC (for example)

ICC            := icc_shell    # (for example)
DATE           := $(shell date +%Y%m%d_%H%M%S)
SCR_DIR        := ./scripts
STEP1_SCR      := $SCR_DIR/step1.tcl
STEP2_SCR      := $SCR_DIR/step2.tcl
STEP3_SCR      := $SCR_DIR/step3.tcl
OPTION_PASSED1 := $(OPTION_TO_PASS1)
OPTION_PASSED2 := $(OPTION_TO_PASS2)

export OPTION_PASSED1 OPTION_PASSED2

.PHONY step1 step2 step3 clean

default: step3

step1:
    $(ICC) -f $(STEP1_SCR) -o $(LOG_DIR)/icc.step1.$(DATE).log

step2: step1
    $(ICC) -f $(STEP2_SCR) -o $(LOG_DIR)/icc.step2.$(DATE).log

step3: step2
    $(ICC) -f $(STEP3_SCR) -o $(LOG_DIR)/icc.step2.$(DATE).log

clean: 
    @rm -rf $(LOG_DIR)/*
    @rm -rf ...
# tcl script read by ICC
# FILE: step1.tcl
set OPTION1 $::env(OPTION_PASSED1)
set OPTION2 $::env(OPTION_PASSED2)
...
Share this post!
태그:

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.