프로젝트 11: 미니 쉘

프로젝트 11: 미니 쉘

간단한 명령어 쉘을 직접 구현해봅니다.

학습 목표

  • 프로세스 생성 (fork)
  • 프로그램 실행 (exec 계열)
  • 파이프와 리다이렉션
  • 시그널 처리 기초

사전 지식

  • 문자열 처리
  • 파일 I/O
  • 포인터와 동적 메모리

1단계: 기본 쉘 구조

쉘의 기본 동작: 읽기 → 파싱 → 실행 → 반복

가장 간단한 쉘

// minishell_v1.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_INPUT 1024
#define MAX_ARGS 64

// 입력을 공백으로 분리
int parse_input(char* input, char** args) {
    int argc = 0;
    char* token = strtok(input, " \t\n");

    while (token != NULL && argc < MAX_ARGS - 1) {
        args[argc++] = token;
        token = strtok(NULL, " \t\n");
    }
    args[argc] = NULL;

    return argc;
}

// 명령어 실행
void execute(char** args) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork 실패");
        return;
    }

    if (pid == 0) {
        // 자식 프로세스: 명령어 실행
        execvp(args[0], args);
        // execvp 실패시
        perror(args[0]);
        exit(EXIT_FAILURE);
    } else {
        // 부모 프로세스: 자식 종료 대기
        int status;
        waitpid(pid, &status, 0);
    }
}

int main(void) {
    char input[MAX_INPUT];
    char* args[MAX_ARGS];

    while (1) {
        // 프롬프트 출력
        printf("minish> ");
        fflush(stdout);

        // 입력 읽기
        if (fgets(input, sizeof(input), stdin) == NULL) {
            printf("\n");
            break;  // EOF (Ctrl+D)
        }

        // 빈 입력 무시
        if (input[0] == '\n') continue;

        // 파싱
        int argc = parse_input(input, args);
        if (argc == 0) continue;

        // exit 명령어
        if (strcmp(args[0], "exit") == 0) {
            printf("쉘을 종료합니다.\n");
            break;
        }

        // 실행
        execute(args);
    }

    return 0;
}

컴파일 및 테스트

gcc -o minish minishell_v1.c
./minish

minish> ls -l
minish> pwd
minish> echo hello world
minish> exit

2단계: 내장 명령어 (Built-in Commands)

일부 명령어는 외부 프로그램이 아닌 쉘 자체에서 처리해야 합니다.

내장 명령어 구현

// builtins.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// 내장 명령어 이름들
const char* builtin_names[] = {
    "cd",
    "pwd",
    "echo",
    "exit",
    "help",
    "export",
    "env",
    NULL
};

// cd: 디렉토리 변경
int builtin_cd(char** args) {
    const char* path;

    if (args[1] == NULL) {
        // 인자 없으면 홈 디렉토리
        path = getenv("HOME");
        if (path == NULL) {
            fprintf(stderr, "cd: HOME 환경변수가 설정되지 않음\n");
            return 1;
        }
    } else if (strcmp(args[1], "-") == 0) {
        // cd - : 이전 디렉토리
        path = getenv("OLDPWD");
        if (path == NULL) {
            fprintf(stderr, "cd: OLDPWD 환경변수가 설정되지 않음\n");
            return 1;
        }
        printf("%s\n", path);
    } else if (strcmp(args[1], "~") == 0) {
        path = getenv("HOME");
    } else {
        path = args[1];
    }

    // 현재 디렉토리 저장
    char oldpwd[1024];
    getcwd(oldpwd, sizeof(oldpwd));

    if (chdir(path) != 0) {
        perror("cd");
        return 1;
    }

    // OLDPWD, PWD 환경변수 갱신
    setenv("OLDPWD", oldpwd, 1);

    char newpwd[1024];
    getcwd(newpwd, sizeof(newpwd));
    setenv("PWD", newpwd, 1);

    return 0;
}

