Panda Noir

JavaScript の限界を究めるブログでした。最近は幅広めに書いてます。

シェルを作ってみる その1

この記事は東北大学 計算機科学研究会 Advent Calendarの10日目の記事です。

部活の飲み会で記憶をふっ飛ばして、そのまま二日酔いで日曜が潰れたために書く暇がなく、大遅刻しています。今回はBourne ShellやZ shell、fish shellといった、いわゆる「シェル」を自作してみようという記事です。シェルの名前はずばりMyShellです。

1回目の目標

1回目では以下の3つを目標とします。

  1. プロンプトの表示と入力待ち
  2. 入力をスペースで区切る

コマンドの実行には色々と解説が必要だとわかったので、次回に回しました。

今回はいつもとは異なりC++で書いていきます。

プロンプトを表示してみる

まずはプロンプトを表示しましょう。これだけでもだいぶそれっぽくなります。

#include <iostream>
using namespace std;

int main() {
    cout << "$ ";
    return 0;
}

めちゃくちゃシンプルです。コンパイルして実行すると「$ 」が表示されます。それっぽいですね。

入力待ちをする

プロンプト(催促)を表示しているくせに入力を待っていないのはおかしい話なので入力待ちを実装します。

#include <iostream>
#include <string>
using namespace std;

int main() {
    string command;
    cout << "$ ";
    cin >> command;
    return 0;
}

コードの量は少ないですが、だいぶ形はそれらしくなってきました。

入力をスペースで区切る

入力をスペースで区切ることでコマンドと引数に分けます。この作業は次回のコマンド実行への布石です。

#include <vector>
#include <string>
using namespace std;

void parse(const string& line, vector<string>& res) {
    bool in_string = false;
    string term = "";
    for (auto& s : line) {
        if (s == '"') {
            if (!in_string) {
                in_string = true;
                term = "";
            } else {
                in_string = false;
                res.push_back(term);
                term = "";
            }
        } else if (in_string) {
            term += s;
        } else if (s == ' ') {
            if (term != "") res.push_back(term);
            term = "";
        } else {
            term += s;
        }
    }
    if (term != "") res.push_back(term);
}

これはどういう処理をしているかというと、

  1. 入力(line)を一文字ずつ見ていく
  2. 文字がダブルクォーテーションなら「文字列中にいるフラグ」(in_string)を立てる
  3. 文字がダブルクォーテーションでなく、文字列中の場合はtermにその文字を追加
  4. 文字列中でなく、文字がスペースの場合、それまでのtermをresに追加してtermを空にする
  5. それ以外の場合はtermに文字を追加する

こんな感じです。わざわざtermというものを使っているのは文字列中にスペースがあるケースを考慮しています。

シングルクォーテーションにもダブルクォーテーションと同様の方法で対応できますが、長くなってしまうのでMyShellでは実装しないことにしました。

合体させる

今回の締めとして、ここまでのプログラムを合体させて、受け取った入力からコマンド名を抜き出して表示させるプログラムを作ります。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

void parse(const string& line, vector<string>& res) {
    bool in_string = false;
    string term = "";
    for (auto& s : line) {
        if (s == '"') {
            if (!in_string) {
                in_string = true;
                term = "";
            } else {
                in_string = false;
                res.push_back(term);
                term = "";
            }
        } else if (in_string) {
            term += s;
        } else if (s == ' ') {
            if (term != "") res.push_back(term);
            term = "";
        } else {
            term += s;
        }
    }
    if (term != "") res.push_back(term);
}

int main() {
    string command, line;
    vector<string> args;

    cout << "$ ";
    getline(cin, line); // 1行まるごと受け付ける

    parse(line, args); // スペースで区切る
    command = args[0]; // 区切られたうちはじめの要素(=コマンド)を取り出す

    cout << command << endl;
    return 0;
}

やや変更した部分もありますが大筋は同じです。

終わりに

今回はとりあえず体裁を整えた感じです。次回はコマンド実行部分を作っていき、最低限必要そうな部分は完成させようと思います。

次回に続く