POSIX sh

bash 拡張や Linux 固有機能に依存しないで書く、 shell スクリプトのネタ集。

目次


pipe()

pipe(2) のような機能を実現するための shell function

実装

pipe() {
    mkfifo -m 600 /tmp/tmp.$$.pipe
    : >/tmp/tmp.$$.pipe &
    eval "exec $1</tmp/tmp.$$.pipe"
    eval "exec $2>/tmp/tmp.$$.pipe"
    rm /tmp/tmp.$$.pipe
}

利用例

pipe 3 4
echo "The Message" >&4
exec 4>&-
cat <&3          # print The Message
exec 3<&-

説明

pipe(2) は pipe を作成する関数。 sh には残念ながら pipe コマンドがなく、 C と同じことはできない。 また、新たなファイルディスクリプタ (fd) を作成するという pipe の機能は、 外部プログラムでは実現できない。 (開かれた fd を sh に渡す方法がない) この関数は、 FIFO を pipe の代わりに利用することで、この問題を回避する。 pipe と FIFO は open するまでの挙動が違っているが、 open してからファイルを unlink してしまえば、おそらく同じように機能する。

一時的にファイルシステム上に FIFO を作成する必要があるという欠点はあるが、 この関数で機能をだいたい代替できる。 $$ の値はサブシェルの中でも変わらないため、 サブシェルの中で呼び出す場合は競合しないように注意する必要がある。

pipe(2) は空いている fd で pipe を作成してその番号を返すが、 この関数は呼び出し側で利用する fd を指定する。 sh のリダイレクトの書き方を考えれば、このやり方は悪くないだろう。

実装中の : の行は地味に重要である。 FIFO はもう一方の端点が open されるまで open をブロックするため、 バックグラウンドで一方の端点の open を予め呼んでおく必要があった。 & は subshell を生成するため、残念ながらここで open した fd は利用できない。 : の実行が終わり次第 close されることになる。 なお、 FIFO を rw で open した場合の挙動は、 POSIX では未定義である。


パイプの左側コマンドの exit code を使う

実装例

上記 pipe() を使う。
pipe 3 4
(echo "hello, world." 3<&- 4>&-; echo $? >&4) | tr "hw." "HW!" 3<&- 4>&-
exec 4>&-
echo status of left: $(cat <&3)
exec 3<&-

説明

sh では、 pipeline の一番右側のコマンドの exit code が 全体の exit code になる。 しかし、 pipeline の左や中のコマンドの exit code を知りたいことは、 たまによくある。

パイプラインを構成する各コマンドは subshell で実行されるため、 親 shell にその情報を受け渡すには、 プロセス間通信を利用する他ないはずである。 そこで、 pipe (正確には、 FIFO) による通信を利用して exit code を伝達する実装をしている。 (上記 pipe() はこのために作成したものである)

pipeline 中のコマンドの 3<&- 4>&- は、 多くの場合つけなくても害がない。 exit code 受け渡し用の fd がコマンドに渡るのはあまり気分が良くないため、 このようにした。 一方 cat <&3 の前の exec 4>&- はかなり重要である。 cat <&3 の実行前に FIFO のすべての書き込み用 fd を close しておかないと、 cat が停止してしまう。

ちなみに、 左側のコマンドが異常終了したときにスクリプトを終了したいだけなら、 このようにして実現できる: (left-command || kill $$) | right-command


配列

配列は使いたい。でも広く POSIX sh で動く可搬性を捨てたくはない。

実装

