【シェルスクリプトツール作成の基本】引数指定で動作するシェルスクリプトを自作する


2021/03/28

シェルコマンドを予めファイルに記述しておくことで、高度なシェルスクリプトとして作成しておけば、まさに一生モノの財産となることでしょう。

今回は自作スクリプトの基本形となるスクリプトのテンプレートに関して解説します。


はじめに

当サイトではオフィス業務のComputer-Aidedなハイブリッドな方法を模索し、より効率的なExcel業務を実現したい多忙なオフィスワーカー向けの主にAwkとSedを使うシェル講座です。

シェルスクリプトはどこでもどんなOSでも基本的に使えて、しかも一度使い方を覚えると、Excelと組み合わせて最高に効率の良いオフィスワークツールが作れることでしょう。

合同会社タコスキングダム|蛸壺の技術ブログ


引数を順番に呼び出すスクリプト

まずはあまり実用性が無いのですがもっとも簡単なスクリプト作成例として、コマンド引数を順番で呼び出す方法から見ていきます。

            
            #!/bin/bash

noarg_err() {
    echo "ERROR: must provide both of arg1 and arg2!" 1>&2
    exit 1
}

noinputfile_err() {
    echo "ERROR: not allowed an input file to be empty!" 1>&2
    exit 1
}

if [ -z "$1" ] || [ -z "$2" ]; then
    noarg_err
fi

if [ -z "$3" ]; then
    noinputfile_err
fi

#👇実行したプログラム名(相対フォルダパス付き)
echo "PROGRAM: $0"

#👇実行したプログラム名(プログラム名のみ)
echo "PROGRAM: $(basename $0)"

#👇引数部分の表示
echo "$@"
echo "$*"

#👇引数の数(スペース文字切り)
echo "$#"

#👇引数の個別の読み出し
echo "FILE: $3, ARG1: $1, ARG2: $2"
        
このスクリプトは以下のように利用します。

            
            $ chmod +x my_simple_script.sh
$ ./my_simple_script.sh piyo fuga hoge.csv
./1.sh piyo fuga hoge.csv
PROGRAM: ./my_simple_script.sh
PROGRAM: my_simple_script.sh
piyo fuga hoge.csv
piyo fuga hoge.csv
3
FILE: hoge.csv, ARG1: piyo, ARG2: fuga
        
このシェルスクリプトの挙動を理解する上で、ダラー文字($)で制御される変数は重要になります。

記号

機能

$@もしくは$*

コマンド以降に指定された引数部分を表示

$#

空白文字切りされた位置で引数の数をカウント

$?

シェルが最後に実行したコマンドの終了状態を保持

$$

現在のシェルのプロセス番号を保持

$-

シェルにセットされているオプションを保持

$0

コマンドに指定された文字列をキャプチャ

$!

直前に実行されたバックグラウンドプロセスのプロセス番号を保持

$1 $2 $3 ... $9

各引数に指定された文字列を順番にキャプチャ

setコマンド

シェルのオプションの設定/解除をおこなう。IFS(変数)で区切られたフィールドの取り出しと位置パラメータとして代入する

shiftコマンド

位置パラメータの内容を左に1つシフトする。($1は$2の内容、$2は$3の内容...と左に送られる)・$#の内容も送る度に1つ減少する

もちろんこの単純なスクリプトを作成する方法でも問題なく機能するわけで、気にならないならそのまま使うと良いのですが、コマンドの引数を$1 $2 $3 ...という変数名で引き出すということが、スクリプトを利用するユーザーにとっては引数の数が増えてくるほど段々と苦痛になってきます。

そこで以降ではさらなるシェルスクリプトの使い心地を求めて、オプションで引数を渡す方法を検討してみます。


getoptsを使って引数をオプションにしたスクリプト

引数をオプションとして指定するもっとも知られた方法としては、getoptsというbashのビルトインコマンドを使う方法が挙げられます。getoptsコマンドでは引数として得られた文字をwhileループとcase文で仕分ける方法が伝統的な作法のようです。

またgetoptsは第一引数に
使用したいオプション文字列、第二引数にコマンドに指定されたオプション文字を入れて利用します。その第一引数で、得敵のオプション引数が値を取る場合(-a hogeなど)はコロンを後に付ける(a:など)ことで、OPTARGという名前の専用変数にその値がセットされます。

この変数において、クエッションマーク(?)が返ってきたときは無効なオプションが指定されたことを示しており、breakにてcase文を抜けさせるか、help関数を作成しておいて利用方法を示しながら一旦プロセスをexitさせる処理が定石です。

