Shellで数値計算もどき【max-最大値コマンド編】オプション解析に四苦八苦

*注意* この記事はQiitaに上げられるほどの信頼性を有していないことからエピソードトークとしてnoteに上がっています

optgetsと道を異にしたときから、決まっていたのだろうか。
それともシェルスクリプトを書き始めたときから、決まっていたのだろうか。
オプション解析に泣かされるということは。

今回のブツはここに置いてあります。
https://github.com/ogawa3427/falsestatistics/blob/main/sbm/max

本体

さて、格納されている整数値を1行ずつ読みだして前行との大小を比較して生き残った値を最後に出力するだけです。蟲毒ですね。

おまけ機能として行数も1000行ごとにしゃべるようにしてます。
これは大した話ではありませんが、1000の倍数を正規表現で書こうとしたら剰余計算であまり0でよくねと教えてくれたGPT-4には感謝しています。

おまけ(オプション解析)

さて、コマンドと切っても切れない関係にあるのがオプションでしょう。コマンドの後にくっついてる`-n`とか`--help`みたいなやつです。
今回用意したオプションは`--`込みで5種類3機能になります。
ヘルプを表示する`-h`と`--help`は第一引数に置くだけで話が比較的単純だったのであんまり気にかけません。

おそらく20時間くらいかけて再発明した出来損ないの車輪たる方針はこれです。
正しい引数の構成になっているか
②それぞれ有効な引数か
③以上を確認し各々のモードに振り分け/エラーを吐く

まずは①を検証すべくこれらありうる引数が現れるパターンをすべて書き出してみました。
最小値モードにするための`-i`,`--min`
以後をエスケープする`--`
入力ファイル名

1であり、0でなしを表す
最終的に行き着く処理は5列目
最大/最小とファイル名指定あり/なしで計4種類ある

最大でも第3引数までしか使わないのでそれより多かった場合はエラーをまず出す。

if [ $# -ge 4 ]; then
  echo "Too many arguments." >&2
  error
fi

以後も引数の数で切り分けることに。
第3引数まで使われた場合は1通りなのですっきりと以下の通り。
(各関数については後述)

# $#=3
if [ $# -eq 3 ]; then
  if [ "$2" = "--" ]; then
    oscreen "$1"
    minfile "$3" #111
  else
    error
  fi
fi

第2引数まで使用する場合は3通りだが、`$2`に`--`が入るとエスケープするファイル名が無くてエラーになるため、まずさようなら。
その後も`--`の位置で容易に場合分けできた。

# $#=2
if [ $# -eq 2 ]; then
  if [ "$2" = "--" ];then
    error #110
  elif [ "$1" = "--" ]; then
    maxfile "$2" #011
  else
    oscreen "$1"
    minfile "$2" #101
  fi
fi

第1引数までの場合はもう単純にif文でどうにかしつつ、1文字目抽出でファイル名とオプションを区別しています。
先代解析機構で1文字目抽出がたくさん使われましたが、1文字目が-(ダッシュ)であることとその文字列が有効なオプションであることが実は同値でないと気付いて泣くのは別のお話です。

# $#=1
if [ $# -eq 1 ]; then
  case "$1" in
    -h|--help)
      helping
    esac
  if [ "$1" = "--" ]; then
    error #010
  else
    if [ "$(echo "$1" | cut -c1)" = "-" ]; then
      oscreen "$1"
      maxpipe #100
    else
      maxfile "$1" #001
    fi
  fi
fi

引数が空っぽの場合の処理も忘れずに書いておきます。

maxpipe

ここまでで①の引数の構成がチェックできました。
言い換えれば第1~第3引数までの間に「強いて言うなら」、内容なし、ファイル名、--、オプションのいずれかが入っていそうだと推定された引数パターン以外はエラーになったということです。
なぜわざわざこんな含みのある言い方をしたかといえば、まだ推定であり確定ではないからです。特に`$#=3,2`のときについては`--`が存在する位置から推測されているにすぎません。

ファイル名だと思っていた文字列が存在しないものかもしれませんし、オプションもハイフンから始めっているだけのでたらめかもしれません。
そこで②の有効な引数であるかの確認が行われるわけです。

3種類の引数をそれぞれ見ていきましょう。

オプションであると推定された引数はoscreen関数に渡されます。

oscreen() {
  case "$1" in
  -i|--min)
    :
    ;;
  *)
    echo "Undifined option." >&2
    error
  ;;
  esac
}

ちゃんと定義されたオプションかどうかcase文でマッチングし、一致すれば次の処理へ通されます。不正なオプションであれば警告を出してエラーを吐き終了します。

ファイル名と推定された引数は最大値/最小値関数に組み込まれたfscreen関数というチェック機構に通されます。

fscreen() {
  if [ ! -f "$1" ]; then
    echo "not a file." >&2
    error
  else
    :
  fi
}

該当のファイルが存在したら次の処理に通し、存在しなければ警告を出してエラーを吐き終了します。シンプルですね。

`--`の場合は少々特殊でif文を使って直接マッチングして場合分けに組み込んでいます。
つまり、そもそも`--`ではない文字列が、`--`の期待されている処理の期待された場所に迷い込む事態がありえないのでチェックらしいことは書いていません。

ゴール

こうして、各変数が望まれた形で入力されそれらが有効であると確認されることで、晴れて最大値/最小値を求める処理が始まるわけです。

①と②、すなわち変数の組み合わせの検証/条件分岐と各変数の正当性の検証を極力別のモジュールにして互いに知らぬ存ぜぬ状態にしたことで(先代と比較して)書きやすく読みやすいコードになったかと思います。

加えて、Shell言語のfunctionで関数を定義できる機能のおかげで分岐先で行われる処理を一言で書けます。これもまた書きやすく読みやすい感じに一役買っていると思います。

深淵(チラッチラッ)

よくわからないんですがおそらく防御的プログラミングみたいな話がこの先にあります。入っちゃいけないところに入っちゃいけないものが入らないようにする。想定外をすべて想定してエラーを準備しておく。全部難しくて困っちゃうわ。
おそらくちゃんとした本がありそうですね。

お読みいただいきましてありがとうございました。

この記事が気に入ったらサポートをしてみませんか?