Search
🐚

minishell

Created
2021/08/24
tag
42서울
42Seoul
minishell
Write a Shell in C
Redirection
Pipe
Built-In Commands
Various Operators

Subjects

1. External Functions

minishell을 구현하는데 있어서 허용된 함수들은 이전 과제들과 비교하여 굉장히 방대한 편인데, 기존에 소개가 되었던 함수들에 대해선 아래 Toggle을 펼쳐서 확인해보자.
get_next_line에서 소개 되었던 함수들
ft_printf에서 소개 되었던 함수들
miniRT에서 소개 되었던 함수들
pipex에서 소개 되었던 함수들
Philosophers에서 소개 되었던 함수들
이외의 함수들
위에서 제시된 함수들 외에는 아래와 같은 함수들이 허용되는데, 이들에 대해서 알아볼 것이다.
<dirent.h> 에서 허용된 함수들
<term.h> 에서 허용된 함수들
<sys/termios.h> 에서 허용된 함수들
<sys/ioctl.h>, <sys/wait.h> 에서 허용된 함수들
<unistd.h>, <stdlib.h>, <signal.h>에서 허용된 함수들
<readline/readline.h>, <readline/history.h>에서 허용된 함수들
위 함수들을 <math.h> 혹은 <pthread.h><semaphore.h>와 같이 라이브러리 별로 뜯어서 글을 써보고 싶었는데, 제시된 라이브러리들을 모두 훑어보니 그 양이 정말 많았다. 따라서 라이브러리 내의 나머지 함수들은 추후에 필요할 때 찾아보기로 하고, 당장은 minishell과 관련된 함수들만이라도 정확히 숙지하는 것으로 타협했다.

2. on <dirent.h>

1) opendir

1. 의존성

#include <dirent.h>
C
복사

2. 함수 원형

DIR *opendir(const char *name);
C
복사

3. 함수 설명

name에 해당하는 이름을 가진 디렉토리 스트림을 참조하는 DIR * 타입의 포인터를 반환한다. 만일 name에 해당하는 디렉토리가 없거나 함수 수행에 문제가 있다면, DIR * 타입의 포인터 반환 값은 NULL이 된다.
DIR이라는 타입은 <dirent.h> 내에 정의된 구조체이다. 구조체 내부에 유지하고 있는 변수는 위 그림과 같으며, 전반적으로 열린 상태의 디렉토리를 기술하기 위한 변수들이 존재하는 것을 확인할 수 있다. 즉, DIR 구조체는 주로 디렉토리 조작을 위한 수단으로 이용된다.
DIR이라는 구조체의 포인터 타입인 DIR *을 흔히 디렉토리 스트림이라고 부르는데, 이는 정규 파일 조작을 위한 파일 스트림FILE *와 비슷한 형식으로 이용된다. 디렉토리 스트림파일 스트림에서 말하는 스트림이란 특정 작업을 용이하게 수행할 수 있도록 추상화된 중간 매개체의 데이터 흐름을 의미한다. 이를 이해하기 위해선 스트림의 유래에 대해 이해할 필요가 있다.
Unix에서는 장치를 표현하기 위해 장치 자체를 추상화하여 파일로써 이용하도록 구현되어 있다. 장치 자체를 파일로 표현한다는 말은 파일의 조작을 통해 장치가 갖는 입출력에 대해 설정할 수 있다는 것을 의미하므로, 기존의 비효율적인 많은 수작업을 줄일 수 있었다. 이 때 해당 파일이 주고 받는 일련의 데이터 흐름을 스트림이라고 정의하게 되었다.
과거의 컴퓨터는 천공 카드, 디스크, 잔자 테이프 등의 수작업을 통해 입출력에 대해 설정이 이뤄졌는데, 장치를 파일로 추상화 해냄으로써 이를 극복할 수 있었다. 연결된 장치들은 /dev 디렉토리에 마운트되어 파일 형태로 유지되고 있다.
위와 같은 유래를 통해 알 수 있듯이, 디렉토리 스트림은 디렉토리와 관련된 작업을 보다 쉽게 수행할 수 있도록 주고 받는 데이터의 흐름이고 이를 추상화한 구조체가 DIR이라고 이해할 수 있다.

2) closedir

1. 의존성

#include <dirent.h>
C
복사

2. 함수 원형

int closedir(DIR *dirp);
C
복사

3. 함수 설명

opendir 함수를 통해 열어둔 디렉토리 스트림을 닫는 역할을 수행한다. closedir의 호출이 성공적으로 수행되면 0을 반환하고, 그렇지 않으면 -1을 반환한다.

3) readdir

1. 의존성

#include <dirent.h>
C
복사

2. 함수 원형

struct dirent *readdir(DIR *dirp);
C
복사

3. 함수 설명

열어둔 디렉토리 스트림을 인자로 받아 해당 디렉토리 내의 엔트리를 참조하는 dirent 구조체의 포인터 타입을 반환한다. 만일 함수 수행에 문제가 발생하는 경우에는 NULL을 반환하고, 디렉토리 스트림의 끝에 도달하여 더 이상의 엔트리가 존재하지 않아도 NULL이 반환된다.
스트림의 끝에 도달한 경우와 문제가 생긴 경우 모두 NULL이 반환되므로, 두 경우에 대해서 구분지을 필요가 있다. 이에 대한 구분은 errno를 통해 구분할 수 있다. 스트림의 끝에 도달한 경우에는 기존에 설정되어 있는 errno에 변동이 없고, 문제가 생긴 경우에는 문제 상황에 맞는 errno로 변동이 생긴다. 따라서 오류에 대한 처리를 위해선 NULL 검사 이후 errno 값에 대한 검증이 요구된다.
반환받은 엔트리를 적절히 활용하기 위해 dirent 구조체의 형태를 파악할 필요가 있다. Mac OS X에서 사용되는 dirent는 내부적으로 __DARWIN_STRUCT_DIRENTY로 선언되어 있으며, 엔트리에 대한 다양한 정보들이 유지되고 있는 것을 확인할 수 있다.
Mac OS X 내에서는 엔트리의 타입을 dirent 구조체 자체에서 확인할 수 있는데, 이는 다른 파일 시스템을 이용하는 환경에서는 이용할 수 없을 수도 있다. 이런 경우에는 <stat.h>에서 stat 구조체와 stat 함수를 이용하여 파일에 대한 정보를 받아온 후, 파일의 타입을 확인하는 매크로 함수를 이용해야 한다. minishell에서는 해당 함수들이 허용되어 있지 않을 뿐더러 단순히 dirent 구조체를 이용하여 이를 대체할 수 있으므로, <stat.h>와 같은 것들을 이용할 수 있다는 사실만 알아두면 된다.
readdir 함수를 통해 얻은 dirent 구조체의 포인터 타입에 대해서, 포인터가 참조하고 있는 메모리 상의 공간은 정적으로 할당되었기 때문에 별도로 free 함수를 호출하지 않아도 된다.

4) 예시

<dirent.h>를 이용한 예시에서는 프로그램 실행 위치를 기준으로 특정 디렉토리를 읽고, 디렉토리 내부의 엔트리들을 확인하는 코드를 작성해볼 것이다.
#include <dirent.h> #include <errno.h> #include <stdbool.h> #include <stdio.h> void classify(struct dirent *ent) { printf("%s\t", ent->d_name); if (ent->d_type == DT_BLK) printf("Block Device\n"); else if (ent->d_type == DT_CHR) printf("Character Device\n"); else if (ent->d_type == DT_DIR) printf("Directory\n"); else if (ent->d_type == DT_LNK) printf("Symbolic Link\n"); else if (ent->d_type == DT_REG) printf("Regular File\n"); else if (ent->d_type == DT_SOCK) printf("Unix Domain Socket\n"); else printf("Unknown Type File\n"); } int main(void) { int temp; DIR *dirp; struct dirent *file; dirp = opendir("test_dir"); if (!dirp) { printf("error\n"); return (1); } while (true) { temp = errno; file = readdir(dirp); if (!file && temp != errno) { printf("error\n"); break ; } if (!file) break ; classify(file); } closedir(dirp); return (0); }
C
복사

3. on <term.h>

<term.h> 내에 존재하는 함수들을 확인하기 전에 <term.h>에서 요구되는 개념들을 먼저 이해할 필요가 있다. Mac OS X에서 사용할 수 있는 <term.h>라는 라이브러리는 터미널 상의 기능들을 제어하기 위한 라이브러리라고 이해할 수 있다. 터미널 상에서 커서를 제어한다거나, 터미널 상의 화면 일부를 지운다거나, 현재 보이는 색상에 변화를 주는 등의 작업을 <term.h>를 통해 수행할 수 있을 뿐 아니라, 터미널 상에서 사용자가 실행한 프로그램은 출력할 수 없는 문자들을 이용하여 터미널과 상호작용을 하는데 이 때도 <term.h>가 이용된다.

1) NCURSES

과제에서 허용된 함수들을 <term.h> 내에서 찾아보면, 위 그림과 같이 NCURSES 매크로 정의로 묶여 있는 것을 확인할 수 있다. NCURSESNew Curses의 약자이고, Terminal Emulator 상에서 실행될 수 있는 GUI 어플리케이션을 만들 수 있는 툴킷으로 다양한 API를 제공한다. 여러 창들을 띄우거나, 키보드 혹은 커서 제어를 한다든가 색상을 넣는 작업 등을 할 수 있으며, 주로 Unix 상에서 이용된다. 위 그림에 따라 <term.h>NCURSES의 터미널 관련 기능들에 기반하여 동작하는 것을 알 수 있다.
NCURSESCursor Optimization을 위한 기존의 CURSES라는 라이브러리에서 유래되었고, 이는 Unix 계열 운영체제를 위한 제어 라이브러리 중 하나이다. 1990년 대에 개발이 중단 되었다가, GNU에서 개발을 시작하면서 NCURSES로 명명되었다. 따라서 Unix 계열 시스템 중 최신 버전을 이용하고 있는 경우에는 GCC의 설치 시 자동으로 함께 설치된다.
NCURSES의 터미널 관련 기능들에 기반하여 동작하는 라이브러리<term.h> 외에도 <termcap.h>가 있는데, 터미널 제어를 위한 두 라이브러리의 역할은 서로 동일하다. 다만 각 라이브러리를 열어 확인해보면, <term.h>NCURSES 외의 기능들도 포함되어 있는 것을 확인할 수 있다. 위 그림은 <termcap.h>에 있는 NCURSES 매크로 정의로 묶인 부분인데, <term.h>의 그림과 비교해보면 NCURSES의 중복 포함을 방지하는 매크로 정의만 다를 뿐 NCURSES로부터 가져온 기능들은 모두 동일한 것을 확인할 수 있다.
<term.h> 혹은 <termcap.h>에서 NCURSES로부터 가져온 함수들을 이용하기 위해선 함수의 실제 코드를 가져오는 과정도 필요하다. 예를 들어 NCURSES로부터 가져온 함수들 중 하나를 호출하는 코드를 컴파일 해보면, 위 그림과 같이 해당 함수를 찾을 수 없다고 컴파일에 실패한 결과를 확인할 수 있다.
컴파일이 되지 않은 이유는 NCURSES_EXPORT를 통해 내보내진 함수들의 실제 코드를 가져오지 않았기 떄문이므로, 위 그림과 같이 -l이라는 컴파일 옵션으로 NCURSES를 명시하면 정상적으로 컴파일이 되는 것을 확인할 수 있다.

