ユーニックス総合研究所

  • home
  • archives
  • c-easy-shell

C言語で簡易シェルを作る

  • 作成日: 2023-08-07
  • 更新日: 2023-12-24
  • カテゴリ: C言語

C言語で簡易シェルを作る

毎日暑いですがみなさま体調の方はよろしいでしょうか。
この暑さで無理をするとあっという間に天国へ行ってしまいますので気を付けてください。

私も熱湯部屋でプログラミングをしてあの世に行かないように気を付けています。
シャレじゃなくほんとにあの世に行ってしまいますからね。

さて今回は暑いので簡易シェルを作ります。
シェルって端末ですね。Bashとかコマンドプロンプトとか。

と言ってもエミュレーターみたいなもので実際には何の仕事もしません。
ただフォルダを作れて親フォルダと子フォルダを移動できるだけです。

関連記事
C言語でcharをintに変換する方法
C言語でenumをtypedefして使う【列挙型】
C言語でforeachマクロを実装する方法
C言語でnull判定する方法【NULL, 比較】
C言語でできることを解説!C言語歴16年の開発者が語る
C言語でオブジェクト指向する【単一継承の方法】
C言語でグローバルに関数を使う方法
C言語でシャローコピーとディープコピーを実装する

実行風景

以下は簡易シェルを実行している所です。

ソースコード全文

ソースコード全文は以下になります。

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

enum {  
    NCHILDS = 8,  
    NFNAME = 256,  
};  

typedef struct Node {  
    char name[NFNAME];  
    struct Node *parent;  
    struct Node *childs[NCHILDS];  
    size_t childs_len;  
} Node;  

typedef struct {  
    Node *root;  
    Node *ref_current;  // do not delete  
} Shell;  

void  
node_del(Node *self) {  
    for (size_t i = 0; i < self->childs_len; i++) {  
        node_del(self->childs[i]);  
    }  
    free(self);  
}  

Node *  
node_new(void) {  
    return calloc(1, sizeof(Node));  
}  

void  
shell_destroy(Shell *self) {  
    node_del(self->root);  
}  

void  
shell_init(Shell *self) {  
    *self = (Shell){0};  
    self->root = node_new();  
    self->ref_current = self->root;  
    strcpy(self->root->name, "/");  
}  

void  
shell_draw(Shell *self) {  
    system("clear");  // Windows: shelltem("cls");  

    if (self->ref_current == NULL) {  
        return;  
    }  

    Node *node = self->ref_current;  
    printf("[ %s ]\n", node->name);  

    for (size_t i = 0; i < node->childs_len; i++) {  
        Node *child = node->childs[i];  
        if (i == node->childs_len - 1) {  
            printf("┗ ");  
        } else {  
            printf("┣ ");  
        }  
        printf("[ %s ]\n", child->name);  
    }  
}  

bool  
shell_update(Shell *self) {  
    printf("cd:n,  mkdir:name, quit > ");   
    fflush(stdout);  

    char cmd[128];  
    if (!fgets(cmd, sizeof cmd, stdin)) {  
        return false;  
    }  
    cmd[strlen(cmd) - 1] = '\0';  

    if (!strncmp(cmd, "cd", 2)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (!strcmp(p, "..")) {  
                if (node->parent) {  
                    self->ref_current = node->parent;  
                }  
            } else {  
                int n = atoi(p);  
                if (n >= node->childs_len) {  
                    fprintf(stderr, "childs overflow\n");  
                    return true;  
                }  

                Node *child = node->childs[n];  
                if (strlen(child->name)) {  
                    self->ref_current = child;  
                }  
            }  
        }  
    } else if (!strncmp(cmd, "mkdir", 5)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (node->childs_len >= NCHILDS) {  
                fprintf(stderr, "childs overflow\n");  
                return true;  
            }  

            Node* child = node_new();  
            strcpy(child->name, p);  
            child->parent = node;  
            node->childs[node->childs_len++] = child;  
        }  
    } else if (!strncmp(cmd, "quit", 4)) {  
        return false;  
    }  

    return true;  
}  

int main(void) {  
    Shell shell;  
    shell_init(&shell);  

    for (;;) {  
        shell_draw(&shell);  
        if (!shell_update(&shell)) {  
            break;  
        }  
    }  

    shell_destroy(&shell);  

    return 0;  
}  

定数

定数は以下になります。

enum {  
    NCHILDS = 8,  
    NFNAME = 256,  
};  

NCHILDSはノードが持つ子ノードの数です。
NFNAMEはノードのnamechar配列)の要素数です。

構造体

構造体は以下になります。

typedef struct Node {  
    char name[NFNAME];  
    struct Node *parent;  
    struct Node *childs[NCHILDS];  
    size_t childs_len;  
} Node;  

typedef struct {  
    Node *root;  
    Node *ref_current;  // do not delete  
} Shell;  

Nodeはフォルダなどを表すオブジェクトの構造体です。
Nodenameはノードの名前(フォルダ名)、parentは親ノードへのリンク、childsは子ノードの配列、childs_lenは子ノードの現在の長さです。

Shellはシェルを表す構造体です。
rootは動的にメモリ確保されたルートノード(根ノード)、ref_currentは現在表示するノードへの参照(free不要なポインタ)です。

ノード間を移動するにはcdコマンドを使いますが、cdを実行するとref_currentが更新されます。
ノードを作るにはmkdirコマンドを使いますが、ノードを作るとchildsに動的にメモリ確保されたノードのアドレスが保存されます。

main関数

main関数は以下になります。

int main(void) {  
    Shell shell;  
    shell_init(&shell);  

    for (;;) {  
        shell_draw(&shell);  
        if (!shell_update(&shell)) {  
            break;  
        }  
    }  

    shell_destroy(&shell);  

    return 0;  
}  

まずshell_init()でシェルを初期化します。
それから無限ループに入ってshell_draw()で描画、shell_update()で更新を行います。
shell_update()falseを返して来たら無限ループから抜けてshell_destroy()でメモリを解放します。

shell_init関数

void  
shell_init(Shell *self) {  
    *self = (Shell){0};  
    self->root = node_new();  
    self->ref_current = self->root;  
    strcpy(self->root->name, "/");  
}  

shell_init()ではself->rootにルートノードを確保します。
node_new()でノードのメモリを確保しています。
self->ref_currentself->rootを設定して現在のノードをルートにします。
self->root->nameに文字列の「/」を設定します。

shell_destroy関数

void  
shell_destroy(Shell *self) {  
    node_del(self->root);  
}  

shell_destroy()ではself->rootnode_del()で削除します。

node_new, node_del関数

Node *  
node_new(void) {  
    return calloc(1, sizeof(Node));  
}  

node_new()Nodeのメモリをcalloc()で確保します。
calloc()はメモリ確保後に確保したメモリを0クリアします。

void  
node_del(Node *self) {  
    for (size_t i = 0; i < self->childs_len; i++) {  
        node_del(self->childs[i]);  
    }  
    free(self);  
}  

node_del()childsの要素をnode_del()して最後にselffree()します。

shell_draw関数

void  
shell_draw(Shell *self) {  
    system("clear");  // Windows: shelltem("cls");  

    if (self->ref_current == NULL) {  
        return;  
    }  

    Node *node = self->ref_current;  
    printf("[ %s ]\n", node->name);  

    for (size_t i = 0; i < node->childs_len; i++) {  
        Node *child = node->childs[i];  
        if (i == node->childs_len - 1) {  
            printf("┗ ");  
        } else {  
            printf("┣ ");  
        }  
        printf("[ %s ]\n", child->name);  
    }  
}  

shell_draw()関数ではシェルを描画します。
まずsystem("clear")で端末の描画をクリアします。
これはWindowsの場合はsystem("cls")になります。
system関数は引数のコマンドを実行します。

self->ref_currentnameを出力。
それからnode->childsの要素を取り出して罫線とchild->nameを出力します。

出力すると以下のようになります。

[ / ]  
┣ [ hige ]  
┣ [ hoge ]  
┗ [ moge ]  

