shell script

Table of Contents

getopt/getopts

引数の処理は、とかく面倒なものだ。でも定型の処理なんでコマンド getopt(1) になっている。普通はスクリプトの中から使うんで、むきだしでの使い道は ほとんど無いだろう。そんな訳なんでhsh(sh)では、組み込みの関数 getopts に なっている。下記はksh(1)からの例になる。

getpots in ksh/sh

while getopts ao: name
do
	case $name in
	a)      flag=1 ;;
	o)      oarg=$OPTARG ;;
	?)      echo "Usage: ..."; exit 2 ;;
	esac
done
shift $(($OPTIND - 1))
echo "Non-option arguments: " "$@"

基本的にはオプションを解析して、相当のフラグを設定するって使用方法に なるんだな。大量に引数が有る場合は便利そう。オプション意外のものは、 自分で、$1, $2 みたいに利用してくれって設計だな。

pickup

前回からの続きで、血圧グラフを表示するアプリをシェルスクリプトにしてる。 通常は直近のデータをグラフ化(医者に提出)するんだけど、 任意の年月日からのグラフも表示出来るようにしてた。この機能があれば、去年の 今頃はどうだったかなと容易に確認できる。

これはもうawkの出番だな。OpenBSDのawkには –csv なんてのが用意されてる。

--csv   Process records using the (more or less) standard comma-separated
        values (CSV) format instead of the input field separator.  When
        the --csv option is specified, attempts to change the input field
        separator or record separator are ignored.

これを使えば楽できそうだけど、汎用性が無くなるので、遠慮しとく。 -F, でセパレータを カンマに設定すれば済む話だから。そう、汎用性大事だよって事です。

そう言えばシェバングを何も考えずに/bin/shにしてるけど、/bin/bash、/bin/ksh、/bin/dash のように、各種のOSで常用されるshellにしたら、今作成中のコードはちゃんと動作するんだろうか?

#!/bin/sh

bldf="./current.csv"      # bld csv source file
bakf="./backup.csv"       # backup file
wd="./$$"                 # work dir
gpc=70                    # gnuplot num of data points

mkdir $wd

pickup() {
    egrep "^......0" $bldf | sed 's/..,/,/' |
    awk -F, -v st="$1" -v lim="$gpc" '{
      if ($1 >= st) {        # $1 is ymd
            print
            n++
            if (n >= lim) exit
        }
    }' > $wd/am.dat

    egrep "^......2" $bldf | sed 's/..,/,/' |
    awk -F, -v st="$1" -v lim="$gpc" '{
      if ($1 >= st) {
            print
            n++
            if (n >= lim) exit
        }
    }' > $wd/pm.dat
}

pickup $1

前回のスクリプトと合体させるので、冒頭部分に共通部分を定義して、それの 確認の意味を兼ねてコード化してみた。

核になるのはawkなんだけど、全体は、egrep .. | sed .. | awk .. > am.dat の 様にパイプライン構成だ。awkの引数の所で、-v awkVar=shVal って形で、sh側の 設定をawk側の変数に引き渡してるのが味噌かな。