2) TERM 환경 변수

<term.h>가 터미널 제어를 위한 라이브러리인 만큼 TERM 환경 변수의 영향을 받는다. 따라서 TERM 환경 변수가 정확히 무엇인지, TERM 환경 변수가 왜 필요한지, TERM 환경 변수로 사용할 수 있는 값들에 대해 이해할 필요가 있다.

TERM 환경 변수란?

TERM 환경 변수는 로그인 터미널의 타입을 지정하는 환경 변수이다. 여기서 말하는 터미널의 타입이란 어떤 터미널 에뮬레이터를 이용하는지를 말하는 것이 아니라는 점에 유의해야 한다. TERM 환경 변수의 역할은 지극히 명확한데, 이는 터미널 상에서 실행될 사용자 어플리케이션에게 터미널과 어떻게 상호작용을 해야하는지를 알리는데 있다. 즉, TERM 환경 변수는 사용자 어플리케이션에 의해 사용된다고 볼 수 있고, 이 때의 사용자 어플리케이션과 터미널과의 상호작용은 입출력 작업까지도 포함된다.

TERM 환경 변수의 필요성?

사용자 어플리케이션과 터미널 간의 상호작용은 Escape Sequence를 통해서 이뤄지는데, 이를 이용하여 키보드, 커서, 터미널 화면에 대해 제어할 수 있다. 단, Escape Sequence의 표준이 명확히 정해진 것이 없었기 때문에 과거의 물리적인 터미널들은 터미널의 타입에 따라서 사용할 수 있는 Escape Sequence에 차이가 있었다.
이와 같은 Escape Sequence들을 사용자 어플리케이션이 정확히 알면 좋겠지만, 의미 없는 문자들이 조합된 모든 Escape Sequence들을 외워서 이용하는 것은 굉장히 어려운 일이다. 따라서 원하는 작업에 대한 쿼리를 운영체제에 보내면, 운영체제는 이에 대한 쿼리를 처리하여 적절한 Escape Sequence를 전달해주도록 설계되었다. 결과적으로 운영체제 입장에서는 이와 같은 작업이 가능하려면 특정 타입의 터미널이 이용하는 Escape Sequence를 모두 이해하고 있어야 하므로, 운영체제는 이를 적절히 변환할 수 있도록 내부에 DB를 갖고 있도록 설계었다. 이를 Terminal Capability (TermCap)이라고 부른다.
최근의 많은 시스템에서는 TermCap 대신에 TermInfo라는 DB가 이용된다. TermCapTermInfo 등 터미널과 관련된 추가적인 정보는 아래 링크에서 얻을 수 있다.
정리해보면, 사용자 어플리케이션이 원하는 작업에 대한 쿼리를 보내서 운영체제가 TermCap에서 적절한 Escape Sequence를 보낼 수 있으려면 사용자 어플리케이션이 사용하려는 터미널의 타입을 먼저 설정할 필요가 있는 것이고, 이 때 TERM 환경 변수가 이용된다.
최근에는 Escape Sequence에 대한 표준이 정리된 상태이고, 대부분의 터미널들이 이를 따른다. 터미널 타입에 따라 제공되는 기능이 다를 수 있기 때문에 여전히 DB 자체는 필요하지만, TERM 환경 변수 값에 따라 기능적으로 엄청 큰 차이가 존재하는 것은 아니다. 표준으로 정의된 Escape Sequence는 아래 링크에서 확인할 수 있다.

TERM 환경 변수로 이용할 수 있는 값?

TERM 환경 변수로 이용할 수 있는 값은 대체적으로 터미널 에뮬레이터와 그 이름이 동일한데, 이 때문에 TERM 환경 변수의 값이 터미널 에뮬레이터를 의미한다고 이해해서는 안 된다. 따라서 현재 사용하고 있는 터미널 에뮬레이터와 TERM 환경 변수로 이용되는 값은 다를 수 있다. 예를 들어 gnome-terminal을 이용한다고 해서 TERM 환경 변수를 꼭 gnome-terminal로만 이용할 수 있는 것은 아니다. gnome-terminal을 이용하더라도 xterm 혹은 xterm-256color를 이용할 수 있고, 터미널 타입에 따라 상호작용 방식만 바뀐다. 그렇다면 TERM 환경 변수로 이용하는 값에 정답이 있을까?
시스템 별로 이용할 수 있는 터미널 에뮬레이터의 목록은 아래 링크에서 확인할 수 있다.
TERM 환경 변수의 값으로 어떤 것을 설정해야 하는지에 대해서 명확히 정해진 답이 있는 것은 아니지만, xterm 혹은 xterm-256color를 이용하는 것이 권장된다. 대부분의 GUI를 지원하는 터미널 에뮬레이터는 xterm이라는 터미널 타입을 이용하거나 호환이 가능하도록 되어있기 때문이다. 이에 대해선 ssh와 같은 원격 통신의 상황을 통해 조금 더 쉽게 이해할 수 있다.
예를 들어 어떤 프로그램이 실행되고 있는 환경을 AA라고 하고, 이를 보여주는 환경을 BB라고 해보자. 즉, BB 환경에서 ssh를 통해 AA 환경에서 프로그램을 실행하고 있는 것이다. 사용자 어플리케이션이 터미널과 상호작용을 하고 있는 환경은 AA이므로, BB 환경에서 제어를 하고 있는 상황에서 적절한 TermCap을 이용하고자 한다면 AA 환경의 엔트리 역시 갖고 있어야 한다. 따라서 원격 통신 상에서는 두 터미널의 타입을 동일하게 맞출 필요가 있는데, xterm이 널리 이용되어 왔고 호환성이 높기 때문에 xterm으로 고정해두고 이용하는 것이 일반적이다.
물론 터미널 에뮬레이터와 TERM 환경 변수의 값이 같으면 터미널 에뮬레이터는 자신의 터미널 타입으로 상호작용을 하면 되므로, 터미널 입장에서는 쿼리를 처리하는 측면에서 득을 볼 수 있다. 하지만 사용자 어플리케이션 입장에서는 쿼리만 보내면 되기 때문에, 이에 따른 영향이 사용자 어플리케이션에게 주는 차이는 미미하다. 따라서 xterm 혹은 xterm-256color와 같이 더 널리 이용되는 터미널 타입을 이용하는 것이 권장된다.
xterm 혹은 xterm-256color 간에도 엄청 큰 차이가 있는 것은 아니다. xterm-256color는 이름처럼 256개의 Text Color를 지원하는데, 그렇다고 xterm이 색상 지원을 안하는 것은 아니다. 색상 표현의 수에서의 약간의 차이가 있을 뿐, xtermText Color를 지원한다.

3) 터미널 제어 방법

터미널 제어를 위해선 위에서 언급되었던 TermCap 혹은 TermInfo의 쿼리를 이용해야 하고, 해당 쿼리를 이용할 수 있도록 사전에 엔트리를 먼저 등록해주는 작업이 필요하다. 이와 같은 엔트리 등록 및 쿼리 작업은 기본적으로 터미널 자체의 기능을 이용한 것이기 때문에, 가장 처음에는 터미널에서 상호작용하려는 대상의 속성 값을 먼저 설정할 필요가 있다.
상호작용 대상의 속성 값 설정은 termios라는 구조체를 이용하여 처리할 수 있다. tcflag_t, cc_t, speed_t와 같은 타입은 unsigned long, unsigned char 등으로 정의된 타입일 뿐이고, termios 내의 멤버 변수들의 역할은 위 그림에 나타난 주석으로 파악할 수 있다.
각 멤버 변수들은 터미널 속성 값 설정을 위한 플래그로써 이용되는데, 각 플래그에 할당되는 값을 다르게 해보면 입력과 출력 작업에 대한 결과가 상이한 것을 확인할 수 있을 것이다. 각 플래그에 할당되는 값은 <sys/termios.h> 내부에 input flag, output flag 처럼 그림에 나타난 주석을 검색해보면 쉽게 찾을 수 있으며, 이 값들은 Bit 연산으로써 이용되는 값임을 쉽게 알 수 있다. 따라서 터미널에서 동작하는 쉘을 구현할 때 기대되는 결과가 나오지 않는다면 termios 구조체를 적절히 조작하면 된다. 라이브러리 코드를 통해 플래그가 찾아지지 않거나 이해가 안 되는 부분이 있다면 아래 링크에서 플래그를 검색하는 것을 권한다.
<term.h>를 포함했다면, 별도로 <sys/termios.h>를 포함시킬 필요는 없다. <term.h>를 자세히 살펴보면 <termios.h>를 포함하고 있는 것을 확인할 수 있고, <termios.h> 내에서는 <sys/termios.h>를 포함하고 있는 것을 확인할 수 있다.
termios 구조체를 조작하여 터미널 속성 값을 결정하기 위해선 기존의 터미널 속성 값을 termios * 타입으로 가져온 뒤, 가져온 termios 구조체 내부의 값을 수정한 후 적용해야 한다. 이와 같은 작업들을 각각 tcgetattrtcsetattr이라는 함수로 수행할 수 있다.
터미널 속성 값 결정이 끝난 후에는 처음에 언급한 대로 TermCap 혹은 TermInfo이 쿼리를 적절히 처리할 수 있도록, 어떤 터미널 타입을 이용하고 있는지 엔트리를 가져올 필요가 있다. 이와 같은 작업을 tgetent이라는 Routine 함수를 통해 수행할 수 있다.
엔트리를 불러오면서 터미널 타입을 정하면, TermCap 혹은 TermInfo의 쿼리를 이용할 수 있다. 쿼리를 통해 수행되는 함수들을 Routine이라고 한다. TermCap을 이용하는지 TermInfo를 이용하는지에 따라 사용하는 Routine 함수가 달라지는데, minishell에서는 TermCap의 함수들이 허용되어 있다. 이에 해당되는 함수들은 tgetstr, tputs, tgetflag, tgetnum, tgoto 등의 Routine이 있다.
이전에 언급했던 대로 <term.h>에는 <termcap.h>의 함수들이 모두 포함되어 있으므로 <termcap.h>를 추가적으로 포함할 필요는 없다. tgetent도 엔트리 이름을 쿼리로 이용하는 Routine이다.
Routine 마다 사용하는 쿼리는 모두 다르며, 쿼리는 Routine 함수의 char * 타입의 id라는 인자로 이용된다. 각 Routine에서 사용할 수 있는 쿼리는 아래 링크에서 확인할 수 있다.

