シェルスクリプト(Bash)入門。できること、基礎文法、業務自動化の方法を解説【事例あり】

シェルスクリプト(Bash)入門

「シェル」や「シェルスクリプト」という用語を聞いたことはありますか? 「シェル」とは、CLI上で動作してOSを操作できるソフトウェアのこと。そのシェルを組み合わせて記述する、簡単なプログラムを「シェルスクリプト」と言います。

この記事では、「シェル」についての前提知識をご紹介した上で、入門者向けにシェルスクリプト(Bash)のできることや基礎文法、実際のコードについて解説していきます。

作業を効率化したいエンジニアの方は、ぜひこの記事を参考にしてみてください。

シェルとは

▲ シェル(Bash)でコマンドを組み合わせればFizzBuzzも1行で書ける

シェルとは、CLI(Command Line Interface)という黒い画面上で動作するソフトウェアのひとつです。「コマンド」と呼ばれる命令文を利用してOSを操作できます。

シェルにはさまざまな種類があり、もっとも標準的なのは「Bash(Bourne-Again Shell)」です。Bash以外に代表的なシェルは、以下のようなものが挙げられます。

  • Bourne Shell(Bashの前身)
  • Csh(C言語風のシェル)
  • Zsh(Bash等の後継・上位互換)

この他にもさまざまな種類のシェルが存在しますが、大まかな文法はどのシェルも共通です。そのため、ひとつのシェルについて学習すれば、違うシェルの扱いも容易になります。

今回は、シェルの中でもっとも代表的な「Bash」のシェルスクリプトについて解説していきます。

シェルスクリプトとは

シェルスクリプトとは、if文/while文/パイプなどの制御構文と、コマンドを組み合わせて動作する簡単なプログラムのことです。

一般的に拡張子「.sh」のファイルとして保存され、一度つくれば何度でも再実行できます。

シェルスクリプトでできること

できること1. ファイルの移動やログの抽出など、PC上での基本的な操作

ファイル移動やコピペ、ログの記録/抽出など、PCでできる基本的な操作はシェルスクリプトで肩代わりできます。

とくにUNIX系のOSであれば、Baahのシェルスクリプトが標準で入っていることがほとんどなので、LinuxやmacOS上の操作をするときに便利です。

できること2. コマンドの利用による、複雑なタスクの効率化

シェルスクリプトの「パイプ」という機能をつかえば、複数の異なるコマンドを連携させることが可能です。

たとえば「grep」というコマンドは、文書内から特定の文字列が含まれる箇所をすべて抽出してくれます。このgrepコマンドが抽出した結果のうち、ラスト3つ分の結果のみを抽出したいというときはどうすればよいでしょうか。

答えは「tailコマンドと組み合せる」です。tailコマンドは、入力された文書について、指定した行数分を末尾から読み取ってくれます。そのため、パイプを使ってgrepコマンドとtailコマンドを組み合せると、「文書中から特定の文字列が発見されたラスト3か所を取得する」という処理を実現できます。

このようにシェルスクリプトのパイプを利用すれば、ひとつのコマンドでは実現できない複雑なタスクをこなせるようになるのです。

できること3. 他言語製のツールやソフトを組み合わせた業務の自動化

シェルスクリプトで組み合せられるのは、コマンドだけではありません。JavaやRuby、Goなどの言語で実装されたツールやソフトを組み合せて、さらなる業務自動化を図ることもできます。

組み合せることのできるツール/ソフトに制限はありません。Gitのような開発に欠かせないCLIツールから、Google ChromeのようなGUIアプリまで、さまざまなツールやソフトをシェルスクリプト内で連携させられます。もちろん、自作ツールとの連携も可能です。

このように、シェルスクリプトによってさまざまなツールを組み合せれば、さらなる業務自動化/効率化を実現できます。

シェルスクリプト(Bash)の基本文法

シェルスクリプトでできることを踏まえた上で、次は実際にシェルスクリプトを書いていくための基本文法について解説していきます。

シバン

シバン(shebang)とは、シェルスクリプトの1行目に記述する文字列です。シバンによって実行するシェルの種類を指定できます。

前述のしたとおり、シェルにはBashやCsh、Zshなどさまざまな種類があります。そのため、シバンを用いて「このシェルスクリプトをどのシェルで動かすのか」を明示的に指定しないと、思わぬ挙動をしてしまう可能性があるのです。

