LXF117:readline

Материал из Linuxformat.

Перейти к: навигация, поиск
Хаки и трюки Несколько приемов, которые сделают ваши Linux-приложения еще лучше

Содержание

Кодируем: Cоветы бывалых

Помните сказки, в которых главный герой отдавал последние дукаты за три мудрые изречения? Андрей Боровский не только не возьмет с вас ни гроша, но и даст лишний совет в придачу.

Главное достоинство открытого ПО – не в том, что программы распространяются бесплатно, а в том, что мы всегда можем найти примеры хорошего кода, пригодного к использованию в наших собственных проектах (которые, если нам повезет, тоже обогатят сокровищницу Open Source). На протяжении многих лет я копировал интересные фрагменты из исходных текстов разных популярных программ и с форумов, посвященных программированию для Unix. Теперь я делюсь некоторыми рецептами с вами.

Командная строка «как у Bash»

Каждый поклонник Unix знает, что при прочих равных условиях программы с консольным интерфейсом гораздо удобнее, чем все эти «окошечки» и «менюшечки». Шутки – шутками, а командная строка Bash, действительно, очень комфортная. Работу с ней здорово ускоряет завершение имен команд и файлов по нажатию Tab и история ранее введенных команд. Эти две функции Bash настолько удобны и привычны линуксоидам, что их реализуют и многие другие программы: например, завершение по Tab работает в диалогах открытия/сохранения файлов KDE.

Если в вашей программе есть что-то вроде командной строки (или просто строки ввода), есть смысл реализовать указанные возможности – благо, это очень просто. В состав Linux (и многих других систем) входит библиотека GNU Readline, реализующая необходимую функциональность. Собственно, ею пользуется и сам Bash!

Главная функция библиотеки Readline называется readline(). Как нетрудно догадаться, она предназначена для чтения строки текста с терминала. В каче­cтве аргумента функция readline() принимает «приглашение командной строки», отображаемое на экране терминала, а возвращает значение типа char *, указывающее на копию строки, введенной пользователем. Если вы реализуете в своей программе аналог командной строки Bash, пользоваться readline() будет удобнее, чем стандартными функциями библиотеки C. Во-первых, readline() позволяет вам не думать о размере буфера для ввода текста – она будет считывать его до тех пор, пока не будет нажата клавиша Enter, а затем вернет вам строку, содержащую все набранные символы. Стоит, однако, учесть, что алгоритм добавления новых символов заметно замедляет свою работу по мере роста строки, так что я не рекомендовал бы использовать вызов для редактирования цельных текстов. Строка, возвращенная функцией readline(), создана специально для вас, и вы должны высвободить занятую ею память с помощью функции free().

Тексты упомянуты не случайно: readline() предоставляет вам полноценный набор команд редактирования вводимой строки в стиле Emacs. Но и это еще не все. Каждый раз, когда пользователь нажимает клавишу табуляции (или другую спецклавишу, например, Esc), readline() прерывает нормальное выполнение и предоставляет программисту возможность выполнить некоторые действия. В программе shelldemo показано, как можно использовать функцию readline для реализации автозавершения имен команд и имен файлов:

#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <readline/readline.h>
 #include <readline/history.h>
 char * centry_func(const char * text, int state);
 int main(int argc, char ** argv) {char *buf;
   read_history(“.history);
   rl_completion_entry_function = centry_func;
   rl_bind_key('\t', rl_complete);
   while((buf = readline(“\nshell> ”))!=NULL) {
    printf(“Команда: [ %s]\n”,buf);
    if (buf[0]!=0)
     add_history(buf);
    if (strncmp(buf,“quit”, 4) == 0)
     break;
    free(buf);
  }
   free(buf);
   write_history(“.history);
   return 0;
 }

Все функции и переменные, связанные с readline(), объявлены в файле <readline/readline.h> (учтите, что сама библиотека Readline наверняка установлена в вашей системе, а вот заголовочные файлы для нее, скорее всего, придется добавлять).

Функция rl_bind_key() позволяет связать специальное действие с некоторым символом. Ее первый аргумент – символ (‘\t’ для табуляции, ‘\e’ для Esc и так далее), второй – адрес функции, которую следует вызвать в ответ на его ввод. Привязка действий к символам – чрезвычайно мощный механизм, позволяющий существенно расширить возможности функции readline(). В нашем примере мы связываем символ табуляции и функцию rl_complete(). Если теперь в потоке ввода readline() появится ‘\t’, он не будет напечатан, а вместо этого будет вызвана функция rl_complete(), которая выполнит автоматиче­cкое завершение команды и все сопутствующие операции (например, демонстрацию возможных вариантов в случае неоднозначного выбора). Сама rl_complete() работает по довольно сложной схеме, но, в конечном счете, полагается на функцию обратного вызова, адрес которой хранится в переменной rl_completion_entry_function (в нашем примере – centry_func()). Именно она позволяет нам задать свой собственный механизм завершения вводимого текста. Функция centry_func() вызывается несколько раз подряд и возвращает либо очередной вариант завершения переданного текста, либо NULL, если их больше нет. Несколько вызовов нужны потому, что введенный текст в общем случае может быть завершен несколькими способами. В этой ситуации функция readline() не дополняет текст, а показывает все возможные варианты завершения. Далее пользователь должен уточнить свой выбор, чтобы завершение сработало.