4) tcgetattr

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tcgetattr(int fd, struct termios *t);
C
복사

3. 함수 설명

상호작용 하려는 대상의 파일 디스크립터를 인자로 받는다. 대상의 속성 값을 기록할 termios 구조체를 포인터 타입으로 할당한다. 속성 값은 포인터를 이용하여 기록하므로 반환 값은 tcgetattr 함수의 수행 결과를 나타낸다. 함수의 수행을 성공적으로 마치면 0을 반환하고, 그렇지 않으면 -1을 반환한다.

5) tcsetattr

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tcsetattr(int fd, int action, const struct termios *t);
C
복사

3. 함수 설명

상호작용 하려는 대상의 파일 디스크립터를 인자로 받는다. 대상의 속성 값을 설정하기 위해 termios 구조체를 포인터 타입으로 할당한다. termios 구조체의 속성 값을 어느 타이밍에 적용할지는 action으로 구분한다.
action으로 사용할 수 있는 값은 위 그림에서 보이는 것처럼 4종류이다. TCSANOWtermios 구조체의 값으로 즉시 변경을 의미한다. TSCADRAINTCSAFLUSH는 대상의 파일 디스크립터에 대해 모든 쓰기 작업이 이뤄진 후에 변경을 의미하는데, 차이가 있다면 TCSAFLUSH는 처리되고 있는 입력 작업을 폐기하고 TCSADRAIN은 그렇지 않다. TCSASOFT가 이용될 경우에는 termios 구조체 내의 c_cflag, c_ispeed, c_ospeed의 값들이 무시된다.
함수의 수행을 성공적으로 마치면 0을 반환하고, 그렇지 않으면 -1을 반환한다.

6) tgetent

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tgetent(char *bp, const char *name);
C
복사

3. 함수 설명

name에 해당하는 터미널 타입의 엔트리로 설정하여, 해당 엔트리에 대한 TermCap의 쿼리를 수행할 수 있도록 만드는 Routine이다. 일반적으로 name에 할당하는 값은 TERM 환경 변수로 할당된 터미널 타입을 이용한다. 이 때 bp라고 하는 Buffer Pointer는 무시되는 인자이므로 NULL을 할당해주는 것이 일반적이다.
Buffer Pointer를 무시하지 않고 이용하는 경우는 몇 없다. 만일 Buffer Pointer를 이용하는 경우에는 이를 다른 Routine 함수에서도 이용한다. 하지만 대부분의 경우에는 Buffer Pointer는 무시되고, Routine들이 공통적으로 사용하는 Buffer를 내부적으로 할당하여 사용한다.
Routine의 작업이 성공적이라면 1을 반환하고, Routine 수행 시 name의 값이 널 문자열과 같이 비어 있는 경우에는 0을 반환한다. name의 엔트리를 DB에서 찾을 수 없는 등의 Routine 수행의 실패의 경우에는 -1을 반환한다.

7) tgetflag

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tgetflag(char *id);
C
복사

3. 함수 설명

쿼리로 사용할 이름을 id라는 인자로 받는다. 플래그를 얻을 수 있는 쿼리라면 true (1)을 반환하고, 그렇지 않다면 false (0)을 반환한다.

8) tgetnum

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tgetnum(char *id);
C
복사

3. 함수 설명

쿼리로 사용할 이름을 id라는 인자로 받는다. 쿼리에 해당하는 값을 얻어올 수 있다면 그 값을 반환하고, 그렇지 않다면 -1을 반환한다.

9) tgetstr

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

char *tgetstr(char *id, char **area);
C
복사

3. 함수 설명

쿼로리 사용할 이름을 id라는 인자로 받는다. 쿼리에 해당하는 Escape Sequence를 얻을 수 있다면 해당 문자열을 반환하고, 그렇지 않다면 NULL을 반환한다. area라는 인자는 tgetent에서 사용되었던 Buffer Pointer를 의미하는데, 무시되는 인자였기 때문에 일반적으로는 NULL을 준다.
tgetent라는 Routine에서 Buffer Pointer가 내부적으로 할당되어 이용되었던 것처럼, area 역시 무시된다고는 하지만 아예 사용되지 않는 것이 아니라 내부적으로 할당되어 이용되고 있다. 표면적으로 이용되지 않는다는 측면에서 무시된다고 이해하면 된다.

10) tgoto

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

char *tgoto(const char *cap, int col, int row);
C
복사

3. 함수 설명

col은 터미널의 세로열의 위치를 의미하고, row는 터미널의 가로행의 위치를 의미한다. capCapability를 의미하며, 일반적으로 Cursor Motioncm에 대한 Escape Sequence를 사용한다. 물론 다른 Escape Sequence를 이용할 수도 있겠지만 Cursor Motion에 대한 Escape Sequence를 사용하는게 가장 안전하기 때문에 tgoto라는 Routine에 대해서는 다른 Escape Sequence를 피하는 것이 좋다.
tgoto라는 Routinecolrow를 고려한 Cursor MotionEscape Sequence를 반환한다. 따라서 이 문자열을 tputs의 인자로 사용하면, 터미널 상의 커서가 이동되는 것을 볼 수 있다. Routine에 대한 작업을 실패할 시에는 NULL을 반환한다.

11) tputs

1. 의존성

#include <term.h>
C
복사

2. 함수 원형

int tputs(const char *str, int affcnt, int (*putc)(int));
C
복사

3. 함수 설명

tputsEscape Sequence에 대한 터미널 출력 결과를 내는 Routine으로써, str이라는 인자가 tgetstr 혹은 tgoto를 통해 얻은 Escape Sequence이다. affcnttputs로 영향을 끼칠 줄 수를 의미하며, 여러 줄에 영향을 끼칠 것이 아니라면 1로 주는 것이 일반적이다. putc라는 인자는 int 타입의 인자를 받고 int 타입을 반환하는 함수 포인터인 것을 확인할 수 있는데, 이는 ASCII 문자 값을 인자로 받아 표준 출력의 쓰기 작업으로 터미널에 ASCII 문자 값을 출력해주는 함수이다.
tputs라는 Routine이 문제 없이 수행되면 0을 반환하고, 그렇지 않으면 -1을 반환한다.

12) 예시

