0
0

More than 1 year has passed since last update.

C言語で簡易的なシェルを作るプログラムを実際に理解しながら書いてみた

Posted at

注意:ソースコード内にコメントアウトを大量に書くタイプです。きれいじゃないです。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>

//○ユーザからのコマンドライン入力を読み取る関数
// 入力文字列を格納するためのバッファの初期サイズを表す定数
#define LSH_RL_BUFSIZE 1024
char *lsh_read_line(void)
{
  int bufsize = LSH_RL_BUFSIZE;
  // 現在の位置を初期化
  int position = 0;
  // バッファのメモリを動的に確保;右辺はchar型の変数の1文字分*(ここでは×の意味)文字列の文字数で指定されている
  char *buffer = malloc(sizeof(char) * bufsize);
  int c;

  // malloc関数はメモリ割り当てに失敗した場合、NULLを返す→確保失敗の際のエラーチェックとエラーハンドリング
  if (!buffer) {
    // strderrは標準出力を指定→printf関数は標準出力ストリーム(stdout)に出力するが、エラー出力ストリーム(stderr)を指定することにより、エラーメッセージを明確に識別できる、という利点がある
    fprintf(stderr, "lsh: allocation error\n");
    // EXIT_FAILUREはC言語の標準ライブラリで定義されているマクロ定数、プログラムの終了ステータスコードを表現、異常終了を示すための標準的な値で通常は1と定義されている(0が正常終了)
    exit(EXIT_FAILURE);
  }

  // while(1)は無限ループを作成する→ループは常に実行される
  while (1) {
    // Read a character、一文字ずつ入力を読み込んで変数に代入
    c = getchar();

    // If we hit EOF, replace it with a null character and return.EOF:End of File、ifの条件文は、読み込んだ文字がEOFか改行文字かを判定、EOFは、キーボード入力で、Ctrl+D(Unix/Linux/macOS)またはCtrl+Z(Windows)というキーボードショートカットを使用して入力
    if (c == EOF || c == '\n') {
      // バッファの現在位置にnull文字(\0)を代入→バッファ内の文字列が終端される
      buffer[position] = '\0';
      return buffer;
    } else {
      buffer[position] = c;
    }
    position++;

    // If we have exceeded the buffer, reallocate.バッファが一杯になった時実行される
    if (position >= bufsize) {
      // バッファサイズを追加のバッファサイズ分(1024と関数の直前に定義)だけ要領を拡張
      bufsize += LSH_RL_BUFSIZE;
      // 新しいバッファサイズに合わせてメモリを再割り当て→既存のデータは新しいメモリブロックにコピー
      buffer = realloc(buffer, bufsize);
      // メモリ割り当てができなかった場合のエラーチェック+ハンドリング
      if (!buffer) {
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
      }
    }
  }
}

// ☆与えられた文字列を指定されたデリミタ(区切り文字)で分割し、トークンの配列を返す関数
// トークン配列の初期サイズの指定
#define LSH_TOK_BUFSIZE 64
// トークンの区切り文字の指定、スペース' ',タブ'\t',キャリッジリターン'\r',改行'\n',ベル'\a'を区切り文字として扱うことを意味→これらのデリミタ文字列をstrtok関数の第二引数として使用することで、与えられた文字列をトークンとして分割する愛にこれらの文字を区切りとして認識する、つまり、文字列をこれらの文字で分割してトークンとして扱う→例として、command1 arg1 arg2という文字列を考えたとき、スペース文字をデミリタとして指定することで、command1,arg1,arg2という3つのトークンに分割される
#define LSH_TOK_DELIM " \t\r\n\a"
char **lsh_split_line(char *line)
{
  int bufsize = LSH_TOK_BUFSIZE, position = 0;
  // トークンの配列を格納するためのポインタ、tokensは文字列の配列へのポインターとなる。char **型はポインタを指すポインタを表す→ポインタへのポインタを使用することで文字列の配列の動的な確保・操作を実現→72行目でtoken,tokensをlineから分割・生成
  char **tokens = malloc(bufsize * sizeof(char*));
  // strtok関数を使用して分割されたトークンを一時的に格納するポインタ
  char *token;

  // トークン文字列に対して、領域が割り当てられなかった際のエラーチェック+エラーハンドリング
  if (!tokens) {
    fprintf(stderr, "lsh: allocation error\n");
    exit(EXIT_FAILURE);
  }

  //☆☆strtokはc言語の標準ライブラリ、文字列を指定したデミリタ(区切り文字)で分割するために使用、第一引数のlineは大元の関数が外部から受け取る文字列型ポインター引数。初回の呼び出し時に与えられた文字列を区切り文字で分割し、以降の呼び出しではNULLを渡すことで分割処理を続行する
  token = strtok(line, LSH_TOK_DELIM);

  // 最新回のwhileループの89行目においてtokenに割り当てられたline変数内のトークンが存在しない場合、ループが終了するが、この時、tokens[position]の要素には新しく追加するtokenが存在しないため、whileループ外の処理として、91行目を入れることでtokens配列の終端を定義
  while (token != NULL) {
    tokens[position] = token;
    position++;

    // tokenの
    if (position >= bufsize) {
      bufsize += LSH_TOK_BUFSIZE;
      tokens = realloc(tokens, bufsize * sizeof(char*));
      if (!tokens) {
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
      }
    }
    // 第一引数にNULLを渡すことで、72行目の分割処理を続行+分割処理において、次のトークンを取り出し、token変数に代入(再割り当て)している
    token = strtok(NULL, LSH_TOK_DELIM);
  }
  tokens[position] = NULL;

  // 完成したtokensを返す
  return tokens;
}