#define MAX_COMMANDS 6
 char * command_list [] ={“cat”, “copy”, “connect”, “ls”, “list”, “quit”};
 char * centry_func(const char * text, int state) {static int command_index, len;
   char * candidate;
   if (!state) {
    command_index = 0;
    len = strlen(text);
  }while (command_index < MAX_COMMANDS) {
    candidate = command_list[command_index++];
    if (strncmp(candidate, text, len) == 0)
     return (strdup(candidate));
  }
   command_index = 0;
   len = strlen(text);
   return rl_filename_completion_function(text, state);
 }

Функция centry_func() получает два параметра – текст, который необходимо автоматиче­cки завершить, и параметр состояния, который, попросту говоря, показывает, сколько раз уже вызывалась centry_func(). Функция берет список известных команд из массива command_list и перебирает его элементы, используя статиче­cкую переменную command_index и определяя те коман­ды, первые символы которых совпадают с введенным текстом. Найдя подходящую строку, мы создаем ее копию с помощью функции strdup() и возвращаем указатель на нее. Дальнейшую заботу о выделенной области памяти возьмут на себя функции, вызывающие centry_func(). Обработав все элементы массива, можно было бы вернуть NULL, но мы поступаем иначе и вызываем функцию rl_filename_completion_function() из библиотеки Readline: она выполняет стандартное завершение имен файлов. В результате centry_func() сначала проверяет переданный текст на соответствие именам команд, а затем ищет совпадение в именах файлов. Чтобы последние не путались в нашем примере с командами, их следует начинать со специальных символов – /,./, ~/. Нажав Tab в пустой командной строке shelldemo, мы увидим полный список команд (все как положено).

Если в какой-то момент вы захотите, чтобы Tab потерял свое специальное значение, измените привязку символа:

rl_bind_key('\t',rl_insert);

Функция rl_insert() просто добавляет связанный с ней символ в строку.

Обогатить нашу программу историей команд в стиле Bash даже проще, чем реализовать автозавершение. После ввода очередной команды мы добавляем ее в список с помощью функции add_history(), объявленной в <readline/history.h>. Можно спросить, почему введенные команды не добавляются в список истории команд автоматиче­cки? Ответ прост – Readline предоставляет программисту самому решать, какие команды и в какой форме следует сохранять в истории. Наверняка вы не захотите, чтобы в нее попадали введенные пользователем пустые строки (а может, и захотите, кто вас знает?).

Как вы, конечно, знаете, Bash умеет сохранять историю команд в перерывах между сеансами. Readline предоставляет нам простые средства для решения и этой задачи. В начале работы программы shelldemo мы загружаем сохраненную ранее историю команд с помощью функции read_history() из файла .history (его имя передается как аргумент). Если файла .history не существует, read_history() не скажет нам ничего плохого. Точно так же в конце работы программы мы сохраняем историю команд с помощью функции write_history().

В заключение отметим, что функции библиотеки Readline нереентерабельны, хотя вряд ли кому-то понадобится открывать несколько оболочек в разных потоках. На всякий случай напомним, что программы, использующие Readline, нужно компилировать с ключом -lreadline.

Поведение Readline можно настроить с помощью внешнего файла конфигурации. Он называется .inputrc и должен располагаться в вашей домашней директории. С помощью .inputrc вы настраиваете поведение сразу всех программ, использующих Readline. На первый взгляд может показаться, что это неудобно, но на самом деле такой унифицированный подход имеет смысл. Разные правила обработки одних и тех же команд для разных программ только создают ненужную путаницу. В файле .inputrc можно присваивать значения переменным, управляющим поведением Readline, связывать известные Readline действия с комбинациями клавиш и даже использовать условные переходы, как в сценариях Bash. Мы, однако, рассмотрим только переменные файла .inputrc – самые интересные из них можно найти ниже.

Переменные GNU Readline

Для присваивания переменным новых значений, в файле .inputrc используется команда set:

set ПЕРЕМЕННАЯ ЗНАЧЕНИЕ

