シェルスクリプトでコマンドを作るまで

最近、俄然、興味が湧いてきたコマンド。もし、欲しい機能に見合うコマンドがなかったら、自分で作るしかない。そのうち作ってみたいという希望もある。調べてみた。

作業環境

基本

#!/bin/sh
echo Hello, World!
  • hello.shに実行権限を与える。
$ chmod +x ~/Documents/hello.sh
  • パスを指定して呼び出せば、Hello, World!と表示された。
$ ~/Documents/hello.sh
Hello, World!

引数の処理

  • 有用なコマンドは、ほぼ間違いなく引数を伴う。
  • 引数で渡されたデータを処理して、価値のある情報を出力してくれるのだ。
  • 引数をちゃんと処理できないようでは一人前のコマンドとは言えない。
  • コマンド処理の中で引数にアクセスするには、専用の変数が利用できる。実験してみた。
#!/bin/sh
echo $0, $1, $2, $3
echo '引数の個数 $# = ' $#
echo

echo 'for item in $@; do echo $item,; done'
echo '=for item in 山田 太郎 伊藤 四郎 鈴木 一郎; do echo $item,; done'
echo '----------結果----------'
for item in $@
do
  echo $item,
done
echo

echo 'for item in "$@"; do echo $item,; done'
echo '=for item in "山田 太郎" "伊藤 四郎" "鈴木 一郎"; do echo $item,; done'
echo '----------結果----------'
for item in "$@"
do
  echo $item,
done
echo

echo 'for item in $*; do echo $item,; done'
echo '=for item in 山田 太郎 伊藤 四郎 鈴木 一郎; do echo $item,; done'
echo '----------結果----------'
for item in $*
do
  echo $item,
done
echo

echo 'for item in "$*"; do echo $item,; done'
echo '=for item in "山田 太郎 伊藤 四郎 鈴木 一郎"; do echo $item,; done'
echo '----------結果----------'
for item in "$*"
do
  echo $item,
done
echo
  • 早速、ターミナルで実験してみた。
$ ./hello.sh '山田 太郎' '伊藤 四郎' '鈴木 一郎'
./hello.sh, 山田 太郎, 伊藤 四郎, 鈴木 一郎
引数の個数 $# =  3

for item in $@; do echo $item,; done
=for item in 山田 太郎 伊藤 四郎 鈴木 一郎; do echo $item,; done
----------結果----------
山田,
太郎,
伊藤,
四郎,
鈴木,
一郎,

for item in "$@"; do echo $item,; done
=for item in "山田 太郎" "伊藤 四郎" "鈴木 一郎"; do echo $item,; done
----------結果----------
山田 太郎,
伊藤 四郎,
鈴木 一郎,

for item in $*; do echo $item,; done
=for item in 山田 太郎 伊藤 四郎 鈴木 一郎; do echo $item,; done
----------結果----------
山田,
太郎,
伊藤,
四郎,
鈴木,
一郎,

for item in "$*"; do echo $item,; done
=for item in "山田 太郎 伊藤 四郎 鈴木 一郎"; do echo $item,; done
----------結果----------
山田 太郎 伊藤 四郎 鈴木 一郎,

なるほど!

オプションの仕様

  • 引数には自由にアクセスできるようになったが、まだ課題は残っている。
  • 引数の状態を見て、適切な処理に振り分ける必要があるのだ。(解析)

例えば、実験中のhelloコマンドをもっとコマンドらしくしてみると...

$ hello 山田
hello, 山田

$ hello -m 山田
good morning, 山田

$ hello -e 山田
good evening, 山田

$ hello -e -s さん 山田
good evening, 山田さん
  • -mオプション...good morningで挨拶
  • -eオプション...good eveningで挨拶
  • -s 文字列オプション...指定した文字列の敬称を添付

以上のような仕様のコマンドにしたい。

オプションの解析

  • しかし、考えてみると、オプション指定の方法は実に様々だ。
    • パラメータが続くものがあったり、1文字オプションが連続(例:ls -al)していたり、あるいは単語のオプションかもしれない。
    • もしかしたら、ユーザーが間違えている可能性もある。
  • そうゆう可能性すべてに対応するのは、結構な手間がかかりそう。
  • しかし、コマンドを作る上では誰もが避けては通れない課題である。
  • そして、誰もが避けて通れないとなると、そこには達人が作ったシンプルな仕組みがあったりする。