今回はBashのシェルスクリプトを作成したいので、次のようにシバンを書きます。

#!/bin/bash

なおBashに指定せず、デフォルトに設定されているシェルを利用する場合は、以下のようにシバンを書きます。

#!/bin/sh

シバンを書いてしまえば、それ以降はシェルスクリプトの処理そのものの記述を自由に書けます。

ここでは文字列を出力する「echo」コマンドを使って、【Hello world.】をコマンドラインに表示するBashのシェルスクリプトを作成してみましょう。

まずテキストファイル「hello.sh」を新規作成して、以下を書きましょう。

#!/bin/bash
echo ‘Hello world.'

保存をしたら、hello.shに「実行権限」を付与します。実行権限がないと、ファイル名だけでシェルスクリプトの実行を行えません。

実行権限の付与には、chmodコマンドを使います。コマンドラインから以下を入力して、hello.shを実行可能ファイルにしましょう。

# 『+x』で実行権限を付与できる
chmod +x hello.sh

実行権限の付与が完了したら、hello.shをファイル単独で実行できるようになります。実際に、以下をコマンドラインで入力してEnterを押してみましょう。

./hello.sh

【Hello world.】がコマンドライン上に表示されましたね。このようにシェルスクリプトは「1行目にシバンでシェルの種類を指定」「2行目以降に処理の本文を記述」という構成で書いていきます。

なおシェルスクリプト実行時に、最初の「./」がないとエラーが出るので注意してください。(「hello.sh」が実行可能ファイルではなく、コマンドと認識されてしまうため)

変数と変数展開

一般的なプログラミング言語と同じで、シェルスクリプトにも変数があります。Bashのシェルスクリプトで変数を使用したい場合は、以下のように書いてください。(以降の解説では1行目のシバンを省略します)

# 変数の宣言と値の設定(注意! イコールの両隣はスペース不可)
str='hello'

# 変数の使用(echoコマンドで変数strの中身を出力)
echo $str

また変数展開を使えば、文字列中に変数の中身を出力できます。たとえば、変数$str(中身は’hello’という文字列)を使って【hello world.】という文字列を表示したい場合は、以下のように書きましょう。

# 変数$strが展開されて【hello world.】と表示される
echo “$str world."

変数展開を許可する文字列の範囲をダブルクォーテーションで囲むことで、「記述した変数」が「変数にセットされた値」に置き換えられるのです。

なお、このときダブルクォーテーションではなくシングルクォーテーションだと、変数展開は許可されず【$str world.】と表示されます。

コマンド置換

文字列中に変数を展開するだけではなく、コマンドの実行結果を文字列中で展開したいときもありますよね。そのようなときに役立つのがコマンド置換です。

たとえば、現在の日時を出力する「date」というコマンドがあります。

# 実行するとyyyy-MM-dd形式で今日の日付が表示される
date +%F

このdateコマンドを利用して【今日の日付は2020-06-27です】と表示させたい場合は、コマンド置換を利用して以下のように書きましょう。

# 『 $( コマンド ) 』でコマンドの実行結果を文字列中に展開できる
echo "今日の日付は$(date +%F)です"

# あるいは以下のような書き方も可能
echo "今日の日付は`date +%F`です"

このように「 ドルマーク + 括弧( $() ) 」もしくは「バッククォーテーション( “ )」で囲まれた範囲は、コマンドとして解釈/実行された結果の文字列に置き換えられます。

if文

「もし~なら、~する」という、特定の条件に当てはまったときのみ処理を実行する文のことをif文と呼びます。

たとえば、「組み込み変数$USERが”sig”のときだけ、【Hi.】と出力する処理」を実装したいときは、以下のように書きましょう。

# 注意! ifの右にある角括弧の左右にはスペースが必須(スペースがないとエラーになる)
if [ $USER = 'sig' ]
then
echo 'Hi.'
fi

このように以下の4パーツで、「もし~なら、~する」という処理の実装が可能です。各パーツの意味合いや書き方も、下にまとめています。

  • if [ 条件句 ]
    「もし~なら」という条件を書く領域。角括弧は、正確には「testコマンド」の省略記法であり、角括弧内で使える「 = 」などの演算子はtestコマンドの仕様に準ずる。
  • then
    上記の条件に当てはまったとき、処理を開始する合図。
  • 処理本文
    処理そのもの。
  • fi
    if文全体の終わりを示すもの。

