· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Devel Filter Bash Skeleton

Skeleton Filter Program in Bash


이 글은 bash를 써서 DevelFilterSkeleton에서 다룬 필터 프로그램의 뼈대를 만드는 법을 소개합니다. DevelFilterSkeleton을 읽지 않았다면 먼저 그 글을 읽으시기 바랍니다. (주의: 이 글은 bash shell script를 쓰는 법을 자세히 소개하는 글이 아닙니다.)

1.1. Output Messages

필터 프로그램은 어떤 상황이 발생했을 때, 사용자에게 보고하거나, 에러를 출력할 때가 종종 발생하기 때문에, 경고 또는 에러 메시지를 내 보내는 경우가 있습니다. 필터 프로그램은 기본적으로 처리한 데이터를 표준 출력을 통해 내 보내므로, 이런 메시지들을 표준 입력으로 보낸다면, 데이터와 섞일 경우가 있기 때문에 대부분 표준 출력을 써서 메시지를 출력합니다.

DevelFilterCSkeleton에서는 시스템 함수 호출 후 일일히 전역 변수 errno의 값을 조사해서 strerror(3)를 써서 메시지를 출력했지만, shell script에서는 그럴 필요가 없습니다.

보통 프로그램이 끝나면 일반적으로 성공적으로 끝났을 때 0을, 실패했을 경우에는 0이 아닌 값을 리턴하게 됩니다. 이 값을 exit status라고 하고, 바로 전 프로그램의 exit status는 항상 변수 $?에 저장되어 있습니다. 아래에 그 예를 보입니다.

$ ls *txt
a.txt  a1.txt  a2.txt  a3.txt  readme.txt
$ echo $?
0
$ ls -y
ls: invalid option -- y
Try `ls --help' for more information.
$ echo $?
1
$ _

프로그램의 성공 여부는 위와 같이 확인할 수 있으므로, 우리는 DevelFilterSkeleton에서 다룬 일반적인 관습에 따라 에러 메시지를 출력해야 합니다. 보통 많은 shell script 개발자들이 에러 메시지를 출력할 때, 가장 많이 쓰는 방법은 다음과 같이 하는 것입니다:

echo "error: description"
exit 1

그러나 이 방식은 필터 프로그램이 갖춰야 할 몇 가지를 어기고 있습니다. 첫째, 프로그램 이름을 출력하지 않았습니다. 둘째, echo는 표준 출력으로 받은 인자들을 출력하기 때문에, 메시지는 표준 에러로 출력한다는 관습을 어겼습니다. 그러므로 우리는 이 두 가지를 해결하는 방법에 대해 알아보려고 합니다.

먼저 항상 우리 프로그램 이름을 다음과 같은 변수에 저장해 놓기로 합시다:
program_name='foo'
참고, 변수 $0에 담긴 이름을 직접 쓸 수도 있지만, 이 이름은 shell에 따라서 다르게 저장될 수 있습니다. 예를 들어 "/usr/bin/foo"가 될 수도 있고, "foo"가 될 수도 있기 때문에, 일관성을 유지하기 위해, 따로 저장해 놓는 것이 좋습니다.

그리고 나서 어떤 메시지를 출력하려고 할 때, 다음과 같이 할 수 있습니다:
echo "$program_name: description"
exit 1

아직까지는 모두 표준 출력으로 메시지가 처리되었습니다. 이제 이 에러 메시지를 표준 에러로 보내려면 다음과 같이 하면 됩니다:

echo "$program_name: description" 1>&2
exit 1

이제 이 기능을 편리하게 쓸 수 있도록 함수로 만들어 등록합니다:

function error() {
    echo "$program_name" "$@" 1>&2
    exit 1
}

그런데, 상황에 따라서 경고 메시지를 출력하게 하려면, 위와 같이 정의한 error()를 쓸 수 없습니다. 왜냐하면 error()는 바로 exit 1를 불러서 프로그램을 끝내버리기 때문입니다. 따라서 우리는 error()의 첫번째 인자를 exit status로 쓰기로 하고, 이 값이 0이 아닐 경우 exit를 호출하도록 합니다:

function error () {
    status="$1"
    shift;
    echo "$progra_name" "$@" 1>&2
    if [ 0 -ne "$status" ]; then
        exit "$status";
    fi
}

위 함수를 쓰는 예를 보이면 아래와 같습니다:

error 1 cannot open file $INPUT.
error 0 cannot open file $INIT_FILE.

첫번째 예는 "$INPUT"이라는 파일을 열 수 없다고 표준 에러로 메시지를 출력하고 프로그램을 종료합니다. 두번째 예는 "$INIT_FILE"이라는 파일을 열 수 없다고 표준 에러로 메시지를 출력하기만 합니다.

error()는 첫 번째 인자를 exit status로 쓰고 나머지 인자는 모두 표준 에러로 출력하기 때문에, 이 함수의 사용자가 실수하지 않도록, 주석도 추가하고, 변수 program_name이 정의되어 있을 때에만 "$program_name: "을 출력하도록 고치면 다음과 같습니다:

function error() {
    # usage: error EXIT_STATUS MESSAGE...
    # Print MESSAGE to the standard error (stderr) and call `exit EXIT_STATUS'
    # if EXIT_STATUS is nonzero.
    status="$1"
    shift;
    if [ -n "$program_name" ]; then
        echo -n "$program_name: "
    fi
    echo "$@" 1>&2
    if [ 0 -ne "$status" ]; then
        exit "$status"
    fi
}

보통 능숙한 shell 프로그래머들은 위와 같이 "if 조건-문장 then 실행-문장 fi" 문장에서 조건-문장이 참일 경우, 실행-명령을 수행하라는 코드를 "조건-문장 && 실행-문장" 형태로 쓰곤 합니다. 그렇게 고쳐 보면 다음과 같습니다:

function error () {
    # usage: error EXIT_STATUS MESSAGE...
    # Print MESSAGE to the standard error (stderr) and call `exit EXIT_STATUS'
    # if EXIT_STATUS is nonzero.
    status="$1"
    shift;
    test -n "$program_name" && echo -n "$program_name: " 1>&2
    echo "$@" 1>&2
    test 0 -ne "$status" && exit "$status";
}

또, error() 함수는 모든 출력이 표준 에러로 출력되기 때문에, 명령 각각에 "1>&2"라는 redirection을 쓰지 말고, 함수 자체의 표준 출력을 표준 에러로 redirection하는 것이 좀 더 깔끔합니다. 코드는 다음과 같습니다:

function error () {
    # usage: error EXIT_STATUS MESSAGE...
    # Print MESSAGE to the standard error (stderr) and call `exit EXIT_STATUS'
    # if EXIT_STATUS is nonzero.
    status="$1"
    shift;
    test -n "$program_name" && echo -n "$program_name: "
    echo "$@"
    test 0 -ne "$status" && exit "$status";
} 1>&2

1.2. Option Processing

직접 처리하는 것은 귀찮아서 하지 않습니다. Bash에서 쓸 수 있는, 옵션 처리를 도와주는 명령은 두 개가 있는데, 하나는 bash 자체에서 제공하는 getopts이고, 하나는 외부 명령으로 존재하는 getopt(1)입니다.

1.2.1. Using Built-in getopts

Bash에 내장되어 있는 getopts는 짧은 옵션만 처리할 수 있습니다. 먼저 도움말을 출력하고 종료하는 함수인 help_and_exit()가 정의되어 있고, 버전을 출력하고 종료하는 version_and_exit()가 정의되어 있다고 하고, 아래 코드를 보기 바랍니다:

while getopts "hvo:q" opt; do
    case $opt in
        '?')
            error 1 "Try \`-h' for more information"
            ;;
        'h')
            help_and_exit
            ;;
        'v')
            version_and_exit
            ;;
        'o')
            output_filename=$OPTARG;
            ;;
        'q')
            verbose_mode=0
            ;;
        *)
            error 1 "Internal getopts error"
            ;;
    esac
done
shift `expr $OPTIND - 1`

getopts는 부를 때마다 하나의 옵션을 처리합니다. 더 이상 처리할 옵션이 없으면 nonzero 값을, 있었으면 0을 리턴합니다. (따라서 while 문장을 써서 다 옵션을 처리할 때까지 반복해서 불러줍니다.) 첫번째 인자는 받아들일 옵션의 종류입니다. 여기서 한 글자가 하나의 옵션에 해당하며, 옵션이 인자를 받을 경우, 그 옵션 글자 뒤에 ':'를 붙여 줍니다. 따라서 위의 경우 처리하는 옵션은 '-h', '-v', '-q', '-o arg'입니다. getopts의 두번째 인자는, getopts가 처리한 옵션 글자를 저장하는 변수 이름입니다. 이는 나중에 case 문에서 실제 어떤 옵션이 들어왔는지를 처리하는데에 쓰입니다. getopts가 처리할 수 없는 옵션이 들어온 경우에는, getopts의 세번째 인자에 '?'를 넣어줍니다. 위를 보면 알겠지만, 우리는 이 에러를 처리해야 합니다. 옵션이 인자를 가지고 있는 경우, 우리는 OPTARG라는 변수를 통해 그 인자를 알 수 있습니다. 또한 getopts가 모든 작업을 끝냈을 경우, 우리는 OPTIND를 통해서, 옵션이 아닌 인자의 위치를 알 수 있습니다. 위 코드에서 맨 마지막 shift에서 OPTIND가 쓰인 것을 볼 수 있습니다.


1.2.2. Using external getopt