getoptsコマンド


例:getopts mes: OPTION

mes: オプション文字列
OPTION 変数名称

  • 呼び出すごとに、引数$@を解析して、オプションであればtrueを返す。
  • 解析された1文字オプションは、指定した変数名称の$OPTIONに代入されている。
  • 値が必要なオプションでは、$OPTARG に値(オプション引数)が代入されている。
  • 解析するのはオプションとオプション引数のみで、それ以外の引数を発見した時点でfalseを返す。
  • $OPTIND には解析中の引数$@の位置が入る。falseを返した終了時点で、$OPTIND は非オプションな引数を指している。
  • オプション文字列
    • すべての1文字オプションを連続表記する。
    • 値を取るオプションには、次に":"を続ける。
    • 例のmes:であれば、"-m -e -s 値"というオプション表記を解析してくれる。
実装

注目したいキーワード:getoptsコマンド、変数 OPTARG、変数 OPTIND

#!/bin/bash

# 引数解析
COMMAND=`basename $0`

while getopts mes: OPTION
do
  case $OPTION in
    m ) OPTION_m="TRUE" ;;
    e ) OPTION_e="TRUE" ;;
    s ) OPTION_s="TRUE" ; VALUE_s="$OPTARG" ;;
    * ) echo "Usage: $COMMAND [-m | -e] [-s suffix] name ..." 1>&2
        exit 1 ;;
  esac
done

shift $(($OPTIND - 1)) #残りの非オプションな引数のみが、$@に設定される

# オプション処理
if [ "$OPTION_m" = "TRUE" ]; then
  SENTENCE='good morning'
  echo '  -m オプションを認識!'
fi

if [ "$OPTION_e" = "TRUE" ]; then
  SENTENCE='good evening'
  echo '  -e オプションを認識!'
fi

if [ "$OPTION_s" = "TRUE" ]; then
  SUFFIX=$VALUE_s
  echo "  -s=${VALUE_s} オプションを認識!"
fi

# 出力処理
for arg in "$@"
do
  echo "${SENTENCE:-hello}, $arg${SUFFIX}!"
done
exit 0

出来上がったコマンドを試してみた。

  • 順調!
$ cd ~/Documents
$ ./hello.sh '山田 太郎' '伊藤 四朗' '鈴木 一郎'
hello, 山田 太郎!
hello, 伊藤 四朗!
hello, 鈴木 一郎!

$ ./hello.sh -m '山田 太郎' '伊藤 四朗' '鈴木 一郎'
  -m オプションを認識!
good morning, 山田 太郎!
good morning, 伊藤 四朗!
good morning, 鈴木 一郎!

$ ./hello.sh -e -sさん '山田 太郎' '伊藤 四朗' '鈴木 一郎'
  -e オプションを認識!
  -s=さん オプションを認識!
good evening, 山田 太郎さん!
good evening, 伊藤 四朗さん!
good evening, 鈴木 一郎さん!
  • 1文字オプションは連結させてもOK(パラメータがない場合)
$ ./hello.sh -es さん '山田 太郎' '伊藤 四朗' '鈴木 一郎'
  -e オプションを認識!
  -s=さん オプションを認識!
good evening, 山田 太郎さん!
good evening, 伊藤 四朗さん!
good evening, 鈴木 一郎さん!
  • 規定外のオプションはエラー
$ ./hello.sh -aes さん '山田 太郎' '伊藤 四朗' '鈴木 一郎'
./hello.sh: illegal option -- a
Usage: hello.sh [-m | -e] [-s suffix] name ...
  • 設定がコンフリクト*1する場合は、シェルスクリプト中の実装により、結果は異なる。
  • 今回のhelloコマンドは、-eオプションの方が常に後で処理されるので、優先された。
$ ./hello.sh -em '山田 太郎' '伊藤 四朗' '鈴木 一郎'
  -m オプションを認識!
  -e オプションを認識!
good evening, 山田 太郎!
good evening, 伊藤 四朗!
good evening, 鈴木 一郎!


以上で、1文字オプションについては概ね扱えるようになった!

  • さて、複数文字(long)オプションの英単語については、どうやって扱うべきだろう?(sh、bash環境で)
  • 複数文字(long)オプションを扱えるgetoptsのような仕組みがあれば良いのだが。

*1:衝突