Ниже перечислены переменные, которые изменяют поведение изученных нами функций библиотеки GNU Readline:

  • set сompletion-ignore-case on По умолчанию, при автозавершении команд учитывается регистр символов. Присвоение переменной значения on делает автозавершение регистронезависимым.
  • set completion-query-items n Число возможных вариантов завершения команды, при превышении которого система задает пользователю вопрос «показать все %n% вариантов y/n?».
  • set disable-completion on Отключает автозавершение.
  • set expand-tilde on Заставляет систему преобразовывать сочетания символов типа «~*» в полный путь к домашней директории соответствующего пользователя.
  • set mark-directories on Автоматиче­cки добав­ляет «/» при автозавершении имен директорий.
  • set match-hidden-files on Учитывать при автодополнении имен скрытые файлы (даже если пользователь не ввел начальную точку).
  • set print-completions-horizontally on Распечатывать возможные варианты завершения горизонтально (по строкам), а не вертикально (по колонкам).
  • set show-all-if-ambiguous on В случае неоднозначности завершения текста возможные варианты распечатываются сразу, без предупредительного звукового сигнала.
  • set visible-stats on При автозавершении добавляет к имени файла символ, указывающий его тип.

Как развернуть тильду

Оболочка Bash (и библиотека Readline) умеют преобразовывать символы типа «~*» в имена домашних директорий пользователей там, где это необходимо, но иногда нам приходится решать данную задачу самостоятельно. На первый взгляд может показаться, что написать процедуру, заменяющую тильду именем домашнего каталога, очень просто, но это не совсем так. Дьявол, как всегда, прячется в деталях – а именно, в правилах использования символа ‘~’. Напомню, что сочетание «~/» в начале имени файла обозначает абсолютный путь к домашней директории текущего пользователя. ~username/ разворачивается в абсолютный путь к домашней директории пользователя username, которая вовсе не обязательно выглядит как /home/username/. Задача осложняется еще и тем фактом, что тильда может использоваться в именах файлов и директорий как обычный символ. С помощью mkdir вы можете создать директорию с именем, начинающимся с ‘~’. Система не позволит вам создать локальную директорию ~user1, если /home/user1 уже существует, но в обратном порядке (сначала локальную ~user1, потом /home/user1) это проделать можно. Возникающая в результате неоднозначность способна запутать даже интерпретатор Bash.

Рассмотрим функцию преобразования строк, содержащих тильду, в полные имена файлов:

string expand_path(const string& path) {if (path.length() == 0 || path[0]!= '~')
    return path;
   const char *pfx = NULL;
   string::size_type pos = path.find_first_of('/');
   if (path.length() == 1 || pos == 1) {
    pfx = getenv(“HOME”);
    if (!pfx) {
     struct passwd *pw = getpwuid(getuid());
     if (pw)
      pfx = pw->pw_dir;
    }} else {string user(path,1,(pos==string::npos)? string::npos: pos-1);
   struct passwd *pw = getpwnam(user.c_str());
   if (pw)
    pfx = pw->pw_dir;
  }if (!pfx)
    return path;
   string result(pfx);
   if (pos == string::npos)
    return result;
   if (result.length() == 0 || result[result.length()-1]!= '/')
    result += '/';
   result += path.substr(pos+1);
   return result;
 }

Как можно видеть, она написана на C++ и использует тип данных std::string. Для получения директории текущего пользователя используется переменная окружения HOME, а если она не задана – сочетание функций getuid() (возвращает идентификатор текущего пользователя) и getpwuid() (возвращает указатель на структуру passwd, хранящую данные учетной записи пользователя). Имя домашней директории пользователя хранится в поле pw этой структуры. Определение домашней директории пользователя, заданного по имени, выполняется с помощью функции pwnam(), которая также возвращает указатель на структуру passwd. Если функция expand_path() не смогла связать тильду с домашней директорией пользователя, она оставляет ее без изменений. Функцию expand_path() можно использовать непосредственно для преобразования имен файлов, и практика показывает, что обмануть ее невозможно!

Как избавиться от терминала

В системах, использующих X Window, нет четкого разделения программ на графиче­cкие и консольные. Если вы запускаете графиче­cкую программу в окне консоли, она выполняется в соответствующей сессии терминала. Иногда это удобно – можно читать диагностиче­cкие сообщения, которые программа выводит на консоль, а в случае зависания приложение, скорее всего, удастся завершить с помощью Control-C. Однако зачастую запущенная из окна консоли графиче­ кая программа просто занимает это окно (и закрыть его нельзя, ведь тогда и программа завершится). Мне очень нравятся графиче­cкие программы, которые сразу после запуска освобождают окно консоли, из которого они запущены. А сделать это можно так: в функцию main(), до того как программа вызовет какую-либо функцию X Window, добавляем следующие строки:

if (fork()!= 0)
    exit(0);
 printf(“Ухожу из терминала\n”);
 close(0);
 close (1);
 close(2);
 int fd = open(“/dev/null”, O_RDWR);
 dup2(fd, 0);
 dup2(fd, 1);
 dup2(fd, 2);

