Pull to refresh

Удобная работа в консоли, или красим STDERR в красный цвет

Reading time7 min
Views21K

Работа в консоли


Многие из нас пользуются консолью каждый день, и, наверное, каждый задавал себе вопрос: как я могу сделать свою работу в консоли эффективнее? Что я могу сделать, чтобы тратить меньше времени на выполнение рутинных операций? В этой статье я бы хотел вкратце рассказать о нескольких простых, но полезных вещах при работе с bash, о которых вы, возможно, не знали.

Сокращаем количество набираемых букв


Алиасы

Одна из полезнейших вещей, которую поддерживают все современные shell'ы — это алиасы. Алиасы позволяют писать меньше букв при наборе команд. Например:

$ git status
# On branch master
nothing to commit, working directory clean
$ alias st='git status'
$ st
# On branch master
nothing to commit, working directory clean

Как можно видеть, синтаксис объявления алиасов очень прост, поэтому алиасами так или иначе пользуются практически все. Один из недостатков использования алиасов — это то, что зачастую перестает работать авто-дополнение для команд.

Пример: (<TAB> — это нажатие на клавишу Tab):

$ git checkout <TAB><TAB>
HEAD            master          origin/HEAD     origin/master

Если же мы объявим алиас co='git checkout', то нажатие на Tab перестает работать так, как мы ожидаем и начинает просто подставлять имена файлов (по крайней мере для bash):

$ alias co='git checkout'
$ co <TAB><TAB>
Display all 124 possibilities? (y or n)
.git/           MANIFEST.doc    array.c         bashline.c ...


Таким образом, если вы хотите писать меньше букв, то вам придется отказаться от авто-дополнения… Или нет? Погуглим немного, и найдем такую интересную функцию:

function make-completion-wrapper () {
	local function_name="$2"
	local arg_count=$(($#-3))
	local comp_function_name="$1"
	shift 2
	local function="
function $function_name {
	((COMP_CWORD+=$arg_count))
	COMP_WORDS=( "$@" \${COMP_WORDS[@]:1} )
	"$comp_function_name"
	return 0
}"
	eval "$function"
}


Эта интересная функция позволяет нам вернуть обратно автодополнение для «сложных» команд (то есть, когда мы делаем алиас, состоящий из команды и доп. аргументов, как в случае с git checkout). Она нам позволяет создать функцию, которая, в свою очередь, может быть использована для оборачивания функций авто-дополнения, чтобы авто-дополнение для алиасов продолжали работать. Звучит сложно..? Так оно и есть :). Давайте лучше посмотрим на пример использования:

$ make-completion-wrapper _git _co git checkout
$ complete -o bashdefault -o default -o nospace -F _co co
$ co <TAB><TAB>
HEAD            master          origin/HEAD     origin/master

Давайте разберем чуть-чуть подробнее:

1) make-completion-wrapper: Функции авто-дополнения для команд обычно начинаются с подчёркивания, и например для команды git такая фукнция называется "_git". С помощью нашей замечательной функции make-completion-wrapper мы создадим новую функцию под названием "_co" (для команды co), которая является алиасом для «git checkout»
2) complete: Зарегистрируем нашу новую функцию "_co", как обработчик для авто-дополнения для команды «co»
3) Работает!

Чтобы алиасы и команды авто-дополнения не потерялись, сохраним эти команды в ".bashrc" (или ".bash_profile" или ".profile") в своей домашней директории, и будем радоваться :).

Функции

Возможности алиасов в bash ограничены, поэтому иногда бывает полезно писать функции (как в предыдущем примере). Функции в bash работают, как будто это отдельная команда, но при этом функции выполняются в том же контексте, что и текущий shell. То есть, аргументы функциям передаются, как "$1", "$2" и т.д., как если бы вы писали shell-скрипт. Также работает сокращение "$@" (в кавычках), которое подставляет все свои аргументы «как есть» в нужное место. На serverfault можно найти пример такой функции, которая раскрашивает в красный цвет stderr у заданной команды:

$ color()(set -o pipefail;"$@" 2>&1>&3|sed $'s,.*,\e[31m&\e[m,'>&2)3>&1
$ color ls nonexistent
ls: nonexistent: No such file or directory # надпись красным цветом

Даже если не писать таких команд, как приведенная выше, функции удобно использовать, если вам нужно, скажем, всегда дописывать нужные аргументы в конец команды:

$ function echoworld () { echo "$@" world; }
$ echoworld Hello
Hello world

Или если нужно делать какие-то простейшие операции над аргументами:

$ function gmo () { git merge "origin/$1"; }
$ gmo master # git merge "origin/master"
Already up-to-date.

Избавляемся от «лагов ввода» SSH с помощью mosh


Если вам часто приходится работать по SSH с сильно удаленными серверами (например, облако Amazon в Америке), или вы работаете по SSH через мобильный интернет, вам знакома проблема задержки ввода. То есть, вы вводите какой-то символ, а он появляется на удаленной стороне только спустя round-trip interval, который может легко составлять 200 мс и более. В случае с мобильным интернетом задержки ввода ощущаются намного сильнее и работать уже становится совсем некомфортно.

Вероятно, авторам утилиты под названием mosh (http://mosh.mit.edu) эта проблема надоела настолько, что они решили написать свою замену SSH, работающую поверх UDP, и решающую многие проблемы SSH, например ощутимые задержки ввода и отсутствие фидбека при потере соединения (write failed: broken pipe, который появляется только когда вы пытаетесь что-то ввести).

У этой утилиты есть также один существенный недостаток — в данный момент нет поддержки просмотра истории. То есть, если вы сделаете cat от большого файла или ls от большой директории, то скорее всего вы получите лишь последние строки вывода, а начало «потеряется». Сами авторы в данный момент рекомендуют использовать screen на удаленной стороне для решения этой проблемы, и в версии 1.3 обещают встроить похожую функциональность прямо в сам сервер (и клиент).

Патчим bash, чтобы stderr был красного цвета


На самом деле, патчить bash для того, чтобы получить stderr красного цвета, совсем не обязательно, но это же интересно! Уже существуют готовые решения, которые легко гуглятся, например вот это: github.com/sickill/stderred. Проект из себя представляет разделяемую библиотеку, которая перехватывает вызовы write(2, ...) и fprintf из libc, и добавляет вокруг обертку из нужных esc-последовательностей, чтобы получить красный цвет.

Итак, мы поняли, что другие решения существуют, и они даже всех устраивают, поэтому давайте всё равно напишем своё :)! Хочу сразу сказать отдельно спасибо ezh за оказанную помощь в реализации патча.

1. Качаем исходники bash ( ftp.gnu.org/gnu/bash )
2. Добавляем их в git ( git init && git add -A && git commit -m 'Initial commit' )
3. Собираем bash ( ./configure && make )
4. Запускаем bash и убеждаемся в том, что всё работает ( ./bash -l )
5. Начинаем разбираться в исходном коде:

Находим файл shell.c и смотрим, где начинается инициализация bash:

#if defined (NO_MAIN_ENV_ARG)
/* systems without third argument to main() */
int
main (argc, argv)
     int argc;
     char **argv;
#else /* !NO_MAIN_ENV_ARG */
int
main (argc, argv, env)
     int argc;
     char **argv, **env;
#endif /* !NO_MAIN_ENV_ARG */
{


Примерно через 400 строк, всё ещё находясь в функции main(), в самом конце находим вызов reader_loop():

#if !defined (ONESHOT)
 read_and_execute:
#endif /* !ONESHOT */

  shell_initialized = 1;

  /* Read commands until exit condition. */
  reader_loop ();
  exit_shell (last_command_exit_value);
}


