Search
Duplicate

표준입력으로 들어오는 Ctrl + d Handling

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
C
Linux
Scrap
태그
noncanonical input mode
termios
9 more properties

터미널 표준입력을 제어해보자

우리가 만든 프로그램이 터미널을 통해 입력을 받을 수 있고, 출력을 할 수 있다면, 각 입력에 대해서도 터미널 기본 속성이 아니라 우리가 원하는 용도로 사용하면 더 좋을 것이다. 예를들어 내가 만든 프로그램이 돌아가는 동안 ctrl + c 를 누른다면 프로그램이 종료되어버리고 이전에 실행 프로그램을 실행했던 쉘(bash or zsh)로 돌아가게된다.
여기서 ctrl + c를 눌렀을 때 프로그램이 종료되는 것이 아니라 작동중인 프로그램 내의 하나의 작업을 중단시키고 싶다면 ctrl + c 입력이 들어왔을 때, 이 기능을 바꿔야 할 것이다.
이때 signal이라는 함수를 사용해 SIG_INT가 들어올 때의 행동을 특정지어주어도 괜찮지만, 만약 ctrl + d와 같이 시그널이 아닌 특수 입력은 어떻게 처리할 수 있을까?
바로 이제부터 설명할 noncanonical input mode를 사용하면 된다.

Noncanonical input mode

그렇다면 canonical, noncanonical의 차이가 뭘까? gnu.org에서는 아래와 같이 설명하고 있다. 우선 canonical input processing mode에 대한 설명이다.
In canonical input processing mode, terminal input is processed in lines terminated by newline ('\n'), EOF, or EOL characters. No input can be read until an entire line has been typed by the user, and the read function (see I/O Primitives) returns at most a single line of input, no matter how many bytes are requested.
canonical input mode(표준 입력 모드)는 우리가 프로그램에서 흔히 사용하는 모델이다. 입력이 들어오면 개행 혹은 EOF와 같은 종료 문자가 나올때까지 처리를 기다리고 있다가 한번에 문장 한 줄을 처리한다.
아래는 noncanonical input processing mode에 대한 설명이다.
In noncanonical input processing mode, characters are not grouped into lines, and ERASE and KILL processing is not performed. The granularity with which bytes are read in noncanonical input mode is controlled by the MIN and TIME settings.
noncanonical input mode(비표준 입력 모드)는 문자가 행으로 그룹화 되지 않으며, ERASE 및 KILL 처리가 수행되지 않는다. 문자를 읽자마자 바로 처리하는 방식으로 동작하기 때문이다.
터미널의 표준 입력 모드에서는 ctrl + d와 같은 특수 기호가 들어왔을 때, 개행이 들어올때까지 기다리기 때문에 우리가 그 입력이 들어오는 순간에 행동을 특정지을수가 없다. 즉, ctrl + d키가 들어오자마자 우리는 무시하거나(문장의 중간에서 c+d가 들어왔을 때) EOF(문장의 처음에 c+d가 들어왔을때)를 넣어주고 싶다면, noncanonical input mode를 활용해야 한다.

C에서 noncanonical input mode를 사용하려면?

우선 프로그램에서 터미널 속성을 가져올 수 있어야 한다. 우리는 termios 구조체를 활용해서 터미널의 데이터를 얻어오거나 적용할 수 있다.

termios 구조체

#include <termios.h> // termios 구조체가 가지고 있는 property struct termios { tcflag_t c_iflag; /* input flags */ tcflag_t c_oflag; /* output flags */ tcflag_t c_cflag; /* control flags */ tcflag_t c_lflag; /* local flags */ cc_t c_cc[NCCS]; /* control chars */ speed_t c_ispeed; /* input speed */ speed_t c_ospeed; /* output speed */ };
C
복사
termios구조체는 터미널의 거의 모든 속성을 다룰 수 있는 변수들로 이루어져있다. input과 관련된 속성을 변경할 수 있는 c_iflag부터 출력 속도를 조절하는 c_ospeed까지.
우리는 여기서 표준 입력으로 들어오는 특수문자(ctrl + d와 같은)를 처리하려고 하기 때문에 c_lflag를 집중적으로 볼 것이다. noncanonical input을 사용하기 위해 c_cc의 MIN, TIME 특수 매개변수를 사용할 것이다. 우선 termios function부터 알아보자.

termios function

termios.h는 많은 함수들을 제공하지만 우리는 일단 터미널 속성을 가져오는 함수 tcgetattrtcsetattr함수만 볼 것이다.

1) tcgetattr