vm$ ./pickup.sh 241003
vm$ wc 50238/*
      70      70    1190 50238/am.dat
      70      70    1190 50238/pm.dat
vm$ head -1 50238/am.dat 50238/pm.dat
==> 50238/am.dat <==
241003,112,61,47

==> 50238/pm.dat <==
241003,125,68,59

ちょっと実行。50238ってdirは実行時のプロセスIDだ。尚、$wdを/tmp/に設定 すれば、後処理で削除しなくてもいいかな。OS起動時に通常は、tmpがクリーンアップ されるからね。

set -x

前回ちょろっと書いたコードに継ぎ足した。追加しようとしてるレコードが直近 のそれより新らしくなってる事を確認する部分。挙動が不信だったので、 set -x, set +x で、トレースしてみる。何かLispみたいだな。appendP なんて 最後にPを付けてるものLispっぽい。schemeなら迷わずに?を付ける場面だけどね。

appendP() {
    set -x
    new=`echo "$rec" | cut -b5-8 `
    if [ "$((new))" -le "$((old))" ]; then
        set +x
        echo "seq err" >&2
        return 1
    fi
    old=$new
    echo "$rec" >> $wd/addnew.csv
}

OpenBSDの場合、関数の中まで -x で追跡してくれないので、関数の中で、独自に トレースのon/off を指定した。Linuxでは、sh -x ./nbld … とするだけで、 関数の中まで、分け隔てなくトレースしてくれる(場合によっては、それがウザイ事 もあるけど)。

te$  ./nbld.sh -ir 2510
2510> 105 120 66 60
+ echo 25100105,120,66,60
+ cut -b5-8
+ new=0105
+ [ 69 -le 100 ]
+ set +x
seq err

冒頭に0が付いている数値の文字列は、正しく数値に変換してくれないみたいだ。 調べてみると、冒頭に0が付いていると、8進数と認識すると言う仕様だった。 いにしえのunixなんだな。これには何回か泣かされた事が有ったな。 10#$new で、10進数を強制させるのが、解決方法とか。

almost main

いよいよデータのハンドリングをまとめられる様になった。そうgnuplotが利用する データファイルの作成の事ね。

add_data() {
    old="100"
    while true; do
        get_record $1
        sts=$?
        case $sts in
            0) break ;;                    # fin
            1) appendP ;;                  # ok, append to file
            255) echo "bad data" >&2 ;;    # missing
        esac
    done
    if [ -s $wd/addnew.csv ]; then
        cat $bldf $wd/addnew.csv > $wd/new.csv
        mv $bldf $bakf
        mv $wd/new.csv $bldf
    fi
}

## main here
mkdir $wd || exit 1
if [ $# -eq 1 ]; then
    pickup $1
else
    [ $# -eq 2 ]  &&  add_data $2
    egrep "^......0" $bldf | tail -n $gpc | sed 's/..,/,/' > $wd/am.dat
    egrep "^......2" $bldf | tail -n $gpc | sed 's/..,/,/' > $wd/pm.dat
fi

add_data 関数の中で、caseを利用したけど、これってLispに有る cond だな。 実行節はくどくどと記述しないのが流儀だ。だから、appendP なんて関数を定義 して、細い事は、そちらに任せている。

mainに相当する部分はgetoptsで引数解析がセオリーなんだけど、今回の場合は そこまで必要無い。スクリプトの引数の個数で場合分けしてるだけだ(ちょいと 手抜きしてる)。

後はこの後ろにgnuplotを駆動する部分を追加するだけだな。

完成版

ひとつ注意がある。gnuplotのデータファイルはcvsだよって 教えてあげる事。それには、下記をgnuplot-script中に記述しておく事。

set datafile separator ","

#!/bin/sh

bldf="/home/sakae/current.csv"      # bld csv source file
bakf="/home/sakae/backup.csv"       # backup file
outd="/home/sakae/Desktop"          # output dir for pdf-file
wd="/tmp/nbld$$"                    # work dir
gpc=70                              # data points of gnuplot 

pickup() {
    egrep "^......0" $bldf | sed 's/..,/,/' |
    awk -F, -v st="$1" -v lim="$gpc" '{
      if ($1 >= st) {        # $1 is ymd
            print
            n++
            if (n >= lim) exit
        }
    }' > $wd/am.dat

    egrep "^......2" $bldf | sed 's/..,/,/' |
    awk -F, -v st="$1" -v lim="$gpc" '{
      if ($1 >= st) {
            print
            n++
            if (n >= lim) exit
        }
    }' > $wd/pm.dat
}

get_record() {
    echo -n "$1> " >&2
    read dh hi lo pu dummy  
    [ "$dh" = "fin" ]         && return 0
    [ "$((dh))" -lt   100 ]   && return 255
    [ "$((dh))" -gt  3123 ]   && return 255
    [ "$((dh%100))" -gt  23 ] && return 255
    
    [ "${#dh}" -eq 3 ] && dh="0$dh"    # adj 4 char

    [ "$((hi))" -lt 80 ] && return 255
    [ "$((lo))" -lt 50 ] && return 255
    [ "$((pu))" -lt 40 ] && return 255

    rec="$1$dh,$hi,$lo,$pu"             # real return data
    return 1    # ok
}

appendP() {
    new=`echo "$rec" | cut -b5-8 `
    if [ "$((10#$new))" -le "$((old))" ]; then
	echo "seq err" >&2
	return 1
    fi
    old=$new
    echo "$rec" >> $wd/addnew.csv
}

add_data() {
    old="100"
    while true; do
	get_record $1
	sts=$?
	case $sts in
            0) break ;;                    # fin
            1) appendP ;;                  # ok, append to file
            255) echo "bad data" >&2 ;;    # missing
	esac
    done
    if [ -s $wd/addnew.csv ]; then
	cat $bldf $wd/addnew.csv > $wd/new.csv
	mv $bldf $bakf
	mv $wd/new.csv $bldf
    fi
}

gpscr(){
    cat <<EOF
# blood graph for gnuplot
set datafile separator ","   # data file is cvs format
set encoding utf8
set terminal pdfcairo mono font ",20" size 21cm, 29cm
set output "./" . when . ".pdf"

stats "am.dat" using 2:3
amh = sprintf(" 最高血圧(平均=%.1f 偏差=%.1f)", STATS_mean_x, STATS_stddev_x)
aml = sprintf(" 最低血圧(平均=%.1f 偏差=%.1f)", STATS_mean_y, STATS_stddev_y)
stats "pm.dat" using 2:3
pmh = sprintf(" 最高血圧(平均=%.1f 偏差=%.1f)", STATS_mean_x, STATS_stddev_x)
pml = sprintf(" 最低血圧(平均=%.1f 偏差=%.1f)", STATS_mean_y, STATS_stddev_y)

set grid
set yrange [50:160]
set ytics 10
unset key                   # no label on right top
set xdata time
set timefmt "%y%m%d%H"
set format x "%m/%d"        # m/d/y -> m/d

set multiplot layout 2,1

set title  '起床時: ' . amh . aml . when
plot "am.dat" using 1:2 with lines lt -1, "am.dat" using 1:3 with lines lt -1

set title  '就寝時: ' . pmh . pml . when
plot "pm.dat" using 1:2 with lines lt -1, "pm.dat" using 1:3 with lines lt -1

unset multiplot
set terminal dumb
## end of gnuplot-script
EOF
}

##### main here #####
mkdir $wd || exit 1
if [ $# -eq 1 ]; then
    pickup $1                      # $1 must be yymmdd 
else
    [ $# -eq 2 ]  &&  add_data $2  # $2 must be yymm (don't care $1)
    egrep "^......0" $bldf | tail -n $gpc | sed 's/..,/,/' > $wd/am.dat
    egrep "^......2" $bldf | tail -n $gpc | sed 's/..,/,/' > $wd/pm.dat    
fi

gpscr >$wd/topdf.plt
pdfname=`head -n 1 $wd/am.dat | cut -b1-6`
cd $wd
gnuplot -e when=$pdfname topdf.plt >/dev/null 2>&1
mv ${pdfname}.pdf $outd
##### end of script #####

最初にユーザーが設定すべき変数の定義。gpcはgnuplotに表示すべきデータ数を 指定する。これを7の倍数にしておくと、グリッド線が1週間の間隔になるので プチ見易いぞ。

続いて関数の設定。最後に、それらを組み合わせて作業を実行してる。C言語を やってた時の名残りで、思わずmainなんてコメントしちゃったけど、全く強制 される事では無い。まあ、こんなコメントを書いていると出身がバレてしまうな。

総括

真面目に、長いコードを書いたので、総括と言うか感想をば。

  1. 関数の定義と利用方法が良く理解できた。この関数って、内部定義のコマンド なんだね。引数の受け方($1,$2,…)、return N で、関数の終了コードが 返却できる。呼んだ方では、それを #? で受領可能。
  2. set -x と set + で、範囲を指定してトレース可能。
  3. read a b c とした場合に、実際にaまでしか入力しないと、b,cには0が補填 される。変数の個数以上を入力すると、最後の変数に、集約されちゃう。
  4. 冒頭に0が付く数字文字列は、8進数と理解される。冒頭が0xだと16進数。

原本のgoコードでは、254行だったものが、shell scriptでは、129行だった。 単純に比較は出来ないけど、shellの方が、書いていて気持が良かったぞ。 変な押しつけも無いし、使うライブラリィー内の関数を都度調査する必要も無いし。

shellの方は、常日頃利用してるコマンドの寄せ集めだから、土地勘が有るってのが 大きいと思う。 awk,sed,cut,egrep,cat,echo,cd,read,cp,mv。それに制御機構として、[(test),while, case,if,else,return,exit。いづれも名立たる物ばかりだ。

ドットと取り込み

スクリプトなんで、一度走ったらそれで終了だ。普段はそれでいいんだけど、何かの 時にそのスクリプトを読み込んでデバッグしたい事が有るかもしれない。 そう、元Lisp屋さんの発想ね。そんな時は、ドットコマンドが便利だ。

ad$ typeset
 :
typeset -x USER
typeset -x _
ad$ . ./nbld.sh
ad$ typeset
 :
typeset -x USER
typeset -x _
typeset bakf
typeset bldf
typeset gpc
typeset outd
typeset wd

ドット・コマンドの前後で、変数が増加してる事が確認できる。じゃ、関数は?

ad$ typeset -f
add_data() {
    old="100"
    while true
    do
        get_record $1
        sts=$?
        case $sts in
        (0)
            break
            ;;
        (1)
            appendP
            ;;
        (255)
            echo "bad data" >&2
            ;;
        esac
    done
    if [ -s $wd/addnew.csv ]
    then
        cat $bldf $wd/addnew.csv > $wd/new.csv
        mv $bldf $bakf
        mv $wd/new.csv $bldf
    fi
}
appendP() {
    new=$(echo "$rec" | cut -b5-8 )
    if [ "$((10#$new))" -le "$((old))" ]
    :

こんな具合に、(正しく?)整形した状態で、表示してくれる。これは便利だなあ。

ad$ echo $wd
/tmp/nbld7699

勿論、変数のバインド状況も、簡単に確認できる。もう、これは、 Lispの開発環境と同等ですよ。あるいは、ruby界のirb相当ね。じゃ、その場で 関数も定義できるに違いない。

ad$ add() {
> return $(($1 + $2))
> }
ad$ add 3 4
ad$ echo $?
7
ad$ typeset -f
add() {
    return $(($1 + $2))
}

ならば、完成版でmainとコメントしておいた部分を、関数にしちゃえ。そうすれば、 main一発で、実行できるぞ(それに、ドットコマンドで読み込み時に実行されないし)。

ad$ main 240101
ad$ main

2回目の実行で、kshが終了しちゃったぞ。何でだ? mkdir $wd || exit 1 だな。同一dirを 作ろうとして失敗して、shellを終了。正に書いた通りに動いているな。

ad$ echo $RANDOM
7552
ad$ echo $RANDOM
8846

こんな時の為に、乱数発生器が用意されてた。

main() {
    wd="/tmp/nbld$RANDOM"
    mkdir $wd || exit 1
    :

これで安心できるな。いやいや、まだ安心はできんぞ。

ad$ cd /usr/src/bin/ksh/
ad$ grep RANDOM *.[ch]
jobs.c: /* Ensure next child gets a (slightly) different $RANDOM sequence */
main.c: "eval", "typeset -i RANDOM MAILCHECK=\"${MAILCHECK-600}\" SECONDS=\"${SECONDS-0}\" TMOUT=\"${TMOUT-0}\"", NULL,
main.c: "eval", "typeset -i RANDOM SECONDS=\"${SECONDS-0}\" TMOUT=\"${TMOUT-0}\"", NULL,
table.h:#define V_RANDOM                8
var.c:          { "RANDOM",             V_RANDOM },
var.c: * if the parent doesn't use $RANDOM.
var.c:  case V_RANDOM:
var.c:  case V_RANDOM:
var.c:  case V_RANDOM:

