初心者向け、「上手い」シェルスクリプトの書き方メモ

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

ここ最近、沢山シェルスクリプトを書くようになりました。

元々あまりシェルスクリプトを書いたこと無かったので、色々と勉強しつつ書いてるのですが、
他のプログラミング言語とはちょっと違って独特なクセというか、発見の度におぉー!ってなることが沢山あって楽しいです。

そんなわけで、最近学んだり参考にした中で特に感動したシェルの上手い書き方をまとめてみます。
きっとまだ知らないこととかもっと上手くやる方法なんかが沢山見つかりそうなので、
もっといいやり方あるよ!って方はコメントください :sparkles:

何もしない : (コロン)コマンド

シェルを書いていた時に非常に欲しかったコマンドがこれ!何もしない!
: というコマンド(?)を利用すると、何もせずに終了ステータス0(つまり正常終了)を返します。
これが様々な事に使える万能コマンドで、これによって面倒なエラー処理を完結にできたり、
入力や出力のリダイレクト元/先として使えたりと、とても重宝します。

コマンドなので引数も受け取ることができたりします(でも何もしない)。
例えば標準出力にも何も返さないので、それを利用してファイルの初期化が簡単にできます。

: > foo.log # ファイルを 0 バイトで上書き

: についてはこの記事の後半でも色々と活躍します。

三項演算子もどき

三項演算子、便利ですよね。でもシェルスクリプトには残念ながら三項演算子がありません。
ですが、同じようなことなら以下のようにしてできます。

# $foo が $bar より大きければ 0 、そうでなければ 1 を $baz に代入する。
[ $foo -ge $bar ] && baz=0 || baz=1

そもそもシェルに於ける [] という条件式は、if 等を使わなくてもこのように単発で実行でき、
その場合は、評価結果が真か偽かによって、終了ステータス 0 か 1 を返すようになっています。
よって、 [] 内の処理が真=成功した場合と、
偽=失敗した場合とで上記のように処理を分けることができるというわけですね。

※ コメントにて、上記の式のような単純な代入式ではなく、条件式より右の記述方法によっては純粋な三項演算子の代わりとはならない可能性があるとのご指摘を受けました。三項演算子の代わりとして使う場合は上記のような単純な代入式等のみで利用すると割り切ると良いと思います。 @FKU さんありがとうございました!

多少冗長かもしれませんが、 if で書くよりずっとマシですね。

ちなみに同じことを if でやろうとするとこうなると思います。

# $foo が $bar より大きければ 0 、そうでなければ 1 を $baz に代入する。
if [ $foo -ge $bar ]; then
  baz=0
else
  baz=1
fi

比べてみるとその差がわかりますね。

変数のデフォルト値を指定

変数を参照した時、もしその変数が未定義だったり、空文字扱いだった場合に、
別の何かをデフォルト値として設定したいという事、プログラミングではよくありますよね。
シェルスクリプトでも、やっぱり同じことがやりたくなって調べた結果、以下の様にするとできました。

# $foo に $bar を代入。但し $bar が未定義だった場合は 0 を代入
foo=${bar:-0}

ちなみに、 - の部分を = に変える事で、 $bar 自身も上書きすることができます。

# $bar が未定義だった場合、 $foo にも $bar にも 0 を代入
foo=${bar:=0}

ちなみに未定義の場合のみ対象としたい場合は、中の : を抜けば良いようです。

@n0kada さんありがとうございました!

実行されている自分自身のディレクトリを取得する

これもかなりよくお世話になります。

シェルスクリプトは、原則自分自身を実行した際のカレントディレクトリが、
そのままシェルスクリプト内のカレントディレクトリとして扱われます。

なので、例えばシェルからシェルを呼び出したい場合等では、相対パスが使いにくいです。
かといって絶対パスにしてしまうと、完全に環境依存となってしまうため、
gitのリポジトリ等により自身の周辺のディレクトリ/ファイル構造が決められているような状況では、
スクリプト自身の絶対パスを、動的に取得したくなったりします。

そこでいつも利用しているのが以下のコマンドです。
どんなシェルにも何も考えずにコピペで使えるのでもはやお決まりのフレーズとなっています。

script_dir_path=$(dirname $(readlink -f $0))

ただBSDやOSX等ではreadlinkの実装が異なるようなので、代わりに coreutils の greadlink (GNU readlink) を利用すると良いそうです。

script_dir_path=$(dirname $(greadlink -f $0))

※ 皆さんのコメントでのご指摘により、上記のような表記に修正させて頂きました。
@yasuhiroki さん、 @hayamiz さん、 @umiyosh さん、ありがとうございました!

解説すると長くなってしまうのですが、 $0 を使って、
実行されたパスからの自身のファイルへの相対パスを取得し、
それを dirname にかけてディレクトリ名だけ取り出したものに対して readlink -f を実行することにより、
シェルスクリプト自身が置かれているディレクトリへの絶対パスがシステムに依存せずに動的に取得できます。

