Panda Noir

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

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

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

第二回目の記事ですので、第一回からご覧ください。

2回目の目標

  1. コマンドを実行する

これだけです。意外と書くことが多かったのでかんべんしてください。

コマンドを実行する関数

コマンドを実行する関数としてはsystem、popen、exec関数が挙げられます。そのうちsystemとpopenは他のシェルを立ち上げて実行するので論外です。ただ他のシェルのラッパーを作ったって何も面白くありませんからね。というわけでexec一択です。

exec関数

exec関数は<unistd.h>内にあります。と、ここではじめのトラップですが、実はexec関数そのものは存在しません。execl、execvというような関数しかありません。これらは「引数を可変長引数として取る」か「引数を配列として取る」かという違いのみで使い方自体は変わりません。

このようにして使います。

#include <unistd.h>
using namespace std;

int main() {
    execl("/bin/echo", "/bin/echo", "hoge", NULL);
    return 0;
}

このプログラムを実行するとhogeと表示されます(echoへのパスは適宜変更してみてください)。

"/bin/echo"が二回渡されていますが、1つ目がコマンドのパスで、2つ目はコマンドへの引数として渡されます*1

forkする

さて、これでプログラムの実行まで出来るようになりました。めでたしめでたし・・・とは行きません。実はexecするとそれ以降はexecしたプログラムが現在のプロセスを上書きしてしまうのでexecl以降の行が実行されません。

#include <unistd.h>
#include <iostream>
using namespace std;

int main() {
    cout << "実行される" << endl;
    execl("/bin/echo", "/bin/echo", "hoge", NULL);
    cout << "実行されない!!" << endl;
    return 0;
}

「実行されない!!」は表示されないかと思います。これでは一つコマンドを実行するたびにシェルが終了してしまい使い物になりません。

そこで、子プロセスを生成し、そこで実行するようにします。

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string>
#include <vector>

using namespace std;

int exec(string command, vector<string> args) {
    int pid, code, status;
    pid_t result;

    // vector<string>をchar**にする作業
    char** arg = NULL;
    arg = new char*[args.size() + 1];
    for(size_t i = 0; i < args.size(); i++) arg[i] = (char*) args[i].c_str();
    arg[args.size()] = NULL;

    pid = fork(); // プロセスをフォークする
    if (pid == -1) cerr << "Error" << endl;
    else if (pid == 0) execv(command.c_str(), arg); // コマンドを実行する
    else {
        // 親プロセスの中
        result = wait(&status);
        if (result < 0) {
            cerr << "Wait Error." << endl;
            exit(-1);
        }
        if (!WIFEXITED(status))
            cout << "wait失敗" << endl << "終了コード: " << status << endl;
    }
    return 0;
}

指定したプログラムを実行するexec関数メモを参考させていただきました。

これでコマンドを実行する準備ができました。

PATHを読み込んで解析する

環境変数を読み込むにはgetenv関数を使えばいいらしいです。得られたPATHをコロンで区切ったvectorにしてあとは順にループすればOKですね。

#include <vector>
#include <string>
#include "exec.cpp"
#include "exists.cpp"
#include "split.cpp"

int main() {
    string command = "echo";
    vector<string> args = {"echo", "hoge", "fuga"}; // コマンドに渡す引数
    vector<string> PATHs = split(getenv("PATH"), ':'); // 環境変数PATHをコロン区切りにしたデータ
    if (exists(command)) exec(command, args);
    else for (auto& PATH : PATHs) {
        // PATHの配下にcommandがあるか確認していく
        if (exists(PATH + "/" + command)) {
            args[0] = PATH + "/" + command;
            exec(PATH + "/" + command, args);
            break;
        }
    }
}

exists.cppとsplit.cppは自分で実装するのが面倒だったので偉大な先人のコードをコピペしたものを使いました。

[C++]ファイルの存在確認 - Qiita

C++におけるstringのsplit - Qiita

いやー先人というのは偉大ですね。前者のコードのcheckFileExistence()は個人的に好きでなかったのでexists()としましたが、あとは同じです。

あとで使いまわしやすいように、この処理を関数にまとめます。

#include <string>
#include "exists.cpp"
#include "split.cpp"

using namespace std;

string searchCommand(string command) {
    if (exists(command)) return command;
    for (auto& path : split(getenv("PATH"), ':'))
        if (exists(path + "/" + command))
            return path + "/" + command;
    return "";
}

今回までの進捗をまとめてみる

まとめてみたらだいぶ大きくなってしまったのでGitHubに上げました。

MyShell

*1:コマンドラインツールを作ったことがある人なら分かると思いますが、コマンドが受け取る引数は実は「実行されたコマンドのパス + ユーザーが渡した引数」となっています。