Программирование ARM Linenoise: библиотека для редактирования строки текста Tue, July 01 2025  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Linenoise: библиотека для редактирования строки текста Печать
Добавил(а) microsin   

Библиотека linenoise [1] представляет собой простую, без сложной конфигурации, лицензируемую как BSD, замену функций библиотеки readline. Linenoise используется в Redis, MongoDB, Android и многих других проектах для обработки интерфейса командной строки. Например, ESP-IDF SDK использует связку argrable3 + linenoise для реализации компонента консоли пользователя [2].

В библиотеке linenoise реализованы функции:

• Однострочный и многострочный режимы редактирования строки с реализованными обычными привязками клавиш (key bindings).
• Поддержка истории.
• Автозавершение.
• Подсказки (hints, предложения рекомендации ввода справа от приглашения ввода (промпта), когда вы вводите команду).
• Режим мультиплексирования, когда промпт прячется/восстанавливается для асинхронного вывода.
• Библиотека занимает около ~850 строк исходного кода (исключая строки комментария и пустые строки), лицензируемого как BSD.
• Используется только подмножество esc-кодов VT100 (совместимое с ANSI.SYS).

Редактирование строки с определенной поддержкой истории команд это действительно важная фича для утилит обработки командной строки пользователя. Вместо того, чтобы заново вводить то, что уже было введено ранее, можно просто использовать клавиши 'стрелка вверх' и 'стрелка вниз' для прокрутки истории, выбора нужной введенной команды, исправлять ошибки синтаксиса или попробовать ввести немного измененную предыдущую команду. Однако код, имеющий дело с терминалами, является своего рода Черной Магией: readline занимает 30k строк кода, libedit 20k. Кажется неразумным привязывать небольшие утилиты к огромным библиотекам, чтобы просто получить минимальную поддержку редактирования строки (что особенно актуально для маломощных встраиваемых систем).

Так что обычно происходит одно из двух:

• Большие программы со скриптами configure, запрещающие редактирование строки, если readline нет в системе, или её поддержка по каким-то причинам невозможна, поскольку readline лицензируется под GPL, а библиотека libedit (её клон BSD) не настолько известна и доступна, как readline (реальный пример такой проблемы: Tclsh).
• Программы поменьше, не использующие скрипт configure, в которых вообще нет редактирования строки (пример: redis-cli).

Как результат - выпуск программ без полезного функционала редактирования строки. Библиотека linenoise [1] во многом решает эту проблему.

Esc-последовательности VT100. Вероятно практически любой терминал, который вы можете использовать сегодня, имеет какую-то поддержку ESC-кодов VT100. Автор библиотеки linenoise постарался сохранить совместимость с самыми часто используемыми ESC-кодами, в результате получилось обеспечить совместимость с терминалами ANSI.SYS, но без поддержки кодов VT220. Библиотека linenoise тестировалась в следующих условиях:

• Консоль Linux, только текст ($TERM = linux)
• Приложение терминала Linux KDE ($TERM = xterm)
• Linux xterm ($TERM = xterm)
• Linux Buildroot ($TERM = vt100)
• Mac OS X iTerm ($TERM = xterm)
• Mac OS X default Terminal.app ($TERM = xterm)
• OpenBSD 4.5 через OSX Terminal.app ($TERM = screen)
• IBM AIX 6.1
• FreeBSD xterm ($TERM = xterm)
• ANSI.SYS
• Emacs comint mode ($TERM = dumb)

[API linenoise]

Linenoise очень проста в использовании, и поставляемый с ней код example.c должен помочь для быстрого внедрения библиотеки. Можно начать использовать библиотеку с простого блокирующего режима:

char *linenoise(const char *prompt);

Это основной вызов библиотеки: он покажет приглашение ввода команды (prompt), обладающее возможностью редактирования строки с поддержкой истории команд. Указанная строка prompt используется как приглашение команды консоли (промпт), которое напечатается слева от курсора. Библиотека возвратит буфер строки, которую ввел пользователь, либо NULL в конце файла, или при ситуации нехватки памяти.

При обнаружении tty (пользователь фактически производит ввод команды в сессии терминала) макрос LINENOISE_MAX_LINE задает максимальную редактируемую длину строки. Когда вместо этого стандартный ввод не является tty, что происходит каждый раз, когда вы перенаправляете файл в вашу программу, или используете этот вызов в конвейере Unix (pipeline), то нет никаких ограничений на длину строки, которая может быть возвращена.

Возвращенная строка должна быть освобождена стандартным системным вызовом free(). Однако бывают ситуации, когда ваша программа использует другую библиотеку динамического выделения памяти, в этом случае вы можете также использовать linenoiseFree для гарантии, что память строки будет освобождена тем же алокатором, которым память была выделена.

Канонический цикл в программе, которая использует linenoise, будет выглядеть примерно так:

while((line = linenoise("hello> ")) != NULL) {
    printf("Вы ввели: %s\n", line);
    linenoiseFree(line); /* или просто free(line), если вы используете libc malloc. */
}

[Однострочный и многострочный ввод]

По умолчанию linenoise использует однострочное редактирование строки (single line editing), когда для ввода теста используется одна строка: когда вводимый текст не умещается на экране, строка будет сдвигаться влево, освобождая пространство для позиции курсора. Этот вариант хорошо подходит для случая, когда пользователю не нужно вводить много текста. В других ситуациях может быть более удобным режим многострочного редактирования (multi line editing).

Чтобы разрешить многострочное редактирование, используйте следующий вызов:

linenoiseSetMultiLine(1);

Запретить многострочное редактирование можно, если в эту функцию передать 0.

[История команд]

Linenoise поддерживает историю введенных команд, реализованную традиционным способом: выбор введенной команды осуществляется клавишами 'стрелка вверх' и 'стрелка вниз'. API-функции для поддержки истории команд:

int linenoiseHistoryAdd(const char *line);
int linenoiseHistorySetMaxLen(int len);
int linenoiseHistorySave(const char *filename);
int linenoiseHistoryLoad(const char *filename);

Используйте linenoiseHistoryAdd каждый раз, когда хотите добавить новый элемент на верхний уровень списка истории команд (он будет первым, который увидит пользователь при нажатии клавиши 'стрелка вверх').

Обратите внимание, что для того, чтобы история работала, вы должны установить необходимую длину списка истории вызовом linenoiseHistorySetMaxLen (по умолчанию длина этого списка 0, что означает запрет истории команд).

С помощью функций linenoiseHistorySave и linenoiseHistoryLoad можно сохранить и загрузить историю команд. Обе они возвратят -1 в случае ошибки и 0 в случае удачного вызова.

[Mask mode]

Иногда необходимо обеспечить ввод пароля или других секретных данных, чтобы вводимые символы не отображались на экране. Для таких ситуаций linenoise поддерживает "mask mode", в котором вводимые пользователем символы отображаются как символы звездочки *. Например:

$ ./linenoise_example
hello> get mykey
echo: 'get mykey'
hello> /mask
hello> *********

Вы можете разрешить и запретить mask mode следующими двумя функциями:

void linenoiseMaskModeEnable(void);
void linenoiseMaskModeDisable(void);

[Автозавершение команды (completion)]

Linenoise поддерживает автоматическое завершение ввода команды (completion), когда пользователь после начала ввода команды нажимает клавишу TAB.

Чтобы использовать completion, вам нужно зарегистрировать completion callback, который будет вызываться при каждом нажатии TAB. Ваш callback будет возвращать список элементов, которые будут представлять завершения для текущей введенной строки.

Пример регистрации completion callback:

linenoiseSetCompletionCallback(completion);

Здесь completion должна быть функцией, возвращающей void, и принимающей в качестве параметров указатель на буфер ввода const char, где находится введенная пользователем в настоящее время строка, и указатель на список объектов linenoiseCompletions, который используется как аргумент для вызова linenoiseAddCompletion, чтобы добавить варианты завершений внутри callback. Следующий пример сделает это понятнее:

void completion(const char *buf, linenoiseCompletions *lc) {
    if (buf[0] == 'h') {
        linenoiseAddCompletion(lc,"hello");
        linenoiseAddCompletion(lc,"hello there");
    }
}

В основном ваш completion callback должен инспектировать ввод (содержимое buf) и возвращать список элементов, которые считаются хорошими вариантами завершения команды (это список формируется вызовами linenoiseAddCompletion).

Если вы хотите проверить, как это работает, то скомпилируйте программу примера командой make, запустите пример, введите h и нажмите клавишу TAB.

[Hints]

Linenoise обладает фичей подсказок (hints), полезную в реализации интерфейса REPL (Read Eval Print Loop) для программ, которые принимают команды и аргументы, однако это может быть полезным и в других случаях.

Эта фича показывает справа от курсора, по мере ввода команды пользователем, полезные подсказки (hints). Эти подсказки могут быть показаны другим цветом, отличающимся от введенного текста, и могут быть также отображены жирным шрифтом (bold).

Например, когда пользователь начал вводить "git remote add", подсказка может показать справа от приглашения строку < name> < url>.

Фича подсказок работает подобно фиче истории, используя callback. Для регистрации callback мы используем вызов:

linenoiseSetHintsCallback(hints);

Сам callback реализуется примерно так:

char *hints(const char *buf, int *color, int *bold) {
    if (!strcasecmp(buf,"git remote add")) {
        *color = 35;
        *bold = 0;
        return " < name> < url>";
    }
    return NULL;
}

Функция callback возвратит строку, которая должна быть отображена как подсказка, или NULL, если нет подсказки для текста, введенного пользователем. Возвращенная строка при необходимости будет урезана в зависимости от доступного количества столбцов на экране.

Есть возможность возвратить строку, память для которой выделена динамически, если также зарегистрировать callback-функцию для освобождения выделенной памяти под строку подсказки:

void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *);

Зарегистрированная callback функция освобождения памяти (переданная в параметре типа linenoiseFreeHintsCallback) просто принимает указатель на освобождаемую память строки, и освобождает эту память необходимым способом (в зависимости от способа, которым эта память была выделена внутри hits callback).

