はじめに
かつてシェルスクリプトではフロー制御に goto
コマンドが使われていました。Bourne シェルが誕生するよりも前の Thmopson シェルの時代の話です。goto
コマンドはシェルビルトインコマンドではなく外部コマンドとして C 言語(初期はアセンブリ言語)で実装されていました。goto
コマンドを外部コマンドとして実装できるということは、シェルを改造したりしなくても、今のシェルで使える goto
コマンドが作れるはずですよね? ということで作ってみました。時代に逆行したシェルスクリプトを書きたい人にどうぞ(念の為ですがジョークです。これは研究用であり実用目的ではありません。実用目的には必ず for
や while
などのシェル組み込みのフロー制御命令を使用してください)。
goto コマンドの実装
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAX_LINE_LENGTH 256
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <label>\n", argv[0]);
return 1;
}
const char *label = argv[1];
char line[MAX_LINE_LENGTH];
char target[MAX_LINE_LENGTH + 3]; // ": " + label + "\n" のため
// ": <label>\n" という形式のターゲットラベルを作成
snprintf(target, sizeof(target), ": %s\n", label);
// ファイルポインタを最初に移動(シーク)
if (fseek(stdin, 0, SEEK_SET) != 0) {
perror("Error seeking to the beginning of stdin");
return 1;
}
// 標準入力から一行ずつ読み込み、ラベルが見つかるまで繰り返す
while (fgets(line, sizeof(line), stdin)) {
if (strcmp(line, target) == 0) {
// ラベルが見つかったら終了
return 0;
}
}
// ラベルが見つからなかった場合
fprintf(stderr, "Label '%s' not found.\n", label);
return 1;
}
シェルスクリプト側
# カレントディレクトリにある goto コマンドを使えるようにするため
PATH=$PATH:.
i=1
: start
if [ "$i" -gt 10 ]; then
goto end
fi
echo "$i"
i=$((i + 1))
goto start
: end
echo end
実行結果
動作するシェルは bash、ksh、mksh、yash です。dash、zsh では動作しませんでした。推測ですが動かないシェルでは最適化のために、シェルスクリプトを 1 行ずつ読み込むのではなく、ある程度まとめて読み込んでいたりするのでしょう。
$ bash < test.sh
1
2
3
4
5
6
7
8
9
10
end
解説
goto
コマンドの実装は、単純にラベルの形式 : <ラベル>
の行が見つかるまで一行ずつ読み取ってファイルポインタを移動しているだけです。:
は Thompson シェルではコメントを記述するためのもので、goto
コマンドはコメントをラベルとして扱っていました。現在のシェルの :
コマンド(ヌルコマンド)が何もしないコマンドなのは Thompson シェルでコメントだったときの名残です。
ここでのポイントは bash
コマンドに対して標準入力からシェルスクリプトをデータファイルとして入力していることです。シバンで実行することはできません。シェルスクリプトの入力に標準入力を使うことで、シェルと goto
コマンドで同一のシェルスクリプトファイルのファイルディスクリプタを共有することができます。シェルがコードを 1 行ずつ実行し、goto
コマンドで次に実行するコードの位置(ファイルポインタ)を移動しているわけです。
この手法の欠点は、標準入力をシェルスクリプトファイルの読み込みに使ってしまっているために、シェルスクリプトの中から別のファイルをデータとして標準入力から読み込むことができないことです。Thompson シェル時代のシェルスクリプト(当時はコマンドファイルなどと呼ばれていました)は、プログラムではなくコマンド実行の履歴を記録したデータファイルでした。後の Bourne シェルではシェルスクリプトはプログラムとして、標準入力ではなくシェルの引数で渡せるように改良されました。
さいごに
「goto
コマンドを作ってみた」と言いましたが、実は嘘です。全部 ChatGPT さんに作ってもらいました。C 言語のソースコードは一切手を加えていません。いやー、楽ですね。
シェルの歴史に興味がある人へ。実は Thompson シェルを現在の OS に移植した人がいて、こんな物を作らなくても良かったりします。
Thompson シェルでは goto
だけではなく if
も外部コマンドです。またグロブパターンを展開する処理もシェルから外部コマンドを呼び出して実行しています(どこかでメモリが少なかったから外部コマンドに分離させたとかいうのを見た気がします)。ちなみに cd
コマンド(当時は chdir
コマンド)はさすがに外部コマンドではありません。Thompson シェルのマニュアルなどはこちらへどうぞ。