apack(){
    while [ $# -gt 0 ]; do
        printf "'"
        printf "%s" "$1" | sed -e "s/'/'\\\\''/g"
        shift
        if [ $# -gt 0 ]; then
            printf "' "
        else
            echo "'"
        fi
    done
}

alen(){
    eval "set -- $1"
    echo $#
}

aat(){
    eval "set -- $1; printf '%s\\n' \"\${$(($2+1))}\""
}

alast(){
    eval "set -- $1"
    eval "printf '%s\\n' \"\${$#}\""
}

afirst(){
    eval "set -- $1"
    printf '%s\n' "$1"
}

apush(){
    echo "$1 $(shift; apack "$@")"
}

aunshift(){
    echo "$(shift; apack "$@") $1"
}

aslice(){
    if [ $# -le 2 ]; then
        eval "set -- $2 $1 0"
    else
        [ $3 -le 0 ] && return
        eval "set -- $2 $1 $(($(alen "$1")-$2-$3+2))"
    fi
    [ $1 -ge $# ] && return
    shift $(($1+1))
    [ $# -ge 2 ] || return
    while :; do
        printf "'"
        printf "%s" "$1" | sed -e "s/'/'\\\\''/g"
        if [ $# -gt $(eval "echo \${$#}") -a $# -ge 3 ]; then
            printf "' "
            shift
        else
            echo "'"
            break
        fi
    done
}

aset(){
    while [ $(alen "$1") -lt $2 ]; do
        set -- "$1 ''" $2 "$3"
    done
    echo "$(aslice "$1" 0 $2)" "$(apack "$3")" "$(aslice "$1" $(($2+1)))"
}

apop(){
    if [ $# -lt 2 ]; then
        aslice "$1" 0 $(($(alen "$1")-1))
    else
        aslice "$1" 0 $(($(alen "$1")-$2))
    fi
}

ashift(){
    if [ $# -lt 2 ]; then
        aslice "$1" 1
    else
        aslice "$1" "$2"
    fi
}

説明

1つの文字列の中に複数の値を閉じ込めることで配列を実現する。 最初の要素のインデックスは 0。 すべての関数は、結果を標準出力から返す。

apack

apack element ...
与えられた各引数を要素とする配列を返す。

alen

alen array
与えられた配列の要素数を返す。

aat

aat array index
配列の指定位置の要素を返す。

alast

alast array
配列の最後の要素を返す。

afirst

afirst array
配列の最初の要素を返す。

apush

apush array element ...
末尾に要素を追加して返す。

aunshift

aunshift array element ...
先頭に要素を追加して返す。

aslice

aslice array from [len]
from 番から len 個の要素のみを取り出して返す。 len を指定しない場合、配列の末尾まで。

aset

aset array index element
配列の index 番の要素を element に置き換えて返す。 配列に十分な数の要素がない場合、空白文字で空きを埋める。

apop

apop array [num]
配列の末尾から num 個の要素を消して返す。 num を指定しない場合、1個。

ashift

ashift array [num]
配列の先頭から num 個の要素を消して返す。 num を指定しない場合、1個。

ローカル変数

(dash でも使えるんだからもう使っちゃっていい気もする local を使わないための) ダイナミックスコープなローカル変数の実装

実装

divert(){
    set -- "$(
        eval "set -- $1";
        while [ $# -gt 0 ]; do
            echo "$1='$(
                eval "printf '%s' \"\$$1\"" | sed "s/'/'\\\\''/g"
                echo \'
            );"
            shift
        done
    )" "$@"
    eval "$(
        shift 2
        while [ $# -gt 0 ]; do
            printf "'%s " "$(printf "%s" "$1" | sed "s/'/'\\\\''/g"; echo \')"
            shift
        done
    ); set -- \$?; $1 return \$1"
}

使用例

myfunc_inner(){
    i=0
    a=
    while [ $# -gt 0 ]; do
        a="$a$1"
        printf "%4d %s\n" $((++i)) "$a"
        shift
    done
}
myfunc(){
    divert "i a" myfunc_inner "$@"
}

説明

第一引数にスペース区切りで "退避" させたい変数を列挙しつつ、 第二以降の引数で関数名とその引数を渡して実行する。

divert 関数は第一引数で渡された変数を退避させてから指定の関数を実行する。 その後、退避した値を復元する。 結果、指定した変数がダイナミックスコースプのローカル変数として振る舞う。 divert 関数自身は位置パラメータしか使わないため、 関数から戻るときに (多分) すべてもとに戻る。


Author: dyknon < dyknon r5f jp >