일반적인 쉘에서는 방향키 등을 통해 키 조작을 하면 이상한 기호가 나오지 않고 정해진 기능을 수행한다. 예를 들면 위 방향키를 누르면 이전에 입력했던 커맨드들을 보여주는 것처럼 말이다. 하지만 프로그램을 구동한 후에는 방향키 등의 키 조작을 하면 위 그림과 같이 이상한 기호가 나오는 것을 볼 수 있다. <term.h>를 다루는 예시에서는 터미널 설정 값과 NCURSES를 이용하여 사용자 어플리케이션이 원하는 동작을 수행할 수 있도록 키 조작에 변화를 주는 코드를 작성해볼 것이다.
제시된 코드에서는 표준 입력에 대한 설정을 바꾸는데, 기존의 Canonical 방식을 따르고 있는 표준 입력Non-Canonical 방식으로 바꾸면서 원하는 결과를 얻을 수 있도록 만들 것이다. 표준 입력에서의 Canonical이란, 한 번에 처리할 수 있는 최대 길이의 문자는 255로 두고 입력을 한 줄 단위로 처리하는 것을 의미한다. 이를 Non-Canonical로 바꿀 때는 ICANON과 같은 로컬 플래그VMIN, VTIME과 같은 컨트롤 문자를 주로 이용한다.
Canonical이라는 용어는 규정을 따르는 것을 의미하고, 반대로 Non-Canonical이라는 의미는 규정을 따르지 않는 것을 의미한다.
기존의 터미널 설정 값을 바꾸면서 Non-Canonical로 만들면서 조작하는 플래그들에 대한 추가적인 예시는 아래 링크에서 확인할 수 있다.
termios 구조체의 플래그 설정 값과 TermCap의 쿼리로 사용할 수 있는 값은 아래 링크들을 참고하도록 하자.
#include <ctype.h> #include <stdbool.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <term.h> #include <unistd.h> #define ERROR -1 #define KEY_LEFT 4479771 #define KEY_RIGHT 4414235 #define KEY_UP 4283163 #define KEY_DOWN 4348699 #define KEY_BACKSPACE 127 #define CURPOS "\033[6n" const char *g_cm; const char *g_ce; const char *g_dc; bool init_term(struct termios *t) { if (tcgetattr(STDIN_FILENO, t) == ERROR) return (false); t->c_lflag &= ~(ICANON | ECHO); t->c_cc[VMIN] = 1; t->c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSANOW, t) == ERROR) return (false); return (true); } bool init_query(void) { if (tgetent(NULL, "xterm") == ERROR) return (false); g_cm = tgetstr("cm", NULL); g_ce = tgetstr("ce", NULL); g_dc = tgetstr("dc", NULL); if (!g_cm || !g_ce) return (false); return (true); } int putchar(int c) { if (write(STDOUT_FILENO, &c, 1) == ERROR) return (0); return (1); } bool get_position(int *col, int *row) { int i; int ret; char buf[1024]; if (write(STDOUT_FILENO, CURPOS, strlen(CURPOS)) == ERROR) return (false); ret = read(STDIN_FILENO, buf, 1024); if (ret == ERROR) return (false); buf[ret] = '\0'; i = 0; while (!isdigit(buf[i])) ++i; *row = atoi(&buf[i]) - 1; while (isdigit(buf[i])) ++i; while (!isdigit(buf[i])) ++i; *col = atoi(&buf[i]) - 1; return (true); } bool cur_left(int *col, int *row) { if (*col) { --(*col); if (tputs(tgoto(g_cm, *col, *row), 1, putchar) == ERROR) return (false); } return (true); } bool cur_right(int *col, int *row) { ++(*col); if (tputs(tgoto(g_cm, *col, *row), 1, putchar) == ERROR) return (false); return (true); } bool cur_up(int *col, int *row) { if (*row) { --(*row); if (tputs(tgoto(g_cm, *col, *row), 1, putchar) == ERROR) return (false); } return (true); } bool cur_down(int *col, int *row) { ++(*row); if (tputs(tgoto(g_cm, *col, *row), 1, putchar) == ERROR) return (false); return (true); } bool cur_backspace(int *col, int *row) { if (*col) { --(*col); if (tputs(tgoto(g_cm, *col, *row), 1, putchar) == ERROR) return (false); } if (tputs(g_dc, 1, putchar) == ERROR) return (false); return (true); } bool key_handle(int ch, int *col, int *row) { if (ch == KEY_LEFT) { if (!cur_left(col, row)) return (false); } else if (ch == KEY_RIGHT) { if (!cur_right(col, row)) return (false); } else if (ch == KEY_UP) { if (!cur_up(col, row)) return (false); } else if (ch == KEY_DOWN) { if (!cur_down(col, row)) return (false); } else if (ch == KEY_BACKSPACE) { if (!cur_backspace(col, row)) return (false); } else { ++(*col); if (!putchar(ch)) return (false); } return (true); } bool read_char(void) { int ch; int ret; int col; int row; while (true) { if (!get_position(&col, &row)) return (false); ret = read(STDIN_FILENO, &ch, sizeof(ch)); if (ret == ERROR) return (false); if (!ret) return (true); if (!key_handle(ch, &col, &row)) return (false); ch = 0; } } int main(void) { struct termios t; if (!init_term(&t) || !init_query() || !read_char()) return (1); return (0); }
C
복사
예시 코드에서 얻는 결과는 직접 구동해서 커서나 키보드 값이 정상적으로 작동하는지 확인해보자.
위 코드에서 터미널 상의 커서 위치는 \033[6n이라는 Escape Sequence를 통해서 [row;colR 형태의 값을 얻은 후에 파싱하여 알아낸다. 위 코드를 실행해보면, 실제 쉘과는 사뭇 다른 점을 느낄 수 있다. 터미널 사이즈를 넘어간 키 조작에 대해선 원하는 대로 조작이 안되기도 하고, 입력된 위에 커서를 두고 입력을 받으면 그 값이 덮어씌워지는 것을 확인할 수 있다. 전자에 대해서는 이어서 소개되는 ioctl을 통해 장치에게 요청을 보내어 터미널 사이즈 등을 쉽게 알아내어 해결할 수 있고, 후자의 경우에는 TermCap의 쿼리를 잘 활용하면 된다.

4. on <sys/ioctl.h>, <sys.wait.h>

1) ioctl

1. 의존성

#include <sys/ioctl.h>
C
복사

2. 함수 원형

int ioctl(int fd, unsigned long request, ...);
C
복사

3. 함수 설명

ioctl 함수는 장치에게 요청을 보낼 때 사용되는 함수이며, 시스템 콜이다. <dirent.h>에서 스트림을 설명하면서 Unix의 모든 장치는 추상화되어 파일로써 조작된다고 했었다. 따라서 ioctl 함수를 통해 장치에게 요청을 보낼 때도 파일 조작을 통해서 이뤄지므로, fd는 장치를 참조하는 파일 디스크립터가 된다.
ioctl을 통해 조작하는 대표적인 장치로는 터미널이 있다. 이 때 ioctl의 인자로 넣는 fdopen을 통해서 얻은 파일 디스크립터가 되는데, 간혹 해당 함수의 부작용으로 예기치 못한 결과가 발생할 수 있다. 이에 따라 open을 수행할 때 O_NONBLOCK 플래그도 이용하는 것이 권장된다.
request라는 인자는 fd에 해당하는 장치에게 보낼 장치에서 제공되는 코드이고, 마지막 인자는 특정 메모리 공간을 참조하는 포인터이다. 마지막 인자가 포인터 타입 임에도 가변 인자를 사용한 이유는 함수 원형에서 해당 포인터의 타입을 명시하지 않기 위함이다. 일반적으로 함수 호출 시 마지막 인자의 가변 인자에는 char * 타입으로 이용한다.
함수 원형에서 포인터 타입을 명시하지 않는 행위는 해당 인자가 다양한 포인터 타입으로 이용될 수 있다는 것을 보장하기 위한 것인데, 이와 일맥상통한 포인터 타입은 void *이다. ioctl 함수에서 void *를 이용하지 않은 이유는 단순히 ioctl 함수를 정의할 당시에 void *가 유효하지 않은 타입이었기 때문이다. 최근 몇 시스템에서는 ioctl의 원형을 void *로 이용하는 곳도 있다.
ioctl 함수 수행에 문제가 없다면 0을 반환하고, 그렇지 않다면 -1을 반환한다. request라는 인자가 사용자의 정의 하에 이용되는 경우에는 request에 따른 작업 수행 중에 ioctl 함수의 반환 값을 이용할 수도 있다. 따라서 해당 경우에는 ioctl의 함수 수행에 문제가 없을 때의 반환 값으로 양수를 이용하기도 한다.
ioctl 함수는 장치 드라이버에 따라 인자 및 반환 값 등 Semantic이 다양할 수 있기 때문에 UnixI/O에 해당하는 스트림에는 정교하게 동작하지 않을 수도 있다.
반환 값에서 언급한 대로 request에 대한 작업을 직접 정의를 하는 것이 가능한데, 이는 <sys/ioctl.h>에서 제공해주는 매크로 함수를 통해서 정의할 수 있다. <sys/ioctl> 내에는 <sys/ioccom.h>라는 라이브러리가 있는데, 위 그림과 같이 _IO, _IOR, _IOW, _IOWR 함수들을 통해 장치가 request를 받았을 때의 행위를 정의할 수 있다.
ioctl을 수행하는 명령어는 32 Bit로 이루어져 있고, 이들은 특정 구조를 갖는다. 2 BitR/W 등의 작업 구분, 14 Bitsize, 8 Bitnumber, 8 Bittype을 의미한다. 이들이 곧 제시된 그림에서 _IO(type, number), {_IOR, _IOW, _IOWR}(type, number, size)에 해당된다.
2 Bit에서 사용되는 R/W 등의 작업은 00: None, 01: Write, 10: Read, 11: Read/Write를 의미한다. 또한 size는 매뉴얼에서 정의된 이름인 만큼 특정 시스템에서도 해당 이름을 이용하기도 하는데, 이는 잘못된 명칭이다. size에서 사용하는 값은 sizeof(size)이므로 전체적으로 각 인자를 다시 명명할 필요성이 제시되었다. 따라서 시스템에 따라서 각 인자를 지칭하는 이름이 따로 정해졌으며, Mac OS X에서는 위 그림에서 볼 수 있듯이 size → type (t), number → number (n), type → group (g)로 나타난 것을 볼 수 있다. sizesizeof(size)로 이용된다는 사실에 따라, 구조체 혹은 Legacy 값을 이용하는 경우에는 호환성이 떨어지기 때문에 주의해서 사용해야 한다.
ioctl 함수의 매크로 함수를 이용한 예시는 아래 링크에서 확인하도록하고, 여기서의 제시될 예시는 ioctl 함수와 winsize 구조체를 이용한 터미널 사이즈 확인이다.
오래 전의 시스템에서는 ttysize라는 구조체를 이용했는데, winsize라는 구조체가 생기면서 ttysizeObsolete되었다. 해당 구조체 역시 <sys/ioctl.h> 내에 존재한다. 이 때 winsizews_xpixelws_ypixel은 사용되지 않는 멤버 변수이다.

4. 예시

#include <stdio.h> #include <sys/ioctl.h> #include <unistd.h> int main(void) { struct winsize size; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &size) == -1) return (1); printf("Terminal Row Size:\t%d\n",size.ws_row); printf("Terminal Col Size:\t%d\n",size.ws_col); return (0); }
C
복사

2) wait3

1. 의존성

#include <sys/wait.h>
C
복사

2. 함수 원형

pid_t wait3(int *status, int options, struct rusage *rusage);
C
복사

3. 함수 설명

wait3 함수는 waitpid 혹은 waitid 함수가 나오면서 Obsolete 된 함수이다. 함수의 기능 자체는 waitpid 함수와 동일한데, 그럼에도 wait3 함수를 이해하고 싶다면 아래 링크를 통해 pipex에서의 waitwaitpid에 기술된 내용을 토대로 statusoptions를 우선적으로 이해하는 것을 권한다.
wait3(status, options, rusage)의 호출은 waitpid(-1, status, options)와 동일하다.
pipex 링크를 통해 wait 함수를 보면 알겠지만, 기존의 wait 함수는 waitpid에서의 options를 이용할 수 없었기 때문에 option에 해당하는 특정 기능을 수행하기 위해서 wait3 함수를 이용했었다. wait 함수 대신에 wait3 함수를 이용했던 또 다른 이유로는 rusage라는 구조체 때문인데, rusage라는 인자는 Resource Usage의 약자이며 자원의 사용량을 의미하는 구조체이다. wait3rsuage라는 구조체의 값이 NULL이 아니라면, wait3의 함수를 수행하면서 자식 프로세스의 자원에 대해서 다양한 정보를 rusage에 기록하게 된다. rusage의 멤버 변수들은 위 그림과 같이 유지되는 것을 볼 수 있다.
wait3의 함수가 Obsolete된 이유는 표준화 때문인데, wait3 함수는 BSD 계열 시스템의 시스템 콜에 해당하는데 POSIX에 의해 표준화 되면서 waitpid를 이용하도록 권장되었다. 이 때 waitpid 함수를 잘 살펴보면, wait3waitpid로 대체되면서 option을 이용하는 것은 가능하지만 rusage를 얻는 것이 불가능한 것을 알 수 있다. 이는 함수의 목적을 구분 짓기 위해서 waitpid에서는 rusage를 더 이상 이용하지 않게 된 것인데, 이 때문에 rusagegetrusage라는 함수를 통해서 얻을 수 있게 만들었다. minishell의 경우에는 getrusage가 허용 함수가 아니기 때문에 만일 자원의 사용량을 얻어야 하는 상황이 있다면, 이 때만큼은 wait3 함수를 사용하면 된다.
getrusage에 대해서는 아래 링크에서 보다 자세히 파악할 수 있다.
wait3 함수의 반환 값과 오류 처리는 waitwaitpid와 동일하다. pid_t 타입의 반환 값은 자식 프로세스pid를 의미하고, 함수 수행에 문제가 있거나 자식 프로세스가 시그널에 의해서 종료되면 -1을 반환한다.
주어진 예시에서는 별도의 options를 이용하지 않을 것이고, status를 취득하지도 않을 것이다. 해당 내용은 pipex에서 waitpid를 통해 파악할 수 있으므로, 아래 예시에서는 임의의 자식 프로세스에 대해서 자원 사용량에 대해서 파악하는 코드를 이해해볼 것이다.