スクリプトが終了/中断されると自動で動く trap

@s-wakaba さんに教えて頂きました!ありがとうございます!

ShellScriptはその性質上、ファイルを作成したり削除したり、何かしらの成果物を作るコマンドを実行させたりすることが多いですよね。

そんな時に面倒なのが後始末…。途中で終了させるような書き方をしたとしても、Ctrl+C 等で終了された時に正しく動いてくれなければ、作業途中のゴミファイルが残ったり、一時的に退避していたようなファイルがそのまま元の場所に戻らなくなっちゃったりして悲しい事故が起こります。

そこで trap コマンドの登場です。
事前にtrap処理を書いておく事で、処理が途中で中断されてしまったりした際にも、必ず中に書いてあるコマンドを後処理として実行させることができるようになります。しかも終了時のシグナルを分けて指定することで、エラーの時はこう、正常終了の時はこう、等といった処理分けもできるようになります!

つまり、たとえスクリプトが Ctrl-C 等で途中終了してしまった場合でも、きちんと後始末できるようになります!

trap "rm /tmp/temporary-file" 0

ちょっと実験してみたところ、 trap は同じスクリプト上では、同じシグナルに対して2つ以上置いても1つしか実行されない?ような気がしますので、設置するときは後処理を一つのコマンドの中に書いてしまうのがいい気がしました(間違ってたら教えてください:bow:)。この時、関数を作ってそれを呼び出すようにしてもいいのですが、自分は文字列内で以下のように書いてしまうのが簡単だと思いました(一応 bash 上では動作を確認しました。)

trap "
  mv /tmp/swap-file original-file
  rm /tmp/target-file
" 0

trap コマンドについてはちゃんとシグナルと併せて理解しておいた方が良いので、参考リンクを置いておきます。
シグナルと trap コマンド - UNIX & Linux コマンド・シェルスクリプト リファレンス

シグナルについてもうちょっと理解したい方は、こちらも参照するとわかりやすいと思います!

Linuxのシグナルまとめ -- ぺけみさお

数だけ見るとたくさんあるのでちょっとげんなりしますが、記事後半の主なシグナルの解説という部分を見て頂ければ多分十分だと思います。

今だけ必要なファイル/ディレクトリを作る mktemp

こちらも @s-wakaba さんに教えて頂きました!ありがとうございます!

mktemp コマンドを利用すると、 適当な名前のファイル/ディレクトリを作る ことができます。
作成されるディレクトリもデフォルトでは /tmp 以下のようで、まさに一時的なファイルやディレクトリを扱うのに最適ですね!
mktemp コマンドは、ランダムな名前でファイル/ディレクトリを作成するのですが、標準出力にその結果を返してくれるので、基本的に サブシェルで実行したものを変数に格納して使う という事になりそうです。

temp_file=$(mktemp) # /tmp 配下にランダムな名前のファイルが作成される。 例: /tmp/tmp.C3N9Ng6IaU
temp_dir=$(mktemp -d) # -d を付けることでディレクトリ作成となる。 例: /tmp/tmp.wDOVMXVcio

これを上の trap コマンドと併用して利用することで、そのシェルスクリプトが動作している間だけ生きているファイル/ディレクトリを作成することができます!

# 一時ディレクトリ/ファイルを作成する。
temp_file=$(mktemp)
temp_dir=$(mktemp -d)

# 後始末を定義
trap "
rm $temp_file
rm $temp_dir
" 0

これでもう、面倒な後始末処理の条件分岐を自前で実装する必要がなくなりますね!

Bash のオプションを活用しよう!

※ コメントでのご指摘により、こちらの項目は shebang ではなく、 set により明示的にスクリプトの先頭に記述する方式へ修正しました。 @magicant さん、 @syohex さん有難う御座いました!

Bash には便利なオプションが多数用意されています。
それらをスクリプトの先頭に set コマンドにより記述しておく事で、
そのスクリプトを実行中は常にそのオプションが効いた状態で実行されるようにしておけば、
今までいちいち書いていたあんな処理やこんな処理を省略できたり、簡潔に書けたりしますので是非活用しましょう。

僕が普段良く使うのは、 set -eux です。
それぞれの意味について説明します。

-e

普通シェルスクリプトを1度実行すると、途中でエラーがあっても止まってくれず、
何がなんでも最後まで処理を続行しようとしてしまいます。

そんな時、実行時に -e オプションを定義しておくと、そのシェルスクリプト内で 何らかのエラーが発生した時点で、
それ以降の処理を中断する
ことができます。

ファイルの作成や消去等、危険なコマンドを肩代わりすることの多いシェルですから、
そういった「取り返しのつかない処理」をやる前に、条件式等で事前チェックを行うと良いですね!