Функция fork() создает новый процесс, который является копией графиче­cкой программы. Мы завершаем родительский процесс, в котором вызов fork() вернул ненулевое значение, и продолжаем работу в дочернем процессе. Дочерний процесс наследует от родителя все открытые дескрипторы, в том числе дескрипторы стандартных потоков ввода-вывода (по умолчанию они имеют номера 0, 1 и 2). Чтобы наша программа не печатала данных на терминал, с которым она «попрощалась», мы закрываем эти дескрипторы. Но оставлять стандартные потоки ввода-вывода закрытыми нельзя, так как многие функции, в том числе и функции X Window, используют их. Поэтому мы открываем пустое устройство /dev/null для чтения и записи и создаем копии открытого дескриптора с номерами 0, 1 и 2. Используемые нами функции и константы объявлены в заголовочных файлах <stdio.h>, <fcntl.h>, <sys/types.h>, <unistd.h>.

На диске вы найдете пример программы X Window (файл events.c), которая отключается от запустившего ее терминала. Чтобы скомпилировать приложение, скомандуйте:

gcc events.c -lXext -lX11 -lm

Как перехватить вызов

Скорая помощь

Перехват библио­течных вызовов полезен не только для отладки. Пере­oпределив функции для работы с сокетами, вы можете перенаправить весь сетевой трафик приложения на выделенный SOCKS-сервер.

Иногда нам бывает нужно контролировать выполняемые программой вызовы библиотечных функций. Если исходные тексты приложения доступны, мы можем просто заменить вызов интересующей нас функции на перехватчик, но доступ к исходным кодам есть не всегда и не у всех. В этом случае нам поможет переменная окружения LD_PRELOAD. С ее помощью мы можем указать имя библиотеки, которая должна быть загружена прежде всех остальных библиотек, используемых приложением (даже раньше, чем библиотека libc). В результате, если в библиотеке-перехватчике определена функция или переменная с именем, которое совпадает с одним из имен, экспортируемых другими библиотеками, программа будет использовать объект из библиотеки-перехватчика вместо одноименного объекта из своей «родной» библиотеки. Очень часто переменная LD_PRELOAD используется для внедрения в программу функций, являющихся обертками для стандартных. Эти функции-обертки выполняют требуемые нам дополнительные действия, а затем вызывают стандартные функции для выполнения основной работы. Однако вызвать в библиотеке-перехватчике стандартную функцию не так просто, как кажется, ведь ее имя совпадает с именем функции-обертки, определенной в той же библиотеке (если не предпринять специальных действий, вместо вызова стандартной функции функция-обертка будет рекурсивно вызывать саму себя). Для решения этой проблемы используется функция dlsym() которая позволяет загрузить функцию, заданную именем, из другого модуля. Вот как, например, может выглядеть обертка для стандартной функции fopen():

FILE* fopen(const char * path, const char * mode) {
    printf(“Открываем %s\n”, path);
    FILE * (*std_fopen)(const char *, const char *) = dlsym(RTLD_NEXT, “fopen”);
    return std_fopen(path, mode);
 }

Наша обертка выводит на консоль диагностиче­cкое сообщение, а затем вызывает стандартную функцию fopen(). Константа RTLD_NEXT указывает, что стандартную функцию fopen() нужно искать в одном из следующих загруженных модулей, а не в текущем. Если теперь скомпилировать функцию-обертку в разделяемую библиотеку (пример и инструкции вы найдете на диске в файле intercept.c), а затем установить переменные окружения

export LD_PRELOAD=libintercept.so
export LD_LIBRARY_PATH=.

то запущенные далее программы будут использовать функцию-обертку fopen(), и на экране будет распечатываться информация об открываемых файлах.

Следует отметить что данный метод перехвата не сработает, если сама программа использует механизм dlopen()/dlsym() для получения адресов библиотечных функций или же если она скомпонована статиче­cки. Если вы хотите использовать этот метод для перехвата функций из модулей, написанных на C++, то вы должны учесть, что C++ применяет преобразование имен экспортируемых объектов (names mangling) с целью исключить неоднозначность, которая может возникнуть при экспорте перегруженных функций (преобразование имен может быть отключено явным образом, и тогда они будут экспортироваться неизменными). Перед тем как перехватывать функцию, следует проверить, как выглядит ее имя в скомпилированном модуле. Это можно сделать с помощью утилиты nm.

Надеемся, что благодаря этим советам ваши программы для Linux (и не только для Linux) станут лучше и надежнее. Будет время – черкните нам пару строк и расскажите, где вы применили то, что узнали на этом уроке (благодарности в исходных текстах всемирно известных проектов тоже приветствуются!). LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию