シェルを作ろう(1)コマンドを実行する。

さて、今回は予告通り入力を受け取って、コマンドを実行するところまで試してみたいと思います。

使用する関数、システムコール

  • printf
  • fgets
  • strcpy
  • strtok
  • calloc/malloc
  • execve

入力を受け取る(fgets)

さて、Cで入力(文字列)を受け取りたい時どんな関数を使おうと思うでしょうか?
多くの人はscanfを思い浮かべるかと思います、この関数も使い方によってはとても便利なのですが、 シェルのコマンドでは基本的にプロンプトが表示された行に入力された文字列を一行丸ごと読みとりたいので、scanfだと少し不便です。 (ここではscanfが本質ではないですが、scanfを使うと空白文字が挟まるごとに文字列を格納するための空間が新しく必要になりますよね?)

というわけで今回は一行ごとに入力を受け取るfgetsという関数を使いたいと思います。
以下プログラム例

#include <stdio.h>

#define SIZE 128

int main(void){
    char buf[SIZE] = "";

    fgets(buf,SIZE,stdin);
    printf("%s",buf);

    return 0;
}

実行例

$gcc -Wall -std=c99 test_fgets.c -o test_fgets
$./test_fgets
   hello world!
   hello world!
$

上記のようにfgets関数を使うと改行が現れるまで、空白' ‘やタブ’\t',改行文字'\n'も合わせて一つの空間に読み込んでくれます。 (改行文字も一緒に読み込んでくれるので,printfで出力する時に\nがなくても改行してくれます。)

プログラムの実行について

さて、次はstrtokの説明です…と言いたいところですが、その前にstrtokを使う理由を説明したいと思います。 例えばlsコマンドなどを使う時,

$ls -la

実はこのようにコマンドを実行する時、入力したコマンドやオプションは空白文字ごとに分割され、main関数の第二引数に渡されて (ちなみにmain関数の第一オプションにはコマンドとオプションの合計の個数 (コマンドライン引数の個数)が渡されます。) それをプログラムが解釈して実行されています。

例えば上の例ようにコマンドを入力すると int main(int argc,char *argv[])main関数の引数を設定すると argcには2 argv[0]には"ls"という文字列(が確保されている場所へのポインタ)、 argv[1]には"-la"という文字列(が確保されている場所へのポインタ) argv[2]にはNULLが格納されます。

このように、コマンドを実行するために受け取った文字列を空白文字区切りで配列に格納するために便利なのがstrtokという関数です。

文字列を分割する。(strtok)

strtokのプログラム例

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

define SIZE 128

int main(void){
    char commands[128] = "";
    char *tmp = (char *)NULL;
    
    printf("$");
    fgets(commands,SIZE,stdin);
    
    tmp = strtok(commands," \t\n");
    
    printf("%s\n",tmp);
    
    while((tmp = strtok(NULL," \t\n"))!= NULL){
        printf("%s\n",tmp);
    }
    return 0;
}

実行例

$gcc -Wall -std=c99 test_strtok.c -o test_strtok
$./strtok
    this is a pen
this
is
a
pen
$

このように strtokを使うと、一つの文字列を指定した文字で簡単に分割できます。

さて次はいよいよコマンドの実行です。

コマンドを実行する(execve)

さて、コマンドを実行するためにはexecveを使用するのですが、このシステムコールには3つの引数があります。 まず第一引数は実行するコマンドのパス名 これで指定したコマンドがあればそれを実行し(厳密には単に実行するわけではない)、なければ-1を返して通過します。 第二引数にはargvのような順でコマンドライン引数に相当する文字列集合を渡します。 第3引数にはmain関数第3引数(一般にenvpと表記される。)に渡される環境を渡します。
これはコマンドライン引数に現れないパスなどの値が格納されています。 さてそれでは以下にプログラム名を示します。
実際に試してみましょう!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>


#define BUFSIZE 128
#define ARGNUM 32

int main(int argc, char *argv[], char *envp[]){

    char buf[BUFSIZE];
    char *p;
    char *arg[ARGNUM];
    int i = 0;    

    fgets(buf,BUFSIZE,stdin);

    p = strtok(buf," \t\n");
    
    arg[i] = calloc(strlen(p),sizeof(char));
    strcpy(arg[i],p);
    //printf("%s\n",arg[i++]);

    while((p = strtok(NULL," \n\t")) != NULL){
    
        arg[i] = calloc(strlen(p),sizeof(char));
        strcpy(arg[i],p);
        //printf("%s\n",arg[i++]);
    }
    
    arg[i] = NULL;
    execve(arg[0],arg,envp);
    printf("failed execve\n");
    return 0;
}

(本当はfreeをして解放するべきですがうっかり時間がないので割愛)

実行例

$ ./test_execve 
/bin/ls -a
.   test_execve    test_fgets    test_strtok
..  test_execve.c  test_fgets.c  test_strtok.c
$./test_execve
hello
failded execve
$

注 この際、まだパス展開の機能がないのでプログラムはフルパスで指定してやる必要があります。

うまく実行できましたね! 短い説明ですが、とりあえず今回はこれで終わりとさせていただきます。