set -e
ls /path/to/file # ls コマンドでファイルが見つからない場合この時点でエラーとなる
cat /path/to/file # 上記でエラーとなった場合この処理が実行される前にスクリプトが中断される。

先ほどの参考演算子の項で説明した通り、条件式はそれ単発で記述すると、
その真偽値によって成功/失敗扱いとなるので、
以下のようにスクリプト内の要所要所に以下のような条件式をおいてあげるだけで、
事前チェック => 偽なら即中止といった処理を実現できます。

set -e

# 〜 中略 〜

[ $hoge -ge $fuga ] 

また、スクリプトの最後を上記のような条件式にすることで、特定の条件を満たしたかどうかで、
そのスクリプト自体の終了ステータスを調整できます。

-u

-u は、未定義の変数に対して読み込み等を行おうとした際に、きちんとエラーとして扱ってくれます。
java でいうところの NullPointerException 的な感じですね!

これを先ほどの : コマンドや -e オプションと組み合わせると、とてもシンプルに引数のチェック等ができます。

set -eu
# 引数が3つ渡されていなければスクリプト実行中止
: $1 $2 $3

たったこれだけで未定義の引数があった場合にエラーで即実行中止してくれます。
スクリプトの先頭付近に何も考えずにとりあえず書いておくだけでも十分効果的です。

もしこれが単純にスクリプト上で $1 $2 $3 を並べただけでは、 $1 がshellのコマンドだと認識されてしまい、
エラーとなってしまうのですが、: の引数として渡すようにすることで、それぞれが上手く数値及び文字列として認識してくれます。
スペースを含む文字列が渡されるような場合は、それぞれの引数を " で囲ってあげることで問題なく処理できると思います。

しかも中止された際のエラーメッセージも、bashが自動的に

./foo.sh: 行 2: $1: 未割り当ての変数です

のようにわかりやすく出力してくれます(上記は日本語の例)。
捗りますね!

-x

最後に -x ですが、これはちょっと他とは方向性が違って、
実行したコマンドを、全て標準エラー出力に出してくれるという代物です。
実行ログと言ったほうがわかりやすいでしょうか?
しかもあくまで標準エラー出力なので、標準出力で何かを期待するようなスクリプトともちゃんと併用ができます!

@melpon さんより、標準出力ではなく標準エラー出力との事だったので修正しました。ありがとうございました!

これをつけておくだけでデバッグ時や開発時にかなり役立ちます。

とりわけこのオプションが真価を発揮するのは、JenkinsによるJobを作成している時に、
内部で呼ばれるシェルスクリプト達
です。

JenkinsはJobのコンソールログとして -x 相当のものを出してくれますが、
外部スクリプトの中まではそれをしてくれません。
外部スクリプトの実行時に bash -x foo.sh 等としても良いですが、
予めスクリプトの先頭に set -x として書いておくと使う側で意識せずとも常に分かりやすいログになって捗ります。

おまけ: -x 指定時に一緒に出力できる魔法のコメントを書く

-x オプションは便利ですが、もう少し欲を出して、出力されるログをもっと見やすくする為に、
実行時に影響を与えないけど標準出力上に現れる魔法のコメントを書いてみます。

といいつついきなり最初に謝っておきますが、実はこれはコメントじゃないです。 :bow:
ただ実際に処理に影響を与えない為、あたかもコメントかのように利用できるただのコマンドです。

何も影響を与えない…何もしない…

勘の良い方はお気づきかもしれませんが、実はここでまたしても : が出てきます。
以下のように : の引数として文字列を渡してあげることで、
ワンラインでも複数行でも思いのままコメントのようなものが書けます。

: ここにコメントが書ける、コンソールに出力される。

: "特殊な記号等を利用するときはクォーテーションで括る(| や & 等)"

: "
こうすれば
複数コメントも書ける。
ちゃんと改行もされて出力される。
"

: コマンドは引数として文字列を受け取りますが、実際受け取った引数をどうするわけでもなく、
無条件にステータスコード0で終了するので、実際には何も処理されていません。
ですが通常のコマンドとは違ってちゃんと一つのコマンドとして成立しているので、
-x オプションの出力結果にはしっかり出力されるというわけですね!

一時的にオプションの効果を無効化したい

set によるオプションは便利ですが、これらをスクリプト中で一時的に無効化したい事がよくあります。
そんな時は、 set +xset +eu 等のように、 - ではなく + を指定してあげると良いです。
無効化したい処理が終わったら、また忘れずに set -eu 等とすれば、そこから元通りになります。

set -eux

# 中略

set +u # 一時的に -u オプションを無効化。

foo=$1 # この $1 は未定義かもしれないが、未定義でもエラーとならない。
echo $foo

set -u # -u オプションを元通り付与し直す。

# 以降の処理

便利ですね!

まとめ

シェルスクリプト…奥が深いッ…!
楽しい…!!