// pwd: 현재 디렉토리 출력
int builtin_pwd(char** args) {
    (void)args;  // 사용하지 않음

    char cwd[1024];
    if (getcwd(cwd, sizeof(cwd)) != NULL) {
        printf("%s\n", cwd);
        return 0;
    }
    perror("pwd");
    return 1;
}

// echo: 인자 출력
int builtin_echo(char** args) {
    int newline = 1;
    int start = 1;

    // -n 옵션: 줄바꿈 없이 출력
    if (args[1] && strcmp(args[1], "-n") == 0) {
        newline = 0;
        start = 2;
    }

    for (int i = start; args[i]; i++) {
        printf("%s", args[i]);
        if (args[i + 1]) printf(" ");
    }

    if (newline) printf("\n");
    return 0;
}

// help: 도움말
int builtin_help(char** args) {
    (void)args;

    printf("\n=== Mini Shell 도움말 ===\n\n");
    printf("내장 명령어:\n");
    printf("  cd [디렉토리]  - 디렉토리 변경\n");
    printf("  pwd           - 현재 디렉토리 출력\n");
    printf("  echo [텍스트]  - 텍스트 출력\n");
    printf("  export VAR=값  - 환경변수 설정\n");
    printf("  env           - 환경변수 목록\n");
    printf("  help          - 이 도움말\n");
    printf("  exit          - 쉘 종료\n");
    printf("\n외부 명령어는 PATH에서 검색됩니다.\n\n");

    return 0;
}

// export: 환경변수 설정
int builtin_export(char** args) {
    if (args[1] == NULL) {
        // 인자 없으면 환경변수 목록 출력
        extern char** environ;
        for (char** env = environ; *env; env++) {
            printf("export %s\n", *env);
        }
        return 0;
    }

    // VAR=value 형식 파싱
    for (int i = 1; args[i]; i++) {
        char* eq = strchr(args[i], '=');
        if (eq) {
            *eq = '\0';
            setenv(args[i], eq + 1, 1);
            *eq = '=';
        } else {
            // = 없으면 빈 값으로 설정
            setenv(args[i], "", 1);
        }
    }

    return 0;
}

// env: 환경변수 출력
int builtin_env(char** args) {
    (void)args;

    extern char** environ;
    for (char** env = environ; *env; env++) {
        printf("%s\n", *env);
    }
    return 0;
}

// 내장 명령어인지 확인하고 실행
// 반환: -1 (내장 명령어 아님), 0+ (실행 결과)
int execute_builtin(char** args) {
    if (args[0] == NULL) return -1;

    if (strcmp(args[0], "cd") == 0) return builtin_cd(args);
    if (strcmp(args[0], "pwd") == 0) return builtin_pwd(args);
    if (strcmp(args[0], "echo") == 0) return builtin_echo(args);
    if (strcmp(args[0], "help") == 0) return builtin_help(args);
    if (strcmp(args[0], "export") == 0) return builtin_export(args);
    if (strcmp(args[0], "env") == 0) return builtin_env(args);

    return -1;  // 내장 명령어 아님
}

3단계: 리다이렉션 구현

>, >>, < 연산자를 처리합니다.

리다이렉션 파서

// redirect.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

typedef struct {
    char* input_file;   // < 파일
    char* output_file;  // > 또는 >> 파일
    int append;         // >> 인 경우 1
} Redirect;

// 리다이렉션 파싱
// args에서 리다이렉션 제거하고 Redirect 구조체에 저장
void parse_redirect(char** args, Redirect* redir) {
    redir->input_file = NULL;
    redir->output_file = NULL;
    redir->append = 0;

    int i = 0;
    int j = 0;

    while (args[i] != NULL) {
        if (strcmp(args[i], "<") == 0) {
            // 입력 리다이렉션
            if (args[i + 1]) {
                redir->input_file = args[i + 1];
                i += 2;
                continue;
            }
        } else if (strcmp(args[i], ">") == 0) {
            // 출력 리다이렉션 (덮어쓰기)
            if (args[i + 1]) {
                redir->output_file = args[i + 1];
                redir->append = 0;
                i += 2;
                continue;
            }
        } else if (strcmp(args[i], ">>") == 0) {
            // 출력 리다이렉션 (추가)
            if (args[i + 1]) {
                redir->output_file = args[i + 1];
                redir->append = 1;
                i += 2;
                continue;
            }
        }

        // 리다이렉션이 아닌 인자
        args[j++] = args[i++];
    }
    args[j] = NULL;
}