Как вы можете увидеть в примере выше, может быть предоставлен цвет color (в кодах цвета терминала xterm) вместе с атрибутом жирности bold. Если color не установлен, то используется текущий цвет чернил терминала (foreground color). Если атрибут bold не установлен, то текст подсказки печатается в обычном стиле (обычный, не жирный текст).

Коды цвета:

Цвет Код
red (красный) 31
green (зеленый) 32
yellow (желтый) 33
blue (синий) 34
magenta (пурпурный) 35
cyan (сине-зеленый) 36
white (белый) 37

[Очистка экрана]

Иногда вы можете захотеть очистить экран как результат чего-то что ввел пользователь. Это можно сделать вызовом следующей функции:

void linenoiseClearScreen(void);

[Асинхронное API]

Иногда необходимо читать ввод с клавиатуры, но также данные из сокетов или других внешних событий, отображая при этом набор текста пользователем. Давайте назовем это "проблема IRC", поскольку если вы захотите написать клиент IRC с использованием linenoise, без какого-либо метода многопоточности типа libcurses, то наверняка столкнетесь с подобной проблемой.

К счастью, для такого случая существует дружественный API, использующий блокирующий вызов внутри себя. Для его использования нам нужно инициализировать контекст linenoise примерно так:

struct linenoiseState ls;char buf[1024];
linenoiseEditStart(&ls,-1,-1,buf,sizeof(buf),"some prompt> ");

Два аргумента -1 и -1 это дескрипторы stdin/stdout. Если они установлены в -1, то linenoise будет просто использовать по умолчанию файловые дескрипторы stdin/stdout. Теперь, как только нам поступят данные из stdin (о чем мы узнаем через select(2) или другим способом), мы можем запросить у linenoise прочитать следующий символ вызовом:

linenoiseEditFeed(&ls);

Эта функция возвратит указатель на char: если пользователь пока не нажал enter для передачи строки в программу, он вернет linenoiseEditMore, что означает, что необходимо вызывать снова linenoiseEditFeed(), когда придет больше доступных данных. Если функция возвратила не NULL, то это данные, место для которых выделено в куче (их надо освобождать linenoiseFree()), и они представляют пользовательский ввод. Когда функция возвратила NULL, это значит, что пользователь нажал CTRL-C или CTRL-D с пустой строкой для выхода из программы, или произошла какая-то ошибка ввода/вывода (I/O error).

После каждой принятой строки (или если вы хотите выйти из программы и выйти из raw-режима) должна быть вызвана следующая функция:

linenoiseEditStop(&ls);

Чтобы начать чтение следующей строки, необходимо вызвать новый linenoiseEditStart(), чтобы сбросить состояние и т. д., поэтому типичный обработчик событий, вызываемый, когда стандартный ввод читается, будет работать аналогично приведенному ниже примеру:

void stdinHasSomeData(void) {
    char *line = linenoiseEditFeed(&LineNoiseState);
    if (line == linenoiseEditMore) return;
    linenoiseEditStop(&LineNoiseState);
    if (line == NULL) exit(0);
printf("line: %s\n", line); linenoiseFree(line); linenoiseEditStart(&LineNoiseState,-1,-1,LineNoiseBuffer,sizeof(LineNoiseBuffer),"serial> "); }

Теперь у нас есть способ избежать блокирования пользовательского ввода, мы можем использовать два вызова для скрытия/отображения редактируемой строки, так что есть возможность показать какие-то принятые данные (через сокеты, BlueTooth, откуда угодно) на экране:

linenoiseHide(&ls);
printf("какие-то данные...\n"); linenoiseShow(&ls);

Для демонстрации этих вызовов API пример использования linenoise на языке C реализует мультиплексирования с помощью select(2) и асинхронного API:

    struct linenoiseState ls;
    char buf[1024];
    linenoiseEditStart(&ls,-1,-1,buf,sizeof(buf),"hello> ");
while(1) { // для упрощения код настройки select(2) удален... retval = select(ls.ifd+1, &readfds, NULL, NULL, &tv); if (retval == -1) { perror("select()"); exit(1); } else if (retval) { line = linenoiseEditFeed(&ls); /* Возврат NULL означает: продолжается редактирование строки. * Иначе пользователь нажал на enter или остановил редактирование * (CTRL+C/D). */ if (line != linenoiseEditMore) break; } else { // Произошел таймаут static int counter = 0; linenoiseHide(&ls); printf("Async output %d.\n", counter++); linenoiseShow(&ls); } } linenoiseEditStop(&ls); if (line == NULL) exit(0); /* Ctrl+D/C. */

Вы можете протестировать этот пример если запустите программу примера с опцией --async.

Связанные с Linenoise проекты:

Linenoise NG [3] это форк от Linenoise, в котором есть более продвинутые фичи, такие как поддержка UTF-8, поддержка Windows и другое. Использует C++ вместо C.
Linenoise-swift [4] реализация Linenoise, написанная на Swift.

[Ссылки]

1. antirez / linenoise site:github.com.
2. ESP-IDF: компонент консоли.
3. arangodb / linenoise-ng site:github.com.
4. andybest / linenoise-swift site:github.com.

 

Добавить комментарий


Защитный код
Обновить

Top of Page