minishell

slee2·2021년 9월 3일
0

42Seoul

목록 보기
7/15
post-thumbnail

일단 이 서브젝트를 함에 있어서 알아야 할것은 함수에 대한 정보를 최대한 많이 공부해야한다. 특히 터미널 옵션을 공부하는 것이 좋다. ECHOCTL 등의 기능을 알게되면 정말 편해진다.

이 글은 팀원과 함께 minishell을 구현하면서 중요하고 어려웠던 부분에 대해 공유하는 글이 되겠다.

참고로 이 글에서 메서드에 대한 설명은 사용하지 않았다.


rl_on_new_line, rl_replace_line, rl_redisplay

이 메서드들은 시그널 관리에서 사용했다.
몇몇 시그널들은 터미널 관련 메서드를 이용해야 하지만,
단순한 시그널(ctrl + c)들은 이 rl~ 메서드로 처리가 가능하다.

간단하게 ctrl + c 에 대한 처리를 보여주면,

write(1, "\n", 1);
rl_on_new_line();
rl_replace_line("", 0);
rl_redisplay();

이와 같이 처리했다. 간단하게 설명하면,
엔터 입력 후에 rl_redisplay()를 통해 readline 버퍼를 한 번더 출력해주는 형식이다.

예를들어,
read_line = readline("minishell > ");
이렇게 작성했다면,

minishell >
minishell >

이렇게 출력해주기 위해서이다.

Heredoc

구현이 힘들었던 부분 중의 하나이다.
이유는 완성도와 관련이 있는데, 하나씩 설명해보겠다.

history

일단 이 프로젝트에서는 history를 추가할 수 있는 메서드가 존재한다.
쉽게말해 일반 bash에서 화살표를 위아래로 입력하면 이전에 입력했던 명령어들이 출력되는 기능이 존재한다.

하지만add_historyreadline를 사용했을때 유용한 메서드이다. readline 메서드에 관련된 기능이 있기 때문이다. 하지만, read 같은 일반 메서드에는 당연히 이런 기능이 없다.

그리고 heredoc을 구현할때는 read를 사용해야한다. (readline으로 구현 가능하다면 문제없다. 근데 우리팀은 read로 구현하기로 결정했다.)

아마 직접 bash로 여러 테스트를 해보면 이유를 알 수 있을 것이다.

그렇다면 heredoc에서는 어떻게 이전 목록을 가져올까?

간단한 방법으로 해결하였는데,
.minishell_history 파일을 생성하여
readline에서 명령어들이 입력될때마다
이 파일에 개행과 함께 입력하고,
heredoc에서 이전 목록을 불러올때 이 파일을 읽어오는 방식을 이용했다.

일반 bash에서는 add_history를 통해 알아서 처리,
heredoc에서는 .minishell_history 이용
으로 보면 되겠다.

커서 이동

read를 사용하면서 보게되는 골치아픈 문제이다.
readline의 경우 커서 구현이 되어있다.
좌, 우 이동 가능하며,
위, 아래 add_history 목록 불러와진다.

하지만, read는 아무리 화살표를 눌러도 이동하지 않는다.
이 때문에 화살표 이동을 터미널 옵션을 이용해 직접 구현해야 한다.

이 때 tcgetattr, tcsetattr, tgetent, ... 이러한 터미널 관련 메서드들이 사용된다.

그리고 heredoc에 진입한다면 기본적으로 터미널 입력모드를 바꿔줘야한다.
터미널 입력모드는 Blocking, Non-Blocking이 있는데,
쉽게 말하자면

Blockingread를 다 읽어서 오는것
Non-Blocking은 한 글자씩 읽는 것

으로 이해했다. 이 부분은 구글링해서 알아보는걸 추천한다.
아무튼 우리는 기본적으로 터미널이 Blocking으로 설정되어있고,
한 글자씩 인식하기 위해서 Non-Blocking으로 바꿔줘야한다.(heredoc에서는)

void	set_input_mode(void)
{
	struct termios	new_term;

	// 현재 터미널 속성을 불러온다.
	tcgetattr(STDIN_FILENO, &new_term);
    
	// ICANON - Enter가 입력될때까지 입력을 받는 모드(Canonical)
    // ECHO - 입력을 받으면 화면에 출력해주는 모드
    // 두 모드를 제거한다.
    new_term.c_lflag &= ~(ICANON | ECHO);
    
	// 글자는 1글자 입력받을때마다 인식
    new_term.c_cc[VMIN] = 1;
    
    // 함수가 사용자 입력을 기다리는 시간 - 0
	new_term.c_cc[VTIME] = 0;
    
    // 설정값을 변경한다.
	tcsetattr(STDIN_FILENO, TCSANOW, &new_term);
}

이런식으로 처음에 heredoc 진입전에 터미널 입력 모드를 바꿔주고 들어간다.

화살표 여부만 봤을 경우, 위 그림과 같은 구조로 이루어져 있다. 그 외에도 시그널 처리 등이 있다.

시그널(ctrl + c, ctrl + d, ...), 화살표 키 들은 모두 특정 숫자를 가지고 있다. 이 숫자를 통해 if문으로 비교하면 된다.

그리고 커서 이동의 경우 왼쪽으로 한칸 이동한다면,

void	move_cursor_left(t_heredoc *heredoc)
{
	char	*cm;

	cm = tgetstr("cm", NULL);
	if (heredoc->col == 2)
		return ;
	heredoc->col--;
	tputs(tgoto(cm, heredoc->col, heredoc->row), 1, putchar_tc);
}

이런느낌이다. 현재 위치를 가져와(*heredoc) 한 칸 앞으로 위치를 바꾸고 tgoto를 이용해 이동시키는 것이다. if문의 경우 맨 앞으로 왔을 때라고 생각하면 될 것 같다.

예시를 몇개 보여줬지만, 직접 출력해보고 구현해봐야 이해가 되는 부분이다. 이 글을 작성할때, 메서드에 대한 설명을 적어야 하나 고민을 했었지만, 그러한 것은 구글링으로 알 수 있다고 생각했고 이 프로젝트에 대한 경험을 공유해야겠다는 생각을 가지고 글을 작성했다.

그 외에 명령어 실행(exec), buildin 등이 있지만, 이러한 명령어들은 구현하면 되기 때문에 큰 문제로 보고있지는 않았다. 그 외에 터미널 옵션 관리 + 시그널 + heredoc에서 처음 보는 메서드와 그냥 숨이 턱 막히는 메서드들을 본 경험이 있기에 다른 분들에게 도움이 되고자 몇 개만 다루어 보았다.

다음 글은 프로젝트를 진행하면서 생각지도 못했던 기능들에 대해 적어보겠다.

0개의 댓글