bash-like shell (minishell)
2025년 7월 6일
minishell #
프로젝트 설명 #
C언어를 사용하여 bash와 비슷한 동작을 수행하는 minishell 구현
구현할 기능 #
- 새로운 명령이 실행될 때 까지 프롬프트 상태로 대기
- 쉘 히스토리
- PATH 환경변수에서 실행파일 경로 찾기
- 환경변수($) 해석 기능
- 입, 출력 리다이렉션과 HEREDOC 기능
- 파이프(|)
- 시그널 처리
- Ctrl+C (새 프롬프트), Ctrl+D (쉘 종료), Ctrl+\ (아무 작업도 수행하지 않음)
- 빌트인 명령어
- echo, cd, pwd, export, unset, env, exit
개발계획 및 구조 #
역할분담 #
- 박영서(팀장) : Makefile, 빌트인 명령어 (echo, pwd, export, unset, env, exit)
- 김동현(팀원) : 명령어 파싱, 실행, 리다이렉션, 시그널 처리, 빌트인 명령어 (cd)
작업일정 #
- 2022.04.25 ~ 04.26 : bash 동작 확인, 구현방법 회의, 1차 파싱 구현
- 2022.04.27 ~ 05.04 : 2차 파싱 구현, 빌트인 명령어 구현
- 2022.05.05 ~ 05.07 : 명령어 실행 구현
- 2022.05.08 ~ 05.10 : 리다이렉션 구현
- 2022.05.11 ~ 05.13 : 파이프 구현, 빌트인 명령어 구현
- 2022.05.14 ~ 05.17 : 오류 수정
- 2022.05.18 ~ 05.20 : 시그널 처리
폴더 트리 #
1libs/ # 전반적으로 사용되는 유틸 함수와 빌트인 명령어
2inc/ # 헤더파일
3srcs/
4 main.c # 가장 먼저 실행되는 main 함수와 banner 함수
5 minishell.c # 실제 주요 동작이 수행되는 함수
6 parsing.c # 1차 파싱과 에러처리 관련 함수
7 elems_to_lst.c, env_transform.c # 2차 파싱과 환경변수 치환 함수
8 redirection.c, set_redirection.c, heredoc.c # 리다이렉션 관련 함수
9 combkey.c # 시그널 관련 처리
10 execute.c, exec_program.c # 명령어 실행 관련 함수
시그널 처리 #
minishell 기본 시그널 동작
| Ctrl+C | Ctrl+D | Ctrl+W | |
|---|---|---|---|
| 프롬프트에 데이터가 입력된 상태 | 프롬프트 출력 | 동작 없음 | 시그널 무시 |
| 데이터가 입력되지 않은 상태 | 프롬프트 출력 | exit 입력 후 종료 | 시그널 무시 |
Ctrl+C와 Ctrl+\ 는 키 입력 시 시그널이 발생하기 때문에 set_signal 함수로 시그널을 변경해줍니다.
1int minishell(t_stat *stat)
2{
3 char *input;
4 t_lst *parsed_input;
5
6 unset_echoctl_termios();
7 // SIGINT -> SH_SHELL, SIGQUIT -> SH_IGN
8 set_signal(SH_SHELL, SH_IGN);
9 while (1)
10 {
11 input = readline("minishell % ");
12 // 아무런 값이 없을때만 CTRL+D 입력 시 enter_ctrld 호출하며 종료
13 if (input == 0)
14 return (enter_ctrld());
15 if (*input != '\0')
16 {
17 init_minishell(stat);
18 add_history(input);
19 parsed_input = get_input_lst(input, stat);
20 if (parsed_input)
21 execute_line(&parsed_input, stat);
22 while (del_node_front(&parsed_input, 2))
23 ;
24 stat->last_return = stat->cmd_return;
25 }
26 free(input);
27 }
28 return (stat->print_err);
29}
명령어 파싱 #
명령어 파싱에서 기호와 명령어 구분, 에러까지 처리해야하고, 명령어 실행 부분에서 필요하다면 파싱 구조가 한번 더 변경될 수 있다고 생각되어 2번으로 나눴습니다.
1차 파싱 및 에러검사 (parsing.c) #
위와 같이 따옴표는 문자를 나누는 기준이 아니기 때문에 특수문자(<, >, |)와 공백 기준으로 잘라 s_elem이라는 구조체 배열에 저장했습니다.
type 은 특수문자의 종류(ET_STR, ET_LTS, ET_GTS, ET_PIP)를 담은 변수입니다.
1struct s_elem
2{
3 char *data;
4 int type;
5 int subtype;
6};
특수문자로 끝나거나, 연속적인 특수문자, 파이프로 시작하는 경우 등 파싱 에러를 검사합니다.
2차 파싱 (elems_to_lst.c) #
명령어 실행 시 하나의 명령어가 파이프| 단위로 되어있기 때문에 1차 파싱의 결과물을 다시 파싱하여 하나의 명령어 실행 단위로 연결리스트에 저장했습니다.
1struct s_lst
2{
3 char *cmd; // 명령어
4 int argc; // 현재 명령어의 argc
5 char **argv; // 현재 명령어의 argv
6 int fdc; // 입출력과 관련된 파일 디스크립터 수
7 struct s_fd *fdv; // 파일 디스크립터 리스트
8 struct s_lst *next; // 다음명령어
9};
파싱이 끝난 후 구조
- 환경변수 치환
- 따옴표 제거
- 명령어와 옵션 구분 (cmd, argv)
- 리다이렉션 파일명과 타입정보 저장 (fd)
- 파이프 단위 연결리스트 형태로 저장
명령어 실행 #
명령어 실행은 리다이렉션 설정 후 빌트인 명령어 실행 or 프로그램 실행 순으로 진행됩니다.
리다이렉션 설정 (redirection.c) #
일반적으로 프로그램은 표준 입력으로 받고, 표준 출력으로 출력하는데, 리다이렉션 기호로 파일이나 HEREDOC을 표준 입력, 출력으로 변경할 수 있습니다.
파이프 #
명령어가 파이프로 연결된 경우 이전 명령어의 표준 출력이 다음 명령어의 표준 입력이 됩니다
입출력 리다이렉션 #
만약 입출력 리다이렉션이 함께 설정됐다면 리다이렉션이 우선시 됩니다.
여러 출력 리다이렉션 들어왔을 때 마지막에 있는 파일에만 출력합니다.
HEREDOC #
HEREDOC은 지정한 문자열이나 Ctrl+D(EOF)가 입력될 때 까지 입력받은 문자열을 표준 입력으로 받고, 환경변수까지 치환해주는 기능입니다.
HEREDOC은 자식프로세스로 동작하고, 동작하는 동안 부모(minishell)의 시그널은 제한합니다.
| Ctrl+C | Ctrl+D | Ctrl+W | |
|---|---|---|---|
| minishell | 프로세스 종료 | EOF 입력 | 시그널 무시 |
| HEREDOC | 시그널 무시 | 동작 없음 | 시그널 무시 |
빌트인 명령어 실행 (execute.c) #
빌트인 명령어인지 확인 후 그에 맞는 함수를 호출합니다. 빌트인 명령어가 아니라면 프로그램 실행 코드로 진입합니다.
파싱 후 명령어는 cmd에 저장되고, 옵션을 포함한 인자는 argc, argv 형태로 저장되어 빌트인 명령어로 전달됩니다.
1static int exec_builtin(t_lst *node, t_stat *stat)
2{
3 if (ft_strcmp_igcase(node->cmd, "echo") == 0)
4 return (ft_echo(node->argc, node->argv));
5 else if (ft_strcmp_igcase(node->cmd, "pwd") == 0)
6 return (ft_pwd());
7 else if (ft_strcmp_igcase(node->cmd, "env") == 0)
8 return (ft_env(stat->env));
9 // ...
10}
- echo : 전달한 인자 출력
- pwd : 현재 경로를 출력
- exit : 프로그램 종료
- env : 환경변수 출력
- cd : 경로를 이동하고, 환경변수 중 PWD(현재 경로)와 OLDPWD(이전 경로)를 세팅합니다.
- export : 환경변수를 세팅하는 명령어 입니다
환경변수는 main 함수의 3번째 인자로 전달받아 초기화 단계에서 t_stat.env 에 문자열 배열key=value형식으로 복사해두고 추가하거나 삭제하는 방법으로 관리하고 있습니다.1// 메인함수 인자로 받으면 minishell을 실행시킨 환경변수를 그대로 받는다. 2int main(int argc, char *argv[], char *envp[]) - unset : 환경변수를 삭제하는 명령어 입니다
프로그램 실행 (execute.c, exec_program.c) #
PATH 설정 후 명령어를 실행합니다. 프로그램마다 시그널에 따른 동작이 정의되어 있기 때문에 exec 실행 전 시그널을 초기화 하고, 시그널이 중복해서 동작하지 않도록 부모프로세스(minishell)의 시그널은 무시하도록 변경했습니다.
1int exec_program(t_lst *node, t_stat *stat)
2{
3 pid_t pid;
4 int fork_ret;
5 int i;
6
7 fork_ret = 0;
8 // (자식프로세스) SIGINT, SIGQUIT는 기본 값으로 돌려놓음
9 set_signal(SH_DFL, SH_DFL);
10 pid = fork();
11 if (pid == 0)
12 {
13 i = 0;
14 while (node->cmd[i] && node->cmd[i] != '/')
15 i++;
16 if (i == ft_strlen(node->cmd))
17 node->cmd = find_path(node->cmd, stat);
18 execve(node->cmd, node->argv, stat->env);
19 print_exec_err(i, node->cmd, stat);
20 exit(EXEC_ERR_CMDNF);
21 }
22 // (부모프로세스: minishell) 자식이 종료될때까지 SIGINT, SIGQUIT 무시
23 set_signal(SH_IGN, SH_IGN);
24 wait(&fork_ret);
25 fork_ret = set_fork_ret(fork_ret);
26 // 자식 프로세스가 종료되면 다시 원래의 minishell 시그널로 돌아옴
27 set_signal(SH_SHELL, SH_IGN);
28 return (fork_ret);
29}
시그널로 종료된 경우 시그널의 종류가 출력되며, 프로그램의 종료 상황에 따라 리턴 값을 $? 변수에 저장합니다.