getopt(1)은 대부분 리눅스 배포판에 포함되어 있습니다. (없으면 따로 설치해야 하는데, 아래를 참고하시기 바랍니다. 이 명령이 기본적으로 설치되어 있지 않는 배포판은 없을 것입니다.

  • Gentoo: util-linux 패키지에 있습니다. "emerge util-linux"를 실행
  • Redhat 또는 Fedora Core: util-linux 패키지에 있습니다 "rpm -Uvh util-linux-*.rpm"를 실행
  • Debian: #red TODO: 누가 채워 주세요!!!
  • SuSE: #red TODO: 누가 채워 주세요!!!
  • 그외: [http]여기에서 받아서 설치하면 됩니다.

getopt(1)를 쓰면 C 함수 getopt_long(3)을 쓴 것과 같이, 긴 옵션들을 처리할 수 있습니다.

#red TODO: blah... blah... 음.. 일하기 싫어서 쓰기 시작한 글인데.. 귀찮네요.. :-) 나중에 쓰겠습니다.

1.3. Source Listings


글쓴이는 항상 이 뼈대 코드를 최신으로 만들려고 하고 있습니다. 최신 코드는 글쓴이의 CVS [http]CVS에서 볼 수 있습니다.

1.3.1. Skeleton Source (getopts version)


#!/bin/sh
# -*-sh-mode-*-
# $Id: DevelFilterBashSkeleton,v 1.3 2005/07/13 04:41:16 kss Exp kss $

PATH=/usr/local/bin:/usr/bin:/bin

program_name='skel'
version_string='0.1'

output_filename=
verbose_mode=1


function error () {
    # usage: error EXIT_STATUS MESSAGE...
    # Print MESSAGE to the standard error (stderr) and call `exit EXIT_STATUS'
    # if EXIT-STATUS is nonzero.
    status="$1"
    shift;
    test -n "$program_name" && echo -n "$program_name: "
    echo "$@"
    test 0 -ne "$status" && exit "$status";
} 1>&2


function help_and_exit() {
    cat <<EOF
usage: $program_name [OPTION...] FILE...

  -q        quite mode
  -o FILE   send output to file FILE. If FILE is \`-',
           send output to stdout.

  -h        display this help and exit
  -v        output version information and exit

Report bugs to <cinsky at gmail dot com>

EOF
    exit 0
}


function version_and_exit() {
    cat <<EOF
$program_name version $version_string
EOF
    exit 0
}


# The actual code begins here -------------

while getopts "hvo:q" opt; do
    case $opt in
        '?')
            error 1 "Try \`-h' for more information"
            ;;
        'h')
            help_and_exit
            ;;
        'v')
            version_and_exit
            ;;
        'o')
            output_filename=$OPTARG;
            ;;
        'q')
            verbose_mode=0
            ;;
        *)
            error 1 "Internal getopts error"
            ;;
    esac
done
shift `expr $OPTIND - 1`

echo "output_filename: $output_filename"
echo "verbose_mode: $verbose_mode"

for f in "$@"; do
    echo "arg = $f"
done

1.3.2. Skeleton Source (getopt version)


#!/bin/sh
# -*-sh-mode-*-
# $Id: DevelFilterBashSkeleton,v 1.3 2005/07/13 04:41:16 kss Exp kss $

PATH=/usr/local/bin:/usr/bin:/bin

program_name='skel2'
version_string='0.1'

output_filename=
verbose_mode=1

function error() {
    # usage: error EXIT_STATUS MESSAGE...
    # Print MESSAGE to the standard error (stderr) and call `exit EXIT_STATUS'
    # if EXIT-STATUS is nonzero.
    status="$1"
    shift;
    test -n "$program_name" && echo -n "$program_name: "
    echo "$@"
    test 0 -ne "$status" && exit "$status";
} 1>&2

function help_and_exit() {
    cat <<EOF
usage: $program_name [OPTION...] FILE...

  -q, --quiet        quite mode
  -o, --output=FILE  send output to file FILE. If FILE is \`-',
                     send output to stdout.

      --help         display this help and exit
      --version      output version information and exit

Report bugs to <cinsky at gmail dot com>

EOF
    exit 0
}

function version_and_exit() {
    cat <<EOF
$program_name version $version_string
EOF
    exit 0
}


# The actual code begins here -------------

GETOPT=`getopt -o o:q --long help,version,quiet,output: -n "$program_name" -- "$@"`
if [ $? -ne 0 ]; then
    error 1 "Try \`$program_name --help' for more information"
fi

eval set -- "$GETOPT"

while true; do
    case "$1" in
        --help)
            help_and_exit
            shift
            ;;
        --version)
            version_and_exit
            shift
            ;;
        -o|--output)
            output_filename="$2"
            shift 2
            ;;
        -q|--quiet)
            verbose_mode=0
            shift;
            ;;
        --)
            shift
            break
            ;;
    esac
done

echo "output_filename: $output_filename"
echo "verbose_mode: $verbose_mode"

for f in "$@"; do
    echo "arg = $f"
done

끝 -- [http]신성국




sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2005-07-13 13:41:16
Processing time 0.0167 sec