// 리다이렉션 적용 (자식 프로세스에서 호출)
int apply_redirect(Redirect* redir) {
    // 입력 리다이렉션
    if (redir->input_file) {
        int fd = open(redir->input_file, O_RDONLY);
        if (fd < 0) {
            perror(redir->input_file);
            return -1;
        }
        dup2(fd, STDIN_FILENO);
        close(fd);
    }

    // 출력 리다이렉션
    if (redir->output_file) {
        int flags = O_WRONLY | O_CREAT;
        flags |= redir->append ? O_APPEND : O_TRUNC;

        int fd = open(redir->output_file, flags, 0644);
        if (fd < 0) {
            perror(redir->output_file);
            return -1;
        }
        dup2(fd, STDOUT_FILENO);
        close(fd);
    }

    return 0;
}

리다이렉션 사용 예

// execute with redirection
void execute_with_redirect(char** args) {
    Redirect redir;
    parse_redirect(args, &redir);

    if (args[0] == NULL) return;

    pid_t pid = fork();

    if (pid == 0) {
        // 자식: 리다이렉션 적용 후 실행
        if (apply_redirect(&redir) < 0) {
            exit(EXIT_FAILURE);
        }
        execvp(args[0], args);
        perror(args[0]);
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
    } else {
        perror("fork");
    }
}

테스트:

minish> ls -l > files.txt
minish> cat < files.txt
minish> echo "추가 내용" >> files.txt
minish> wc -l < files.txt

4단계: 파이프 구현

| 연산자로 명령어를 연결합니다.

파이프 처리

// pipe.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_PIPES 10

// 파이프로 분리된 명령어 수 카운트
int count_pipes(char** args) {
    int count = 0;
    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) count++;
    }
    return count;
}

// 파이프 위치에서 args 분리
// commands[0] = 첫 번째 명령어의 args
// commands[1] = 두 번째 명령어의 args
// ...
int split_by_pipe(char** args, char*** commands) {
    int cmd_count = 0;
    commands[cmd_count++] = args;

    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) {
            args[i] = NULL;  // 파이프 위치를 NULL로
            if (args[i + 1]) {
                commands[cmd_count++] = &args[i + 1];
            }
        }
    }

    return cmd_count;
}

// 파이프 실행
void execute_pipe(char** args) {
    char** commands[MAX_PIPES + 1];
    int cmd_count = split_by_pipe(args, commands);

    if (cmd_count == 1) {
        // 파이프 없음: 일반 실행
        execute_with_redirect(commands[0]);
        return;
    }

    int pipes[MAX_PIPES][2];  // 파이프 파일 디스크립터

    // 파이프 생성
    for (int i = 0; i < cmd_count - 1; i++) {
        if (pipe(pipes[i]) < 0) {
            perror("pipe");
            return;
        }
    }

    // 각 명령어 실행
    for (int i = 0; i < cmd_count; i++) {
        pid_t pid = fork();

        if (pid == 0) {
            // 자식 프로세스

            // 이전 파이프의 읽기 끝을 stdin으로
            if (i > 0) {
                dup2(pipes[i - 1][0], STDIN_FILENO);
            }

            // 다음 파이프의 쓰기 끝을 stdout으로
            if (i < cmd_count - 1) {
                dup2(pipes[i][1], STDOUT_FILENO);
            }

            // 모든 파이프 닫기
            for (int j = 0; j < cmd_count - 1; j++) {
                close(pipes[j][0]);
                close(pipes[j][1]);
            }

            // 명령어 실행
            execvp(commands[i][0], commands[i]);
            perror(commands[i][0]);
            exit(EXIT_FAILURE);

        } else if (pid < 0) {
            perror("fork");
            return;
        }
    }

    // 부모: 모든 파이프 닫기
    for (int i = 0; i < cmd_count - 1; i++) {
        close(pipes[i][0]);
        close(pipes[i][1]);
    }

    // 모든 자식 프로세스 대기
    for (int i = 0; i < cmd_count; i++) {
        wait(NULL);
    }
}

