はじめに
いろいろ遊べて楽しいというラズパイを手に入れました。

写真は公式ホームページから
とりあえずCUIのみのraspbianを入れて、有名な破滅の呪文sudo rm -rf /*をやったらどうなるんだろうってことでやってみました。

大量に出るエラー
案の定lsとかpythonとかいろんなコマンドが使えなくなっていたのですが、中にはcdのように使えるコマンドもあります。

cdは使える!
という訳で、sudo rm -rf /*に耐え抜いたコマンドだけを使ってbashのいろんな機能を解説しながら選択ソートを実装したいと思います。消え去ってしまったコマンド(ls、catなど)は自作しつつ、入力した数値列を選択ソートしてファイル出力するところまでがゴールです。
解説する機能の目次
- 組み込みコマンド
- 補完
- 入出力のリダイレクト
- シェル変数
- 関数
- フロー制御(if、for)
- 配列
追記(2020年3月24日)
この記事であまり扱えていないpsコマンドを実装した記事も書きました。リンクはこちら。
・参考文献
オライリー・ジャパン 入門bash
・実行環境
Raspberry Pi 4 Model B+
Raspbian Buster Lite 2020-02-13-raspbian-buster-lite.img
そもそもbashとは
bashはUNIXシェルの1つです。UNIXシェルというのは、簡単に言うと「ユーザーからの入力を受け付け、それをOS(=UNIX)が理解できる命令に変換して、OSからの出力をユーザーに伝えるプログラム」となります。
UNIXシェルには様々なものがあります。最初に主流になったのはBourneシェルで1979年に初めて搭載されました。shというコマンドで馴染みのある方もいると思います。次に広く使用されたのはCシェル(csh)でこれも1970年代後半に開発されています。これらの有名なシェルを元にtcshやksh、zshなど様々なシェルが作られました。
bashはこれらのUNIXシェルの中でも、標準的なシェルとして位置付けられています。MacOSもはつい最近までデフォルトでbashを使用していましたし、Ubuntuなどでは今でも標準のシェルです。
それではいきましょう。
1.組み込みコマンド
cdやlsというコマンドには、組み込みコマンド(=内部コマンド)と外部コマンドの2種類があります。組み込みコマンドとはシェルに組み込まれているコマンドのことで、外部コマンドはシェルとは独立して存在するコマンドのことです。
実際の例を紹介しましょう。例えばechoコマンドは、標準出力(ここでは画面)に文字を出力するコマンドです。echo aaaというコマンドを実行することで、aaaという文字列を出力します。

このechoコマンドはbashに組み込まれていて、ただechoと入力することで、組み込みコマンドであるechoが実行されます。しかし、echoには外部コマンドである/bin/echoという実行ファイルもあります。/bin/echo aaaと実行すれば、外部コマンドのechoが実行されてaaaと出力されるはずです。

外部コマンドの/bin/echoは消えていて、実行に失敗してしまいました。
このようにsudo rm -rf /*すると外部コマンドはあらかた消滅しています。よってこれからはbashの組み込みコマンドのみを使用していきます。ちなみに組み込みコマンドはhelpで全て表示することができます。
2.補完
さて、sudo rm -rf /*したものの、lsコマンドが使えないのでなんのファイルが残っているのかわかりません。ここで、bashの補完機能を使います。
補完機能はみなさん使ったことがあると思いますが、最初の何文字かを入力した後にtabキーを押すと、その文字列を補完してくれるやつです。
例えば、echまで入力した後にtabキーを押すと、echoと入力されます。

↓ tab

これはコマンド名を補完しているのでコマンド名補完といいます。
/に何のファイルが残っているかはファイル名補完を用います。/と入力した後にCtrl+x /と入力するとファイル名としての補完候補を出力してくれます。(なお、tabを2回押すでもOK)

6個のディレクトリが残っていることがわかりました。それぞれの中身を見ていきましょう。

/bootには何も入っていないようなので、.の補完だけ表示させておきました。それぞれのファイルの内容を解説し始めると記事が何本もかけてしまうので省略します。基本的にここに残っているのは特殊ファイルだったり、パーミッションなどの関係でrootユーザーでも消せないようなものばかりです。
なお、補完にはファイル名補完、コマンド名補完以外にも、ユーザー名補完、変数名補完、ホスト名補完があります。基本的に補完はtabでなんとかなりますが、補完がうまく行かない時にはこれらの補完を直接指定するとQOLが上がります。
3.入出力のリダイレクト
次にリダイレクトを使ってファイルの生成をします。そもそもUNIXプログラムには以下の共通した3つの機能を備えています。
- 標準入力という入力の受け付け
- 標準出力という出力の生成
- 標準エラー出力というエラーメッセージの生成
基本的に、標準入力はキーボードからの入力で、標準出力と標準エラー出力は画面です。
この標準入出力先は変えることができます。例えば「標準入力をファイルから読み込んで、標準出力もファイルに出力する」などです。標準出力先を変更するには>という記号を使います。例えばecho aaa > bbb.txtとすると、aaaと画面(=標準出力)に表示されずに、内容がaaaであるbbb.txtという名前のファイルが生成されます。このように入出力先をリダイレクトして変更できるのです。
普通/ディレクトリ等で上記のファイル生成コマンドを実行すると権限がないと怒られて、ファイルが生成されません。

sudoしたら作れるのではと思う方もいると思いますが、試しに正常な状態で/ディレクトリでsudoしてもだめでした。まあそもそも今はsudoコマンドすら消えてしまっているので、どちらにしろダメですが…
というわけで、今回はルート権限がなくてもファイルが作れるディレクトリを探しました(あたり前ですが、ディレクトリを作成するmkdirも権限を変更するchmodも今は存在しません)。見つけたのは/run/user/1000ディレクトリです。

エラーが出ませんでした。ファイル名補完でちゃんと表示されるか確認しましょう。

↓ ESC+/でファイル名補完

無事ファイルが作成されているようです。このファイルの中身がちゃんとhelloになっているか確認したいですが、catは消えてしまいました。よってシェル変数を使って中身を見ていこうと思います。
4.シェル変数
ここからはシェルプログラミングの内容に近づいていきます。bashも普通のプログラミング言語と同じように変数という概念を持っています。
変数には=で代入します。等号の両端にスペースは入れません。表示するには$を使います。

この例ではwonderlandという変数にaliceという文字列を代入しています。
それでは先ほど作成したhello.txtの中身を読んでみましょう。これにはreadコマンドを使います。これはシェル変数に値を読み込ませるコマンドです。

上の例では、キーボードから入力されたaaaという文字列を受け取って、シェル変数val1に代入しています。このreadコマンドにおいて、標準入力からの入力をファイルからの入力に変えましょう。それには<を使ってリダイレクトします。

変数val2にはhello.txtに書き込んだhelloという文字列がちゃんと入っていることが確認できました!
このシェル変数には、bashを起動した時点で既に組み込まれているものがたくさんあります。
- コマンドの履歴を保存する数を表す
HISTSIZE - プロンプト文(この例では
pi@raspberry:/ $と書かれているところ)を表示するためのPS1 - ユーザーが入力したコマンドを検索するために使われる
PATH - ホームディレクトリを示す
HOME
などです。sudo rm -rf /*されていてもこいつらは参照できます。

これらの組み込み変数は全て大文字で記述されています。全ての組み込み変数を見たい方はdeclareを実行してください。
この組み込み変数の中には環境変数と呼ばれる特殊な変数もあります。これは全てのサブプロセスが知ることのできる変数です。上の4つの例でいうとPATHとHOMEは環境変数です。例えばbashからpythonファイルを実行する時に、そのプロセスはコマンドの履歴に関するHISTSIZEなんて使いませんが、PATHやHOMEがないとファイルの実行時や保存時に困ることになると思います。
環境変数をすべて見たい方はexportを実行してください。
5.関数
bashでは関数を定義することができます。定義の仕方の一例はこんな感じです。
function < 関数名 >
{
< シェルコマンド >
}
実際に関数を作ってみましょう。

aliceという名前の関数です。ここでは位置パラメータと呼ばれる組み込み変数を使用しています。位置パラメータは、あるスクリプトが呼び出された時に、そのコマンドライン引数を保持します。0にはコマンドの名前、1には1つ目の引数、2には2つ目の引数、といった感じです。@は位置パラメータ0を除く全ての引数が、#は引数の数を保持します。
これをalice in wonderlandとして実行すると以下のようになります。

今回の例だと$3と$4には何も入っていません。
これを使って単純なcatコマンドを生成しましょう。引数の1つ目($1)だけをみて、そのファイルの中身を出力する関数です。

実際に使ってみます。3.入出力のリダイレクトで作ったhello.txtの中身を表示させてみましょう。

ちゃんと動きました! 1
6.フロー制御
bashにもif/else、for、while、until、case、selectなど様々な制御構文がありますが、今回はif文とfor文に話を絞ります。
6.1 if文
if文の構文は以下の通り。
if < 条件 >
then
< 一連の文 >
elif < 条件 >
then < 一連の文 >
else
< 一連の文 >
fi
普通のプログラミング言語と同じようにelifは複数挿入可能で、elifとelseは省略可能です。
ここで大事なのは、ifの< 条件 >には通常のブール式ではなく普通のコマンドが入るということです。ではコマンドの何を見て条件式を考えるかというと、終了ステータスです。
全てのUNIXコマンドは、終了時に呼び出し元のプロセス(この場合はシェル)に整数コードを返します。これを終了ステータスと言い、通常は0が正常終了、それ以外が異常終了を表します。終了ステータスはシェル変数?に自動で代入されます。

上の例ではlsコマンドの実行に失敗しているので終了ステータスに127が入り、cdコマンドには成功しているので0が入っています。
ここで、シェルのtestコマンドを使うことで数式も評価できるようになります。例えばtest 3 -ne 4なら真なので0を返し(-neは両辺が等しくない(not equal)の意味)、test 4 -lt 2なら偽なので1を返します(-ltは右辺が左辺より小さい(less than)の意味)。このコマンドを使えばifの中でも適切に真偽判定ができるようになります。

なおtestコマンドは[ ... ]と書くこともできます。例えばtest 3 -eq 4は、[ 3 -le 4 ]と同値です(-leは右辺が左辺以下(less than equal)の意味)。具体的なif文の例を見てみましょう。

変数aと変数bの大小を比べて、echoで結果を出力する例です。今回はaの方が小さいので、a is smallという文字列が返ってきています。
また算術演算に関しては、$((と))を使って表すこともできます。

記号の意味は基本的にC言語と同じです。ただ注意としては、$(( 5 > 3 ))は真なので1を返しますが、[ 5 -gt 3]では終了ステータスとして0を返します。気をつけましょう。
6.2 for
次にfor文を見ていきます。for文の構文は以下です。
for name in list
do
< $nameを使用する一連の文 >
done
listにはファイルの名前を指定します。もしin listを省略すると、デフォルトでは$@(コマンドラインの引数のリスト)になります。for文の簡単な具体例としては以下です。

見た通りimg1.jpgからimg3.jpgまでを表示しています。ただこのfor文はもっと簡単に書けます。

ここではブレース展開を用いています。例えばaa{1..3}.cはbashによってaa1.c aa2.c aa3.cと展開されます。他には,を用いることもでき、例えばb{ed, ar}sはbeds barsと展開されます。
それではここで、消え去ってしまったlsコマンドを作ってみましょう。

*はワイルドカードの一種で、そのディレクトリ内のファイル名の任意の文字列に一致します。echo *とすれば*はそのディレクトリ内のファイル名に展開されます。それではこのlsを動かしてみます。

ちゃんと動きました!2
実はbashのfor文には、C言語やJavaに近い算術演算forループと呼ばれる構文もあります。構文は以下です。
for (( < 初期条件 > ; < 終了条件 > ; < 更新処理 > ))
do
< 一連の文 >
done
これを使って九九の表を作ってみます。

iとjの2つのfor文を回しています。echoの-nオプションは1つの数字で勝手に改行をしないようにし、-eオプションは\tをタブとして出力するために付けています。内側のfor文の終了後のechoで改行します。
7.配列
これで最後の章です、実はbashでは、配列を使うことができます。例えば次のように定義できます。

この中身をechoで見てみましょう。

echo $aを見るとわかるように、引数を省略すると0番目の要素が出てきます。また、$a[0]と${a[0]}の違いに注意してください。実は今まで使用していた$variableという構文は${variable}の省略形でした。$a[0]と指定すると、$aだけが12に変換されるので12[0]が出力されます。
また配列には特殊な記号の@や#を使用することもできます。@は配列の全ての要素を参照するもので、配列のインデックスに入れて使用します。

全ての要素が表示されているのが分かります。
また#は配列の長さを参照するもので、配列の先頭に付けます。

a[0]は12なので長さが2、a[@]は配列全体なので長さは3になります。
それでは、ここまでの構文を使ってユーザーが入力した数値列を選択ソートをしてファイル出力するというタスクに取り組みます。
ここでは配列の大きさは5とします。まずはユーザーに数字を入力させ、配列arrに代入する関数input_arrayを作ります。

echoでユーザー入力を促し、readを使って配列に値を代入しています。実際に実行してみましょう。数字は適当に入れます。

配列の中身を見てみます。

配列arrに5、12、2、15、7がちゃんと入りました。
それでは配列arrの中身を選択ソートで入れ替えましょう。これは「範囲内の最小の数字を先頭に持っていく」ことを繰り返すことでソートするアルゴリズムです。

ちゃんと動くか確認しましょう。

先ほど入れたarrの中身がちゃんとソートされているのが分かります!
これらをまとめる関数output_arrayを作って完成です。

ソートした結果はoutput.txtに書き出します。この関数を実行してみましょう。

ちゃんと無事動きました。
終わりに
組み込みコマンドlogoutで、ログアウトします。このディスクはもう起動しないので、またSDカードにOSを焼き直します。

実はSSHでラズパイに繋いでいました。sudo rm -rf /*してもSSH接続が切れなかったのはちょっと驚きでしたね。
bashにはまだまだ紹介しきれなかった機能がたくさんあります。psコマンドが使えないこともあり、プロセス関連は触れられませんでした。ぜひ皆さんも、sudo rm -rf /*した極限状態のシェルプログラミングを試してみてください。