at var.c

case V_RANDOM:
        vp->flag &= ~SPECIAL;
        setint(vp, (int64_t) (rand() & 0x7fff));

はて、同一の番号が出現する確率はどの程度?

dash

Lubuntuで/bin/shってシェバングを指示すると、dashってshellが使われる。 どうして気がついたかと言うと、man shした時、dashが案内されたから。

HISTORY
       dash  is a POSIX-compliant implementation of /bin/sh that aims to be as
       small as possible.  dash is a direct descendant of the  NetBSD  version
       of ash (the Almquist SHell), ported to Linux in early 1997.  It was re‐
       named to dash in 2002.

サイズ的には、こんな違いがある。

sakae@lu:bin$ ls -l bash dash
-rwxr-xr-x 1 root root 1446024  3月 31  2024 bash*
-rwxr-xr-x 1 root root  129784  3月 31  2024 dash*
sakae@lu:bin$ ls -l sh
lrwxrwxrwx 1 root root 4  3月 31  2024 sh -> dash*

dashは小さいのを自慢してた。確かにサイズはbashの 1/10以下。そんじゃ、kshとも 比較。

vm$ ls -l /bin/ksh
-r-xr-xr-x  3 root  bin  797712 Apr 13 23:08 /bin/ksh*

/etc/alternatives/ 経由かと思ったら違ったわい。そりゃそうだ。shはLinuxでも まだ根幹な地位のはず。勝手に変更されちゃまずいので、直リンクにしてるんだな。