테스트:

minish> ls -l | grep ".c"
minish> cat file.txt | wc -l
minish> ps aux | grep bash | head -5

5단계: 완성된 미니 쉘

전체 코드

// minishell.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>

#define MAX_INPUT 1024
#define MAX_ARGS 64
#define MAX_PIPES 10

// ============ 전역 변수 ============
static int last_exit_status = 0;

// ============ 시그널 핸들러 ============
void sigint_handler(int sig) {
    (void)sig;
    printf("\n");
    // 프롬프트 다시 출력하지 않음 (메인 루프에서 처리)
}

// ============ 유틸리티 ============

// 문자열 양쪽 공백 제거
char* trim(char* str) {
    while (*str == ' ' || *str == '\t') str++;

    if (*str == '\0') return str;

    char* end = str + strlen(str) - 1;
    while (end > str && (*end == ' ' || *end == '\t' || *end == '\n')) {
        *end-- = '\0';
    }

    return str;
}

// ============ 파싱 ============

int parse_args(char* input, char** args) {
    int argc = 0;
    char* token = strtok(input, " \t\n");

    while (token && argc < MAX_ARGS - 1) {
        args[argc++] = token;
        token = strtok(NULL, " \t\n");
    }
    args[argc] = NULL;

    return argc;
}

// ============ 리다이렉션 ============

typedef struct {
    char* infile;
    char* outfile;
    int append;
} Redirect;

void parse_redirect(char** args, Redirect* r) {
    r->infile = NULL;
    r->outfile = NULL;
    r->append = 0;

    int i = 0, j = 0;
    while (args[i]) {
        if (strcmp(args[i], "<") == 0 && args[i+1]) {
            r->infile = args[i+1];
            i += 2;
        } else if (strcmp(args[i], ">") == 0 && args[i+1]) {
            r->outfile = args[i+1];
            r->append = 0;
            i += 2;
        } else if (strcmp(args[i], ">>") == 0 && args[i+1]) {
            r->outfile = args[i+1];
            r->append = 1;
            i += 2;
        } else {
            args[j++] = args[i++];
        }
    }
    args[j] = NULL;
}

int setup_redirect(Redirect* r) {
    if (r->infile) {
        int fd = open(r->infile, O_RDONLY);
        if (fd < 0) { perror(r->infile); return -1; }
        dup2(fd, STDIN_FILENO);
        close(fd);
    }
    if (r->outfile) {
        int flags = O_WRONLY | O_CREAT | (r->append ? O_APPEND : O_TRUNC);
        int fd = open(r->outfile, flags, 0644);
        if (fd < 0) { perror(r->outfile); return -1; }
        dup2(fd, STDOUT_FILENO);
        close(fd);
    }
    return 0;
}

// ============ 내장 명령어 ============

int builtin_cd(char** args) {
    const char* path = args[1] ? args[1] : getenv("HOME");

    if (strcmp(path, "-") == 0) {
        path = getenv("OLDPWD");
        if (!path) {
            fprintf(stderr, "cd: OLDPWD not set\n");
            return 1;
        }
        printf("%s\n", path);
    } else if (strcmp(path, "~") == 0) {
        path = getenv("HOME");
    }

    char oldpwd[1024];
    getcwd(oldpwd, sizeof(oldpwd));

    if (chdir(path) != 0) {
        perror("cd");
        return 1;
    }

    setenv("OLDPWD", oldpwd, 1);
    char newpwd[1024];
    getcwd(newpwd, sizeof(newpwd));
    setenv("PWD", newpwd, 1);

    return 0;
}