//☆argsという引数で指定されたコマンドが外部のものである場合にそのコマンドを実行するための関数。具体的には新しいプロセスを作成して、コマンドを実行し、その終了を待機する
int lsh_launch(char **args)
{
  // pid_t:プロセスの識別子(プロセスID)を表現するために使用されるデータ型、整数型(通常は符号付き整数)として定義されている→pid,wpidはどちらもプロセスの識別子を格納するために使用
  pid_t pid, wpid;
  int status;

  // fork関数:新しいプロセスを作成するシステムコール。新しく作成される新しいプロセスは現在のプロセスの複製、新しいプロセスは元のプロセスと同じプロセスイメージ(コード、データ、ヒープ、スタックなど)をもつが異なるプロセスIDを持つ。プロセス:実行中のプログラムのインスタンス、メモリやCPU時間などのリソースを割り当てられて独立して動作する。親プロセスは元のプロセスのままであり、子プロセスは新しいプロセスとのプロセスの複製を作成し、親プロセスと子プロセスの2つの実行フローを持つことができる、fork関数の戻り値が子プロセスの場合は0、親プロセスの場合は子プロセスのプロセスIDが返される;プロセス:コンピュータ上で実行されるプログラムの実体。プロセスはメモリやCPU時間などの子ロースを割り当てられ、独立して動作する。実行中のプログラムとその関連データの集合体としてとらえられる。プロセスとは他のプログラムと独立して動作する1つの動作単位。オペレーティングシステムによって管理され、必要なリソースの割り当てやプロセス間の通信を制御する。独立性から互いに影響を受けずに安定して実行されるように管理される

//[参考]カレントプロセスについて:
//   カレントプロセスとは、実行中のプログラムを実行しているプロセスのことを指します。以下に具体例を用いて説明します。

// 考えてみましょう。あなたがターミナルでコマンドを実行しているとします。その時点で、あなたが使用しているターミナル自体が1つのプロセスです。これがカレントプロセスです。

// 例えば、次のようにターミナルでコマンドを入力しました。

// shell
// Copy code
// $ ls -l
// この場合、カレントプロセス(ターミナルプロセス)はlsコマンドを実行するために新しいプロセスを作成します。

// その際、execvp関数を使用してlsコマンドを実行すると、カレントプロセスのイメージはlsコマンドのイメージで置き換えられます。これにより、新しいプロセスが作成され、新しいプロセスがlsコマンドの実行を担当します。

// つまり、カレントプロセスはlsコマンドを実行するプロセスに変わります。lsコマンドが終了すると、新しいプロセスの役割は終わり、カレントプロセスは再び元のターミナルプロセスに戻ります。

// カレントプロセスが他のプログラムに置き換えられることにより、異なるプログラムの実行を実現することができます。それぞれのプロセスは独立して実行され、自身のイメージ(コード、データ、ヒープ、スタックなど)を持つことができます。
//   pid = fork();
  // fork関数の戻り値が0の場合、子プロセスであり、その判定
  if (pid == 0) {
    // Child process
    // execvp関数により、args[0]で指定されたコマンドを実行、指定されたコマンドをカレントプロセスのイメージ(実行可能なプログラムやそのデータ)で置き換え、新しいプログラムを実行する
    if (execvp(args[0], args) == -1) {
      // エラーメッセージの表示
      perror("lsh");
    }
    // 子プロセスの終了
    exit(EXIT_FAILURE);
  } else if (pid < 0) {
    // Error forking
    perror("lsh");
  } else {
    // Parent process
    // 親プロセスは子プロセスの終了を待機
    do {
      // 子プロセスの終了を待機し、子プロセス終了した後に親プロセスが処理を再開する。引数:子プロセスのPID、ステータスを格納する変数へのポインタ、およびオプション
      
      // waitpid関数:子プロセスが終了するまで親プロセスを待機させるための関数
      //WIFEXITED(status):マクロ、子プロセスが正常に終了した場合に真を返す
      // WIFSIGNALED(status)もマクロ、子プロセスがシグナルによって終了した場合に真を返す
      wpid = waitpid(pid, &status, WUNTRACED);
      // wpidには終了した子プロセスのPIDが格納される
    } while (!WIFEXITED(status) && !WIFSIGNALED(status));
  }

  return 1;
}