4. 예시

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <sys/time.h> int main(void) { struct rusage ru; pid_t pid; pid = fork(); if (pid == -1) return (1); else if (!pid) { printf("Child Process\n"); return (0); } else { wait3(NULL, 0, &ru); printf("Parent Process\n"); printf("=========Resource Usage of Child========\n"); printf("Number of Context Switch (Voluntary)\t%ld\n", ru.ru_nvcsw); printf("Number of Context Switch (Involuntary)\t%ld\n", ru.ru_nivcsw); printf("Number of Page Swap\t%ld\n", ru.ru_nswap); printf("Number of Page Fault\t%ld\n", ru.ru_majflt); printf("Signal Received\t%ld\n", ru.ru_nsignals); } return (0); }
C
복사

3) wait4

1. 의존성

#include <sys/wait.h>
C
복사

2. 함수 원형

pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
C
복사

3. 함수 설명

wait4 함수는 wait3 함수와 마찬가지로 waitpid 혹은 waitid 함수가 나오면서 Obsolete 된 함수이다. 함수의 기능 자체는 waitpid 함수와 동일하며, wait3 함수와의 차이점은 함수의 작업을 어떤 자식 프로세스를 특정할 수 있다는 점이다.
wait4(pid, status, options, rusage)의 호출은 waitpid(pid, status, options)와 동일하다.
wait4 함수는 자식 프로세스를 특정할 수 있다는 점을 제외하고는 모두 wait3 함수와 동일하게 작동하기 때문에 위에서 wait3 함수를 읽는 것을 권한다.
wait4 함수의 반환 값과 오류 처리는 wait3 함수와 마찬가지로 waitwaitpid와 동일하다. pid_t 타입의 반환 값은 자식 프로세스pid를 의미하고, 함수 수행에 문제가 있거나 자식 프로세스가 시그널에 의해서 종료되면 -1을 반환한다.
주어진 예시에서는 별도의 options를 이용하지 않을 것이고, status를 취득하지도 않을 것이다. 해당 내용은 pipex에서 waitpid를 통해 파악할 수 있으므로, 아래 예시에서는 임의의 자식 프로세스에 대해서 자원 사용량에 대해서 파악하는 코드를 이해해볼 것이다.

4. 예시

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <sys/time.h> int main(void) { struct rusage ru; pid_t pid; pid = fork(); if (pid == -1) return (1); else if (!pid) { printf("Child Process\n"); return (0); } else { wait4(pid, NULL, 0, &ru); printf("Parent Process\n"); printf("=========Resource Usage of %d========\n", pid); printf("Number of Context Switch (Voluntary)\t%ld\n", ru.ru_nvcsw); printf("Number of Context Switch (Involuntary)\t%ld\n", ru.ru_nivcsw); printf("Number of Page Swap\t%ld\n", ru.ru_nswap); printf("Number of Page Fault\t%ld\n", ru.ru_majflt); printf("Signal Received\t%ld\n", ru.ru_nsignals); } return (0); }
C
복사

5. on <unistd.h>, <stdlib.h>, <signal.h>

1) getcwd

1. 의존성

#include <unistd.h>
C
복사

2. 함수 원형

char *getcwd(char *buf, size_t size);
C
복사

3. 함수 설명

getcwdgetcwd를 호출한 프로그램이 실행되고 있는 절대 경로를 문자열로 얻게 해주는 함수이다. 절대 경로buf라는 char * 타입의 인자에 기록을 하게 되고, sizebuf의 크기를 의미한다. buf에 기록되는 문자열은 '\0'이라는 널 문자로 끝나기 때문에, size널 문자까지 포함한 크기임을 명심해야 한다. 만일 널 문자까지 고려한 채로 buf에 기록하려는 절대 경로의 문자열 길이가 size를 넘게 되면, NULL을 반환한다. 만일 절대 경로의 기록이 성공적이라면 buf의 주소를 반환한다,
getcwd의 특이한 점으로는 buf의 인자로 NULL을 넣게 되면, 내부적으로 size만큼의 크기로 동적 할당을 받아 해당 공간의 주소를 반환하게 된다. 만일 size 만큼의 크기로 동적 할당을 받아서 절대 경로를 기록하는 것이 불가능하다면, 절대 경로를 기록할 수 있을 만큼의 크기로 동적 할당을 받게 된다. 이 경우에는 동적 할당 받은 공간에 대해서 사용자가 직접 free를 호출해야 한다. 만일 함수 수행에 문제가 있다면, NULL을 반환한다.

4. 예시

#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { char *path; path = getcwd(NULL, 0); if (!path) return (1); printf("%s\n", path); free(path); path = NULL; return (0); }
C
복사

2) chdir

1. 의존성

#include <unistd.h>
C
복사

2. 함수 원형

int chdir(const char *path);
C
복사

3. 함수 설명

chdir 함수는 현재 구동되고 있는 프로그램의 경로를 path로 변경하는데 사용된다. 경로의 변경이 성공적이라면 0을 반환하고, 오류가 발생한다면 -1을 반환한다.

4. 예시

#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { char *path; path = getcwd(NULL, 0); if (!path) return (1); printf("Before:\t%s\n", path); free(path); path = NULL; if (chdir("../") == -1) return (1); path = getcwd(NULL, 0); if (!path) return (1); printf("After:\t%s\n", path); free(path); path = NULL; return (0); }
C
복사

3) isatty

1. 의존성

#include <unistd.h>
C
복사

2. 함수 원형

int isatty(int fd);
C
복사

3. 함수 설명

인자로 받은 fd라는 파일 디스크립터가 터미널을 참조하고 있는지의 여부를 반환하게 된다. 터미널을 참조하고 있다면 1을 반환하고 그렇지 않다면 0을 반환한다.
isatty의 호출로 발생할 수 있는 오류는 EBADF, ENOTTY 2가지인데, 각각에 대해서 사용할 수 없는 fd를 이용하거나 fd가 터미널을 참조하고 있지 않아서 발생하는 값들이다. 0을 반환하는 상황에 대해서는 제시된 2가지 오류에 대해서도 모두 포함되는데, 이 때문에 isatty 호출 전과 후로 errno를 비교하여 오류에 대해서 검출할 필요는 없다. 제시된 2가지 오류들은 터미널을 참조하는지 여부를 판단하는 함수 자체의 목적을 흐리는 것은 아니기 때문이다. 조금 다르게 말하면, 2가지 오류 중 어떤 상황이 되더라도 인자로 받은 fd가 터미널을 참조하지 않는다는 것은 명확한 사실이다.
아래 링크에서 볼 수 있는 IBMisatty 함수의 예시에서도 별도의 오류 검사를 하지 않은 것을 볼 수 있다.

4. 예시

#include <fcntl.h> #include <stdio.h> #include <unistd.h> void censor(int fd, const char *s) { if (isatty(fd)) { if (s) printf("%s is referring to a terminal\n", s); else printf("File Descriptor %d is referring to a terminal\n", fd); } else { if (s) printf("%s is not referring to a terminal\n", s); else printf("File Descriptor %d is not referring to a terminal\n", fd); } } int main(void) { int fd; fd = open("test", O_RDONLY); if (fd < 0) return (1); censor(STDIN_FILENO, "STDIN"); censor(STDOUT_FILENO, "STDOUT"); censor(STDERR_FILENO, "STDERR"); censor(fd, NULL); censor(42, NULL); close(fd); return (0); }
C
복사

4) ttyname

1. 의존성

#include <unistd.h>
C
복사

2. 함수 원형

char *ttyname(int fd);
C
복사

3. 함수 설명

인자로 받은 fd라는 파일 디스크립터가 터미널을 참조하고 있다면, 해당 터미널의 경로를 '\0'라는 널 문자로 종료되는 문자열로 반환한다. 만일 함수 수행에 문제가 있거나 fd가 터미널을 참조하고 있지 않다면, NULL을 반환한다. 반환 받은 문자열은 내부적으로 static 형태로 할당되어 있기 때문에 ttyname의 연이은 호출에 의해 그 값이 덮어쓰일 수 있다. 또한 static 형태로 할당되었으므로 별도의 free 호출은 하지 않아도 된다.

4. 예시

#include <fcntl.h> #include <stdio.h> #include <unistd.h> void censor(int fd, const char *s) { if (isatty(fd)) { if (s) printf("%s is referring to a terminal\n", s); else printf("File Descriptor %d is referring to a terminal\n", fd); } else { if (s) printf("%s is not referring to a terminal\n", s); else printf("File Descriptor %d is not referring to a terminal\n", fd); } printf("TTYNAME:\t%s\n", ttyname(fd)); } int main(void) { int fd; fd = open("test", O_RDONLY); if (fd < 0) return (1); censor(STDIN_FILENO, "STDIN"); censor(STDOUT_FILENO, "STDOUT"); censor(STDERR_FILENO, "STDERR"); censor(fd, NULL); censor(42, NULL); close(fd); return (0); }
C
복사

5) ttyslot

1. 의존성

#include <unistd.h>
C
복사

2. 함수 원형

int ttyslot(void);
C
복사

3. 함수 설명

ttyslot을 호출한 프로그램이 참조하고 있는 터미널의 index를 반환한다. 함수 수행에 문제가 생길 경우 이용하고 있는 시스템에 따라 0 혹은 -1이 반환된다. 반환 받은 값은 터미널에 대한 DB의 엔트리 index로 이용되는데, ttyslot 함수의 경우 Legacy 함수라는 점을 참고하자.

4. 예시

#include <fcntl.h> #include <stdio.h> #include <unistd.h> void censor(int fd, const char *s) { if (isatty(fd)) { if (s) printf("%s is referring to a terminal\n", s); else printf("File Descriptor %d is referring to a terminal\n", fd); } else { if (s) printf("%s is not referring to a terminal\n", s); else printf("File Descriptor %d is not referring to a terminal\n", fd); } printf("TTYNAME:\t%s\n", ttyname(fd)); } int main(void) { int fd; printf("TTYSLOT:\t%d\n", ttyslot()); fd = open("test", O_RDONLY); if (fd < 0) return (1); censor(STDIN_FILENO, "STDIN"); censor(STDOUT_FILENO, "STDOUT"); censor(STDERR_FILENO, "STDERR"); censor(fd, NULL); censor(42, NULL); close(fd); return (0); }
C
복사