int builtin_pwd(void) {
    char cwd[1024];
    if (getcwd(cwd, sizeof(cwd))) {
        printf("%s\n", cwd);
        return 0;
    }
    perror("pwd");
    return 1;
}

int builtin_echo(char** args) {
    int newline = 1, start = 1;
    if (args[1] && strcmp(args[1], "-n") == 0) {
        newline = 0;
        start = 2;
    }

    for (int i = start; args[i]; i++) {
        // 환경변수 확장 ($VAR)
        if (args[i][0] == '$') {
            char* val = getenv(args[i] + 1);
            printf("%s", val ? val : "");
        } else {
            printf("%s", args[i]);
        }
        if (args[i + 1]) printf(" ");
    }
    if (newline) printf("\n");
    return 0;
}

int builtin_export(char** args) {
    if (!args[1]) {
        extern char** environ;
        for (char** e = environ; *e; e++) {
            printf("export %s\n", *e);
        }
        return 0;
    }

    for (int i = 1; args[i]; i++) {
        char* eq = strchr(args[i], '=');
        if (eq) {
            *eq = '\0';
            setenv(args[i], eq + 1, 1);
        }
    }
    return 0;
}

int builtin_unset(char** args) {
    for (int i = 1; args[i]; i++) {
        unsetenv(args[i]);
    }
    return 0;
}

int builtin_help(void) {
    printf("\n");
    printf("╔═══════════════════════════════════════╗\n");
    printf("║        Mini Shell 도움말              ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ 내장 명령어:                          ║\n");
    printf("║   cd [dir]    디렉토리 변경           ║\n");
    printf("║   pwd         현재 디렉토리           ║\n");
    printf("║   echo [...]  텍스트 출력             ║\n");
    printf("║   export V=X  환경변수 설정           ║\n");
    printf("║   unset VAR   환경변수 삭제           ║\n");
    printf("║   help        이 도움말               ║\n");
    printf("║   exit [N]    쉘 종료                 ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ 리다이렉션:                           ║\n");
    printf("║   cmd > file  출력을 파일로           ║\n");
    printf("║   cmd >> file 출력을 파일에 추가      ║\n");
    printf("║   cmd < file  파일에서 입력           ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ 파이프:                               ║\n");
    printf("║   cmd1 | cmd2 출력을 다음 명령 입력으로 ║\n");
    printf("╚═══════════════════════════════════════╝\n");
    printf("\n");
    return 0;
}

// 내장 명령어 실행 (-1: 내장 아님)
int run_builtin(char** args) {
    if (!args[0]) return -1;

    if (strcmp(args[0], "cd") == 0) return builtin_cd(args);
    if (strcmp(args[0], "pwd") == 0) return builtin_pwd();
    if (strcmp(args[0], "echo") == 0) return builtin_echo(args);
    if (strcmp(args[0], "export") == 0) return builtin_export(args);
    if (strcmp(args[0], "unset") == 0) return builtin_unset(args);
    if (strcmp(args[0], "help") == 0) return builtin_help();

    return -1;
}

// ============ 파이프 실행 ============

int split_pipe(char** args, char*** cmds) {
    int n = 0;
    cmds[n++] = args;

    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) {
            args[i] = NULL;
            if (args[i + 1]) {
                cmds[n++] = &args[i + 1];
            }
        }
    }
    return n;
}

void run_pipeline(char** args) {
    char** cmds[MAX_PIPES + 1];
    int n = split_pipe(args, cmds);

    // 파이프 없으면 단일 명령 실행
    if (n == 1) {
        Redirect r;
        parse_redirect(cmds[0], &r);

        if (!cmds[0][0]) return;

        // 내장 명령어 체크
        int builtin_result = run_builtin(cmds[0]);
        if (builtin_result != -1) {
            last_exit_status = builtin_result;
            return;
        }

        // 외부 명령어
        pid_t pid = fork();
        if (pid == 0) {
            setup_redirect(&r);
            execvp(cmds[0][0], cmds[0]);
            fprintf(stderr, "%s: command not found\n", cmds[0][0]);
            exit(127);
        } else if (pid > 0) {
            int status;
            waitpid(pid, &status, 0);
            last_exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
        }
        return;
    }

    // 파이프가 있는 경우
    int pipes[MAX_PIPES][2];
    for (int i = 0; i < n - 1; i++) {
        pipe(pipes[i]);
    }

    for (int i = 0; i < n; i++) {
        pid_t pid = fork();

        if (pid == 0) {
            // 입력 연결
            if (i > 0) {
                dup2(pipes[i-1][0], STDIN_FILENO);
            }
            // 출력 연결
            if (i < n - 1) {
                dup2(pipes[i][1], STDOUT_FILENO);
            }

            // 모든 파이프 닫기
            for (int j = 0; j < n - 1; j++) {
                close(pipes[j][0]);
                close(pipes[j][1]);
            }

            // 리다이렉션 처리 (첫/마지막 명령에만 적용)
            Redirect r;
            parse_redirect(cmds[i], &r);
            if (i == 0 && r.infile) {
                int fd = open(r.infile, O_RDONLY);
                if (fd >= 0) { dup2(fd, STDIN_FILENO); close(fd); }
            }
            if (i == n - 1 && r.outfile) {
                int flags = O_WRONLY | O_CREAT | (r.append ? O_APPEND : O_TRUNC);
                int fd = open(r.outfile, flags, 0644);
                if (fd >= 0) { dup2(fd, STDOUT_FILENO); close(fd); }
            }

            execvp(cmds[i][0], cmds[i]);
            fprintf(stderr, "%s: command not found\n", cmds[i][0]);
            exit(127);
        }
    }

    // 부모: 파이프 닫고 대기
    for (int i = 0; i < n - 1; i++) {
        close(pipes[i][0]);
        close(pipes[i][1]);
    }

    int status;
    for (int i = 0; i < n; i++) {
        wait(&status);
    }
    last_exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}

// ============ 프롬프트 ============

void print_prompt(void) {
    char cwd[256];
    char* dir = getcwd(cwd, sizeof(cwd));

    // 홈 디렉토리를 ~로 표시
    char* home = getenv("HOME");
    if (home && dir && strncmp(dir, home, strlen(home)) == 0) {
        printf("\033[1;34m~%s\033[0m", dir + strlen(home));
    } else {
        printf("\033[1;34m%s\033[0m", dir ? dir : "?");
    }

    // 종료 코드에 따라 색상 변경
    if (last_exit_status == 0) {
        printf(" \033[1;32m❯\033[0m ");
    } else {
        printf(" \033[1;31m❯\033[0m ");
    }

    fflush(stdout);
}

// ============ 메인 ============

int main(void) {
    char input[MAX_INPUT];
    char* args[MAX_ARGS];

    // 시그널 핸들러 설정
    signal(SIGINT, sigint_handler);

    printf("\n\033[1;36m=== Mini Shell ===\033[0m\n");
    printf("'help' 입력하여 도움말 보기\n\n");

    while (1) {
        print_prompt();

        if (fgets(input, sizeof(input), stdin) == NULL) {
            printf("\nexit\n");
            break;
        }

        char* trimmed = trim(input);
        if (*trimmed == '\0') continue;

        // 주석 무시
        if (trimmed[0] == '#') continue;

        // 입력 복사 (strtok이 원본 수정)
        char input_copy[MAX_INPUT];
        strncpy(input_copy, trimmed, sizeof(input_copy));

        // 파싱
        int argc = parse_args(input_copy, args);
        if (argc == 0) continue;

        // exit 명령어
        if (strcmp(args[0], "exit") == 0) {
            int code = args[1] ? atoi(args[1]) : last_exit_status;
            printf("exit\n");
            exit(code);
        }

        // 실행
        run_pipeline(args);
    }

    return last_exit_status;
}

컴파일 및 실행

gcc -o minishell minishell.c -Wall -Wextra
./minishell

테스트 예제

=== Mini Shell ===
'help' 입력하여 도움말 보기