上記シェルスクリプトの場合、ユーザ名が”sig”なら【Hi.】と表示されますが、ユーザ名が”sig”以外なら、なにも表示されないようになっています。

この処理に、以下の処理を追加したいときにはどうすればいいでしょうか。

  • ユーザ名が”guest”のときに【Welcome.】を出力する処理
  • ユーザ名が”sig”でも、”guest”でもないときに【Who are you?】を出力する処理

結論から述べると「elif句」と「else句」を利用することで実現できます。以下のように記述をしてみてください。

# if句
if [ $USER = 'sig' ]
then
echo 'Hi.'

# 上の『ifの条件句』に当てはまらなかった場合、elif句に処理が移る(記法はifと同じ)
elif [ $USER = 'guest' ]
then
echo 'Welcome.'

# 『すべてのif・elifの条件句』に当てはまらなかった場合、else句に処理が移る(then不要)
else
echo 'Who are you?'

fi

このようにif文を使えば、条件に応じた処理の出し分けができます。

なお、上記のシェルスクリプトでは「イコール記号( = )」で文字列の比較を行っていましたが、比較する値が数値の場合は使うべき演算子が変わるので注意してください。

数値の場合の演算子にはいくつか記法があります。そのなかでも、最も移植性が高い記法は以下のとおりです。

# 『左辺と右辺が等しいなら』
if [ 1 -eq 1 ]

# 『左辺が右辺より大きいなら』
if [ 1 -gt 0 ]

# 『左辺が右辺より小さいなら』
if [ 0 -lt 1 ]

パイプと標準出力/標準入力

パイプを使えば「コマンドの実行結果」を「異なるコマンドの入力」として渡し、複雑なタスクを処理できるようになります。

たとえば、以下の3つのコマンドがあります。

  • catコマンド:複数または単一のファイルの中身を表示する
  • grepコマンド:文書中から特定の文字がある行を表示する
  • tailコマンド:文書の末尾から指定した行数を表示する

これらをパイプで以下のように組み合せると、「hoge.logの中で”ERROR”が登場した最後の行」を取得できます。

cat hoge.log | grep ERROR | tail -n 1

なぜこのシェルスクリプトが「hoge.logの中で”ERROR”が登場した最後の行」を取得する処理になるのでしょうか。それを理解するためには、「標準入力」と「標準出力」の理解が欠かせません。

標準入力/標準出力は、プログラミング言語全般において「暗黙的な入出力」を示します。シェルスクリプト中で利用するコマンドにおいては、以下の役割を果たします。

  • 標準入力
    コマンドを実行するにあたって、特に指定がなかった場合に利用する入力情報。ユーザがキーボードから入力する文字列のほか、パイプから渡された文字列を標準入力としても扱える。
  • 標準出力
    コマンドを実行した結果のうち、標準的な出力先へ表示する実行結果のこと。指定がない場合はコマンドラインの画面が標準出力先となるが、パイプを利用することで「パイプで繋いだ次のコマンドの標準入力」が標準出力先となる。

このように標準入出力は、キーボードやコマンドラインの画面といった一般的な入出力のみならず、パイプを利用した情報の受け渡しにも対応しています。

これを踏まえた上で、「cat hoge.log | grep ERROR | tail -n 1」の処理の流れを追いかけてみましょう。

  1. cat hgoe.log により、hoge.logの文書が標準出力される。
    cat hoge.log
  2. パイプを経由して、標準出力(hoge.logの文書)がgrepコマンドの標準入力へ渡る。
    cat hoge.log | grep
  3. grep ERRORにより、hoge.logの文書のうち、”ERROR”が登場する行だけが標準出力される。
    cat hoge.log | grep ERROR
  4. パイプを経由して、標準出力(”ERROR”が登場するすべての行)がtailコマンドの標準入力へ渡る。
    cat hoge.log | grep ERRPR | tail
  5. tail -n 1 により、”ERROR”が登場するすべての行のうち、もっとも最後の行だけが標準出力される。
    cat hoge.log | grep ERRPR | tail -n 1
  6. 最後はパイプを利用していないので、標準出力がコマンドラインの画面そのものになり、画面上に結果が表示される。

