Linux/CLI実践:bash/zshとシェルスクリプトで作業を自動化する ―「毎回同じコマンドを打つ」をやめる技術(フルスタック養成講座 #07)
「デプロイ前に、まずこのコマンドを3つ順番に打って、出力を目で確認してから次のコマンドを打ってください」
こういう手順書を見たことはないでしょうか。あるいは自分自身が、そういう手順書を作ったことは。私はSIer・金融機関・SaaS企業・外資系コンサルティングファームでArchitectとして数百を超えるシステムに携わる中で、こうした「手順書に書かれた手作業の連続」が事故の温床になる現場を何度も見てきました。理由は単純です。人間は、決まった手順を、決まった順番で、疲れていても間違えずに繰り返すことが苦手だからです。深夜のリリース作業、繁忙期の月次バッチ、障害対応中の緊張した状態。こういう場面ほど、手順書の1行を読み飛ばす、コピペする行を間違える、といったミスが起きます。
第04回「実践:top/htop/vmstatでリソースのボトルネックを見る」、第05回「実践:curl/dig/tcpdumpで通信を目で見る」、第06回「実践:Route53/Cloudflareで名前解決を設計・検証する」と、ここまでの実践回では、コマンドを1つずつ手で打って状況を「見る」「検証する」ことを扱ってきました。しかし実務では、この「見る」「検証する」という作業自体が繰り返し発生するルーティンになった瞬間、手で打つことそのものがリスクになります。今回の第07回では、bash・zshというシェルの基本的な仕組みを理解した上で、繰り返し作業をエイリアス・関数・シェルスクリプトとして再現可能な形に落とし込み、さらにcronやsystemdタイマーで人間の代わりに実行させる、という「自動化」の入り口に立ちます。
シェルスクリプトは、フロントエンドやバックエンドのような華やかな技術ではありません。しかし私がこれまで見てきた現場では、優秀なエンジニアほど、手元の細々とした繰り返し作業を惜しみなく自動化に投資しているという共通点がありました。逆に、障害の原因を辿っていくと「本来スクリプト化されているはずの手順が、いつの間にか手作業に戻っていた」というケースにも何度も遭遇しています。この第07回では、無料部分でbash/zshの基本的な仕組みとエイリアス・関数・シンプルなスクリプトの書き方を解説します。有料部分では、set -euo pipefailが実際に何を防ぎ何を防げないかという踏み込んだ理解、クォートを誤ったときに起きる事故、配列やlocal変数を使った壊れにくいスクリプトの書き方、trapによるクリーンアップ、getoptsによるオプション引数、そしてcron・systemdタイマーでの実行時に発生する環境差の罠、shellcheckによる静的解析、実務でよく踏む落とし穴のチェックリストまで、実際に本番で使える自動化スクリプトを書くための実践知識を提供します。
なぜ「手で打つ」から「スクリプトに任せる」へ移行すべきなのか
実務でシェルスクリプトが必要になる場面は、大きく3つに分類できます。1つは、同じコマンド群を何度も実行する定型作業(ログの集計、バックアップの取得、デプロイ前後の確認コマンド群など)です。2つ目は、人間が寝ている間や見ていない間に、決まった時刻・決まった条件で実行させたい作業(夜間バッチ、定期的なヘルスチェック、証明書の有効期限確認など)です。3つ目は、手順そのものをドキュメントではなくコードとして残し、誰が実行しても同じ結果になることを保証したい作業(環境構築、リリース手順、障害対応の初動コマンド群など)です。
このいずれの場面でも、共通して得られる効果は「再現性」と「レビュー可能性」です。手順書に書かれた自然言語の手順は、読む人によって解釈がぶれます。しかしシェルスクリプトとしてコード化された手順は、実行すれば必ず同じ処理が同じ順番で走ります。さらに、そのスクリプトをGitで管理すれば(第08回で扱います)、変更内容を差分としてレビューでき、「いつ・誰が・なぜこの手順を変えたか」という履歴も残ります。
一方で、シェルスクリプトには落とし穴も多いことを、最初に率直に伝えておきます。シェルスクリプトの構文は、Python やJavaScriptのような一般的なプログラミング言語と比べて、エラーが起きても黙って先に進んでしまう挙動がデフォルトという、事故を誘発しやすい設計になっています。この特性を理解しないままスクリプトを書くと、「動いているように見えて、実は途中のコマンドが失敗していた」という、発見が遅れる種類のバグを生みます。この点は有料部分で詳しく扱います。まずは無料部分で、bash・zshの基本的な仕組みと、安全に使い始めるための土台を整えます。
bashとzshの違いと設定ファイルの読み込み順序
多くのLinuxディストリビューションではbash(Bourne Again SHell)が標準のログインシェルとして採用されており、macOSではCatalina(2019年リリース)以降、標準のログインシェルがzsh(Z Shell)に変更されています。bashとzshは、どちらもBourne Shell系の文法を受け継いだ互換性の高いシェルであり、この記事で扱う基本的なスクリプトの書き方(変数、条件分岐、ループ、関数)はほぼ共通して動作します。一方で、補完機能の柔軟さ、テーマ機能、プラグインエコシステム(oh-my-zshのような設定管理フレームワークなど)といった、対話的に使う際の使い勝手の面ではzshの方が拡張性が高く、この違いが「開発機ではzsh、サーバーではbash」という実務での使い分けにつながっています。
シェルを対話的に使う際に読み込まれる設定ファイルには、明確な役割分担があります。bashの場合、ログインシェルとして起動したときは~/.bash_profile(存在しなければ~/.bash_login、それもなければ~/.profile)が読み込まれ、ログインシェルではない対話シェル(すでにログイン済みの状態で新しくターミナルを開いたときなど)では~/.bashrcが読み込まれます。zshの場合も同様に、ログインシェルでは~/.zprofile、対話シェル全般では~/.zshrcが読み込まれます。実務でよくある混乱は、「~/.bash_profileに書いたエイリアスが、新しく開いたターミナルタブでは効かない」という現象です。多くのターミナルアプリケーション(macOSのTerminal.appやiTerm2など)は、新しいタブを開くたびにログインシェルとしてではなく通常の対話シェルとして起動する設定になっていることが多く、この場合~/.bashrc(zshなら~/.zshrc)側に書かないと反映されません。エイリアスや関数、PATHの追加設定は、基本的に~/.bashrcまたは~/.zshrcに書く、というのが実務での定石です。
エイリアスと関数:使い分けの基準
繰り返し作業の自動化における最初の一歩が、エイリアスと関数です。エイリアスは、長いコマンドに短い別名をつける最もシンプルな仕組みです。
alias ll='ls -la'
alias gs='git status'
alias k='kubectl'
エイリアスは非常に手軽ですが、引数の位置を柔軟に扱えないという制約があります。例えば「ディレクトリを指定してバックアップを取り、成功したらログに日時を記録する」といった、複数の処理を条件分岐や引数の加工を伴って組み合わせたい場合は、エイリアスではなく関数を使います。
backup() {
local target_dir="$1"
local backup_name="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
tar -czf "$backup_name" "$target_dir"
echo "$(date '+%Y-%m-%d %H:%M:%S') backup created: $backup_name" >> ~/backup.log
}
この関数を~/.bashrcや~/.zshrcに書いておけば、シェルを起動するたびに読み込まれ、backup ~/projects/myappのように呼び出せます。エイリアスと関数の使い分けの基準は明快です。単純な置き換えで済むならエイリアス、引数を受け取って条件分岐や複数コマンドの組み合わせが必要になった時点で関数、と覚えておけば実務で迷いません。
シェルスクリプトの基本構造:shebang・実行権限・終了コード
エイリアスや関数は、あくまで「自分のシェルの中だけ」で有効な仕組みです。他の人と共有したい、cronのような別のプロセスから呼び出したい、という段階になったら、独立したシェルスクリプトファイルとして書きます。
最小構成のシェルスクリプトは、次のような形になります。
#!/usr/bin/env bash
echo "Hello, automation."
1行目の#!/usr/bin/env bashはシェバン(shebang)と呼ばれ、「このファイルをどのインタプリタで実行するか」をOSに伝える宣言です。#!/bin/bashと直接パスを書く方法もよく見かけますが、#!/usr/bin/env bashと書く方が、環境によってbashの実際のインストール場所が異なる場合(/bin/bashではなく/usr/local/bin/bashなど)でも、PATHから解決してくれるため、より移植性が高い書き方として実務では推奨されます。
このファイルをscript.shという名前で保存しただけでは、まだ実行できません。実行権限を付与する必要があります。
$ chmod +x script.sh
$ ./script.sh
Hello, automation.
chmod +xは、そのファイルに「実行可能」というパーミッションを付け加えるコマンドです。./script.shのように、カレントディレクトリを明示した相対パス(または絶対パス)で呼び出す必要がある点にも注意してください。単にscript.shとだけ打っても、多くの環境ではPATHにカレントディレクトリが含まれていないため、「コマンドが見つかりません」というエラーになります。
もう1つ、シェルスクリプトを理解する上で欠かせないのが終了コード(exit code)です。すべてのコマンド・スクリプトは、実行を終えると0から255までの整数を返します。0は成功、0以外は何らかの失敗を意味する、という規約が広く使われています。直前に実行したコマンドの終了コードは、$?という特殊変数で確認できます。
$ ls /nonexistent-directory
ls: cannot access '/nonexistent-directory': No such file or directory
$ echo $?
2
この終了コードの仕組みこそが、次にお話しする「なぜシェルスクリプトはエラーが起きても黙って先に進んでしまうのか」という問題の核心につながります。
位置引数とシンプルな自動化の第一歩
スクリプトを実行する際に渡す引数は、位置引数としてスクリプト内で参照できます。$1が1番目の引数、$2が2番目の引数、$0はスクリプト自身のパス、$#は引数の個数、$@はすべての引数のリストです。
#!/usr/bin/env bash
echo "スクリプト名: $0"
echo "1番目の引数: $1"
echo "引数の個数: $#"
$ ./greet.sh Alice
スクリプト名: ./greet.sh
1番目の引数: Alice
引数の個数: 1
ここまでの内容、つまり「エイリアス・関数で日常のショートカットを作る」「シェバン・実行権限・終了コードの意味を理解する」「位置引数でスクリプトに柔軟性を持たせる」の3点を押さえれば、日々の繰り返し作業の多くを、自分の手でスクリプト化できる土台が整います。ただし、ここまでの内容だけで書いたスクリプトを本番のバッチ処理やデプロイ手順に使うのは、まだ早計です。次の有料部分では、「動いているように見えるが実は壊れているスクリプト」を防ぐための、実務で必須の安全策を扱います。
ここから先は
この記事が気に入ったらチップで応援してみませんか?