~  help
~  pwd
/Users/username
~  cd /tmp
/tmp  ls -la
/tmp  echo $HOME
/Users/username
/tmp  export MY_VAR=hello
/tmp  echo $MY_VAR
hello
/tmp  ls -l | grep ".txt" | wc -l
/tmp  cat /etc/passwd | head -5 > first5.txt
/tmp  cat first5.txt
/tmp  cd -
/Users/username
~  exit

6단계: 추가 기능

히스토리 기능

#define HISTORY_SIZE 100

static char* history[HISTORY_SIZE];
static int history_count = 0;

void add_history(const char* cmd) {
    if (history_count < HISTORY_SIZE) {
        history[history_count++] = strdup(cmd);
    } else {
        // 오래된 것 제거
        free(history[0]);
        memmove(history, history + 1, (HISTORY_SIZE - 1) * sizeof(char*));
        history[HISTORY_SIZE - 1] = strdup(cmd);
    }
}

int builtin_history(char** args) {
    int n = history_count;
    if (args[1]) {
        n = atoi(args[1]);
        if (n > history_count) n = history_count;
    }

    int start = history_count - n;
    for (int i = start; i < history_count; i++) {
        printf("%5d  %s\n", i + 1, history[i]);
    }
    return 0;
}

void free_history(void) {
    for (int i = 0; i < history_count; i++) {
        free(history[i]);
    }
}

백그라운드 실행 (&)

// & 체크
int is_background(char** args) {
    int i = 0;
    while (args[i]) i++;

    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
        args[i - 1] = NULL;  // & 제거
        return 1;
    }
    return 0;
}

// 실행 함수 수정
void run_command(char** args) {
    int bg = is_background(args);

    pid_t pid = fork();

    if (pid == 0) {
        // 자식
        execvp(args[0], args);
        perror(args[0]);
        exit(127);
    } else if (pid > 0) {
        if (bg) {
            printf("[%d] %d\n", 1, pid);
            // 백그라운드: 대기하지 않음
        } else {
            int status;
            waitpid(pid, &status, 0);
        }
    }
}

와일드카드 확장 (*)

#include <glob.h>

// 와일드카드 확장
int expand_wildcards(char** args, char** expanded, int max_expanded) {
    int count = 0;

    for (int i = 0; args[i] && count < max_expanded - 1; i++) {
        // * 또는 ? 포함된 인자
        if (strchr(args[i], '*') || strchr(args[i], '?')) {
            glob_t results;
            int ret = glob(args[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &results);

            if (ret == 0) {
                for (size_t j = 0; j < results.gl_pathc && count < max_expanded - 1; j++) {
                    expanded[count++] = strdup(results.gl_pathv[j]);
                }
            }
            globfree(&results);
        } else {
            expanded[count++] = args[i];
        }
    }

    expanded[count] = NULL;
    return count;
}

연습 문제

연습 1: 세미콜론 지원

cmd1 ; cmd2 형태로 여러 명령어를 순차 실행하도록 구현하세요.

연습 2: && 와 || 연산자

  • cmd1 && cmd2: cmd1 성공시에만 cmd2 실행
  • cmd1 || cmd2: cmd1 실패시에만 cmd2 실행

연습 3: 따옴표 처리

echo "hello world"에서 "hello world"를 하나의 인자로 처리하세요.

연습 4: 탭 자동완성

readline 라이브러리를 사용하여 탭 자동완성을 구현하세요.


핵심 개념 정리

함수 설명
fork() 프로세스 복제
exec*() 프로그램 실행
wait() 자식 프로세스 대기
pipe() 파이프 생성
dup2() 파일 디스크립터 복제
open() 파일 열기
signal() 시그널 핸들러 등록
개념 설명
파이프 프로세스 간 단방향 통신
리다이렉션 입출력 방향 변경
환경변수 프로세스에 전달되는 설정
시그널 프로세스에 보내는 알림

다음 단계

미니 쉘을 완성했다면 다음 프로젝트로 넘어가세요: - 프로젝트 12: 멀티스레드 프로그래밍 - pthread 활용

to navigate between lessons