linenoise 源碼分析(二)

上一篇博客我分析了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,刪除前一個單詞

此處不一一列舉出來了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章