とりあえず説明文だけでは理解しづらいので、以下のような具体例を示します。

            
            #!/bin/bash

usage_exit() {
    echo "USAGE: $(basename $0) [-1 arg1] [-2 arg2] [-h help] [input_file]" 1>&2
    exit 1
}

noarg_err() {
    echo "ERROR: must provide both of arg1 and arg2!" 1>&2
    exit 1
}

noinputfile_err() {
    echo "ERROR: not allowed an input file to be empty!" 1>&2
    exit 1
}

while getopts 1:2:h OPT; do
    case $OPT in
        1 ) ARG1="$OPTARG"
            ;;
        2 ) ARG2="$OPTARG"
            ;;
        h ) usage_exit
            ;;
        \? ) usage_exit
            ;;
    esac
done

#👇①不要となったオプション部分をshiftコマンドで切り捨て
shift $((OPTIND - 1))

if [ -z "$ARG1" ] || [ -z "$ARG2" ]; then
    noarg_err
fi

if [ -z "$1" ]; then
    noinputfile_err
fi

echo "FILE: $1, ARG1: ${ARG1}, ARG2: ${ARG2}"
        
またオプションありの引数とオプション無しの引数を混合して使う場合には、上のスクリプトでの①の箇所で、オプションあり引数の数($OPTIND)を考慮してshiftすることで、オプション無し引数が$1...としてスクリプト内の変数としてキャプチャされます。

これを実行してみます。

            
            $ chmod +x my_interactive_script.sh

$ ./my_interactive_script.sh -h
USAGE: my_interactive_script.sh [-1 arg1] [-2 arg2] [-h help] [input_file]

$./my_interactive_script.sh -1 piyo -2 fuga hoge.csv
FILE: hoge.csv, ARG1: piyo, ARG2: fuga
        
getoptsコマンドを使用したスクリプトでは引数を-a hoge -b piyo -cなどのようにオプションに渡された値が分かりやすくなりましたので、上記で示したシンプルなスクリプトの場合より断然使いやすくなりました。

ですが、getoptsコマンドには以下の項目に示す弱点があり、場合によっては完璧とは言えません。

弱点① ~ ロングオプションが作れない

たとえば、シェルコマンドにおいてプログラムのバージョンを表示させたい場合に、--versionとするとほとんどのプログラムでバージョン情報を確認できます。場合によっては--versionは、-v-Vでショートカットできるわけですが、前者をロングオプション、後者をショートオプションと呼んでいます。

そして残念ながらgetoptsコマンドでは、ショートオプション形式でしか扱えません。

なので、
--verbose--versionの2つのオプションを実装してみたいという要望があっても、-vを奪い合うしか選択肢が無くなってしまいます。

弱点② ~ オプションを指定する順序に制限がある

たとえばgetoptsでオプションありの引数とオプション無しの引数を混合して使う場合、

            
            $ ./hoge.sh -1 piyo -2 fuga -3 mofu awesome1.csv awesome2.csv
        
として先にオプション有りの引数を指定しておいて、後付でオプション無し引数を取るような作法でコマンドを書くことになります。

当然、以下のコマンドはNGです。

            
            $ ./hoge.sh awesome1.csv -1 piyo -2 fuga awesome2.csv -3 mofu
        
つまりはコマンドオプションを柔軟に使用できないという制限があるのを理解して使わないといけません。


引数の解析を自前でカスタマイズする方法

究極的に、自由度の高い引数の指定ルールを組み込んだ自作スクリプトを作成するには、引数部分を$@で引き込んでパースするルールをカスタマイズする処理を行うようにします。

            
            #!/bin/bash
PROGNAME="$(basename $0)"

usage() {
cat << EOS >&2
Usage: ${PROGNAME} [--hoge] [-1, --fuga [VALUE]] [-2, --piyo VALUE]
    A sample script of parsing on bash.
Options:
    --hoge        A single option.
    --fuga        A option with optional value.
    --piyo        A option with required value.
    -h, --help    Show usage.
EOS
    exit 1
}

#👇オプションのパース結果の格納
PARAM=()