このようにパイプを利用することで、あるコマンドの実行結果を、次に実行するコマンドの入力情報として渡せます。

while文

while文を使えば、指定した条件を満たす間は処理をループすることが可能です。たとえば、カウントアップしながら「現在のカウントはnです」と表示するループ処理は、次のように書きます。

#!/bin/bash

end=10
i=0
# while [ 条件句 ] で『条件を満たす間以下の【do~done】の処理をループする』という意味になる
while [ $i -lt $end ]
do
# exprコマンドは四則演算を行うコマンド
i=`expr $i + 1`
echo "現在のカウントは$iです"
done

これを実行すると、while [ 条件句 ] で指定したとおり、変数$iが10未満の間は「現在のカウントはnです」とカウントアップをできます。

このときwhile [ 条件句 ] で利用している角括弧は、if文と同じくtestコマンドの省略記法です。そのため、-lt や -gt など比較の種類に応じてさまざまなオプションを利用できます。

またwhileの右隣には、testコマンド以外のコマンドを記述することも可能です。とくに「while + readコマンド」の組み合わせはよく使われるので、次でご紹介します。

while + readコマンド

以下のようにwhileとreadコマンドをパイプで組み合せると、文書を読み取り、1行ごとに処理を実行できます。

cat hoge.log | while read line
do
echo “内容:$line"
done

このシェルスクリプトを実行すると、hoge.logの最終行まで1行ずつ「内容:xxxxxxx(その行の内容)」と文字列が出力されていきます。このときの処理の流れは以下のとおりです。

  1. hoge.logをcatコマンドで読み取り、その内容を標準出力する。
    cat hoge.log
  2. パイプを経由して、標準出力(hoge.logの内容)が、while条件句のreadコマンドの標準入力へ渡る。
    cat hoge.log | while read
  3. while read lineで、hoge.logの1行目を変数$lineにセットする。
    cat hoge.log | while read line
  4. do~doneの間に記述した処理を実行する。
    cat hoge.log | while read line
    do
    echo “内容:$line"
    done
  5. doneに処理が到達したら、再度whileの条件句のread lineで2行目を変数$lineにセットする。
  6. do~doneの処理を実行する。
  7. 3行目、4行目、5行目…と処理を繰り返し、最終行の処理を終えた時点でループを終了する。

このようにwhile文とreadコマンドを組み合せれば、標準入力に渡された文書を1行ずつ処理できるようになります。

なおreadの前に実行するコマンドは、catコマンド以外でも「結果を標準出力するコマンド」なら大丈夫です。

たとえば、連番の数字を出力するseqコマンドを、以下のように「while文 + readコマンド」に渡すこともできます。

# 『seq 5』 で1~5までの数字を1行ずつ出力する
seq 5 | while read line
do
# while read line で『seq 10』の結果を1行ずつ処理する
#(exprコマンドですべての数字を2倍にしている)
echo `expr $line \* 2`
done

# 以下が出力される
# 2
# 4
# 6
# 8
# 10

このように「標準入出力」「while文」「readコマンド」を組み合せることで、さまざまなコマンドの実行結果を1行ずつ処理できるようになります。ぜひ覚えておいてくださいね。

実際にシェルスクリプト(Bash)で業務の自動化に挑戦!

ここまでに紹介してきた基本文法を踏まえて、タスク自動化/効率化を実際にBashのシェルスクリプトで実装してみましょう。

業務自動化と聞くと難しそうに思えるかもしれませんが、安心してください。タスク自動化のコツは、簡単な定型作業から少しずつ自動化/効率化を施していくことです。はじめから難しいことに取り組む必要はありません。

ここでは業務中によくある作業の例として、以下の3つのタスクをシェルスクリプトで自動化してみます。

1.連番ディレクトリ作成

2.ファイルの一括バックアップ

3.ログの監視

業務自動化1. 連番ディレクトリ生成

業務をしているとき、「No.01」「No.02」「No.03」……のように、連続する番号のディレクトリを作成したい場面があると思います。手作業でも出来るものの、数が多くなるとかなりの時間を取られてしまいますよね。

Bashのシェルスクリプトをつかえば、このような作業を簡単に自動化できます。

「seqコマンド」「while文 + readコマンド」「mkdirコマンド」の3つを使って、No.01~No.20の連番ディレクトリを作成するシェルをつくってみましょう。

