よんちゅBlog

― このブログは自分用のメモや日々の問題などを共有するためのものです ―

20121005185841 お知らせ:  2013/07/17 ブログデザインをリニューアルしました。

シェルスクリプトのデバッグは typeset または declare を使うと良いかも

はじめに

つい最近知った便利なデバッグ方法
(長年シェルスクリプトを書いているのに知らなかった。これが常識だったら恥ずかしい…)

シェルスクリプトデバッグでは echo で変数の中身を見るという原始的な方法をよく使うかと思います。
いわゆる プリントデバッグ というやつですね。

もう少し詳しいデバッグが必要な場合は、 set -xset +xデバッグしたい部分を囲むという方法もあります。

今回は プリントデバッグ で使う echo の代わりに typeset or declare を使うと良いというお話です。

プリントデバッグtypeset or declare を使おう

typeset or declare は変数宣言などでよく使うコマンドですが、変数の中身を見るのにも使えます。

echo と比べて何が良いのかというと、変数の中身はもちろん変数名や変数の型も表示してくれ、配列の場合にも良い感じに表示してくれるのです。

わざわざ echo "hoge=$hoge" とか echo "hoge=${hoge[@]}" などとしなくても良い。

typeset or declare はビルトインコマンドなので使用するシェルによって若干の違いがあります。
今回は私がよく使う sh/bashzsh について触れます。
(sh と bash は今回の場合は同じなので区別しません)

sh/bash

bash では、typeset は obsolete となっています。
declare が使用できる環境では declare を使うと良いでしょう。
(typeset の方が馴染み深いかもしれませんが)

## 文字列
$ str='This is a pen.'

$ declare -p str
declare -- str="This is a pen."


## 配列
$ array=('This is' a pen.)

$ declare -p array
declare -a array='([0]="This is" [1]="a" [2]="pen.")'

変数名や型が表示されていて、配列も見やすく整形されているのが分かりますね。

zsh

zsh では typesetdeclare は全く同じです。

## 文字列
$ str='This is a pen.'

$ typeset str
str='This is a pen.'

$ typeset -p str
typeset str='This is a pen.'


## 配列
$ array=('This is' a pen.)

$ typeset array
array=('This is' a pen.)

$ typeset -p array
typeset -a array
array=('This is' a pen.)

# 配列内のデータが多い場合などは以下のようにして改行区切りで表示させると良いでしょう
# j フラグについては後述
$ echo "${(j:\n:)array[@]}"
This is
a
pen


## 連想配列
$ typeset -A associative
$ associative=(key1 val1 key2 val2)

$ typeset associative
associative=(key1 val1 key2 val2 )

$ typeset -p associative
typeset -A associative
associative=(key1 val1 key2 val2 )

zsh だと 型情報なしですが -p オプションを付けなくても表示されるので、タイプ数が少なくてすみますね。

(連想配列の表示はもう少し良い感じに表示してくれても良いのではと思わなくもない。
というか、配列もbashの方が分かりやすいような気も…)

それと、気づいた人もいるかもしれませんが、typeset or declare の出力をそのままファイルに保存して、あとでそのファイルを source することでデータを復元することができます。(sh/bash, zsh ともに可能)
(というか元々こういう用途なのかもしれない)

シリアアライズのようなものですね。

色々と使い道がありそうです。

おまけ - zsh の変数展開のデバッグについて

ここからはちょっと濃い話になります。
zsh を使わない方でも、zsh の凄さが分かるので、処理結果だけでも見てみて下さい。

zsh には、フラグを使った変数展開という機能があります。

とても便利な機能なのですが、デバッグするのが非常に大変です。
見慣れてない人だと、場合によっては発狂しかねません。

そこで今回紹介するのが、変数展開の q フラグです。

実際に例を見てみましょう。

例えば、以下の様な複数行のカンマ(,)区切りのデータがあるとします。

$ data='1,2,3
a,b,c
höge,fuga,piyo'

$ echo "$data"
1,2,3
a,b,c
höge,fuga,piyo

このデータを改行で分割したい。
f フラグを使うと改行で分割できます。

$ echo "${(f)data}"
1,2,3 a,b,c höge,fuga,piyo

これで改行で分割できました。出来てるはずです。

…でもこれじゃ改行は消えてるけど、どこで分割されてるのか分からないですよね。

では、qフラグを使ってみます。

$ echo "${(qqqf)data}"
"1,2,3" "a,b,c" "höge,fuga,piyo" 

分割された部分がダブルコーテーションで囲われて分かりやすくなっていますね。
これが q フラグです。

さらにカンマでも分割したい。
j フラグ(join)を使うと、j:,: のように書いて、要素をカンマ(,)で結合できます (任意の文字でも可)
s フラグ(split)は s:,: のように書いて、要素をカンマ(,)で分割できます (任意の文字でも可)

$ echo "${(j:,:s:,:qqq)${(f)data}}"
"1" "2" "3" "a" "b" "c" "höge" "fuga" "piyo"

いかがでしょう。
非常に分かりやすいですよね。

最後に応用編です。

for文で1個ずつ処理をしたい場合。

$ for val in "${(j:,:s:,:)${(f)data}}"; do echo "Hello $val san."; done
Hello 1 san.
Hello 2 san.
Hello 3 san.
Hello a san.
Hello b san.
Hello c san.
Hello höge san.
Hello fuga san.
Hello piyo san.

行ごとに & カラムごとに別の処理をしたい。

i=0
for row in "${(f)data}"; do
    (( i++ ))
    echo -n "$i: "
    uppers=()
    for val in "${(s:,:)row}"; do
        uppers+=("${(U)val}")
    done
    echo "${(j:, :)uppers}"
done

# 結果 (行番号を入れつつ、大文字に変換してみました)
# 1: 1, 2, 3
# 2: A, B, C
# 3: HÖGE, FUGA, PIYO

※ 上記の例では qqq を使用しましたが、q の数によって使用されるコーテーションが変わります。
q が1つの場合はバッククォート(`)、2つの場合はシングルコーテーション(')、3つの場合はダブルコーテーション()"、4つの場合は $' ' でクォートされます。

このように zsh はとても高機能です。
外部コマンドを使わずとも、シェルの機能だけでこれだけのことが出来てしまいます。
今回紹介したものも、ほんの一部にすぎません。

みなさんも対話シェル (Interactive shell) としてだけでなく、シェルスクリプトでも zsh を使ってみてはいかがでしょうか。

zsh についてもっと詳しく知りたい方は、以下の書籍 "zshの本" が超おすすめです。
zshシェルスクリプトを書かない方でも、zsh ユーザなら絶対に持っておきたい1冊です。
zsh の代名詞である補完関数についても詳しく書かれています。


zshの本 (エッセンシャルソフトウェアガイドブック)

広瀬 雄二 技術評論社 2009-06-17
売り上げランキング : 231694
by ヨメレバ

おわり

最後は zsh の宣伝になってしまいましたが、最低限 typeset or declare については覚えておいて下さい。

それと、zsh 好きな方は是非お友達になりましょう!