42seoul:: minishell

jahlee·2023년 3월 28일
0

개인 공부

목록 보기
10/23

허용함수 정리

허용함수 정리 노션링크

기본적으로 모든 명령어는 bash와 비교해 가면서 공부한다면 큰 도움이 된다.

파싱부

토큰화

다음과 같은 토큰화과정을 거쳤다. 기본적으로 토큰화를 진행할때는 토큰화를 해야하는 구문의 왼쪽부터 오른쪽 방향으로 진행하며 나누어 준다. 토큰화를 할때 순서가 상당히 중요하다는 점을 유의하면서 토큰화를 하여야 한다. 예시에서 CHUNK 는 {} ARGV 는 [] 그외는 ()으로 표현했으니 참고하자.

typedef enum e_token_type
{
	TOKEN_TYPE_CHUNK,// 해석될 여지가 아직 존재하는 단계
	TOKEN_TYPE_ARGV,// 더이상 해석할 수 없는 단계
	TOKEN_TYPE_SPACE,// 빈칸
	TOKEN_TYPE_PIPELINE,// 파이프
	TOKEN_TYPE_REDIRECTION// 리다이렉션
}	t_token_type;

예시 구문) cat << $"USER" -e > a | echo "$USER"$USER
맨처음에 들어온 구문을 하나의 큰 chunk로 만들어 주고 7단계를 거쳐서 토큰화를 해주면 된다.

  1. heredoc에서의 limiter 처리
    (CHUNK => CHUNK(<< 포함), ARGV(limiter), CHUNK)
    heredoc의 limiter의 경우 환경변수 해석이 되면 안되고 quotes에 묶인 전체가 limiter가 되기때문에 이를 먼저 해석해준다. limiter를 정할때의 기준은 bash를 사용하여 확인해보자. 특이한 케이스 들이 상당히 많다.

    특이한 예시)
    cat << $USER (limiter: $USER)
    cat << $US”ER” (limiter: $USER)
    cat << $”USER” (limiter: USER)

    ex )
    {cat << } [USER] { -e > a | echo “$USER”$USER}

  1. quotes 처리
    CHUNK => CHUNK, ARGV, CHUNK
    “” 와 ‘’의 차이에 유의하며 처리해 준다.

    ex )
    {cat << } [USER] { -e > a | echo } [$USER] {$USER}

  2. 환경변수 처리
    CHUNK => CHUNK, ARGV, SPACE, ARGV, CHUNK

    ex)
    {cat << } [USER] { -e > a | echo } [$USER] {} [jahlee] ( ) [] {}

  1. 빈칸 처리
    CHUNK => CHUNK, BLANK, CHUNK

    ex)
    {cat} ( ) {<<} ( ) {} [USER] {} ( ) {-e} ( ) {>} ( ) {a} ( ) {|} ( ) {echo} ( ) {} [$USER] {} [jahlee] ( ) [] {}

  1. 파이프 처리
    CHUNK => CHUNK, PIPE, CHUNK

    ex)
    {cat} ( ) {<<} ( ) {} [USER] {} ( ) {-e} ( ) {>} ( ) {a} ( ) (|) ( ){echo} ( ) {} [$USER] {} [jahlee] ( ) [] {}

  1. 리다이렉션 처리
    CHUNK => CHUNK, REDIRECTION, CHUNK

    ex)
    {cat} ( ) (<<) ( ) {} [USER] {} ( ) {-e} ( ) (>) ( ) {a} ( ) (|) ( ) {echo} ( ) {} [$USER] {} [jahlee] ( ) [] {}

  1. 빈 CHUNK 토큰 삭제 및 CHUNK를 ARGV로 변환

    ex)
    [cat] ( ) (<<) ( ) [USER] ( ) [-e] ( ) (>) ( ) [a] ( ) (|) ( ) [echo] ( ) [$USER][jahlee] ( ) []

  1. 연속된 ARGV 토큰 병합

    ex)
    [cat] ( ) (<<) ( ) [USER] ( ) [-e] ( ) (>) ( ) a (|) ( ) [echo] ( ) [$USERjahlee] ( ) []

  1. SPACE 토큰 삭제

    ex)
    [cat] (<<) [USER][-e] (>) [a] (|) [echo][$USERjahlee] []

문법체크

이전의 과정에서 토큰화를 다 하게된다면 결국에 남는 토큰의 타입은 ARGV, PIPE, REDIRECTION 3가지이다.

PIPE의 앞뒤에는 무조건 파이프가 아닌 다른 타입의 토큰이 와야하며,
REDIRECTION의 뒤에는 무조건 ARGV가 있어야 한다.

cmd_list 로 변환

이렇게 나누어진 연결리스트 토큰을 cmd_list로 나누어주면된다.