6) getenv

1. 의존성

#include <stdlib.h>
C
복사

2. 함수 원형

char *getenv(const char *name);
C
복사

3. 함수 설명

name에 해당하는 환경 변수의 값에 대한 문자열을 반환한다. 만일 환경 변수에 해당하는 값을 찾지 못하거나, 함수 수행에 문제가 생긴 경우에는 NULL을 반환한다. getenv에 의해 참조되고 있는 값들은 내부적으로 static 형태로 할당되어 있기 때문에 free 해서는 안 된다는 점을 명심해야 한다.

4. 예시

#include <stdlib.h> #include <stdio.h> int main(void) { char *term; term = getenv("TERM"); if (!term) return (1); printf("Term Type is %s\n", term); return (0); }
C
복사

7) signal

1. 의존성

#include <signal.h>
C
복사

2. 함수 원형

typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
C
복사

3. 함수 설명

시그널은 일종의 IPC이면서 동시에 Interrupt이다. 시그널은 소프트웨어 측면에서 발생되는 Interrupt라고 부르는데, Philosophers에서 Interrupt에 대한 개념을 다뤘을 때는 Interrupt의 발생 주체가 하드웨어라고 했었다. 이에 따라 시그널이 Interrupt의 정의에 위반되는 것이라 생각할 수도 있지만, 소프트웨어 상에서 ctrl 조합 키를 입력하여 Interrupt를 발생시키는 것은 터미널의 제어를 이용하므로 결과적으로 하드웨어를 이용하는 Interrupt가 된다. 즉, 시그널이라는 소프트웨어 측면의 Interrupt 발생은 일종의 Interrupt Delegation이라 볼 수 있다.
signal 함수는 특정 시그널에 대해 수행할 동작을 정의하는데 사용된다. 일전에 pipex 글에서 시그널에 대한 처리에 대해서 언급한 적이 있다. 시그널의 처리 동작은 SIG_IGN (무시), SIG_DFL (기본 정의), 핸들러 (사용자 정의)로 나뉘는데, 위 그림에서 제시된 각 동작들의 타입들이 동일한 것을 통해 알 수 있듯이 signal 함수를 통해 특정 시그널의 처리 동작을 정할 수 있다.
각 시그널들의 SIG_DFL의 동작은 아래 링크의 Default Action을 통해 확인할 수 있다. signal 함수를 통해 동작을 정의할 수 있는 시그널들은 SIGKILL, SIGSTOP, SIGCONT 등을 제외한 시그널들이다.
signal 함수의 signum은 정의하고자 하는 시그널 번호를 의미하고, handlersighandler_t 타입의 정의하고자 하는 함수를 의미한다. handler가 함수를 의미함에도 함수 포인터 타입이 아닌 것처럼 보일 수 있는데, 이는 sighandler_t 타입의 정의를 따라가보면 의문을 해결할 수 있다. sighandler_tvoid를 반환하고 signum을 받기 위한 int 타입의 인자를 받는 함수를 참조하는 함수 포인터 임을 확인할 수 있다.
signal 함수의 반환 타입을 보면 sighandler_t 인 것을 확인할 수 있는데, 반환 타입에 명시된 것처럼 signal 함수는 특정 핸들러를 반환한다. 이 때 반환되는 핸들러는 signal 함수 호출에 이용된 handler가 아니라, 시그널의 처리 동작이 handler로 정의되기 이전에 정의된 핸들러를 의미한다. 즉, signal 함수의 동작 자체는 특정 시그널의 동작에 대한 정의이지만, 조금 더 정확하게는 특정 시그널의 동작에 대한 정의를 교체하는 것이다. 만일 signal 함수 수행 도중에 문제가 생긴다면, signal 함수는 이전에 등록된 핸들러를 반환하는 것이 아니라 SIG_ERR를 반환하므로 이에 대한 검출이 가능하다.
sighandler_tGNU Extension에서 정의된 타입인데, glibc에는 GNU Extension 이외에도 sig_t와 같은 BSD Extension으로 정의된 타입으로도 signal 함수가 명시되어 있다. 만일 이러한 타입 없이 signal 함수를 표현하면 void ( *signal(int signum, void (*handler)(int)) ) (int); 와 같이 읽기 힘든 구문으로 나타나는 것을 확인할 수 있다.
signal 함수 자체의 문제점도 있는데, 멀티 쓰레딩 환경에서의 signal 함수의 이용은 Unspecified Behavior라는 점과 signal 함수의 이용은 이식성 측면에서 그리 좋은 함수가 아니라는 점이다. 후자에 대해서 조금 더 다뤄보자면, SIG_IGN, SIG_DFL을 제외하고 핸들러를 이용하는 것은 시스템마다 이를 정의한 Semantic이 굉장히 다르다는 것이 가장 큰 이유이다. 예를 들어 과거의 Unix 계열의 시스템에서는 signal 함수를 통해 등록한 핸들러로 시그널이 처리되고 나면, 시그널의 처리 동작은 SIG_DFL로 재설정되도록 되어 있었다. 이에 따라 핸들러 내에 다시 시그널에 대한 핸들러를 정의하는 행위가 요구가 되었고, 핸들러 내에서 signal 함수에 도달하기 전에 발생되는 시그널에 대해서는 시스템 상에서 막지 않아 SIG_DFL로 처리되면서 예기치 못한 결과를 얻는 상황이 발생하게 된 것이다. 물론 일부 Unix 시스템에서만 발생하는 문제이긴 하나 분명 이식성에서는 문제가 되는 함수가 된 것이다.
POSIX에서는 이와 같은 문제를 signal 함수 대신에 sigaction이라는 함수를 이용하도록 만들면서 문제를 해결하게 되었다. sigactionsigaction이라는 구조체를 통해 signal 함수의 기능 뿐만 아니라 더 많은 기능들을 제공하게 되면서, 결과적으로 signal 이라는 함수를 피하고 sigaction 사용을 권장하게 되었다. minishell에서는 signal에서 발생하는 문제점에 크게 해당되지 않을 뿐더러 sigaction 및 이와 관련된 여러 함수들을 허용하지 않기 때문에 signal 함수를 이용해야 하지만, signal 함수 대신에 이용할 수 있는 함수가 있다는 것을 명심해야 한다.
signal 함수를 사용할 때 주의해야 할 점들도 있다. 첫 째는 signal 함수 내에서는 반드시 Async-Signal-Safe 함수들만 이용해야 한다. Async-Signal-Safe한 함수들은 대체적으로 Reentrant가 가능한 함수들이다. Reentrant가 불가능한 대표적인 함수로는 printf가 있는데, printfAsync-Signal-Safe하지 않다. 만일 printf의 호출로 출력 중인 상태에서 시그널을 받아서 시그널 처리 동작에서도 printf를 호출하려고 하면, 원하는 출력 결과가 나오지 않을 수도 있다. printf의 출력 자체는 Buffer Management를 통해 출력을 하게 되므로 내부에 별도로 둔 Buffer를 토대로 메모리에 쓰는 작업을 진행하게 되는데, 제시된 상황에서는 메모리에 쓰는 작업에 이용되는 기존의 Buffer의 내용이 손실될 수 있기 때문이다. 따라서 핸들러 내에서 처리되는 동작들이 문제가 없다는 것을 보장하기 위해선 반드시 Async-Signal-Safe 함수들만 이용되어야 한다. 이와 같은 함수들은 Reentrant가 가능한 대부분의 시스템 콜들에 해당하는데, signal 함수 내에서 이용할 수 있는 함수들은 아래 링크에서 확인할 수 있다.
둘 째로 주의해야 할 점은 kill이나 raise 등과 같은 함수에 의한 시그널이 아닌 SIGFPE, SIGILL, SIGSEGV 등의 시그널에 대해서는 signal 함수를 이용하여 처리 동작을 정의해서는 안 된다는 것이다. 이와 같은 시그널들의 무시 혹은 사용자 정의는 오류 상황에서도 프로그램이 종료되지 않아 문제 상황 속에서 무한히 지속되는 프로그램이 될 수 있기 때문이다.

4. 예시

#include <signal.h> #include <stdbool.h> #include <unistd.h> void handler(int signum) { (void)signum; write(STDOUT_FILENO, "write From Signal\n", 18); } int main(void) { signal(SIGINT, handler); while(true) ; return (0); }
C
복사

6. on <readline/readline.h>, <readline/history.h>

<readline/readline.h><readline/history.h>의 이름을 보면 알 수 있듯, 두 라이브러리는 모두 readline 디렉토리에 속하는 것을 볼 수 있다. Mac OS X에서는 해당 readline이라는 디렉토리가 이미 존재하므로 이를 이용해도 된다.
Unix 계열에서 기본적으로 제공되는 readline 디렉토리를 이용하여 각 함수들을 호출한 뒤 컴파일 해보면, rl_replace_line과 같은 함수의 경우에는 위 그림과 같이 컴파일이 제대로 되지 않는 것을 볼 수 있다. 이는 Unix 계열의 readline은 기본적인 기능만 제공하고, rl_replace_line과 같은 추가적인 기능을 이용하기 위해선 GNU Library가 필요하기 때문이다. GNU Libraryreadline에서 제공하는 함수들은 아래 링크에서 확인할 수 있다.
Mac OS X 버전에 따라서 위 함수 말고도 없는 함수가 존재할 수 있다.
따라서 컴파일 시에 -l, -L, -I 옵션으로 GNU Libraryreadline을 이용할 수 있도록 명시해줘야 한다. 위 그림과 같이 정확한 경로를 명시한 후에야 비로소 문제 없이 컴파일 되는 것을 볼 수 있다. 만일 GNU Library에서 추가적으로 제공하는 readline의 기능들을 이용할 것이 아니라면, 시스템에 내장된 readline을 이용하면서 별도의 컴파일 옵션을 주지 않고 사용하는 것도 가능하다.
Mac OS X에서는 Xcode를 설치하면서 기본적으로 GCC가 함께 설치되기 때문에 GNU Library가 이미 존재하고 있을 가능성이 높지만, 만일 그림과 같이 명시한 GNU Library의 위치를 아예 찾을 수 없다면 별도로 GNU Libraryreadline을 설치해야 한다. 이는 아래에 명시된 명령어를 통해서 쉽게 설치할 수 있다.
brew install readline
위와 같이 GNU Libraryreadline을 이용할 때 주의해야할 점이 있다. GNU Libraryreadline에서는 FILE이라는 구조체를 이용하는데, 해당 구조체는 <stdio.h>내에 존재한다. GNU Library<readline/readline.h><stdio.h>가 포함되어 있다면 좋겠지만, 그렇지 않기 때문에 반드시 GNU Library<readline/readline.h>를 포함하기 전에 <stdio.h>의 포함이 선행되어야 호출하려는 함수들이 FILE 구조체를 적절히 이용할 수 있다.

