일단 이 서브젝트를 함에 있어서 알아야 할것은 함수에 대한 정보를 최대한 많이 공부해야한다. 특히 터미널 옵션을 공부하는 것이 좋다. ECHOCTL 등의 기능을 알게되면 정말 편해진다.
이 글은 팀원과 함께 minishell
을 구현하면서 중요하고 어려웠던 부분에 대해 공유하는 글이 되겠다.
참고로 이 글에서 메서드에 대한 설명은 사용하지 않았다.
이 메서드들은 시그널 관리에서 사용했다.
몇몇 시그널들은 터미널 관련 메서드를 이용해야 하지만,
단순한 시그널(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 >
이렇게 출력해주기 위해서이다.
구현이 힘들었던 부분 중의 하나이다.
이유는 완성도와 관련이 있는데, 하나씩 설명해보겠다.
일단 이 프로젝트에서는 history
를 추가할 수 있는 메서드가 존재한다.
쉽게말해 일반 bash
에서 화살표를 위아래로 입력하면 이전에 입력했던 명령어들이 출력되는 기능이 존재한다.
하지만 이 add_history
는 readline
를 사용했을때 유용한 메서드이다. 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
이 있는데,
쉽게 말하자면
Blocking
은 read
를 다 읽어서 오는것
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에서 처음 보는 메서드와 그냥 숨이 턱 막히는 메서드들을 본 경험이 있기에 다른 분들에게 도움이 되고자 몇 개만 다루어 보았다.
다음 글은 프로젝트를 진행하면서 생각지도 못했던 기능들에 대해 적어보겠다.