Release: 2008/04/24
Update: 2012/03/02
UNIX & Linux コマンド・シェルスクリプト リファレンス > シェルスクリプト Tips
ここに書かれている内容は、あくまで筆者の好みでありほぼ完全に主観ではあるが、経験的に行き着いたスタイルでもあるので推奨します。
最近はあまり使用されることもないのかもしれないが @(#) の記述と、 スクリプトの使用方法、および概要をファイルの前方にコメントとして記述しておくようにする。
※ @(#) は what コマンドで参照する情報を記述するための記号です。詳細はこちらを参照。
#!/bin/bash # # @(#) hoge.sh ver.1.0.0 2008.04.24 # # Usage: # hoge.sh param1 param2 # param1 - パラメータ1です. # param2 - パラメータ2です. # # Description: # hoge.shスクリプトです. # ########################################################################### # ここから処理を記述します. echo "start..."
半角スペース4つが理想ではあるが、シェルスクリプトはパイプ処理があるため、 横に長くなりやすい。そのためインデントは少なめに取る。
{ touch hoge ls hoge rm hoge }
| は前後に1スペース空ける。
hoge | fuga
> は前に1スペース空ける。
hoge >hoge.txt
>> も同様に前に1スペース空ける。
hoge >>hoge.txt
< 、<< も > と同様に前に1スペース空ける。
hoge <hoge.txt hoge <<'__EOT__'
※ ちなみに、| や > 、>> および <、<< は前後にスペースが全くなくても文法エラーにはならない。
then を if の後ろに記述すると if と fi が揃うので読みやすい(if と fi は対応する単語という意味で)。 then が if の直下行にあると、可読性の低下と行の無駄遣いにしかならない。
if [ "$str" = "hoge" ]; then echo "strはhoge." fi
if の then 場合とは異なり、do は for、while の直下行に記述する。 その方が do と done が揃うため可読性が高い(do と done は対応する単語という意味で)。
for i in 1 2 3 4 5 do total=`expr $total + $i` done while [ $total -lt 10 ] do total=`expr $total + 1` done
関数は function Hoge()
と定義するが、このとき function は省略可能だ。
function を省略しても可読性が低下したと感じることはなかったので、原則省略とする。
※シェルスクリプト初心者は function を明示的に指定されている方が分かりやすいようなので、 筆者は初心者に配慮して最近は function は省略しないようにしている。 初心者が参照する可能性がないのであれば、原則省略がよいと思う。 いずれにせよ、明示するか省略するかどちらか一方に統一すること。
Hoge() { echo "関数 Hoge() がコールされました." return 0 }
コマンドが成功した場合のみ次のコマンドを実行する && を使用した応用で、
[ $cnt -eq 10 ] && exit 0
といったように if 文に相当する処理を簡素に記述することが可能である。
しかし、そのルートに対して処理を追加したい場合には、結局 if 文で記述することになる。 また、if 文を省略していることにより処理の分岐箇所がわかりにくくなる弊害もあり、簡素に記述できること以外のメリットがあまりにも薄い。
if [ $cnt -eq 10 ]; then # あとでここに処理を追加する可能性がある. exit 0 fi
連続した複数コマンドの実行結果の出力先が同一ファイルである場合は、 {} でコマンドのグルーピングを行い、そのグループの出力をファイルへリダイレクトする。
echo "hoge" >>logfile.log
のように、各コマンドごとにリダイレクトは行わないようにする。
詳細は「複数コマンドのリダイレクトとパイプ」を参照。
{ echo "hoge" echo "fuga" echo "foo" echo "bar" } >>logfile.log
この Tips は実践すると突飛な処理になるため非推奨ですが、ヒアドキュメントの理解を深めるため、 また応用力向上のために一読しておくことをおすすめします。
シェルスクリプトで複数行をコメントアウトする場合は、
対象となる各行の先頭に # を記述する必要がある。
C 言語、等の /* コメント */
に相当する、複数行を一括してコメントにする機能は存在しない。
しかし、ヌルコマンドとヒアドキュメントを応用することで、複数行の一括コメントアウトが可能になる。
: (ヌルコマンド)は一切の処理を行わずに終了するコマンドで、 コメントアウトしたい箇所をこのコマンドへのヒアドキュメントとすることで、 該当箇所の処理を無効化し、結果的にコメントアウトした場合と同様の結果を得ることができる。
: <<'#__COMMENT_OUT__' echo "ここの処理は全て無効になります." # hogeファイルは作成されません. touch hoge # 終了文字をシングルクォートで囲んでいるのでコマンド置換も行われません. dummy=`touch fuga` #__COMMENT_OUT__
ヒアドキュメントは終了文字(※)をクォートもしくはエスケープすることで、ヒアドキュメント内を完全に文字列として扱うことができる。
※ ヒアドキュメントが終了する箇所を識別する文字列、この場合は「#__COMMENT_OUT__」。
したがって、変数の展開やバッククォートによるコマンド置換も発生しない、 つまり一切の処理が行われないただの文字列としてヌルコマンドの標準入力に渡されることになる。
ヌルコマンドは何も処理を行わないコマンドなので、標準入力からの入力があってもこれを無視して終了する。 結果的には一切の処理が行われないことになり、コメントアウトした場合と同じ結果となる。
また、終了文字の先頭に # を使用しているので、コメントアウトを解除したい場合は、 ヌルコマンドの先頭に # を記述するのみでよい。 それによってヒアドキュメントの開始が無効化されると、下方にある終了文字はあらかじめ先頭に # が付けられているため、 自動的にコメントとなる。
シェルスクリプトの変数名には慣習的に大文字が使われることが多い。(筆者の元いた職場がたまたまそうだっただけかもしれない。)
筆者は他の言語でもそうであるように、定数は大文字、定数以外の変数は小文字を使用するようにしている。
変数名が複数の単語からなる場合は「_」で結合した、いわゆるスネークケースを使用する(ex. variable_name)。
また、定数は readonly 宣言を付加して、読み取り専用にすることを推奨する。
# forループ用の変数は非定数なので小文字 for i in 1 2 3 4 5 do echo $i done # readコマンドで処理を一時停止する echo -e "Enter to continue...\c" read dummy
# 定数は readonly 宣言付きの大文字で readonly MAX_COUNT=10 readonly FILENAME="hoge.txt"
非定数の変数名には a や b などの意味のない名前、temp など使用目的がはっきりしない変数名は使用しない。 (temp はつい使いそうになるが、何の temp なのか分からないため可読性が著しく低下する。そもそも変数はもとから全て temp だ。)
メンテナンス性向上のために、ファイル名やディレクトリ名をスクリプト上方で変数として定義し、 それらの指定には変数を使用するようにしている人が大半だと思うが、この命名規則が個人により まちまちであるため(そもそもまったく規則性のない変数名を使用している人もいる)、可読性の低下やそれによるバグにつながることが多い。
筆者はファイル名、ディレクトリ名を格納する変数名には次の命名規則を用いている。
例としてこの命名規則を使用して、ログファイルとその格納ディレクトリを変数で定義すると次のようになる。
# ディレクトリ名 readonly LOG_DIRNAME="hoge" # フルパス指定ディレクトリ名 readonly LOG_DIR="/tmp/${LOG_DIRNAME}" # ファイル名 readonly LOG_FILENAME="fuga.log" # フルパス指定ファイル名 readonly LOG_FILE="${LOG_DIR}/${LOG_FILENAME}"
簡潔に説明すると、
となる。
また、各変数を readonly としていることで、処理の途中で値を変更され、思わぬバグが発生することを抑止している。
関数の振る舞いを変更する方法としてはパラメータが一般的だが、 むやみにパラメータ化するとパラメータ処理が複雑化し、可読性の低下やバグにつながる。
パラメータ化するほどでもない細かな振る舞い変更は、変数化して関数外部で定義するとよい。 例として次のような関数を作成してみる。
Message() { _SEPARATOR=${_SEPARATOR:-"-----"} echo "$1" echo "$_SEPARATOR" echo "$2" return 0 }
この関数をそのまま実行してみる。
$ Message hoge fuga hoge ----- fuga
次に第1パラメータと第2パラメータの境界部分の文字列を変更してみる。 変更するには関数呼び出し前に、変数 _SEPARATOR に変更後の文字列を設定する。
$ _SEPARATOR="*****" $ Message hoge fuga hoge ***** fuga
このように変数を使用した関数の振る舞い変更ではパラメータ処理を複雑化することなく、 簡素に処理に変更を加えることができる。
※ もちろん同様の処理を関数の出力に対して sed 等を使用して行ってもかまわないが、この方法の方が簡素であり、変数を一度定義するだけで振る舞いを変更できる点で推奨している。
今回使用した ${_SEPARATOR:-"-----"}
は、変数 _SEPARATOR が使用されていないか、
もしくは設定値が空("")であれば、デフォルト値(-----)を使用するという意味である。
また、変数名先頭のアンダーバーは関数内で使用される変数であることを意味している(筆者の個人的なコーディング規約)。
シェルスクリプトの変数は基本的に全てグローバル変数となるため、関数内で使用される変数で、特に local で宣言されていない変数は、
関数外の変数との衝突を避けるため、変数名の先頭にアンダーバーを付けるようにするとよい。
※ local で宣言した場合は特にその必要はないが、関数内で使用される変数は変数名を一律にアンダーバーで始めるようにした方がよい。
同様の方法で任意の値を変更可能な値とすることができるが、当然多用しすぎるとバグにつながるため、 本当に必要な値だけを変更可能にすること。
「パイプライン処理を制す者は、シェルスクリプトを制す」と、筆者は思っている。 while ループや for ループで行っていた処理をパイプ処理で置き換えられないか、を常に意識しているとレベルアップの近道になる。 (もちろん置き換えられない処理も多いが・・・。)
xargs という便利なコマンドもあるので、興味があれば以下を参照してほしい。
もちろん全く使うなという意味ではなく、無駄にループ処理を使用するな、という意味だ。 例えば次のような場合を考える。
for str in "hoge" "fuga" do echo "$str" >${str}.txt done
たまにこういう書き方をしている人を見かけるが、 問題なのは hoge や fuga がもうこれ以上増えることはないのに、わざわざ for ループを使用している場合だ。
こんな処理にループを使用する必要は全くない。似たような処理が100個、200個なら話は別だが、2~5個程度ならそのまま記述した方が、遙かに可読性が高い。
echo "hoge" >hoge.txt echo "fuga" >fuga.txt
少し増えてもループは使用せずにそのまま記述するべきだ。
echo "hoge" >hoge.txt echo "fuga" >fuga.txt echo "foo" >foo.txt echo "bar" >bar.txt
※ この Tips は比較的大規模なスクリプトを想定しています。
例えば次のような場合を考えてほしい。
for i in 1 2 3 do # 信じられないくらい長い処理 ・・・ for j in "hoge" "fuga" do # 死ぬほど長い処理 ・・・ done done
ループ処理のネストはメンテナンス性が低い上に可読性の面で不利なので、 関数で置き換えられないか検討するとよい。
ループ処理のネストを関数で置き換えると次のようになる。
SubNestedLoop() { # 相当長い処理 ・・・ return 0 } SubNestedLoop 1 "hoge" SubNestedLoop 2 "hoge" SubNestedLoop 3 "hoge" SubNestedLoop 1 "fuga" SubNestedLoop 2 "fuga" SubNestedLoop 3 "fuga"
このようにするとループ処理のネストに比べて、ずいぶん読みやすくなるはずだ。 重要なのは関数の定義を、呼び出し箇所の直上で行うことである。 こうすることでソースを上から下に読んでいくだけで処理を理解することが可能になる。 (※ 呼び出し箇所直上ではなくスクリプト上方に定義すると、呼び出される箇所から上にスクロールして読まなければならなくなる。)
複数箇所から呼び出される関数は、スクリプトの上方に定義するべきだが、特定の場所からしか呼び出されない関数はその直上に定義するとよい。
bash ではパイプで結合したその先がサブシェルで実行される、という仕様(バグ?)が存在している。 先日、シェルスクリプト初心者から、「なぜか変数に値が設定されない」という相談を受けたが、 原因はまさにこの仕様であった。
以前、筆者も同じ問題に突き当たったことがあったため、すぐに原因は突き止められたが、 この仕様が頭に入っていないと、解決できないバグになる可能性が高い。
以下に例を示す。
pipe_bug.sh
#!/bin/bash str="dummy" cat <<'__EOT__' >temp.$$ hoge hoge fuga fuga foo foo bar bar __EOT__ cat temp.$$ | while read line do str="$line" done rm -f temp.$$ echo $str
このスクリプトは、変数 str にあらかじめ文字列「dummy」を設定した上で 、 cat コマンドの出力をパイプで while ループに流し込み、 read コマンドで流し込まれたデータを1行ずつ読み取る。 さらに、読み込んだその値(行)で変数 str の値を上書きしている。
おそらく多くの人はこのスクリプトの実行結果は、 while ループ内で変数 str に最後に設定される値、 すなわち「bar bar」が出力されることを期待すると思う。
だが、実際の実行結果は以下の通りとなる。
$ ./pipe_bug.sh dummy
これは前述の通り、パイプで結合した先の while ループがサブシェルで実行されているためである。 サブシェルで実行されることにより、変数 str は while ループの中と外でスコープが異なっているのである。 そのため、while ループ内で設定された値(ループ変数 str)は、while ループ終了とともに破棄され、 最後の echo コマンドの出力結果はカレントシェルで変数 str に設定した値「dummy」となる。
while ループを抜けるとサブシェルが終了するので、以下のように export しても元の変数が破棄されるため、 結果は変わらない。
※ pipe_bug.sh の while 部分のみ書き換え
cat temp.$$ | while read line do str="$line" export str done
※ export を追加した場合の実行結果。
$ ./pipe_bug_export.sh dummy
export しても while ループ内の変更は反映されないのが確認できる。
ちなみにこの while ループ内では exit も無効となる(正確には exit コマンドを実行してもサブシェルを終了するだけで、カレントシェルは終了しない、という意味)。
※ pipe_bug.sh の while 部分のみ書き換え
cat temp.$$ | while read line do str="$line" exit done
※ exit を追加した場合の実行結果。
$ ./pipe_bug_exit.sh dummy
exit を実行してもスクリプトは終了せず、最後の echo コマンドまで実行されているのが確認できる。
今回のような場合、問題を回避するには、パイプではなくリダイレクトを使用するとよい。
※ pipe_bug.sh の while 部分のみ書き換え
while read line do str="$line" exit done <temp.$$
リダイレクトであれば while ループ内はカレントシェルのままとなるため、 期待通りの結果(「bar bar」が出力される)となる。
実際の実行結果は以下の通り。
$ ./pipe_bug_redirect.sh bar bar
この問題に関しては、以下のサイトに詳しくまとめられている。
© 2008 SUNONE