1) readline

1. 의존성

#include <readline/readline.h>
C
복사

2. 함수 원형

char *readline (const char *prompt);
C
복사

3. 함수 설명

readline 함수 자체는 get_next_line으로 구현한 함수를 표준 입력을 대상으로 한 기능과 비슷하다. 인자로 받는 prompt표준 입력을 받기 이전에 보여줄 문구를 의미한다. prompt의 인자가 널 문자열이거나 NULL이라면 별도로 문구를 출력하지는 않는다. prompt로 이용되는 문자열은 내부적으로 프롬프트 값으로 등록되어 이용된다.
get_next_line과 마찬가지로 반환되는 char * 타입의 문자열은 동적 할당을 받아서 생성된 것이기 때문에, readline을 호출한 사용자의 직접적인 free가 뒷받침 되어야 한다. 사용자로부터 받은 입력에서 개행 문자까지의 문자열을 readline의 반환 값으로 사용하는데, 이 때 반환되는 문자열에서는 개행 문자는 포함되지 않는다. 만일 공백 문자만 입력을 한 후 개행 문자를 입력한 경우에는 널 문자열만을 반환한다.
readline의 반환 값에서 EOF에 대한 처리는 2가지 경우로 나뉜다. EOF를 만났지만 그 전에 처리할 문자열이 없는 경우에는 NULL이 반환되고, EOF를 만났지만 그 전에 처리할 문자열이 있는 경우에는 EOF를 개행 문자처럼 처리하여 적절히 문자열을 반환하도록 동작한다.
readline 함수가 get_next_line 함수와 동일한 것이 아니라 비슷하다고 한 이유는, readline의 기능 중에는 vi 혹은 emacsEditing을 지원하는 기능이 있기 때문이다. 예를 들어 위에서 언급한 것처럼 EOF를 만났을 때 처리할 문자열이 있는 경우를 재현하기 위해선 문자열 입력 후 Ctrl + D를 입력해야 하는데, 해당 경우에는 Ctrl + D를 입력하여도 EOF로써 인식이 안 되는 것을 볼 수 있다. 이는 readlineEditing 방식에 따른 Key Binding을 지원하기 때문이다.
해당 현상을 더 자세히 알아보기 위해 매뉴얼을 확인해보면, 위 그림과 같이 제시된 것을 볼 수 있다. readline에서 EOF를 입력하기 위해선 아무런 문자가 없을 때 Ctrl + D를 입력해야 하며, 문자가 존재할 때의 Ctrl + D는 문자 하나를 지우는 Key Binding으로써 동작한다. 따라서 문자열이 존재할 때 EOF를 보낼 수 있도록 만들기 위해선 기본적으로 정해진 설정 값을 따로 바꿔주어야 한다. 설정 값은 ~/.inputrc에서 변경할 수 있으며, 만일 해당 파일이 존재하지 않는다면 /etc/inputrc에서 변경할 수 있다.
두 파일 모두 존재하지 않는다면 별도로 만들어서 운용해도 된다. inputrc라는 파일은 readline을 초기화하는데 이용되는 파일이며, 해당 파일은 vimrc를 작성하는 것과 굉장히 유사하게 작성할 수 있다.
readline에서 이용할 수 있는 Key Binding이라든가, 언급된 설정 파일 내에서 사용할 수 있는 Directive들은 아래 링크에서 확인하여 설정하는 것이 가능하다. 필요에 따라서 이들을 적절히 이용하면 된다.
readline 함수에서는 특정 Editing을 지원한다고 했으므로, Tab을 이용한 자동 완성 기능을 제공하기도 한다. 해당 자동 완성 기능은 Oh-My-Zsh와 같은 자동 완성 만큼 뛰어난 완성도를 보여주지는 않지만, 기본 쉘의 자동 완성 기능만큼은 보장해준다. 따라서 해당 기능을 Customizing 한다면 완성도 높은 minishell을 만드는 것도 가능할 것이다.

2) rl_on_new_line

1. 의존성

#include <readline/readline.h>
C
복사

2. 함수 원형

int rl_on_new_line(void)
C
복사

3. 함수 설명

rl_on_new_line 함수는 readline 디렉토리 내에서 Update 관련 함수들에게 커서가 개행 문자를 통해 다음 줄로 이동했음을 알려줄 때 이용되는 함수이다. 알림 용도의 함수이므로 직접적으로 rl_on_new_line 함수가 개행 문자를 수행해주지는 않는다. 따라서, rl_on_new_line 함수는 개행 문자 출력 이후에 이용된다.
Update 관련 함수로는 rl_redisplay도 포함된다.
rl_on_new_line의 함수 수행에 문제가 없다면 0을 반환하고, 그렇지 않다면 -1을 반환한다.

3) rl_replace_line

1. 의존성

#include <readline/readline.h>
C
복사

2. 함수 원형

void rl_replace_line(const char *text, int clear_undo)
C
복사

3. 함수 설명

rl_replace_line 함수는 rl_line_buffer라는 변수를 이용한다. readline 디렉토리 내의 함수들은 readline 디렉토리로부터 전역으로 제공되는 변수들을 이용할 수 있는데, 위 그림과 같이 rl_line_buffer도 이들 중 하나이다. 이 때 rl_line_buffer는 사용자가 입력한 문자열을 별도로 유지한다.
readline 디렉토리 내의 함수들이 이용하는 전역 변수에 대해서는 아래 링크에서 확인할 수 있다.
rl_replace_line 함수의 역할은 rl_line_buffer에 입력 받은 내용을 text라는 문자열로 대치하는데 있다. 이 때 clear_undo는 내부적으로 유지 중인 undo_list를 초기화할 지의 여부를 결정 짓는 값이다. clear_undo의 값이 0이라면 초기화하지 않고, 0 이외의 값이라면 undo_list를 초기화한다. rl_replace_line의 반환 값은 따로 없다.
undo_listreadline 함수에서 지원하는 Editing 기능 중 Undo 작업에 이용된다.
rl_replace_line의 사용은 다방면에서 이뤄질 수 있지만, rl_line_bufferFlush하여 초기화하는데 이용할 수도 있다. 특히 해당 경우에는 시그널을 받았을 때의 상황에서 이용될 수 있다. 이에 대한 내용은 예시에서 그 사용법을 정확히 이해할 수 있다.

4) rl_redisplay

1. 의존성

#include <readline/readline.h>
C
복사

2. 함수 원형

void rl_redisplay(void);
C
복사

3. 함수 설명

rl_redisplay 함수 역시 rl_replace_line 함수와 마찬가지로 rl_line_buffer라는 변수를 이용한다. rl_redisplay 함수를 이용하면 사용자가 입력하여 유지 중인 rl_line_buffer의 값을 프롬프트와 함께 출력해준다. 이 때 프롬프트 값은 readline 함수에 prompt로 준 문자열로 이용된다.
프롬프트를 바꾸고 싶다면 readline 디렉토리 내에 존재하는 특정 함수를 통해 바꿀 수 있다. 하지만 minishell에서는 프롬프트 설정 관련한 함수들을 허용하지 않는다는 것을 명심하자.
rl_redisplay 함수에서는 별도로 받는 인자도 없고 별도의 반환 값이 있는 것도 아닌 것을 확인할 수 있다. 이에 따라 readline 함수를 호출하는 일반적인 상황에서는 rl_redisplay를 왜 이용해야 하는지 이해가 잘 안 될 수도 있는데, 주로 시그널을 받았을 때의 상황에서 rl_redisplay를 이용한다. 이에 대한 내용은 예시에서 그 사용법을 정확히 이해할 수 있다.

5) add_history

1. 의존성

#include <readline/history.h>
C
복사

2. 함수 원형

int add_history(const char *line); void add_history(const char *line);
C
복사

3. 함수 설명

add_historyreadline 함수의 기본 동작 중에 사용자가 입력했던 문자열을 다시 얻게 해주는 함수이다. add_history의 인자인 line으로 기재한 문자열은 위와 아래 방향키를 통해서 readline 함수 실행 도중에 다시 불러올 수 있다.
Unix 계열에서 내장된 readline 디렉토리를 이용하는 경우에는 int 타입으로 반환 값을 만드는데, 함수 수행에 문제가 없다면 0을 반환하고, 그렇지 않다면 -1을 반환한다. 만일 Unix 계열에 내장된 readline 디렉토리가 아니라 GNU Libraryreadline 디렉토리를 이용한다면, 이전과 달리 void 타입의 반환 값을 만드는 것을 볼 수 있다. 사용하고 있는 readline의 디렉토리가 어떤 것인지 정확히 확인 후에 반환 값을 이용할 수 있도록 하는 것이 좋다.

6) 예시

readline 디렉토리의 예시는 각 함수 별로 드는 것도 좋겠지만, readline 함수와 add_history 함수만 이용하여 예시를 작성해볼 것이다. 예시에서는 사용자로부터 입력을 받은 문자열을 출력하고, 위와 아래 방향키를 통해 이전에 입력 받았던 문자열을 불러오는 것을 확인할 수 있다. 또한 실제 쉘에서 Ctrl + C를 이용하여 새로운 프롬프트를 출력하는 것처럼, 예시 프로그램에서도 이와 유사하게 동작하도록 rl_on_new_linerl_replace_line, rl_redisplay 함수들을 적절히 이용해볼 것이다.
#include <signal.h> #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <readline/readline.h> #include <readline/history.h> #include <unistd.h> void handler(int signum) { if (signum != SIGINT) return ; write(STDOUT_FILENO, "\n", 1); if (rl_on_new_line() == -1) exit(1); rl_replace_line("", 1); rl_redisplay(); } int main(void) { int ret; char *line; signal(SIGINT, handler); while (true) { line = readline("input> "); if (line) { ret = strcmp(line, "bye"); if (ret) printf("output> %s\n", line); add_history(line); free(line); line = NULL; if (!ret) break ; } else return (1); } return (0); }
C
복사

