· 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]신성국


ID
Password
Join
You will attract cultured and artistic people to your home.


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.0088 sec