Bashプログラミング
 Computer >> コンピューター >  >> プログラミング >> Bashプログラミング

Bashでプログラミングする方法:ループ構文の使い方徹底解説

Bashは強力なプログラミング言語であり、コマンドラインやシェルスクリプトでの利用に最適な設計がなされています。この3部構成のシリーズ記事は、筆者の全3巻からなるLinux自習コースをもとに、コマンドラインインターフェース(CLI)上でBashをプログラミング言語として活用する方法を解説していきます。

シリーズ第1回では、変数や制御演算子を使った基本的なコマンドラインプログラミングを取り上げました。第2回では、実行フローを制御するための論理演算子(ファイル・文字列・数値など)や、Bashにおけるさまざまなシェル展開について説明しました。そして今回の第3回(最終回)では、さまざまな反復処理を実現するループの使い方と、その制御方法について詳しく見ていきましょう。

ループとは

これまで使ってきたどのプログラミング言語にも、繰り返し処理を行うためのループ構造が必ず1つ以上用意されています。筆者はforループを最も頻繁に使いますが、whileループやuntilループも状況によって非常に役立ちます。

forループ

Bashのforコマンドは、多くの言語のものより柔軟であると筆者は考えています。その理由は、数値以外の値も扱える点にあります。対照的に、たとえばC言語の標準的なforループは数値しか扱えません。

Bash版forコマンドの基本構造はとてもシンプルです。

for Var in list1 ; do list2 ; done

これは次のように読み解けます。「list1内の各値に対して、$Varにその値を設定し、その値を使ってlist2内のプログラム文を実行する。list1のすべての値を使い終えたら、ループを抜けて終了する」。list1には明示的な値のリストを直接書いてもよいですし、コマンド置換(シリーズ第2回で解説)の結果を指定することもできます。筆者はこの構成を日常的によく使っています。

実際に試してみましょう。~/testdirがカレントワーキングディレクトリ(PWD)になっていることを確認してください。まずディレクトリを掃除してから、明示的な値のリストを使ったごく簡単なforループの例を見てみます。このリストは英数字が混在していますが、すべての変数は文字列として扱われることを忘れないでください。

[student@studentvm1 testdir]$ rm *
[student@studentvm1 testdir]$ for I in a b c d 1 2 3 4 ; do echo $I ; done
a
b
c
d
1
2
3
4

続いて、変数名をより意味のあるものにした、少し実用的なバージョンです。