Логично было бы вклиниться прямо перед тем, как bash начнет читать пользовательский ввод и как-то перехватить дескриптор с номером 2 (stderr), если стоит нужная переменная окружения:

  shell_initialized = 1;

  color_stderr = get_string_value("COLOR_STDERR");
  if (color_stderr && color_stderr[0]) {
    init_color_stderr();
  }

  /* Read commands until exit condition. */
  reader_loop ();


Как перехватить дескриптор с номером 2? Очевидное решение — это создать pipe, и заменить дескриптор с номером 2 нашим pipe'ом, а в другом треде читать оттуда, и добавлять нужные esc-последовательности:

static void *colorize_stderr(void *void_thread_args) {
  struct stderr_thread_data* data = (struct stderr_thread_data*)void_thread_args; int n; char buf[1024];

  #define STDERR_PREFIX "\033[31m"
  #define STDERR_SUFFIX "\033[m"

  for (;;) {
    n = read(data->pipe, buf, sizeof(buf));
    if (n <= 0) {
      if (errno == EINTR) continue;
      pthread_exit(NULL);
    }

    write(data->err, STDERR_PREFIX, sizeof(STDERR_PREFIX) - 1);
    write(data->err, buf, (size_t) n);
    write(data->err, STDERR_SUFFIX, sizeof(STDERR_SUFFIX) - 1);
  }
}

static void init_color_stderr () {
  pthread_t thr; int pipes[2]; static struct stderr_thread_data data;

  pipe(pipes);

  data.err = dup(2);
  dup2(pipes[1], 2);

  data.pipe = pipes[0];
  pthread_create(&thr, NULL, colorize_stderr, (void*) &data);
}


Однако весь пользовательский ввод тоже становится красным :(. Видимо, библиотека readline выводит наш ввод на экран как раз в stderr… Покопавшись в библиотеке readline, вставим в файл lib/readline/display.c в функцию rl_redisplay (функция, кстати, всего-лишь на 1300 строк) следующее:

/* Basic redisplay algorithm. */
void
rl_redisplay ()
{
/* ... локальные переменные ... */
  if (_rl_echoing_p == 0)
    return;

  _rl_output_some_chars("\033[m", 3); /* наш супер-костыль: перед тем, как вывести на экран пользовательский ввод, сбросим текущий цвет */

  /* Block keyboard interrupts because this function manipulates global
     data structures. */
  _rl_block_sigint ();  
  RL_SETSTATE (RL_STATE_REDISPLAYING);


Если всё сделано правильно и добавлены нужные заголовки и сигнатуры методов (эти действия были опущены для краткости), то когда мы пропишем в .bashrc строчку «export COLOR_STDERR=1» и запустим новый скомпилированный bash, весь stderr станет красным, как на скриншоте в начале статьи.

Поскольку заменять системный bash — плохая идея, можно положить новый, собранный bash, скажем, в ~/bash и дописать следующее в .bashrc:

if [ ! -z "$PS1" ] && [ -z "$MY_BASH" ] && [ -x ~/bash ]; then
    export MY_BASH=1
    exec ~/bash -l "$@"
fi

export COLOR_STDERR=1


При логине будет проверяться, существует ли "~/bash" и является ли он исполняемым, и если это так, то заменит текущий процесс на "~/bash -l" (то есть, login shell). Опция COLOR_STDERR=1 будет нам раскрашивать stderr у bash в красный цвет.

Пропатченная версия bash выложена на github: github.com/YuriyNasretdinov/bash

Поскольку правки оформлены в виде «костыля» к bash, то вряд ли этот патч примут в основную ветку, но сама по себе реализация лично мне представляется довольно забавной: для работы, по сути, требуется выставить только одну переменную окружения (использование которой легко можно выпилить из кода, и тогда этот режим будет включаться по умолчанию), а всё остальное будет работать без изменений.

Надеюсь, уважаемый читатель, ты смог вынести из этой статьи что-нибудь полезное :). С наступающим новым годом!
Tags:
Hubs:
Total votes 86: ↑75 and ↓11+64
Comments15

Articles