이 함수는 터미널 파일에 대한 속성을 얻어서 termios_p에 저장한다.
int tcgetattr(int fd, struct termios *termios_p);
C
복사
Parameters
fd : 터미널 file discriptor
termios_p : 터미널 속성을 저장할 포인터
Return Value
성공 : 0
실패 : -1 (errno에 오류값 설정)
EBADF : 유효하지 않은 fd
ENOTTY : 터미널이 아닌 fd
EINVAL : termios_p is NULL

2) tcsetattr

이 함수는 터미널 파일에 대한 속성을 설정한다.
int tcsetattr(int fildes, int optional_actions, struct termios *termios_p);
C
복사
Parameters
fd : 터미널 file discriptor
optional_actions : 동작 선택
TCSNOW → 속성을 바로 변경한다
TCSADRAIN → 송신을 완료한 후 변경한다.
TCSAFLUSH → 송수신 완료 후 변경한다.
termios_p : 터미널 속성을 저장할 포인터
Return Value
성공 : 0
실패 : -1 (errno에 오류값 설정)
EBADF : 유효하지 않은 fd
ENOTTY : 터미널이 아닌 fd
EINVAL : termios_p is NULL
아래와 같이 터미널 속성을 받아서 구조체에 저장할 수 있다.
#include <termios.h> struct termios new_term; int main(void) { tcgetattr(STDIN_FILENO, &new_term); }
C
복사

c_lflag 속성 조정하기

c_lflag는 우리의 로컬 터미널 속성과 관련되어 있다. 초기값은 모두 ON 상태이다.
Search
c_lflag의 속성
이름
설명
이 플래그가 on이면, 정규모드로 입력이 이루어진다.
queue flush를 비활성화 시킨다.
반향을 설정한다. 이 플래그가 off되어 있으면 입력은 반향되지 않는다.
erase 문자를 반향한다. ECHO플래그가 on되어 있는 경우에, 스크린의 마지막 문자를 지운다.
ECHO, ECHOPRT가 on인 상태에서 ERASE가 발생하면 삭제되는 문자가 \뒤에 표시된다. 만약 모든 문자를 삭제했다면 /가 출력된다.
NL문자가 반향된다. ECHO플래그와 상관없이 NL문자를 반향한다.
제어문자가 반향되도록 한다. 이 플래그를 on 시킨 상태에서 ctrl + x를 누르면 ^X로 화면에 표시된다.
COUNT8
위 속성들을 자세히 살펴보면 우리가 자주 사용하는 터미널 설정과 동일하게 동작하는 것을 알 수 있다. 실제로 현재 터미널 속성을 가져와 모든 변수들을 출력해보면 초기값이 모두 on 되어 있는 상태임을 알 수 있다.
우리는 noncanonical input mode를 사용할 것이기 때문에 ICANON을 off 해주면 된다. 그리고 이때 입력하는 문자를 터미널에 반향(echo)해주는 옵션도 off로 지정하고 문자가 들어오고 내가 해당 문자를 판단한 후 동작하도록 설정해 줄 것이다.
... new_term.c_lflag &= ~(ICANON | ECHO);
C
복사

c_cc 속성값 조정하기

이제 c_cc의 VMIN과 VTIME 변수의 값을 조정해보자. gnu.org에 나오는 메뉴얼을 보면 각 변수의 값들이 어떤 의미를 가지는지 알 수 있다.
두 변수들은 모두 noncanonical input mode에서만 사용하는 의미가 있다.
VMIN
it specifies the minimum number of bytes that must be available in the input queue in order for read to return.
VTIME
it specifies how long to wait for input before returning, in units of 0.1 seconds.
우리는 noncanonical input모드에서 캐릭터 하나씩 입력을 받고 바로 처리를 해야하기 때문에 아래의 속성값을 설정할 것이다.
TIME is zero but MIN has a nonzero value. In this case, read waits until at least MIN bytes are available in the queue. At that time, read returns as many characters as are available, up to the number requested. read can return more than MIN characters if more than MIN happen to be in the queue.
즉, 우리는 VMIN은 1, VTIME은 0으로 설정하면 된다. 추가로 가능한 다른 옵션들에 대한 설명도 아래에 옮겨보았다.
new_term.c_cc[VMIN] = 1; new_term.c_cc[VTIME] = 0;
C
복사
Both TIME and MIN are nonzero.
In this case, TIME specifies how long to wait after each input character to see if more input arrives. After the first character received, read keeps waiting until either MIN bytes have arrived in all, or TIME elapses with no further input.
read always blocks until the first character arrives, even if TIME elapses first.  read can return more than MIN characters if more than MIN happen to be in the queue.
Both MIN and TIME are zero.
In this case, read always returns immediately with as many characters as are available in the queue, up to the number requested. If no input is immediately available, read returns a value of zero.
MIN is zero but TIME has a nonzero value.
In this case, read waits for time TIME for input to become available; the availability of a single byte is enough to satisfy the read request and cause read to return. When it returns, it returns as many characters as are available, up to the number requested. If no input is available before the timer expires, read returns a value of zero.