/*
  Function Declarations for builtin shell commands:
 */
// cdコマンドの実装
int lsh_cd(char **args);
// helpコマンドの実装
int lsh_help(char **args);
// exitコマンドの実装
int lsh_exit(char **args);

/*
  List of builtin commands, followed by their corresponding functions.
 */
char *builtin_str[] = {
  "cd",
  "help",
  "exit"
};

int (*builtin_func[]) (char **) = {
  &lsh_cd,
  &lsh_help,
  &lsh_exit
};

// ビルトインコマンドの数を計算する関数
int lsh_num_builtins() {
  return sizeof(builtin_str) / sizeof(char *);
}

/*
  Builtin function implementations.
*/
int lsh_cd(char **args)
{
  // cdコマンドの引数がnull(ない)場合、cdコマンドは引数を必要とするため、エラー出力をエラーハンドリングとして実装
  if (args[1] == NULL) {
    fprintf(stderr, "lsh: expected argument to \"cd\"\n");
  } else {
    // ディレクトリの変更に失敗した場合、エラー出力、chdirでディレククトリを変更、変更先が正しくない場合は、変更されない
    if (chdir(args[1]) != 0) {
      perror("lsh");
    }
  }
  return 1;
}

int lsh_help(char **args)
{
  int i;
  printf("Stephen Brennan's LSH\n");
  printf("Type program names and arguments, and hit enter.\n");
  printf("The following are built in:\n");

  for (i = 0; i < lsh_num_builtins(); i++) {
    printf("  %s\n", builtin_str[i]);
  }

  // ここではmanコマンドは残念ながら定義されていないけど笑
  printf("Use the man command for information on other programs.\n");
  return 1;
}

int lsh_exit(char **args)
{
  // 0の返り値はシェルのメインループで使用され、プログラムの終了条件として扱われる
  return 0;
}


// 与えられたコマンドを実行するための関数
// args:コマンドとその引数を格納した文字列の配列、分割済み
int lsh_execute(char **args)
{
  int i;

  // エラーハンドリング、まず与えられたコマンドが空であるかどうかをチェック、空の場合、1を返して終了
  if (args[0] == NULL) {
    // An empty command was entered.
    return 1;
  }

  // lsh_num_builtins()はビルトインコマンドの数(builtin_strの要素数)を返す関数、シェルプログラム内で使用され、ビルトインコマンドの数を確認するために使用される;いると院コマンド:シェルの機能として直接実行される
  for (i = 0; i < lsh_num_builtins(); i++) {
    // ifの条件ブロック内は、args[0]のコマンドとbuiltin_str内で定義されたコマンドとを比較してその一致不一致を調べている→文字列比較で一致する場合、strcmp関数は0を返すのでその場合の処理を定義している
    if (strcmp(args[0], builtin_str[i]) == 0) {

      // 155-行目のbuiltin_funcの要素には関数のアドレスが格納されていることから本体を扱うために*でキャストし、その引数にargsを渡している
      return (*builtin_func[i])(args);
    }
  }
  // 与えられたコマンドが組込みコマンドではない場合、lsh_launch関数を呼び出して、外部コマンドを実行→lsh_launch関数は新しいプロセスを生成して、指定されたコマンドを実行する→実行が成功した場合、親プロセスに制御を返し、関数は1を返して終了する
  return lsh_launch(args);
}




//○対話型のシェル(コマンドラインインターフェース)のメインループを表現する関数、lsh=Linux Shellの略、おそらく、Linux環境下におけるシェル(コマンドラインインターフェース、ユーザとコンピュータの間で対話的にコマンドを実行するためのインターフェース)を指している
void lsh_loop(void){
  // ユーザが入力したコマンドラインの文字列を格納するためのポインタ
  char *line;
  // lineのコマンドライン文字列をトークン化(分割)した結果のトークン配列を格納するためのポインタ
  char **args;
  // lsh_execute()関数によって実行されたコマンドの終了ステータスを格納するための変数
  int status;

  do{
    printf(">");
    // 右辺の関数により、コマンドラインから文字列を読み取り、lineに格納
    line=lsh_read_line();
    // コマンドライン文字列をトークン化し、トークンの配列をargsに格納する→コマンドラインの各引数が個別の文字列として利用可能になる!!
    args=lsh_split_line(line);
    // トークン化されたコマンドライン引数を使用して、コマンドを実行する→コマンドラインの各引数が個別の文字列として利用可能になる+返り値として終了ステータス(int型の数値)を返す
    status=lsh_execute(args);

    // メモリ解放→不要なメモリリークを防ぐ
    free(line);
    free(args);
  }while(status);
}



int main(void){

  // Load config files,if any

  // Run command loop.
  lsh_loop();

  // perform any shutdown/cleanup
  return EXIT_SUCCESS;
}
0
0
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
0
0