そしてkshで発見しちゃった(typesetの結果を眺めていて)RANDOMはdashには無い。 何でも有りすぎてヤバイbashには、有るぞ。 まあ、shellにも個性が有るってのは、Lispが辿ってきた道でも有るわけだけどね。 /bin/dash で、動作する様に作成しておけば、kshだろうとbashだろうと、問題 なさそうだね。

最後にtest

図書館から借りた本を全て読破しちゃったもので、禁断症状が出た。20年前のユニマガ 雑誌をパラパラ。構文解析の例としてtestを取り挙げていた。

コードを読んでみたら、割と小粒だ。気合を入れて読書してみる。それから、複数の 式を記述できるのね。知らなかったぞ。

expression1 -a expression2
        True if both expression1 and expression2 are true.

expression1 -o expression2
        True if either expression1 or expression2 are true.

サポートしてて当然だわな。 やはり、頼りになるのはmanだなあ。

OpenBSD 7.8 予定日

9月18日の発表によると、次のリリース予定日は

ob$ head etc/root/root.mail
From deraadt@do-not-reply.openbsd.org Fri Oct 24 07:08:00 MDT 2025
Return-Path: root
Date: Oct 24 07:08:00 MDT 2025
From: deraadt@do-not-reply.openbsd.org (Theo de Raadt)
To: root
Subject: Welcome to OpenBSD 7.8!

This message attempts to describe the most basic initial questions that a
system administrator of an OpenBSD box might have.  You are urged to save
this message for later reference.

README

取り残されるデジタル弱者 なんて本を読んだ。

これってオイラーの事だな。スマホは持っていません。 SNSなんて、やってません。FaceBook,X,Instagram,Lineって何の事? 全く知りません。これがオイラーのストレスフリーな生活を支えています。

この本によると、年寄の半数は、スマホなんて関係ない、ってかインターネット と無縁な生活を営んでいるそうです。年寄の半分の集団に属するのも悪くは ないですな。細かい文字は見えない、狙ってボタンを押せません。イライラは 血圧上昇で、死期を早めるぞ。

キャッシュレス化が進んでいるそうだ。デビッド決済で、銀行口座から直通で 決済する事も可能らしい。そうなると、ネット経由で、口座確認とかする訳 だな。恐しいこっちゃ。

友人にはATMも利用しない頑固な人がいる。必要な分だけ、銀行の窓口まで 行って、金をおろしてくるそうだ。窓口では有名人で、顔を見せないと、 あの人大丈夫かしらと心配されたりして。これは本人は気付かないだろうけど コミュニケーションだわな。こういうの良いかも知れないね。


This year's Index

Home