#!/bin/bash

# seqで1~20を順に出力. readに渡してひとつずつ処理
seq 20 | while read line
do
if [ $line -lt 10 ]
then
# 10未満なら0埋め
mkdir "No.0$line"

else
# 10以上なら数値そのまま
mkdir "No.$line"

fi
done

このようにseq, readを組み合わせたwhile文中で

  • $lineが10未満なら数値2桁目を0埋め
  • $lineが10以上なら数値そのまま

とすることで、No.01~No.20のディレクトリを作成できます。(mkdirはディレクトリ作成のコマンドです)

またseqの「wオプション」を利用すれば、自力でif文を利用した0埋めをせずとも、自動的に上位桁の0埋めをしてくれます。そのため上記シェルスクリプトを、以下のように短縮することが可能です。

#!/bin/bash

seq -w 20 | while read line
do
# seqコマンドは『-w』オプションを使えば自動で0埋めしてくれる
mkdir "No.$line"
done

シェルスクリプトは、コマンドのオプションを利用することで短縮できる場合があります。コマンド利用時には、有用なオプションがあるかどうかを知っていると、より短時間で処理を実装できる場合も。便利なオプションがないかを、ときどき確認しておくとよいでしょう。

業務自動化2. ファイルの一括バックアップ

ファイルの一括バックアップも、Bashのシェルスクリプトを使えば簡単に自動化できます。たとえば、以下の要件を満たすシェルスクリプトを実装してみましょう。

  • 現在のディレクトリにある「拡張子が.logのファイル」のバックアップを取りたい
  • バックアップは「bk年月日_時分秒」という名前のディレクトリ配下に置きたい
  • 上記ディレクトリはシェルスクリプト実行時に自動作成したい

これら要件を満たすには、以下の4つのコマンドを使っていきます。

  • dateコマンド
  • mkdirコマンド
  • lsコマンド
  • cpコマンド

書き方は以下のとおりです。

#!/bin/bash

# 「bk年月日_時分秒」形式の文字列を作成(dateコマンド + コマンド置換)
DIRNAME="bk`date +%Y%m%d_%H%M%S`"

# バックアップ用のログを置くディレクトリ作成(mkdirコマンド)
mkdir "$DIRNAME"

# 現在のディレクトリから.logファイルをすべて取得(lsコマンド)
ls *.log | while read line
do
# 1ファイルずつコピーして、バックアップ用ディレクトリに配置していく(cpコマンド)
cp "$line" "$DIRNAME/$line"
done

これをシェルスクリプトとして保存して実行すると、.logファイルを”bk年月日_時分秒”形式のディレクトリに、一括でバックアップできます。

業務自動化3. ログの監視

なんらかのシステムのログを、リアルタイムで監視したいとき役に立つのが「tailコマンド」です。tailコマンドは「fオプション」を利用することで、監視中のファイルに書き込まれていく内容をリアルタイムで取得できます。

またBashのシェルスクリプトを使えば、特定の文字列が出現したときのみ、コマンドラインに新規行を表示することも可能です。

たとえば、「特定のファイルを監視して、”ERROR”が登場したらその行をリアルタイムで表示するシェルスクリプト」を実装するなら次のように書きます。

#!/bin/bash

# もし第1引数がファイルなら
# (変数$1はシェルスクリプトに渡された第1引数を示す)
if [ -f "$1" ]
then
# grep --line-buffered でtail -f の出力結果をリアルタイムでgrepに渡し、1行ずつ抽出結果を出力する
echo "ファイル [ $1 ] の監視を開始します"
echo '----------------'
tail -f $1 | grep --line-buffered ERROR

else
# もし第1引数がファイルでなければ
echo '引数にファイル名を指定してください'
fi

このように「tail -f」と「grep –line-buffered」を組み合せることで、リアルタイムのログ監視/抽出ができます。

おわりに

今回はシェルスクリプトでできることと、その中でもっとも代表的な「Bash」のシェルスクリプトの基礎文法について紹介しました。

簡単なタスクでも、ひとつずつ自動化していくことで、1日の業務に余裕を持たせることができます。シェルスクリプトを使って、まず単純な作業から着実に自動化/効率化していきましょう。

(執筆:sig_Left 編集:Kimura Yumi)

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
エンジニア副業案件
Workship