4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シェルスクリプトで使えるgotoコマンドを作ってみた

Last updated at Posted at 2024-08-31

はじめに

かつてシェルスクリプトではフロー制御に goto コマンドが使われていました。Bourne シェルが誕生するよりも前の Thmopson シェルの時代の話です。goto コマンドはシェルビルトインコマンドではなく外部コマンドとして C 言語(初期はアセンブリ言語)で実装されていました。goto コマンドを外部コマンドとして実装できるということは、シェルを改造したりしなくても、今のシェルで使える goto コマンドが作れるはずですよね? ということで作ってみました。時代に逆行したシェルスクリプトを書きたい人にどうぞ(念の為ですがジョークです。これは研究用であり実用目的ではありません。実用目的には必ず forwhile などのシェル組み込みのフロー制御命令を使用してください)。

goto コマンドの実装

goto.c
#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;
}

シェルスクリプト側

test.sh
# カレントディレクトリにある 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 シェルのマニュアルなどはこちらへどうぞ。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?