読者です 読者をやめる 読者になる 読者になる

あ、それ包んでください。

tech shellscript

今日も、始めて知って悶絶した。な系の話w

shellscript は、
test コマンド使っての文字列長の評価について。

The
The "Library" / quinn.anya

シェルスクリプト(sh)で、ある変数が空文字列かどうかを調べるには、testコマンドの -n (Nonzero length) または -z (Zero length) を使う……と書いてあるんだけど、コツがいる。

if [ -n $HOGE ]; then
echo "nonzero だよ。"
fi
↑これはダメ

if [ -n "$HOGE" ]; then
echo "nonzero だよ。"
fi
これならOK。

test コマンド、-n やら -z オプション使って文字列長を正しく評価したいなら、評価対象はクォートするべき*1。とゆ事を学んだ。今日。
特にそれが変数とかの場合。

要は、変数が文字列埋め込みされてないと上手く動かない。何故か?はよくわからない。(シェルに於ける変数の概念はどういうものなのか?)

まったくだっ。

実に地味な話なのもあり、中々情報に辿り着けず。*2

さらにチラチラ見えてきているその原因が、あまりのイージーさ故、すんなり受け入れられなかったのも助け、変な遠回りをしたような...で、2、3 週した感じで上述引用のエントリーで、「あぁ、これでいいんだ」と。

迎えたオチがオチだけに、ちょいと疲れました。

あ。
ちなみにダブルクォートで囲むの "何故か" は、こちらで述べられてました。

""で囲む事によって比較する型が合わなくなる事を防ぐそうだ。

そういうことなのですね。

背景

コマンドの終了ステータスは、0 正常終了 を返すけど、さらに出力がブランクの場合も捕えたいと思ったのです。

シェルスクリプトに当該の処理を追加する。
test コマンドで -n オプションをかまし、出力された答えの文字列長が 0 より大きい場合のみ処理を先に進めるようにしました。

で、試してみると、何とも、想定した振る舞いをしてくれていない様子。

見てみると、出力結果は確かにブランクなので文字列長は 0。
なので test -n で弾かれるはずなのに、なぜか違う処理の方に入ってしまう。

ターミナルで、チョロチョロ試してみると...

% var=""
% echo "var:$var(${#var})"
var:(0)
% if [ -n $var ]; then echo "not blank"; else echo "blank"; fi
not blank
% if [ -z $var ]; then echo "blank"; else echo "not blank"; fi
blank

ここで、あれっ?!、思い。

% var="xyz"
% echo "var:$var(${#var})"
var:xyz(3)
% if [ -n $var ]; then echo "not blank"; else echo "blank"; fi
not blank
% if [ -z $var ]; then echo "blank"; else echo "not blank"; fi
not blank

またまた、あれっ?!。
-z オプションは正しく判定できて、-n は判定できない?

-z 使えってこと?

え? ブランクを検証するには -z が推奨とかってあったりすんの?

じゃ、-n って何ってなのかしら?
もう何が何だか分かんない。

つか、そんな仕様だとしてきもてぃわるすぎるんだけど
きもてぃわるいのがそれならそれでハッキリさせたい、と思い地味に調べて見る事にしました。

がこの始まり。

ちゃんと見ると言及しているとこはある

自分が如何に気にしていないか、ちゃんと読んでいないか、ということだったみたいです。

最初は「なかなかないなぁ」と思っていたのですが。

上述冒頭の引用以外で、
以下に関連すると思った情報、書き留めておきたいと思います。何と言うか、自戒をも込めてw。

まずはこちら、クオートすることを一番最初に意識し始めた記述です。;

testコマンド
:(略
※ 第一引数は "で括ること。
(第一引数が[空|未定義]のとき、エラーになるため)

if [ "$1" = "xxx" ]; then echo $1; fi

変数は二重引用符で囲む。下記の例のように変数を二重引用符で囲まない場合、変数に何らかの値が設定されていれば正常に動作するが、変数に値が設定されていないとエラーとなる。

if [ $1 = "xxx" ]; then echo $1; fi

if [ "$var1" = 'test' ];
: (略
このとき、変数参照をすべて "-" で囲むのがコツである。これは変数が未定義のために展開されないと、test コマンドに対する引数が不足してしまうからである。注意されたい。

文字列比較演算子

比較対象の文字列はダブルクォーテーションで囲んでおかないと動作しない、と思われる。
if [ -n "${VAL}" ] ; then
:
:
if [ ${VAL} = "Hello" ] ; then

注意:変数を[ ]の判定式で使用する際には、 " "でくくる必要があるので注意せよ。(変数の中身が空の場合、 " "でくくっていないと、比較演算ができないというエラーが表示 されるため)

また、空文字じゃないかどうか調べる際に、「-n 変数」という書式になるわけですが、変数をダブルクォーテーションでくくらないといけません。
-z (空文字かどうか)調べるときも同じです。

といったことで、結構、きちんと言及されていたりする。
自身が知らないだけの話 orz

ちなみにコマンド置換もやれる

今回、これが本当にやりたかったことなのですが、
コマンド置換の標準出力を直接ぶつけることだったりします。

一応、ここにメモさせておいてください。;

bash-3.2$ type cd
cd is a shell builtin
bash-3.2$ type -p cd
bash-3.2$ if [ -n "$(type -p cd)" ]; then echo "not blank"; else echo "blank"; fi
blank

コマンドがあれば、そのパスを穫る。
なければ、あるいはパスを穫れなければエラーとする、と言った感じ。*3

変数のブランク判定はパラメータ展開でも

最後に、パラメータ展開 Parameter Expansion 使ってよりスマートに描くことも可能であることを知ったのでメモ。

シェルでパラメータの空文字チェックを行う際、今まではif文を使ってこんな感じでやってました。
:(略

echo ${aaaa:?"空文字です"}

こうすると変数aaaaに値が入っていない場合は、標準エラー出力に?以降のメッセージが出力されます。

パラメータ展開については、以前、変数のネストに関するエントリーでも触れましたが、
ここでやっと使うことの意味が分かりました(笑

でも上述したような、コマンド置換はだめみたい? ;

bash-3.2$ echo ${$(type -p cd):?"error"}
bash: ${$(type -p cd):?"error"}: bad substitution
bash-3.2$ echo ${"$(type -p cd)":?"error"}
bash: ${"$(type -p cd)":?"error"}: bad substitution

似たようなので、{aaaa:-"空文字です"} と、- ハイフンを使う方法がありますが、
今回の目的ですと、? を使った ${aaaa:?"空文字です"} になりますかね。

なぜならば、aaaa空白であればvalueを「標準エラー出力」に出力し、シェルスクリプトの実行を終了するから。

ちなみに を使った {aaaa:-"空文字です"} の方は同様のケースでも、終了ステータスは 0 となる。

なるほど。...でいいのかな?(笑

おしまい。

*1:正確には "した方が良い" なのですが、今回体験したリスクと、結局全部付けても良い、付けた方が確実かも、となるのならば使い分けるのも面倒ですし(笑、個人的には must としました。

*2:どう調べたら良いか、少々途方に暮れ。キーワードは、'shellscript test "-n" 文字列 長さ 0' 辺りだったような気がします。

*3:ここでは、ビルトインコマンドなどは対象とせず、ユーザーがインストールするようなコマンドを前提にしていたりしますので。