shell_update関数

shell_update関数はシェルを更新します。

bool  
shell_update(Shell *self) {  
    printf("cd:n,  mkdir:name, quit > ");   
    fflush(stdout);  

    char cmd[128];  
    if (!fgets(cmd, sizeof cmd, stdin)) {  
        return false;  
    }  
    cmd[strlen(cmd) - 1] = '\0';  

    if (!strncmp(cmd, "cd", 2)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (!strcmp(p, "..")) {  
                if (node->parent) {  
                    self->ref_current = node->parent;  
                }  
            } else {  
                int n = atoi(p);  
                if (n >= node->childs_len) {  
                    fprintf(stderr, "childs overflow\n");  
                    return true;  
                }  

                Node *child = node->childs[n];  
                if (strlen(child->name)) {  
                    self->ref_current = child;  
                }  
            }  
        }  
    } else if (!strncmp(cmd, "mkdir", 5)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (node->childs_len >= NCHILDS) {  
                fprintf(stderr, "childs overflow\n");  
                return true;  
            }  

            Node* child = node_new();  
            strcpy(child->name, p);  
            child->parent = node;  
            node->childs[node->childs_len++] = child;  
        }  
    } else if (!strncmp(cmd, "quit", 4)) {  
        return false;  
    }  

    return true;  
}  

まずプロンプトを出して一行入力をユーザーにさせます。

    printf("cd:n,  mkdir:name, quit > ");   
    fflush(stdout);  

    char cmd[128];  
    if (!fgets(cmd, sizeof cmd, stdin)) {  
        return false;  
    }  
    cmd[strlen(cmd) - 1] = '\0';  

cd:0と入力すると現在のノードの0番目の子要素に移動。
cd:1では1番目の要素に移動します。
cd:..では現在のノードの親ノードに移動します。

mkdir:higeと入力すると現在のノードの子ノードとしてhigeという名前のノードを作ります。

    if (!strncmp(cmd, "cd", 2)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (!strcmp(p, "..")) {  
                if (node->parent) {  
                    self->ref_current = node->parent;  
                }  
            } else {  
                int n = atoi(p);  
                if (n >= node->childs_len) {  
                    fprintf(stderr, "childs overflow\n");  
                    return true;  
                }  

                Node *child = node->childs[n];  
                if (strlen(child->name)) {  
                    self->ref_current = child;  
                }  
            }  
        }  
    }  

上記ではcdコマンドの処理を書いています。
まず入力されたcmdstrncmp()で先頭に2文字だけ"cd"と比較します。

それからstrchr():を探索しそのポインタを取得します。
ポインタがあればp++して:を読み飛ばします。
それからp..ならnodeself->ref_current)のparentref_currentに保存します。
それ以外、つまりpが数字ならatoi()で数値に変換します。
そしてnode->childsの数字の要素のノードを取り出し、そのノードをref_currentに保存します。

    else if (!strncmp(cmd, "mkdir", 5)) {  
        char *p = strchr(cmd, ':');  
        if (p) {  
            p++;  
            Node *node = self->ref_current;  

            if (node->childs_len >= NCHILDS) {  
                fprintf(stderr, "childs overflow\n");  
                return true;  
            }  

            Node* child = node_new();  
            strcpy(child->name, p);  
            child->parent = node;  
            node->childs[node->childs_len++] = child;  
        }  
    }  

上記ではmkdirコマンドの処理を書いています。

node_new()でノード(child)を作り、そのノードのchild->namepをコピーします。
つまりmkdir:higeならhigechild->nameにコピーされます。
child->parentに親ノードとしてnoderef_current)を入れておきます。
そしてnode->childschildを保存します。

    else if (!strncmp(cmd, "quit", 4)) {  
        return false;  
    }  

上記ではquitコマンドの処理です。
shell_update()は返り値がboolfalseを返すと無限ループが終了します。
trueの場合は無限ループが継続します。

おわりに

今回はC言語で簡易シェルを作りました。
なにか参考になれば幸いです。

🦝 < シェルシェルシェル

🦝 < クーラー付きシェル