シェルスクリプトの処理境界が鮮明になる「名前付きブロック記法」なるものを考えてみた
シェルスクリプトは長くなると処理の境界が不鮮明になりがち。
コメントで処理の境界を表現する工夫はよく見かけるが、もっと良い方法はないか考えてみた。
:
コマンド、&&
演算子、複合コマンド()
や{}
を組み合わせて書くと、処理の境界線がはっきりする。
# Install libraries
command
command
command
# Build app
command
command
# Deploy app to server
command
command
: "Install libraries" && {
command
command
command
}
: "Build app" && {
command
command
}
: "Deploy app to server" && {
command
command
}
> シェルスクリプトが長くなるとよく起こる問題シェルスクリプトが長くなるとよく起こる問題
シェルスクリプトは作業を自動化するのに便利なツールですが、記述するタスクが増えるにつれて可読性が下がってしまいます。それでも読みやすさを維持するために、様々な工夫をされているのではないでしょうか。
コメントも工夫の1つです。処理の境界ごとに宣言的なコメントを書いている人も多いとおもいます。たとえば、リリースを自動化するスクリプトなら、おおまかに次のような構成になっているのではないでしょうか?
# Install libraries
command
command
command
# Run tests
command
# Build app
command
command
# Deploy app to server
command
command
# Notify result to Slack channel
command
コメントの部分は一定の粒度の処理を宣言していて、コマンドの部分はその具体的な実装になっているパターンです。実装の部分は、if分岐があったりと実際はもっと複雑なコードになっていると思います。
シェルスクリプトが長くなってくると、どのコマンドからどのコマンドまでがどの処理なのか一見分かりにくくなる問題が出てきます。1
> 名前付きブロック記法名前付きブロック記法
Bashで処理の境界線をはっきり表現できる名前付きブロック記法2というものを考えてみました。
処理の境界線を表現する方法として真っ先に思いついたのが、{}
を使って処理のまとまりを複合コマンドとして表現する方法です。例えば次のように:
#!/bin/bash
# Install libraries
{
command
command
command
}
# Run tests
{
command
}
# Build app
{
command
command
}
# Deploy app to server
{
command
command
}
# Notify result to Slack channel
{
command
}
これだけでも処理がグルーピングされ、だいぶ分かりやすくなります。しかし、これだけだと、コメントとブロックの関係性があまり表現できてない感じがします。そこで、:
コマンドと&&
で説明文と処理を強く関連付けした形に表現しなおしてみます。
#!/bin/bash
: "Install libraries" && {
command
command
command
}
: "Run tests" && {
command
}
: "Build app" && {
command
command
}
: "Deploy app to server" && {
command
command
}
: "Notify result to Slack channel" && {
command
}
いかがでしょうか?処理の説明と処理の内容が、コメントと比べてなんとなく関連しているように見えませんか?また、シンタクスハイライトで説明文がコメントよりも目立っていて、見出し感も出ていますね。
処理の境界をコメントで表現する方法と、名前付きブロック記法を一般化すると次のようになります。
# 処理名
処理...
: "処理名" && {
処理...
}
> 名前付きブロック記法の6つのメリット名前付きブロック記法の6つのメリット
コメントで処理の境界線を表現するのと比べて、名前付きブロック記法には次のメリットがあると思いました。
- 処理の境界線がはっきりする
- ネストできる
- Bash標準の機能で書ける
- デバッグしやすい
- エディタで折りたためる
- 一時的に処理を無効にするのも容易
> メリット1__colon__ 処理の境界線がはっきりするメリット1: 処理の境界線がはっきりする
ブレースで処理がグルーピングされ、インデントもつくため、コードを眺めたときに処理の境界を把握しやすくなります。
> メリット2__colon__ ネストできるメリット2: ネストできる
複合コマンドはネストすることができるため、サブ処理をネストで表現することができます。
: "setup database" && {
: "create database" && {
command
command
}
: "migrate database schema" && {
command
command
}
: "store dummy data" && {
command
command
}
}
> メリット3__colon__ Bash標準の機能で書けるメリット3: Bash標準の機能で書ける
記法の構成要素については後述しますが、使用するコードはすべてBashの標準機能です。必要な外部ライブラリなしに記述できるのが特徴です。
> メリット4__colon__ デバッグしやすいメリット4: デバッグしやすい
Bashは-x
オプションで実行コマンドを出力することができます。
コメントは-x
オプションでデバッグすることはできませんが、:
はコマンドの一種なので、-x
オプションで処理名を表示することができデバッグがはかどります。
#!/bin/bash
set -x # デバッグオプション
: "Install libraries" && {
command
command
command
}
: "Run tests" && {
command
}
: "Build app" && {
command
command
}
: "Deploy app to server" && {
command
command
}
: "Notify result to Slack channel" && {
command
}
+ : 'Install libraries'
+ command
+ command
+ command
+ : 'Run tests'
+ command
+ : 'Build app'
+ command
+ command
+ : 'Deploy app to server'
+ command
+ command
+ : 'Notify result to Slack channel'
+ command
> メリット5__colon__ エディタで折りたためるメリット5: エディタで折りたためる
エディタで折りたたみ機能を使う人にとっては1つのメリットになるかと思います。
> メリット6__colon__ 一時的に処理を無効にするのも容易メリット6: 一時的に処理を無効にするのも容易
処理を一時的に無効にしたい場合は、&&
を||
に置き換えるだけで対応できます。
#!/bin/bash -eux
: "Install libraries" || {
command
command
command
}
> 名前付きブロック記法の構成要素の説明名前付きブロック記法の構成要素の説明
名前付きブロック記法の「:
や{}
って何だろう?」と疑問を持たれた方もいると思うので、構成要素についても簡単に説明しておきます。
> __colon__コマンド:
コマンド
:
コマンドはBashでは何もしないコマンドです。そして、常にsuccessで終わります。
: "do nothing"
コメントとの違いは実行されるコマンドという点です。なので、サブシェルや変数を扱うこともできます。
#!/bin/bash
set -x
: "current directory is $PWD"
+ : 'current directory is /Users/suin/Desktop'
名前付きブロック記法では、:
コマンドを説明文として活用しています。
> &&演算子&&
演算子
&&演算子は前のコマンドが成功した場合、次のコマンドを実行する演算子です。
#!/bin/bash
true && echo 1 # 1は出力される
false && echo 2 # 2は出力されない
:
コマンドは常にsuccessで終わるので、&&
演算子以降のコマンドは必ず実行されるというわけです。
> 複合コマンド{}複合コマンド{}
Bashでは{}
や()
で複数のコマンドを1つのコマンドとして複合することができます。
#!/bin/bash
{
echo 1
echo 2
echo 3
} > /tmp/output
cat /tmp/output
1
2
3
{}
は現在のシェルで、()
はサブシェルで実行される違いがあります。
#!/bin/bash
foo=1
(
foo=2
echo $foo # 2が出力される。サブシェル内なので親シェルのfooは書き換えない
)
echo $foo # 1が出力される
{
foo=2 # 現行シェルで実行されるのでfooが書き換わる
}
echo $foo # 2
> まとめまとめ
- シェルスクリプトは長くなると処理の境界が不鮮明になりがち。
- コメントで処理の境界を表現する工夫はよく見かけるが、もっと良い方法はないか考えてみた。
-
:
コマンド、&&
演算子、複合コマンド()
や{}
を組み合わせて書くと、処理の境界線がはっきりする。