シェルスクリプトのコーディングルール2014

  • 239
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

以前、シェルスクリプトを書くときに気をつける9箇条という記事を公開しましたが、あれから整理され洗練されてきたのでまとめてみました。今回は「気をつける」というよりも書くときに守っている/意識していることです。

1. 成功した時には何も返さない

簡単なコマンド mkdir を例に説明します。
ディレクトリをつくるときにこのコマンドを使用しますが、タイトル通り、正常にディレクトリの作成が行われれば、ターミナル画面上には何も表示されません。

$ mkdir a
$

一見、これは成功したのか、不成功なのか判断がつきにくいですが、これは UNIX なりの親切さ故の設計なのです。UNIX はパイプとフィルタで対象を操作します。

$ mkdir a
mkdir: a: File exists

また、終了ステータスによる説明にも合点がいきます。UNIX では、コマンドが成功したか否かは、実行後の終了ステータスにより判断することが出来ます。true コマンドは終了ステータス0です。一方で、false は1を返します。これもこの思想に基づいています。成功時は出力なし、エラー時はその旨を述べるというのが UNIX なりの優しさです。

2. 静かなエラー

UNIX のエラー出力は静かです。静かという表現なのは、端的にその原因を告げるだけです。それ以降の操作について UNIX は言及しません。

$ rm dir
rm: dir: is a directory

rm でディレクトリを削除しようとしました。もちろん、rm -r とする必要があります。ですが、UNIX はそのやり方を指示したりはしません。 「rm -r としなさい」とも「rmdir を使いなさい」とも言いません。ただ単に「それはディレクトリです」と言うだけです。これは暗示的に「rm」ではディレクトリは削除できませんよ、としているだけなのです。

これはシェルスクリプトを設計するときも同様です。エラーを表示する際に必要のない情報を出力する必要はありません。例えば、Usage などです。この無駄な出力によってフィルタが難しくなったり、大事な出力が埋れたりするのはナンセンスです。

3. エラーは標準エラー出力に出力する

$ echo "This is an error" 1>&2

このようにきちんと標準エラー出力にエラー出力することで、フィルタやコマンドとしての振る舞いがとてもいい感じになります。上品に。

$ cat error.sh
#!/bin/sh
echo "Echo!"
echo "This is an error" 1>&2
$ ./error.sh >/dev/null
This is an error
$ ./error.sh 2>/dev/null
Echo!

エラーだけを捨てたいとき、標準出力だけを捨てたいとき、それぞれやりやすくなります。これを一緒くたにしてしまうと、標準出力を /dev/null したとき全て流れてしまいます。よってきちんと分けるべきでしょう。

4. フィルタとパイプを意識する

UNIX ツールのほとんどはこの思想のもとに作成されている。データ入力には stdin を使用し、データ出力には stdout を使用するべきである。こうすることで、パイプによるフィルタリング処理がしやすくなる。パイプでフィルタしたデータを受け渡し出来るようになれば、他のツールとの連携が行える。

$ cat pipe.sh
#!/bin/bash
if [ -p /dev/stdin ]; then
    echo "Pipe"
    cat -
fi
$ echo "abc" | ./pipe.sh
Pipe
abc

こうすることでパイプを捕捉し、フィルタコマンドとしての役割を担うことができるようになります。

5. 変数を管理する