typedef struct s_cmd
{
	char			**argv;
	t_redirection	*redirection;
	int				pipe[2];
	struct s_cmd	*prev;
	struct s_cmd	*next;
}	t_cmd;

typedef struct s_redirection
{
	char					*type;
	char					*file;
	struct s_redirection	*next;
}	t_redirection;

나누는 조건같은 경우에는 파이프를 기준으로 새로운 노드를 이어주면 되고, REDIRECTION과 그뒤의 ARGV만 따로 redirection 리스트에 연결 시켜주면 된다.

구동부

built_in

생각보다 간단할 것 같지만 예외처리를 하다보면 코드가 점점 늘어난다.

몇가지 여러가지 정보들을 적어두지만 구현을 해야할지 말아야 할지는 판단을 잘해보고 구현하자.

  1. OLDPWD 는 이전 경로를 들고있는 환경변수이다. export로 확인가능. 특이하게 bash를 실행시키고 아직 경로를 이동한 적 없다면 value값이 NULL 이다. 구현하고자 한다면 초기 init설정에서 값을 설정해주고 cd 로 이동할때마다 이전 경로를 갱신해주어야한다.
  2. SHLVL 은 현재 쉘의 깊이가 어느정도인지를 알려주는 환경변수이다. 타고 들어갈 수 록 하나씩 커지는 것을 확인해 볼 수 있다.
  3. echo -nnnnnnnn 과같이 n이 연속으로 나온다면 echo -n과 같이 동작한다.
  4. exit을 할때 임의의 exit code를 줄 수도 있다. 또한 시그널에 의한 종료에 의한 종료 코드도 잘 판단하고 있어야한다.
  5. export의 경우
    1. export a=aaa b=bbbb c=ccc 와 같이 여러개를 한번에 할 수 도 있다. unset도 유사하다.
    2. key는 첫번째 문자가 무조건 알파벳 혹은 ‘’여야만 하며 이후 문자들은 숫자 또는 알파벳 또는 ‘’여야한다.

here_doc

명령을 실행하기전에 순차적으로 here_doc을 먼저 실행시켜주어야 한다. 필자의 경우 cmd_list의 몇번째 명령어인지에 따라 임시 히어독 파일을 만들어 주었다.(0.tmp, 1.tmp 과같이)

몇가지 예시들을 참고하여 구현하길 바란다.

ex)

cat << a << b << c << d 의 경우 limiter가 순차적으로 a b c d 를 통해 here_doc이 종료되며 결론적으로는 마지막 here_doc만 사용된다.

here_doc을 하는 도중의 시그널 종료를 신경써주어야 하며 이에따른 종료코드 또한 신경써주자.

void	ms_execute(t_info *info, t_cmd *cmd_list)
{
	pid_t	pid;

	pid = fork();
	if (pid < 0)
	{
		ms_error("fork", NULL);
		g_exit_status = 1;
		return ;
	}
	else if (pid == 0)
		execute_heredoc(info, cmd_list);
	signal(SIGINT, SIG_IGN);
	wait(&g_exit_status);
	if (WIFEXITED(g_exit_status))
		g_exit_status = WEXITSTATUS(g_exit_status);
	if (WIFSIGNALED(g_exit_status))
		g_exit_status = 1;
	else
	{
		if (cmd_list->next)
			execute_multiple_cmd(info, cmd_list);
		else
			execute_single_cmd(info, cmd_list);
	}
	unlink_heredoc_tmp(cmd_list);
}

단일 cmd

먼저 built_in이라면 부모 프로세스에서 실행되어야 하기 때문에 따로 빼주어서 실행시켜준다.
이외의 경우라면 fork를 떠서 실행시켜주면 된다.

다중 cmd

커맨드의 개수만큼, 즉 cmd_list를 순회하며 그만큼 fork를 떠준다. 병렬처리를 위해 부모프로세스에서는 다음과 같이 코드를 작성해 주었고 자식에서는 구동을 시켜준다.

static void	wait_and_set_exit_status(pid_t pid, int cnt)
{
	signal(SIGINT, SIG_IGN);
	waitpid(pid, &g_exit_status, 0);
	while (--cnt)
		wait(0);
	if (WIFSIGNALED(g_exit_status))
		g_exit_status = 128 + WTERMSIG(g_exit_status);
	if (WIFEXITED(g_exit_status))
		g_exit_status = WEXITSTATUS(g_exit_status);
}

마치며

욕심을 부리다보면 쉘이 점점 눈덩이처럼 불어나게 되므로 단순 노가다 구현부는 과감히 쳐내고 개념만 알고가는 것도 좋은 판단이라고 생각되어진다. 정말 많은 예외케이스들이 존재하고 시간이 지날수록 이전코드에 대한 기억들이 희미해지므로 항상 진행과정을 기록하고 코드에대한 간략한 주석이라도 달아놓는 편이 좋다.

0개의 댓글