[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Department $Dept" ; done 
Department Human Resources
Department Sales
Department Finance
Department Information Technology
Department Engineering
Department Administration
Department Research

今度はディレクトリを作成しながら、進行状況も表示してみます。

[student@studentvm1 testdir]$ for Dept in "Human Resources" Sales Finance "Information Technology" Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept"  ; done 
Working on Department Human Resources
Working on Department Sales
Working on Department Finance
Working on Department Information Technology
Working on Department Engineering
Working on Department Administration
Working on Department Research
[student@studentvm1 testdir]$ ll
total 28
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Administration
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Engineering
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Finance
drwxrwxr-x 2 student student 4096 Apr 8 15:45 'Human Resources'
drwxrwxr-x 2 student student 4096 Apr 8 15:45 'Information Technology'
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Research
drwxrwxr-x 2 student student 4096 Apr 8 15:45 Sales

mkdirステートメントの中で$Dept変数を引用符で囲む必要がある点に注意してください。囲まないと、「Information Technology」のようなスペース入りの部署名が2つの別々の部署として扱われてしまいます。ここから、筆者が大切にしているベストプラクティスが見えてきます。すなわち、ファイル名やディレクトリ名は常に1単語にするべきだということです。最近のOSの多くは名前中のスペースを扱えますが、システム管理者はスクリプトやCLIプログラム内でそうした特殊ケースに確実に対応しなければならず、そのぶん余計な手間がかかります。(面倒でも考慮すべきです。どんなファイルが現れるかわからないのですから。)

そこで、~/testdirの中身をもう一度削除して、今度はハイフン区切りの名前でもう一度やってみます。

[student@studentvm1 testdir]$ rm -rf * ; ll
total 0
[student@studentvm1 testdir]$ for Dept in Human-Resources Sales Finance Information-Technology Engineering Administration Research ; do echo "Working on Department $Dept" ; mkdir "$Dept" ; done
Working on Department Human-Resources
Working on Department Sales
Working on Department Finance
Working on Department Information-Technology
Working on Department Engineering
Working on Department Administration
Working on Department Research
[student@studentvm1 testdir]$ ll
total 28
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Administration
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Engineering
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Finance
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Human-Resources
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Information-Technology
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Research
drwxrwxr-x 2 student student 4096 Apr 8 15:52 Sales

ここであるシナリオを想定してみましょう。あるLinuxマシンにインストールされている全RPMパッケージの一覧と、それぞれの簡単な説明を求められたら、どうアプローチしますか? 筆者は以前ノースカロライナ州で働いていたとき、実際にこの場面に遭遇しました。当時はオープンソースが州機関での使用として「承認」されておらず、筆者はデスクトップPCでだけLinuxを使っていたため、上司たち(いわゆるPHB)は例外を「承認」するために、筆者のコンピュータにインストールされたソフトウェアの一覧を要求してきたのです。

まず、rpm -qaコマンドがRPMの完全な情報を出力し、その中にPHBたちが求めている2項目――ソフトウェア名と概要――が含まれているという知識を出発点にしましょう。

最終形へ向かって、一歩ずつ組み上げていきます。まず、すべてのRPMを一覧表示します。

[student@studentvm1 testdir]$ rpm -qa 
perl-HTTP-Message-6.18-3.fc29.noarch
perl-IO-1.39-427.fc29.x86_64
perl-Math-Complex-1.59-429.fc29.noarch
lua-5.3.5-2.fc29.x86_64
java-11-openjdk-headless-11.0.ea.28-2.fc29.x86_64
util-linux-2.32.1-1.fc29.x86_64
libreport-fedora-2.9.7-1.fc29.x86_64
rpcbind-1.2.5-0.fc29.x86_64
libsss_sudo-2.0.0-5.fc29.x86_64
libfontenc-1.1.3-9.fc29.x86_64
<snip>

次に、sortuniqコマンドを追加して、リストをソートし重複を排除します(同じ名前のRPMが複数インストールされている可能性があるため)。

[student@studentvm1 testdir]$ rpm -qa | sort | uniq
a2ps-4.14-39.fc29.x86_64
aajohan-comfortaa-fonts-3.001-3.fc29.noarch
abattis-cantarell-fonts-0.111-1.fc29.noarch
abiword-3.0.2-13.fc29.x86_64
abrt-2.11.0-1.fc29.x86_64
abrt-addon-ccpp-2.11.0-1.fc29.x86_64
abrt-addon-coredump-helper-2.11.0-1.fc29.x86_64
abrt-addon-kerneloops-2.11.0-1.fc29.x86_64
abrt-addon-pstoreoops-2.11.0-1.fc29.x86_64
abrt-addon-vmcore-2.11.0-1.fc29.x86_64
<snip>

これで目的のRPMの正しいリストが得られました。このリストをループの入力として使い、各RPMの詳細情報をすべて表示させてみます。

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done

しかし、これでは欲しい情報よりはるかに大量のデータが出力されます。それでもループとしては完全に動作しています。次のステップは、PHBたちが求めた情報だけを抽出することです。そこで、^Nameまたは^Summaryを選択するegrepコマンドを追加します。キャレット(^)は行頭を意味するので、「Name」または「Summary」で始まる行のみが表示されます。

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary"
Name : a2ps
Summary : Converts text and other types of files to PostScript
Name : aajohan-comfortaa-fonts
Summary : Modern style true type font
Name : abattis-cantarell-fonts
Summary : Humanist sans serif font
Name : abiword
Summary : Word processing program
Name : abrt
Summary : Automatic bug detection and reporting tool
<snip>

上のコマンドでegrepの代わりにgrepを試すこともできますが、期待どおりには動作しません。また、出力をlessフィルタにパイプすれば、結果をゆっくり確認できます。最終的なコマンド列は次のようになります。

[student@studentvm1 testdir]$ for RPM in `rpm -qa | sort | uniq` ; do rpm -qi $RPM ; done | egrep -i "^Name|^Summary" > RPM-summary.txt

このコマンドラインプログラムは、パイプライン、リダイレクト、forループをすべて1行に収めています。CLIプログラムの出力をファイルにリダイレクトすることで、メールへの添付や他の用途への入力として利用できます。

このように一歩ずつプログラムを組み立てていく手法なら、各段階の結果を目視確認でき、期待どおりに動作し望む結果が得られていることを保証できます。

この試みの結果、PHBたちに渡したのは1,900を超える個別のRPMパッケージの一覧でした。誰かがそのリストを最後まで読んだかどうかは甚だ疑問ですが、筆者は求められたものをそっくりそのまま提供しました。以来、この件について二度と話を持ちかけられることはありませんでした。

その他のループ:whileとuntil

Bashにはさらに2種類のループ構造があります。whileuntilです。この2つは構文も機能も非常によく似ています。基本構文は以下の通りシンプルです。

while [ expression ] ; do list ; done

および

until [ expression ] ; do list ; done

前者のロジックは「式が真と評価される間、プログラム文のリストを実行し続ける。式が偽と評価された時点でループを抜ける」、後者は「式が真と評価されるまで、プログラム文のリストを実行し続ける。式が真と評価された時点でループを抜ける」と読めます。

whileループ

whileループは、論理式が真と評価される限り一連のプログラム文を実行し続けるときに使います。カレントディレクトリはまだ~/testdirにあるはずです。

whileループの最も単純な形は、永遠に実行され続けるものです。以下の例では、常に「真」のリターンコードを返すtrueステートメントを使用しています。単純な「1」を使っても同じように動作しますが、ここではtrueステートメントの使い方を示しています。

[student@studentvm1 testdir]$ X=0 ; while [ true ] ; do echo $X ; X=$((X+1)) ; done | head
0
1
2
3
4
5
6
7
8
9
[student@studentvm1 testdir]$

各部分を分解して見てきたので、このCLIプログラムの動作が理解できるようになったはずです。まず、前のプログラムやCLIコマンドの値が残っている場合に備えて$Xをゼロに初期化します。次に、論理式[ true ]が常に1(真)と評価されるため、dodoneの間にある命令群が永久に実行されます。停止させるにはCtrl+Cを押すなどして、シグナル2をプログラムに送ります。この命令群は算術展開であり、$Xの現在値を表示した後に1ずつ増加させます。

『The Linux Philosophy for Sysadmins』の信条のひとつに「エレガントさを追求する」があり、エレガントさを得る手段のひとつがシンプルさです。変数のインクリメント演算子++を使えば、このプログラムを簡潔にできます。まず、変数の現在値を表示してから増加させるパターンです。これは++演算子を変数の後に置くことで指定します。

[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((X++)) ; done | head
0
1
2
3
4
5
6
7
8
9

次に、プログラム末尾の| headを削除してもう一度実行してみてください。

今度のバージョンでは、値を表示する前に変数が増加されます。これは++演算子を変数の前に置くことで指定します。違いがわかりますか?

[student@studentvm1 ~]$ X=0 ; while [ true ] ; do echo $((++X)) ; done | head
1
2
3
4
5
6
7
8
9

こうして、変数の値を表示しつつ増加させる2つのステートメントを、1つにまとめることができました。デクリメント演算子--も存在します。

次に、特定の数値でループを止める方法が必要になります。そのために、true式を実際の数値評価式に変更しましょう。5でループを止める例を見てみます。以下のコードでは、-leが「以下(less than or equal to)」を表す論理数値演算子であることがわかります。つまり「$Xが5以下である限りループは継続し、$Xが6に増えた時点でループは終了する」という意味になります。

[student@studentvm1 ~]$ X=0 ; while [ $X -le 5 ] ; do echo $((X++)) ; done 
0
1
2
3
4
5
[student@studentvm1 ~]$

untilループ

untilコマンドはwhileコマンドとよく似ています。違いは、論理式が「真」と評価されるまでループを継続する点です。まず、この構造の最も単純な形を見てみましょう。

[student@studentvm1 ~]$ X=0 ; until false  ; do echo $((X++)) ; done | head
0
1
2
3
4
5
6
7
8
9
[student@studentvm1 ~]$

論理比較を使えば、特定の値までカウントすることもできます。

[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ]  ; do echo $((X++)) ; done
0
1
2
3
4
[student@studentvm1 ~]$ X=0 ; until [ $X -eq 5 ] ; do echo $((++X)) ; done
1
2
3
4
5
[student@studentvm1 ~]$

まとめ

このシリーズでは、Bashのコマンドラインプログラムやシェルスクリプトを構築するための強力なツールの数々を探ってきました。とはいえ、Bashでできる興味深いことのほんの表面をなぞっただけです。残りの可能性を切り拓くのは、あなた自身です。

筆者の経験上、Bashプログラミングを習得する最善の方法は、実際に手を動かすことです。複数のBashコマンドが必要になる簡単なプロジェクトを見つけて、それをCLIプログラムに仕上げてみましょう。システム管理者の日常業務にはCLIプログラミングに向いた作業がたくさんあります。自動化できるタスクはきっとすぐに見つかるはずです。

筆者自身、何年も前に、他のシェル言語やPerlに慣れ親しんでいましたが、すべてのシステム管理自動化作業をBashで行うことを決断しました。その結果、ときには少し調べものが必要になることもありましたが、必要なことはすべてBashで実現できています。

  1. Bashの代わりにPythonを使おう——コマンドライン言語として頼れる相棒にする方法

    私とBashの関係は、愛憎半ばするものです。ターミナルで過ごす時間が長いせいか、Bashはいつの間にか私のデフォルトの「プログラミング言語」になっていました。時々「find、grep、xargsこそがインフラを動かしているんだ」と人に話すと、みんな大笑いします。そして、私が本気だと気づくのです。 システム管理者にとってPythonを身につけるのは完璧な選択です。さらに、ターミナルでの作業が必要だけれどBashは使いたくない人や、Bashでは複雑すぎる処理を抱えている人にとっても最適です。タスクが次のようなワンライナーを超えたら—— find $(pwd) -name *.txt | xargs

  2. Excelで学生データベースを作成する方法|初心者向け簡単ステップ解説

    データベースは、大量のデータや情報を確実に管理できる仕組みです。ワークシートでは扱いきれないほどのデータ量も、効率的に保存・整理できます。データベースは多くの場合、必要なデータを素早く取り出せるようにするために作られます。Excelデータベースとは、行と列に配置されたデータが構造化されており、ワークシートの数式ですぐに活用できる状態になっているシートのことです。この記事では、Excelで学生データベースを作成する具体的な手順を解説します。 テンプレートのダウンロード 学生データベースのテンプレートをダウンロードして、実際に練習しながら進めることもできます。 Excelで学生データベースを作成す