for opt in "$@"; do
    case "${opt}" in
        #👇ヘルプ表示は単独で利用
        '-h' | '--help' )
            usage
            ;;
        #👇オプションに指定の値が無い場合
        '--hoge' )
            HOGE=true; shift
            ;;
        #👇オプションに指定する値を任意にする場合
        '-1' | '--fuga' )
            FUGA=true; shift
            if [[ -n "$1" ]] && [[ ! "$1" =~ ^-+ ]]; then
                FUGA_VALUE="$1"; shift
            fi
            ;;
        #👇オプションに指定する値を必須にする場合
        '-2' | '--piyo' )
            if [[ -z "$2" ]] || [[ "$2" =~ ^-+ ]]; then
                echo "${PROGNAME}: Option needs an argument -- $( echo $1 | sed 's/^-*//' )" 1>&2
                exit 1
            fi
            PIYO=true; PIYO_VALUE="$2"; shift 2
            ;;
        #👇オプションの終端を検出(このオプション以降に指定したオプションは値として解釈される)
        '--' | '-' )
            shift
            PARAM+=( "$@" )
            break
            ;;
        #👇上記で定義した'-<文字>'以外のオプションは許容しない
        -* )
            echo "${PROGNAME}: Invalid option detected -- '$( echo $1 | sed 's/^-*//' )'" 1>&2
            exit 1
            ;;
        #👇'-'無しのオプションは値としてキャプチャされる
        * )
            if [[ -n "$1" ]] && [[ ! "$1" =~ ^-+ ]]; then
                PARAM+=( "$1" ); shift
            fi
            ;;
    esac
done

#👇オプション無しの値を使う場合の処理
INPUT_FILE="${PARAM}"; PARAM=("${PARAM[@]:1}")
[[ -z "${INPUT_FILE}" ]] && usage

#👇無効なオプションがある場合にusageで抜ける
if [[ -n "${PARAM[@]}" ]]; then
    usage
fi

#👇結果を出力
cat << EOS
HOGE > ${HOGE:-false} : FUGA > ${FUGA:-false} : PIYO > ${PIYO:-false}
FUGA_VALUE > ${FUGA_VALUE} : PIYO_VALUE > ${PIYO_VALUE}
INPUT_FILE < ${INPUT_FILE}
EOS
        
ではこれをmy_custom_script.shという名前で保存して使ってみます。

まずはヘルプ表示で
-h--helpを試します。

            
            $ chmod +x my_custom_script.sh

$ ./my_interactive_script.sh -h
Usage: my_custom_script.sh [--hoge] [-1, --fuga [VALUE]] [-2, --piyo VALUE]
    A sample script of parsing on bash.
Options:
    --hoge        A single option.
    --fuga        A option with optional value.
    --piyo        A option with required value.
    -h, --help    Show usage.

$ ./my_interactive_script.sh --help
#先ほどと同じ出力
        
では実際にスクリプトを利用するケースをいくつか考えて試してみましょう。

            
            $ ./my_custom_script.sh --hoge --fuga abc --piyo 123 awesome.csv
HOGE > true : FUGA > true : PIYO > true
FUGA_VALUE > abc : PIYO_VALUE > 123
INPUT_FILE < awesome.csv

$ ./my_custom_script.sh awesome.csv --hoge --fuga abc --piyo 123
HOGE > true : FUGA > true : PIYO > true
FUGA_VALUE > abc : PIYO_VALUE > 123
INPUT_FILE < awesome.csv

$ ./my_custom_script.sh awesome.csv -1 abc --piyo 123
HOGE > false : FUGA > true : PIYO > true
FUGA_VALUE > abc : PIYO_VALUE > 123
INPUT_FILE < awesome.csv

$ ./my_custom_script.sh --hoge awesome.csv -2 123
HOGE > true : FUGA > false : PIYO > true
FUGA_VALUE >  : PIYO_VALUE > 123
INPUT_FILE < awesome.csv

$ ./my_custom_script.sh awesome.csv --piyo
my_custom_script.sh: Option needs an argument -- piyo
        
ここから分かるように、この方法でスクリプトを作成すると、ロングオプションも利用でき、オプションの順序も柔軟な位置で呼び出すことが可能です。


まとめ

以上、自作のシェルスクリプトに引数を与える方法を三通り紹介していきました。

項目が進むにつれて引数を与える自由度が大きくなっていきますが、今度はシェルコマンドの用法や構文にある程度の理解と知識が無いと解読も難しくなっていきます。

ここらへんはご自分の学習深度に併せてどのやり方でシェルスクリプトを組みのかを選択すると良いと思います。

参考サイト

Bashでちょっと凝ったオプションの解析をする

bash によるオプション解析

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。