bash-like shell (minishell)

bash-like shell (minishell)

2025년 7월 6일

minishell #

프로젝트 설명 #

C언어를 사용하여 bash와 비슷한 동작을 수행하는 minishell 구현

0bf253d8-0255-4581-9218-5cc6688181f1

구현할 기능 #

  • 새로운 명령이 실행될 때 까지 프롬프트 상태로 대기
  • 쉘 히스토리
  • 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) #

57263e27-bff3-4f0c-8f03-34feeffc27bf

위와 같이 따옴표는 문자를 나누는 기준이 아니기 때문에 특수문자(<, >, |)와 공백 기준으로 잘라 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};

특수문자로 끝나거나, 연속적인 특수문자, 파이프로 시작하는 경우 등 파싱 에러를 검사합니다.

bbe47962-7089-4b84-938a-50ed6120ee4c


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)
  • 파이프 단위 연결리스트 형태로 저장

7d36bab0-7b89-47b1-85bb-074cfa67abbe


명령어 실행 #

명령어 실행은 리다이렉션 설정 후 빌트인 명령어 실행 or 프로그램 실행 순으로 진행됩니다.

리다이렉션 설정 (redirection.c) #

일반적으로 프로그램은 표준 입력으로 받고, 표준 출력으로 출력하는데, 리다이렉션 기호로 파일이나 HEREDOC을 표준 입력, 출력으로 변경할 수 있습니다.

파이프 #

명령어가 파이프로 연결된 경우 이전 명령어의 표준 출력이 다음 명령어의 표준 입력이 됩니다 01b06f4f-7f98-4a20-b3a3-849de77150c9


입출력 리다이렉션 #

만약 입출력 리다이렉션이 함께 설정됐다면 리다이렉션이 우선시 됩니다.

da1fa0ef-6b52-4ce1-9e19-0a15b5ef4761

여러 출력 리다이렉션 들어왔을 때 마지막에 있는 파일에만 출력합니다.

dc1ca90c-e9a4-4b99-8906-b2e7fc1f6a42


HEREDOC #

HEREDOC은 지정한 문자열이나 Ctrl+D(EOF)가 입력될 때 까지 입력받은 문자열을 표준 입력으로 받고, 환경변수까지 치환해주는 기능입니다.

da40b9ad-4031-44f0-aae4-927602114c6d

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(이전 경로)를 세팅합니다. 8a45cac2-a8a7-431b-8c14-69b862c9bae7
  • export : 환경변수를 세팅하는 명령어 입니다
    환경변수는 main 함수의 3번째 인자로 전달받아 초기화 단계에서 t_stat.env 에 문자열 배열 key=value 형식으로 복사해두고 추가하거나 삭제하는 방법으로 관리하고 있습니다.
    1// 메인함수 인자로 받으면 minishell을 실행시킨 환경변수를 그대로 받는다.
    2int	main(int argc, char *argv[], char *envp[])
    
  • unset : 환경변수를 삭제하는 명령어 입니다 06a331a6-ee57-42e4-8322-4079785c0f67

프로그램 실행 (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}

시그널로 종료된 경우 시그널의 종류가 출력되며, 프로그램의 종료 상황에 따라 리턴 값을 $? 변수에 저장합니다.

827640d5-3cd5-4759-b46e-f304d8958756

comments powered by Disqus