上一篇博客我分析了linenoise中的數據結構linenoiseCompletions和abuf,這篇我分析一下linenoise的歷史命令取回和linenoiseState結構體。linenoiseState結構體即是這個庫核心的部分,起到控制當前用戶輸入狀態的功能。而history部分並不複雜,只是僅僅操作一個char** history數組,這個數組是固定大小的“雙端隊列”。
一、history模塊:這部分功能都寫在linenoise.c文件,以下是幾個主要的全局變量,用於控制:
static int history_max_len = LINENOISE_DEFAULT_HISTORY_MAX_LEN;
static int history_len = 0;
static char **history = NULL;
history_max_len,所能記錄的history條數; history_len ,目前history變量中裝填的history數量; history, 用於歷史記錄存儲,每個歷史記錄是一個char*。
history提供四個導出函數,一個內部函數:
int linenoiseHistoryAdd(const char *line);
int linenoiseHistorySetMaxLen(int len);
int linenoiseHistorySave(const char *filename);
int linenoiseHistoryLoad(const char *filename);
static void freeHistory(void);
單看名字應該能猜到這些函數的用途了。
(1)linenoiseHistoryAdd ():將字符串存入history中,這其中說一點,history變量的初始化是通過Load函數或者Add函數完成的,當用戶有history文件時,調用Load函數直接加載到history變量,history即完成了初始化,並且填充了元素。若用戶沒有調用Load函數,在使用Add時,會檢測history是否已經初始化,若沒有初始化,則malloc(),元素個數就是history_max_len。接着完成添加任務,上一次的歷史若與這一次欲添加的歷史相同,則直接返回,不添加。若不同,檢測history是否已經裝滿,若裝滿,將history[0]釋放,用memmove騰出最後一個元素空間,把欲添加的字符串strdup()後賦值。
/* This is the API call to add a new entry in the linenoise history.
* It uses a fixed array of char pointers that are shifted (memmoved)
* when the history max length is reached in order to remove the older
* entry and make room for the new one, so it is not exactly suitable for huge
* histories, but will work well for a few hundred of entries.
*
* Using a circular buffer is smarter, but a bit more complex to handle. */
int linenoiseHistoryAdd(const char *line) {
char *linecopy;
if (history_max_len == 0) return 0;
/* Initialization on first call. */
if (history == NULL) {
history = malloc(sizeof(char*)*history_max_len);
if (history == NULL) return 0;
memset(history,0,(sizeof(char*)*history_max_len));
}
/* Don't add duplicated lines. */
if (history_len && !strcmp(history[history_len-1], line)) return 0;
/* Add an heap allocated copy of the line in the history.
* If we reached the max length, remove the older line. */
linecopy = strdup(line);
if (!linecopy) return 0;
if (history_len == history_max_len) { // no free space.
free(history[0]);
memmove(history,history+1,sizeof(char*)*(history_max_len-1));
history_len--;
}
history[history_len] = linecopy;
history_len++;
return 1;
}
(2)int linenoiseSetMaxLen (int len):此函數用於設置變量history_max_len,當然,設置後需要擴展或收縮history指向的char*數組。這裏先新malloc()一個空間,之後將隊列中的元素複製到新的空間,再用這個空間賦值給history。 if (len < 1) return 0;
if (history) {
int tocopy = history_len;
new = malloc(sizeof(char*)*len);
if (new == NULL) return 0;
/* If we can't copy everything, free the elements we'll not use. */
if (len < tocopy) {
int j;
for (j = 0; j < tocopy-len; j++) free(history[j]);
tocopy = len;
}
memset(new,0,sizeof(char*)*len);
memcpy(new,history+(history_len-tocopy), sizeof(char*)*tocopy);
free(history);
history = new;
}
代碼中注意判斷len < tocopy的塊,若新設置的len小於原來的history_len大小,則無法將原來的history全部複製到history數組,只能截取一部分複製。設計者的處理方式是將最近的歷史保留下來,即是將“隊尾”(history末尾len個元素)的元素複製到新分配的內存空間中。(3)int linenoiseHistorySave (const char* filename):將history中的數據存入文件中,成功則返回0,失敗返回-1。函數很簡單,直接調用open打開文件並for 寫入history數據。
(4)int linenoiseHistoryLoad (const char* filename); 加載歷史數據到history,也是用open () 打開文件,並用循環調用了linenoiseHistoryAdd (),所以可以看出,其實真正的history內存分配只有一個,就是linenoiseHistoryAdd () 函數。
二、linenoiseState結構體:
/* The linenoiseState structure represents the state during line editing.
* We pass this state to functions implementing specific editing
* functionalities. */
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. */
};
這個結構體並不對外導出。作者給了一個example.c,其中只是調用了一個入口函數linenoise(),循環接收用戶的輸入,當用戶回車時,這個函數返回,並返回用戶輸入的字符串命令。
50 while((line = linenoise ("hello> ")) != NULL)
在linenoise() 中,將調用linenoiseRaw (),而linenoiseRaw () 再調用linenoiseEdit () 。一個linenoiseState實體將在這個函數中定義,當然是分配到棧上。換言之,當進行一次while循環時,用一個linenoiseState控制這次循環,每次循環這個linenoiseState都是新的。我們先從這三個函數看起。
(1)char* linenoise (const char* prompt); 這是linenoise庫的核心函數,其中先進行了terminal支持性判斷,根據判斷做相應的操作。這裏linenoise不支持以下幾種終端類型:
static char *unsupported_term[] = {"dumb","cons25","emacs",NULL};
可以通過在terminal執行命令 echo $TERM, 打印這個環境變量來檢測當前是什麼terminal。我的Kali2系統使用的是xterm。若是linenoise不支持的終端,則那些終端快捷鍵將無法使用,只能等待用戶輸入,並將整行命令返回。
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);
}
此處通過調用isUnsupportedTerm()檢測,具體是遍歷數組unsupported_term,不做說明。
若是支持的terminal,則繼續調用linenoiseRaw(), 這個函數將用戶的輸入存入buf,並返回count,命令字串的長度。返回時使用了strdup(),所以每次while 循環linenoise()後,都要進行free處理。此處也可以進行改進,比如linenoise函數可以接收一個存儲緩衝區,while運行單線程,這樣即可只分配一次內存,長期使用了,不用每次都調用strdup和free。
(2) linenoiseRaw 函數:
/* 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);
在這個函數中,主要檢測是否STDIN_FILENO是否是tty,若不是,則將stdin按文件或管道讀取,否則,調用linenoiseEdit (),這個函數是與用戶交互的真正入口。
/* Interactive editing. */
if (enableRawMode(STDIN_FILENO) == -1) return -1;
count = linenoiseEdit(STDIN_FILENO, STDOUT_FILENO, buf, buflen, prompt);
disableRawMode(STDIN_FILENO);
printf("\n");
先開啓RawMode,再調用linenoiseEdit (),最後關閉disableRawMode.而在linenoiseEdit函數中,將存在一個while循環,用於接收用戶的每個字符輸入,除非接收到enter鍵、其他字符,或者ctrl+c,退出循環。解讀每一個字符,使用的跳轉是switch。以下列出了linenoise庫支持的終端快捷鍵,在這個switch中都有跳轉,很多直接實現成函數,主要是調整state的pos,len變量,並調用refreshLine()刷新state結構體:
a)光標移動到行尾,end鍵或者esc 0 F
b)光標移動到行首,home鍵或者esc 0 H
c)光標左移動,方向鍵<-或者esc [ D
d)光標右移動,方向鍵->或者esc [ C
e)enter鍵
f)ctrl + c,中斷
g)backspace,退格鍵
h)ctrl+h,同backspace
i)ctrl+D,同delete鍵
j)ctrl+t,交換光標和光標前一個字符的位置。
k)ctrl +b,光標左移一格
l)ctrl+f,光標右移一格
m)ctrl+p,取回上一個歷史命令
n)ctrl+n,取回下一個歷史命令
o)esc [ 3 ~,同delete鍵
p)ctrl+u,刪除整行
q)ctrl+k,刪除從當前光標到行尾的所有字符
r)ctrl+a,同home
s)ctrl+e,同end
t)ctrl+l,清屏,同終端命令clean
u)ctrl+w,刪除前一個單詞
此處不一一列舉出來了。