多くのプログラミング言語では、変数にはスコープが存在しますが、シェルスクリプトの場合、関数内で宣言した変数もグローバル変数となります(明示的に local コマンドで局所変数化する必要がある)。またシェルスクリプトでは、環境変数も扱うことが可能です。そこで以下のルールで管理すると良いでしょう(自分なりのルールがあればよい)。

  • 環境変数: 全て大文字(export VARIABLE
  • グローバル変数: 大文字から始まる(Variable
  • ローカル変数: 全て小文字(local variable

また、定数は readonly を使用し、大文字で宣言します。

  • readonly HISTORY_FILE="/path/to/hist_file"
  • readonly HISTORY_FILE_NAME="hist_file"

6. 可読性をよく、表記の統一

他の言語でもこれは同じでしょう。しかし、シェルスクリプトでは(個人的に)可読性は低い部類に属すると思います。ほか以上に気を使う必要があります。複数の記述法がある文法は統一するのがいいでしょう。特にユーザ定義関数などでしょうか。
ユーザ定義関数は function Hoge ともfunction Hoge() とも単に Hoge() とすることも出来ます。

また、ブラケットの指定も同様です。

function Hoge() {
    echo "Func"
}

ではなく、

function Hoge()
{
    echo "Func"
}

とすることを推奨します。対応を見やすくするためです。シェルスクリプトの関数表記は様々あります。

  • function Func() {
  • function Func(){
  • function Func ()
  • function Func
  • function Func()
  • Func()

など組み合わせパターンも加味すれば結構な書き方パターンがあります。

  • function func() { /bin/ls -la; }

とワンライナーで書くことも出来ますが、関数内のコマンドの行末のは ; セミコロンが必須になります。

iffor も同様です。書き方が複数あります。

if の場合

if [ -f ~/file ]; then
    source ~/file
fi

if

if [ -f ~/file ];
then
    source ~/file
fi

という書き方も出来ますが、これはお勧めできません。インデント的に iffi の対応をしっかりと見せたほうがいいためです。

for, while の場合

for i in "$@"; do
    echo $i
done

この書き方は推奨されません。if の場合にも書いたとおり、対応をはっきりさせるほうが良いです。1行を余分に使ってでも、dodone の対応を見せたほうがいいでしょう。

for i in "$@"
do
    echo $i
done

7. bash に依存しているのに #!/bin/sh と書かない

普段シェバンとしてシェルスクリプトの冒頭に書くアレですが、#!/bin/sh の実態は環境によってまちまちです。さまざまなシェルのシンボリックリンクになっていることが多いです(Ubuntu では ash 亜種の dash となっている模様)。なので、予期せぬエラーや動作しないといったことが起きる場合があります。bash やその他シェルに依存したスクリプトを書くのなら、それ上での動作を予定しているわけだから、シェバンには #!/bin/bash と書いたほうが良いに決まっています。

Linux ディストリビューションのデファクトと化している Ubuntu では bin/sh/bin/dash になっています。つまり Ubuntu で動かすスクリプトに bash の文法を書いておきながらシェバンは #!/bin/sh だとエラーで動かないという自体も起きかねません。

#!/bin/sh
list=(`ls`)
echo "$list"

これは Ubuntu 環境だと dash であるので

$ ./list.sh
./list.sh: 2: ./list.sh: Syntax error: "(" unexpected

となりうまく実行できませn。これは dash が (…) の表記をサポートしていないためです。
他、 if [ … の書き方(test コマンドの代用 [)もサポートしていないです。これは大きくスクリプトの動作を変えてしまいます。

8. 移植性、ポータビリティ

シェルスクリプト最大の魅力は、ポータビリティにあると思います。C言語によって作られたプログラムはOSやCPUによって挙動が左右され、使用できなかったり、ソースコードの改変が必要になったりします。しかしシェルスクリプトはシェルがあれば動作します。すこしの方言とバージョンさえ気をつければ10年前のシェルスクリプトであっても、他のマシン上で動かすことができます。

それを守るためには最新技術を追いかけないことにあると思います。bash は今 4.x 系ですがこれを載せてないマシンも非常に多いです。しかし 3.x 世代や古いバージョンの bash は広く搭載されているので移植性を考えるのなら最新の文法や技術は使用しないほうがいいでしょう(連想配列など)。

また、BSD 系と GNU 系の問題もあります。Mac の OS X は BSD 系のコマンドを搭載しています。FreeBSD や Solaris なんかもそうです。しかし GNU/Linux は GNU 系(coreutils)のコマンドです。これらについて習熟し、互換性のあるコマンドやそのオプションだけを使用するように注意した方がいいでしょう。

======

ここに書いてあるほとんどは UNIX という考え方 をいう名著の精神に基づいています。

参考文献