--- tags: 研讀筆記, trace, C --- # 研讀筆記 linenoise [antirez/linenoise](https://github.com/antirez/linenoise) #### `linenoise` 這個 function 是整個程式主要的 API,一開始會檢查是否是 `tty`,然後在看終端機是否有支援,有支援了話就會呼叫 `linenoiseRaw`。 ```cpp= char *linenoise(const char *prompt) { char buf[LINENOISE_MAX_LINE]; int count; if (!isatty(STDIN_FILENO)) { /* Not a tty: read from file / pipe. In this mode we don't want any * limit to the line size, so we call a function to handle that. */ return linenoiseNoTTY(); } else if (isUnsupportedTerm()) { size_t len; printf("%s",prompt); fflush(stdout); if (fgets(buf,LINENOISE_MAX_LINE,stdin) == NULL) return NULL; len = strlen(buf); while(len && (buf[len-1] == '\n' || buf[len-1] == '\r')) { len--; buf[len] = '\0'; } return strdup(buf); } else { count = linenoiseRaw(buf,LINENOISE_MAX_LINE,prompt); if (count == -1) return NULL; return strdup(buf); } } ``` #### `linenoiseRaw` 這個函式會先呼叫 `enableRawMode`,然後呼叫 `linenoiseEdit` 進入編輯模式,最後會呼叫 `disableRawMode`。 ```cpp= /* This function calls the line editing function linenoiseEdit() using * the STDIN file descriptor set in raw mode. */ static int linenoiseRaw(char *buf, size_t buflen, const char *prompt) { int count; if (buflen == 0) { errno = EINVAL; return -1; } if (enableRawMode(STDIN_FILENO) == -1) return -1; count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt); disableRawMode(STDIN_FILENO); printf("\n"); return count; } ``` #### `enableRawMode` 這邊要先了解到 [Terminal mode](https://en.wikipedia.org/wiki/Terminal_mode),不同的模式下面會決定終端機要如何轉換(interpreted)字元。 - cooked mode 又稱 canonical mode,一般 linux terminal 的模式,在 data 傳入程式之前會先被預處理。 - raw mode 又稱 non-canonical mode,會直接把 data 傳到執行程式,不會對任何特殊字元預先做轉換。 一般的終端機是 line-based system,輸入的字元會被放到 buffer 直到收到 carriage return (Enter or Return),這個叫做 cooked,會允許特殊字元被先處理,可以看 stty(1) 像是 Ctrl+D, Ctrl+S, Backspace 這些都是,終端機的驅動會先 "cooks" 處理這些字元。 再來看看 `termios` 這個結構,是用來設定終端機的屬性,這邊會先用 `tcgetattr` 拿到原本終端機的屬性並且保存到 `orig_termios`,最後在 `disableRawMode` 的時候透過 `tcsetattr` 把 `orig_termios` 設定回去。 ```cpp= static int enableRawMode(int fd) { struct termios raw; if (!isatty(STDIN_FILENO)) goto fatal; if (!atexit_registered) { atexit(linenoiseAtExit); atexit_registered = 1; } if (tcgetattr(fd,&orig_termios) == -1) goto fatal; raw = orig_termios; /* modify the original mode */ /* input modes: no break, no CR to NL, no parity check, no strip char, * no start/stop output control. */ raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); /* output modes - disable post processing */ raw.c_oflag &= ~(OPOST); /* control modes - set 8 bit chars */ raw.c_cflag |= (CS8); /* local modes - choing off, canonical off, no extended functions, * no signal chars (^Z,^C) */ raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); /* control chars - set return condition: min number of bytes and timer. * We want read to return every single byte, without timeout. */ raw.c_cc[VMIN] = 1; raw.c_cc[VTIME] = 0; /* 1 byte, no timer */ /* put terminal in raw mode after flushing */ if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; rawmode = 1; return 0; fatal: errno = ENOTTY; return -1; } ``` #### `linenoiseEdit` 這邊會用到一個結構 `linenoiseState` 用來記錄下一些在命令列編輯時的特定資訊,一開始會先對這些結構初始化。 ```cpp= struct linenoiseState { int ifd; /* Terminal stdin file descriptor. */ int ofd; /* Terminal stdout file descriptor. */ char *buf; /* Edited line buffer. */ size_t buflen; /* Edited line buffer size. */ const char *prompt; /* Prompt to display. */ size_t plen; /* Prompt length. */ size_t pos; /* Current cursor position. */ size_t oldpos; /* Previous refresh cursor position. */ size_t len; /* Current edited line length. */ size_t cols; /* Number of columns in terminal. */ size_t maxrows; /* Maximum num of rows used so far (multiline mode) */ int history_index; /* The history index we are currently editing. */ }; ``` 這個主要就是用在編輯命令列上的文字,可以看到第 5 行這邊是先把 prompt 的文字寫到終端機,也就是 `hello>`。接著進入迴圈,每次迴圈第 10 行都會去 `read` 1 個字元放到變數 `c`,下面就會對這個字元做對應的動作。 第 12 行先判斷 `c == 9` 在 ASCII 當中 9 代表 `tab`,所以這邊如果是使用者輸入 tab,如果有註冊 autocomplete 的 callback function 就會執行 auto complete,呼叫 `completeLine`。 `completeLine` 做完後會回傳最後一個收到的字元,再往下看到 `switch`,會對應不同的輸入進行不同的行為,如果是一般的輸入字元的話會呼叫 `linenoiseEditInsert`,插入一個字元到游標目前位置,其他對應的動作如下。 - `linenoiseEditBackspace` - `linenoiseEditDelete` - `linenoiseEditMoveLeft` - `linenoiseEditMoveRight` - `linenoiseEditMoveHome` - `linenoiseEditMoveEnd` - `linenoiseEditHistoryNext` - `linenoiseClearScreen` ```cpp= static int linenoiseEdit(int stdin_fd, int stdout_fd, char *buf, size_t buflen, const char *prompt) { struct linenoiseState l; ... if (write(l.ofd,prompt,l.plen) == -1) return -1; while(1) { char c; int nread; char seq[3]; nread = read(l.ifd,&c,1); if (nread <= 0) return l.len; if (c == 9 && completionCallback != NULL) { c = completeLine(&l); /* Return on errors */ if (c < 0) return l.len; /* Read next character when 0 */ if (c == 0) continue; } switch(c) { ... } } return l.len; ``` #### `completeLine` 第 6 行會呼叫 `completionCallback` 也就是一開始註冊的 callback function,可以先看到 `example.c` 第 45 行,有先透過 `linenoiseSetCompletionCallback` 先註冊好 callback function `completion`,所以在這邊呼叫 `completionCallback` 就是呼叫 `completion`。 第 7 行開始判斷 `lc` 是否為 0,如果是 0 代表現在輸入的這個 buffer 沒有 match 到任何 completion 的規則,就會呼叫 `linenoiseBeep`。 如果有的話就會進入到 `while` 迴圈當中,這邊就會根據輸入的字元做對應的動作,按一次 `tab` 就會去 `lc.cvec` 裡面的字串做 auto complete,如果還有就會繼續補下去,`lc.cvec` 裡面的全部做完會回到 `i` 會回到 0,就會是原本的輸入。 如果輸入是 `escape` 或是其他字元,`while` 迴圈就會停止,並且把最後收到的字元 `return`。 ```cpp= void completion(const char *buf, linenoiseCompletions *lc) { if (buf[0] == 'h') { linenoiseAddCompletion(lc,"hello"); linenoiseAddCompletion(lc,"hello there"); } } ``` ```cpp= static int completeLine(struct linenoiseState *ls) { linenoiseCompletions lc = { 0, NULL }; int nread, nwritten; char c = 0; completionCallback(ls->buf,&lc); if (lc.len == 0) { linenoiseBeep(); } else { size_t stop = 0, i = 0; while(!stop) { /* Show completion or original buffer */ if (i < lc.len) { struct linenoiseState saved = *ls; ls->len = ls->pos = strlen(lc.cvec[i]); ls->buf = lc.cvec[i]; refreshLine(ls); ls->len = saved.len; ls->pos = saved.pos; ls->buf = saved.buf; } else { refreshLine(ls); } nread = read(ls->ifd,&c,1); if (nread <= 0) { freeCompletions(&lc); return -1; } switch(c) { case 9: /* tab */ i = (i+1) % (lc.len+1); if (i == lc.len) linenoiseBeep(); break; case 27: /* escape */ /* Re-show original buffer */ if (i < lc.len) refreshLine(ls); stop = 1; break; default: /* Update buffer and return */ if (i < lc.len) { nwritten = snprintf(ls->buf,ls->buflen,"%s",lc.cvec[i]); ls->len = ls->pos = nwritten; } stop = 1; break; } } } freeCompletions(&lc); return c; /* Return last read character */ } ``` #### `linenoiseAddCompletion` 這個函式式用來建立 auto complete 的內容,會透過剛剛在 `completion` 當中呼叫給定的字串來建立。這邊用到一個結構 `linenoiseCompletions`,變數 `cvec` 是指標的指標,用來把拿到的字串存起來,所以這邊開頭為 `h` 的輸入就會有 "hello"、"hello there" 兩個 auto complete。 ```cpp= typedef struct linenoiseCompletions { size_t len; char **cvec; } linenoiseCompletions; ``` ```cpp= void linenoiseAddCompletion(linenoiseCompletions *lc, const char *str) { size_t len = strlen(str); char *copy, **cvec; copy = malloc(len+1); if (copy == NULL) return; memcpy(copy,str,len+1); cvec = realloc(lc->cvec,sizeof(char*)*(lc->len+1)); if (cvec == NULL) { free(copy); return; } lc->cvec = cvec; lc->cvec[lc->len++] = copy; } ``` #### `refreshLine` 這個是用來更新目前的 buffer 內容、游標位置到正確的位置。 有兩種模式 `MultiLine`、`SingleLine`,可以在一開始的時候指定,如果沒有指定了話會是 SingleLine mode,所以這邊就會進到 `refreshSingleLine`。 ```cpp= struct abuf { char *b; int len; }; static void refreshSingleLine(struct linenoiseState *l) { char seq[64]; size_t plen = strlen(l->prompt); int fd = l->ofd; char *buf = l->buf; size_t len = l->len; size_t pos = l->pos; struct abuf ab; while((plen+pos) >= l->cols) { buf++; len--; pos--; } while (plen+len > l->cols) { len--; } abInit(&ab); /* Cursor to left edge */ snprintf(seq,64,"\r"); abAppend(&ab,seq,strlen(seq)); /* Write the prompt and the current buffer content */ abAppend(&ab,l->prompt,strlen(l->prompt)); if (maskmode == 1) { while (len--) abAppend(&ab,"*",1); } else { abAppend(&ab,buf,len); } /* Show hits if any. */ refreshShowHints(&ab,l,plen); /* Erase to right */ snprintf(seq,64,"\x1b[0K"); abAppend(&ab,seq,strlen(seq)); /* Move cursor to original position. */ snprintf(seq,64,"\r\x1b[%dC", (int)(pos+plen)); abAppend(&ab,seq,strlen(seq)); if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ abFree(&ab); } ``` 這邊有用到一個結構 `abuf` 是用來 append buffer,目的是要一次更新完到終端機上,避免文字在螢幕上閃爍。 這邊還要先了解到 [ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code) 是一種制定的標準,還可以搭配 [VT100、ANSI 碼說明](https://www.csie.ntu.edu.tw/~r88009/Java/html/Network/vt100.htm)。用來控制終端機的游標位置、顏色等,大多數已 `ESC` 跳脫字元和 `[` 字元開始,後面跟著參數,可以看 wiki 的說明如下。 > The general format for an ANSI-compliant escape sequence is defined by ANSI X3.41 (equivalent to ECMA-35 or ISO/IEC 2022). The ESC (27 / hex 0x1B / oct 033) is followed by zero or more intermediate "I" bytes between hex 0x20 and 0x2F inclusive, followed by a final "F" byte between 0x30 and 0x7E inclusive 一般的格式是由 `ESC`(二進位 27 十六進為 0x1B) 開頭,第二個位元則是 0x40–0x5F (ASCII `@A–Z[\]^_`) 範圍內的字元,最後一個則是範圍 0x30-0x7E。 ```c snprintf(seq,64,"\r"); abAppend(&ab,seq,strlen(seq)); ``` 這邊一開始先用 `\r` carriage return 把游標位置回到最左邊。 ```c abAppend(&ab,l->prompt,strlen(l->prompt)); abAppend(&ab,buf,len); ``` 這邊是把 prompt 和原本的 buffer 內容寫回去。 ```c snprintf(seq,64,"\x1b[0K"); abAppend(&ab,seq,strlen(seq)); ``` 這邊的 `0K` 對應到的動作是 EL (Erase in Line),n 為 0 的時候會從游標的位置開始清除到最後行尾。 ```c snprintf(seq,64,"\r\x1b[%dC", (int)(pos+plen)); abAppend(&ab,seq,strlen(seq)); ``` 這邊則是先把游標先移到最左,這邊的動作是 CUF (Cursor Forward),是把游標向右移動 n 個,然後 `%d` 這邊是吃 `pos+plen`,也就是從最左邊移動到原本的游標位置。 ```c if (write(fd,ab.b,ab.len) == -1) {} /* Can't recover from write error. */ ``` 最後一併 write 到終端機輸出。 #### `refreshShowHints` 這邊會呼叫到 `hintsCallback`,這個 callback function 也是在一開始就指定的,所以對應 `example.c` 的 function 就是 `hints`,會設定好顏色、粗細,回傳 hint 的字串。 ```cpp= char *hints(const char *buf, int *color, int *bold) { if (!strcasecmp(buf,"hello")) { *color = 35; *bold = 0; return " World"; } return NULL; } ``` ```cpp= void refreshShowHints(struct abuf *ab, struct linenoiseState *l, int plen) { char seq[64]; if (hintsCallback && plen+l->len < l->cols) { int color = -1, bold = 0; char *hint = hintsCallback(l->buf,&color,&bold); if (hint) { int hintlen = strlen(hint); int hintmaxlen = l->cols-(plen+l->len); if (hintlen > hintmaxlen) hintlen = hintmaxlen; if (bold == 1 && color == -1) color = 37; if (color != -1 || bold != 0) snprintf(seq,64,"\033[%d;%d;49m",bold,color); else seq[0] = '\0'; abAppend(ab,seq,strlen(seq)); abAppend(ab,hint,hintlen); if (color != -1 || bold != 0) abAppend(ab,"\033[0m",4); /* Call the function to free the hint returned. */ if (freeHintsCallback) freeHintsCallback(hint); } } } ``` #### `linenoiseHistory` 相關 這是為了記錄下每次出入的指令,作法是用指標的指標 `history` 記錄下來每次輸入的字串。 ```cpp= static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN; static int history_len = 0; static char **history = NULL; ``` 最一開始啟動的時候會呼叫 `linenoiseHistoryLoad`,去從實體檔案當中將內容讀出來到記憶體, ```cpp= linenoiseHistoryLoad("history.txt"); /* Load the history at startup */ ``` 之後在每次輸入完指令都會呼叫 `linenoiseHistoryAdd` 新增一筆,然後用 `linenoiseHistorySave` 存到實體檔案中。 ```cpp= while((line = linenoise("hello> ")) != NULL) { /* Do something with the string. */ if (line[0] != '\0' && line[0] != '/') { printf("echo: '%s'\n", line); linenoiseHistoryAdd(line); /* Add to the history. */ linenoiseHistorySave("history.txt"); /* Save the history on disk. */ } else if (!strncmp(line,"/historylen",11)) { /* The "/historylen" command will change the history len. */ int len = atoi(line+11); linenoiseHistorySetMaxLen(len); } else if (!strncmp(line, "/mask", 5)) { linenoiseMaskModeEnable(); } else if (!strncmp(line, "/unmask", 7)) { linenoiseMaskModeDisable(); } else if (line[0] == '/') { printf("Unreconized command: %s\n", line); } free(line); } ``` 然後透過 `linenoiseEditHistoryNext` 把當前的輸入改成上一個或下一個 `history` 的內容。