실행파일에서 내가 원하는 입력모드 세팅하기

이제 위애서 배운 내용을 토대로 간단하게 입력모드를 세팅해보자. 우선 기존 터미널 속성을 저장해 둘 구조체와 새로 변경해서 적용할 구조체 두개를 만들어보자.
struct termios org_term; struct termios new_term;
C
복사
org_term에는 기존 터미널 속성을 저장하고 new_term에는 내가 적용하고 싶은 터미널 속성을 적용해보자.
// org_term에 초기 터미널 세팅 저장 void save_input_mode(void) { tcgetattr(STDIN_FILENO, &org_term); // STDIN으로부터 터미널 속성을 받아온다 } // new_term에 원하는 터미널 속성 설정 void set_input_mode(void) { tcgetattr(STDIN_FILENO, &new_term); // STDIN으로부터 터미널 속성을 받아온다 new_term.c_lflag &= ~(ICANON | ECHO); // ICANON, ECHO 속성을 off new_term.c_cc[VMIN] = 1; // 1 바이트씩 처리 new_term.c_cc[VTIME] = 0; // 시간은 설정하지 않음 tcsetattr(STDIN_FILENO, TCSANOW, &new_term); // 변경된 속성의 터미널을 STDIN에 바로 적용 }
C
복사
그리고 다시 원래의 터미널 속성으로 변경시켜 줄 함수도 만들어준다.
// 기존의 터미널 세팅으로 다시 변경 void reset_input_mode(void) { tcsetattr(STDIN_FILENO, TCSANOW, &org_term); // STDIN에 기존의 터미널 속성을 바로 적용 }
C
복사
이제 간단하게 c+d를 입력했을 때 handling할 수 있는 간단한 실행문을 만들어보자. 실제 터미널과 비슷하게 동작하는 noncanonical input mode 실행파일이다.
#include <stdio.h> #include <termios.h> #include <unistd.h> ... 터미널 함수들 ... int main(void) { int ch = 0; save_input_mode(); // 터미널 세팅 저장 set_input_mode(); // 터미널 세팅 변경 while (read(0, &ch, sizeof(int)) > 0) { if (ch == 4) break ; else write(0, &ch, sizeof(int)); ch = 0; } reset_input_mode(); // 터미널 세팅 초기화 return (0); }
C
복사
나머지 문자는 정상적으로 터미널에 출력되고 ctrl+d를 눌렀을 때에 프로그램이 종료되었다.
이제 조금 더 복잡한 기능을 넣어보자. c+d, backspace, '\n'이 들어왔을 때 각각의 이벤트를 지정해 줄 것이다. 아래는 pseudo코드이다. 이제 이것을 토대로 실제 코드로 옮겨보자.
while (read(0, &ch, 1) > 0) { if (ch == 4) // ctrl + d // do something else if (ch == 127) // backspace idx 감소 // do something else if (ch == '\n') // new line 읽기 종료 else idx 증가 && 출력 ch 초기화 }
C
복사
idx값을 통해 현제 커서의 위치를 추적해보자.

Ctrl + d event handler

문자열의 처음에 들어왔을 때에는 프로그램 종료, 아닌 경우에는 무시하도록 하자
... if (ch == 4) { if (idx == -1) m_exit(NULL); else continue ; }
C
복사

backspace rollback...

backspace가 들어왔을 때, idx의 크기를 줄이고, 화면에 보이는 한 글자를 삭제해준다.
... else if (ch == 127) { if (idx >= 0) { write(0, "\b \b", 3); --idx; } }
C
복사

Result

이제 실제로 터미널에서 동작하는 것과 아주 유사한 형태가 되었다. 기본적인 형태만 만든 것이니 여기에 추가 기능들만 붙인다면 원하는 입력에 원하는 동작을 구현할 수 있을 것이다.
#include <stdio.h> #include <termios.h> #include <unistd.h> ... 터미널 함수들... int main(void) { int ch = 0; int idx = -1; save_input_mode(); set_input_mode(); while (read(0, &ch, 1) > 0) { if (ch == 4) { if (idx == -1) exit(0); else continue; } else if (ch == 127) { if (idx >= 0) { --idx; write(0, "\b \b", 3); } } else if (ch == '\n') break; else { ++idx; write(0, &ch, sizeof(int)); } ch = 0; } reset_input_mode(); return (0); }
C
복사

Reference