7. Approach

아쉽게도 꽤나 귀찮은 과제였고, 어서 마치고 싶은 마음이 커서 ㅎㅎㅎ... 간단하게만 기록하겠다.

1) Cooperation

메인 레포지토리는 bigpel66/42-cursus 를 이용하였고, 팀원은 GitHubIssuePull Request 형식으로 코드에 기여했다. Pull Request를 머지하기 이전에는 반드시 GitHub 기능으로 지원하는 코드 리뷰를 진행하는 것을 원칙으로 하였다.
협업 기간이 길지는 않았지만 팀원이 진도를 쫒아오는 공백기 동안 구조를 오래 고민한 덕인지, 프로그램 구조 및 자료 형태의 결정과 분할은 크게 어렵지 않았다.
특히 Makefile의 경우 GNUreadline을 이용해야 하는데 이것이 팀원과의 경로와 다를 수 있고, 클러스터 환경 역시 경로가 다를 수 있다는 문제가 있었다. 이는 현재 레포지토리에서는 지워졌지만 아래와 같은 형태로 작성하여 make MODE=$(NAME) 명령어로 진행할 수 있도록 만들었다.
MODE = EVAL LIB_NAME = readline ifeq ($(MODE), EVAL) LIB_HEADER = /Users/jseo/.brew/Cellar/readline/8.1.1/include/ else ifeq ($(MODE), HYSON) LIB_HEADER = /Users/hyson/.brew/Cellar/readline/8.1.1/include/ else ifeq ($(MODE), JSEO) LIB_HEADER = /usr/local/opt/readline/include/ endif ifeq ($(MODE), EVAL) LIB_FOLDER = /Users/jseo/.brew/Cellar/readline/8.1.1/lib/ else ifeq ($(MODE), HYSON) LIB_FOLDER = /Users/hyson/.brew/Cellar/readline/8.1.1/lib/ else ifeq ($(MODE), JSEO) LIB_FOLDER = /usr/local/opt/readline/lib/ endif %.o : %.c @echo $(YELLOW) "Compiling...\t" $< $(EOC) $(LINE_CLEAR) @$(CC) $(CFLAGS) -I $(HEADER) -o $@ -c $< -I $(LIB_HEADER) $(NAME) : $(OBJ) @echo $(GREEN) "Source files are compiled!\n" $(EOC) @echo $(WHITE) "Building $(NAME) for" $(YELLOW) "Mandatory" $(WHITE) "..." $(EOC) @$(CC) $(CFALGS) -I $(HEADER) -o $(NAME) $(OBJ) -I $(LIB_HEADER) -l $(LIB_NAME) -L $(LIB_FOLDER) @echo $(GREEN) "$(NAME) is created!\n" $(EOC)
Plain Text
복사
Makefile은 일부 발췌한 것이고, 클러스터에서 평가를 받을 때는 brew를 이용하여 readline을 설치 후에 install_name_tool을 이용하여 동적 라이브러리 경로를 잡아주는 식으로 동작시켰다.
install_name_tool 등 동적 라이브러리 이용에 문제가 있다면, 아래 링크에서 install_name_tool을 검색해보자.

2) Implementation

자료 구조를 크게 2개 정도 썼다. 첫 째는 환경 변수 저장을 위한 RB Tree, 둘 째는 명령어 실행을 위한 AS Tree이다.
일전에 push_swap 과제에서도 RB Tree를 이용하여 Set을 구현한적이 있었는데, 개인적으로 많이 아쉬웠던 점이 남아 있었다. Delete 로직이 정상적으로 작동하지 않았던 점이라든가, 가 난무해서 결국에 나중에는 스스로도 알아보기 힘들어서 유지보수가 안 된다는 점이라든가 말이다. 그래서 개인적인 욕심으로 RB Tree를 이용한 점도 있었고, 사실 환경 변수 전체 목록을 출력하는 횟수보다 특정 값을 찾아내는 행위가 더 많을 것으로 생각하여 이를 구현한 것도 있었다. 전체 목록을 출력하는 데에는 리스트로 구현 시 O(n)O(n)이면 되지만, RB Tree는 중복 노드 탐색 때문에 O(nlogn)O(nlogn)을 요구한다. 반면에 특정 노드를 찾을 때는 리스트에서 O(n)O(n)을 요구하지만, RB Tree에서는 O(logn)O(logn)이면 충분하기 때문이다. 따라서 전체 탐색에서 조금 손해를 보더라도 빈도가 더 높은 명령어에서 득을 보기 위해서 RB Tree를 이용하기록 했다.
지금 생각해보면 리스트로도 RB Tree로도 유지해서 전체 탐색에는 리스트를, 특정 노드 탐색에는 RB Tree를 쓰도록 두는 것이 더 나을 수도 있겠다는 생각이 든다.
전체적인 진행은 환경 변수 값으로 대치 (Expand) → 덩어리 나누기 (Tokenize) → AS Tree 형태루 진행이 되는데, 맨데토리 상에서는 문제가 없지만 보너스의 &&, || 등을 고려하면 Tokenize → AS Tree → Expand 순이 엣지 케이스에 걸리지 않는다는 것을 나중에서야 알게 된 점은 정말 아쉬웠다.
AS Tree를 도입한 이유는 다른 쉘들이 이를 이용하고 있다는 점이어서 경험을 해보고 싶었고, 특이 Syntax Parsing에 굉장히 용이하다고 들었기 때문에 분명 추후에 겪을 여러 난해한 엣지 케이스들로부터 대처가 그나마 쉽지 않을까 하는 생각이었기 때문이다.
여러 케이스들 중 RedirectionPipe의 조합에서 결국에 득을 보았다.
코드들 대부분은 재귀 구조를 띄고 있어서 처음에는 구현이 꽤나 까다로웠지만, 시간 지나고 보니 꽤나 괜찮았던 것 같다. 남들 오래 걸리는 리뷰, 디버깅 등에서도 크게 문제가 없었는데, 이는 주석, 단언, 구조에서 득을 봤다고 생각했다.
이번만큼은 팀원과 진행을 하는 과제였기 때문에, 주석 스타일을 정하여 꼼꼼히 기록하도록 규칙을 정했다. 레포지토리에 들어가보면 매 함수마다 주석을 달아두었고, 어떤 목적의 함수인지, 반환 값은 무엇인지, 인자들은 무엇인지 등을 빼놓지 않고 기록하도록 정했다.
단언에 대해서는 assert를 구현하여 이용하였다. 이는 일반적인 코딩에서 이용하는 assert와 최대한 동일하게 구현하여 이용했다. assert를 이용하면서 깔끔한 코드를 만들어내기 위해서 다음과 같은 식으로 로직을 구성하였다.
1.
구현 상 동적할당 횟수가 꽤 되는 편인데, 매 순간마다 이를 검사하고 bool로 반환 받고 fallback을 하는 과정이 바람직하지는 않다고 생각이 되었다.
2.
전반적인 로직들은 NULL Safety를 보장하도록 (NULL에 안전하게 동작하도록) 만들었다.
3.
다음 로직에 요구되는 값들의 경우, 이전 로직에서 NULL이 나왔을 때 이를 검증하는 식으로 assert를 이용하였다.
따라서 아래은 코드에서 구현 혹은 디버깅에서 문제가 생기면 mini_assert 함수에서 프로그램을 종료 시키고 어떤 파일, 어떤 라인, 어떤 함수에서 문제가 발생했는지 알려주어 꽤나 많은 시간을 절약했다.
/* ** loop () - Main Runtime Function of Minishell ** ** return - void ** input - Variable for a User Input ** chunks - Variable for Tokens of User Input ** syntax - Variable for a Syntax Tree from Chunks ** envmap - Variable for Maps the Environment Variables */ void loop(char *input, t_lst *chunks, t_as *syntax, t_rb *envmap) { while (true) { jfree((void **)(&input)); input = readline(get_value(envmap, "PS1")); if (input == NULL) { jputendl("exit", STDOUT_FILENO); exit(VALID); } if (!jstrlen(input) || empty(input)) continue ; add_history(input); if (!quotes(input) && set_rl(input, QUOTES, STDERR_FILENO, false)) continue ; input = expand(input, envmap, false); mini_assert(input != NULL, \ MASSERT "(input != NULL), " LOOP MLOOP_FILE "line 60."); tokenize(input, &chunks); mini_assert(chunks != NULL, \ MASSERT "(chunks != NULL), " LOOP MLOOP_FILE "line 64."); execute(chunks, syntax, envmap); jlstclear(&chunks, jfree); } }
C
복사
assert는 다음과 같이 구현 되었다.
/* ** mini_assert () - Assert Whether Condition True or False ** ** return - void ** condition - Condition to Check ** context - Context Information in Runtime */ void mini_assert(bool condition, char *context) { if (condition) return ; jputendl(context, STDERR_FILENO); exit(GENERAL); }
C
복사
특히 프로그램 구조에 대해서는 최대한 의존도를 낮추고, 각각의 기능들을 라이브러리처럼 이용할 수 있도록 고민을 많이 했다. 이 덕분에 협업 시 서로의 의존도를 많이 낮추는데 도움이 되었다. 이는 구현에 필요한 것들을 사전에 정의해보고 의사 코드를 많이 작성해봤기 때문에 가능했던 것 같다. 이에 따라 우리 팀은 아래 사진과 같이 5개의 include 들을 이용하였다.
추가적으로는 구현한 내용들을 직접 시각적으로 볼 수 있도록 하는 것이 큰 도움이 될 것 같아서, 위의 각 기능들을 구현하면서 디버깅 함수들을 여럿 만들어두었다. 따라서 명령어를 하나 치더라도, 눈으로 따라가며 팀에서 만들어낸 구조에 문제가 있는지 없는지 지속적으로 확인하며 구현하는 것이 가능했다. 덕분에 엣지 케이스에서 문제가 생기더라도 확인이 빨랐으며, AS Tree 덕분에 이를 고치는 것도 유연하게 대처할 수 있었다.
비록 아쉬운 점도 몇 있었고, 구현하면서 어려웠던 점들도 많았지만 충분히 좋은 협업이었고 Inner Circle의 마지막 C 언어 코딩으로는 썩 나쁘지 않았던 것 같다. GitHub Issue + Pull Request + Code Review는 정말 재밌는 기능이었다.
정리하면서 stat, lstat, fstat 얘기들이 빠진 것을 뒤늦게 확인했는데, 이들은 그렇게 어렵지 않으므로 직접 찾아보길 권한다 ㅎㅎ...

8. Reference