В терминале всего 33 Ctrl-шортката — разбор от Julia Evans

В терминале можно зажать Ctrl только в 33 комбинациях — остальные пропускаются или становятся ANSI escape. Julia Evans разбирает, где живёт каждый код и почему Ctrl-M = Enter.

Обложка: В терминале всего 33 Ctrl-шортката — разбор от Julia Evans

Если вы когда-нибудь нажимали Ctrl-1 в терминале и удивлялись, почему ничего не происходит — это не баг, а ограничение ASCII: только 33 комбинации с Ctrl дают различимый control-код, остальные терминал пропускает как обычный ввод или превращает в ANSI escape-последовательность. Понимание этой таблицы помогает в тех самых моментах, когда терминал «сломан», Ctrl-S внезапно всё заморозил, а Ctrl-W в одной программе удаляет слово, а в другой — закрывает окно. Julia Evans, автор блога jvns.ca и иллюстрированных гайдов (zines) про Linux и командную строку, объяснила, почему Ctrl-M — это Enter, а Ctrl-S замораживает терминал. Ниже — перевод её разбора.

Недавно я много думала про терминал, и вчера мне стало интересно: что вообще происходит со всеми этими «control codes» — Ctrl-A, Ctrl-C, Ctrl-W и так далее? Я собрала таблицу всех 33 ASCII control characters и того, что они делают на моей машине (macOS). Оговорок там миллион, но ниже я расскажу, что это всё значит и какие есть нюансы.

Ключевые выводы
Что нужно знать про ASCII control characters в терминале
Коротко о главном из разбора
  • В ASCII всего 33 control-кода: A–Z (26 штук) плюс 7 дополнительных (@, [, \, ], ^, _, ?). Сделать Ctrl-1 как шорткат невозможно.
  • Коды обрабатываются в трёх разных местах: OS terminal driver (например, Ctrl-C → SIGINT), библиотека readline (Ctrl-A, Ctrl-E) или само приложение (emacs использует Ctrl-X).
  • Ctrl-M = Enter, Ctrl-I = Tab — поэтому их нельзя использовать как отдельные шорткаты без спец-настройки эмулятора.
  • Режим терминала — canonical или noncanonical — определяет, кто обрабатывает Backspace, Ctrl-W и Ctrl-U: OS или сама программа. Разбор обоих режимов — в теле статьи.
  • Утилита stty -a показывает текущие маппинги OS-кодов, а stty sane лечит «сломанный» терминал.
  • Backspace на разных системах посылает либо байт 127, либо 8 — отсюда исторические войны и конфигурации через stty erase.

Коды обрабатываются в разных местах

Первое, что меня удивило: 33 control-кода делятся (очень условно) на три категории в зависимости от того, где они обрабатываются.

  • Обрабатываются OS terminal driver — например, когда ОС видит байт 3 (Ctrl-C), она посылает сигнал SIGINT текущей программе.
  • Передаются приложению как есть, и приложение делает с ними что хочет. Внутри этой группы ещё три подгруппы: соответствуют реальной клавише (Enter → байт 13, Tab, Backspace); используются библиотекой readline для редактирования строки (Ctrl-A, Ctrl-E, Ctrl-W); используются конкретными приложениями (Ctrl-X в emacs).

Никакой логики в том, какой код к какой категории относится, нет — просто так исторически сложилось.

Почему именно 33 — и почему «Ctrl-1» не существует

Ещё один сюрприз: control-кодов всего 33 штуки — буквы A–Z плюс семь дополнительных символов (@, [, \, ], ^, _, ?). Это значит, что если вы хотите сделать Ctrl-1 шорткатом в терминальном приложении — такого шортката просто не существует. На моей машине Ctrl-1 — это ровно то же самое, что нажать 1, а Ctrl-3 эквивалентно Ctrl-[.

Ctrl+Shift+C тоже не control-код — это комбинация, которую обрабатывает сам эмулятор терминала. В Linux Ctrl+Shift+X часто используется эмулятором для копирования, открытия новой вкладки или вставки — до TTY они не доходят.

Я всё время использую Ctrl+Left Arrow, но это тоже не control-код — это ANSI escape sequence (ESC[1;5D, где ESC — это сам байт 27 = Ctrl-[), совсем другая история.

Это устроено совсем не так, как шорткаты в GUI, где можно сделать Ctrl+любая клавиша.

Официальные ASCII-имена бесполезны

Каждый из 33 control-кодов имеет имя в ASCII (например, байт 3 — это ETX). Но эти имена придумывали не для компьютеров, а для телеграфа. Когда коды перешли в UNIX-терминалы, половина из них сменила смысл. В итоге ASCII-имена сегодня бесполезны в 50% случаев — проще вообще игнорировать их, чем пытаться понять, какие имена ещё соответствуют оригинальному значению.

Почему Ctrl-M и Ctrl-I — это Enter и Tab

Ctrl-M буквально идентичен Enter, а Ctrl-I — это Tab. Это делает их почти непригодными для шорткатов.

Некоторые всё равно используют Ctrl-I и Ctrl-M как шорткаты, но для этого надо настроить эмулятор терминала, чтобы он обрабатывал эти нажатия иначе, чем по умолчанию. Основной вывод: если пишете терминальное приложение — не используйте Ctrl-I и Ctrl-M как шорткаты.

Как узнать, какой именно код посылается

Пока я писала этот разбор, мне пришлось много экспериментировать с разными комбинациями клавиш. Я написала маленький Python-скрипт echo-key.py, который распечатывает получаемые байты. Наверняка есть более официальный способ, но мне удобнее иметь скрипт, который можно подкрутить под себя.

Оговорка: canonical vs noncanonical mode

Два кода — Ctrl-W и Ctrl-U — я в таблице пометила как «обрабатываются OS». На самом деле это не всегда так — зависит от режима терминала.

В canonical mode (построчный ввод) программа получает ввод только после нажатия Enter — до этого OS сама обрабатывает Backspace, Ctrl-W и другие правки. В noncanonical mode (посимвольный ввод) программа получает каждое нажатие сразу, и коды Ctrl-W и Ctrl-U проходят в программу, которая обрабатывает их как хочет.

Примеры программ в canonical mode:

  • Любые неинтерактивные утилиты вроде grep или cat.
  • git, по моему опыту.

В noncanonical mode:

  • python3, irb и другие REPL.
  • Ваш шелл.
  • Любой полноэкранный TUI (less, vim).

Оговорка: все OS-коды настраиваются через stty

Я написала «Ctrl-C посылает SIGINT», но технически это не обязательно так. Все коды, которые обрабатывает OS terminal driver, плюс Backspace, можно переназначить утилитой stty. Текущие маппинги смотрят через stty -a.

			$ stty -a
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
    eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;
    min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T;
    stop = ^S; susp = ^Z; time = 0; werase = ^W;
		

Я лично ни разу ничего не переопределяла через stty и не могу придумать, зачем бы мне это делать — это рецепт для путаницы и катастрофы. Но когда я спросила в Mastodon, люди назвали такие сценарии:

  • Починить сломанный терминал через stty sane.
  • Настроить работу Backspace: stty erase ^H.
  • Включить поток управления: stty ixoff.
  • Некоторые даже переназначают SIGINT на другую клавишу, например на DELETE.

Оговорка: сигналы можно отключить

  • Если режим терминала ISIG (флаг, разрешающий сигналы от Ctrl-C и Ctrl-Z) выключен, OS вообще не посылает сигналы. Vim, например, отключает ISIG при запуске.
  • На macOS и других BSD-системах есть дополнительный control-код Ctrl-T, который посылает SIGINFO.

Какие режимы ставит программа, можно посмотреть через strace: режимы терминала устанавливаются системным вызовом ioctl.

			$ strace -tt -o out vim
$ grep ioctl out | grep SET
		

Комбинация режимов, которую ставит vim при запуске, по сути и есть так называемый «raw mode» — подробности в man cfmakeraw.

Конфликтов очень много

Раз кодов всего 33, конфликтов за один и тот же код — навалом. По умолчанию Ctrl-S замораживает экран, но если выключить этот flow-control, readline использует Ctrl-S для forward search.

Другой пример: Ctrl-T на моей машине то посылает SIGINFO, то переставляет местами два последних символа, то делает что-то совсем третье — в зависимости от того, установлен ли в программе ISIG и использует ли она readline (или имитирует его поведение).

Backspace, «другой» Backspace и историческая боль

В таблице я пометила байт 127 как «backspace», а байт 8 как «другой backspace». История оказалась настолько запутанной, что именно этот пункт собрал больше всего откликов в Mastodon.

Вот как это работает на моей машине:

  1. Я нажимаю клавишу Backspace.
  2. В TTY уходит байт 127, в ASCII он называется DEL.
  3. OS terminal driver и readline оба замапили 127 на «удалить символ» — работает и в canonical, и в noncanonical mode.
  4. Предыдущий символ исчезает.

Если нажать Ctrl+H, в readline-приложении это сработает как Backspace, а в программе без readline (например, cat) просто напечатается ^H.

У кого-то шаг 2 другой: клавиша Backspace посылает байт 8 вместо 127, и чтобы она работала, надо настроить OS через stty erase ^H. В Debian Policy Manual есть целый раздел про конфигурацию клавиатуры — по моему пониманию, его написали в 90-х, когда царила путаница с тем, что должен делать Backspace, и нужен был какой-то стандарт, чтобы всё заработало.

На разных машинах всё по-разному

Я почти наверняка упустила ещё десяток способов, которыми «как это работает на моей машине» может отличаться от «как это работает у других», и в моих описаниях тоже могут быть неточности. По stty -a у меня есть ещё три экзотических маппинга — Ctrl-O как «discard», Ctrl-R как «reprint» и Ctrl-Y как «dsusp» — но на практике они редко что-то делают и чаще всего проходят в приложение как есть.

Честно говоря, это необязательно знать

Мне кажется, содержимое этого поста интересное, но не обязательно полезное. Я использовала терминал каждый день последние 20 лет, не зная почти ничего из этого — я просто знала на практике, что делают Ctrl-C, Ctrl-D, Ctrl-Z, Ctrl-R, Ctrl-L (ну и Ctrl-A, Ctrl-E, Ctrl-W), и не задумывалась о деталях. Этого почти всегда хватало, кроме случая, когда я возилась с xterm.js.
Julia Evansавтор блога jvns.ca и зинов про Linux

От редакции: знание это становится практическим в одном-двух сценариях, и их стоит держать в голове. Во-первых, когда терминал «ломается» — ввод не виден, Enter работает странно — вместо перезапуска шелла поможет stty sane или reset: теперь понятно, что именно они восстанавливают. Во-вторых, Ctrl-S, «замораживающий» терминал, — не баг, а реликт аппаратного flow control: отключить его навсегда можно через stty -ixon в .bashrc / .zshrc. Остальное — любопытная инженерная археология.

FAQ
1
Почему именно 33 кода, а не 32 или 128?

ASCII control characters занимают позиции 0–31 плюс 127 — это 33 байта с диапазоном ≤ 31 или = 127. Исторически старшие биты использовались для печатных символов, а диапазон 0–31 был зарезервирован под управление периферией телеграфа.

2
Что делает Ctrl-C на самом деле?

Когда терминал в canonical mode с включённым ISIG и дефолтными маппингами, нажатие Ctrl-C посылает в TTY байт 3. OS terminal driver перехватывает этот байт и отправляет сигнал SIGINT всей foreground process group — группе процессов, связанной с текущим TTY. Обычно это просто одна запущенная программа, но если запущен pipeline (a | b | c) — сигнал получат все процессы в нём.

3
Как отличить canonical от noncanonical mode?

Если программа ждёт нажатия Enter перед тем, как получить ваш ввод (cat без аргументов, grep) — canonical. Если реагирует сразу на каждую клавишу (vim, less, REPL) — noncanonical. Проверить точно можно через strace -e ioctl или чтение через stty -a.

4
Что делать, если терминал «сломался»?

Наберите вслепую stty sane и нажмите Enter — команда сбросит все настройки терминала в дефолтные. Если не помогло — reset (отрисовывает заново) или закрыть сессию и открыть новую. Подборку других удобных трюков мы собрали в материале «Трюки в терминале, которые реально экономят время (и нервы)».

5
Можно ли в терминальном приложении использовать Ctrl+цифра?

Нет, если только вы не просите пользователя перенастроить эмулятор терминала отдельно. Большинство эмуляторов (xterm, Alacritty, iTerm2, Windows Terminal) отправляют обычный код цифры вне зависимости от Ctrl. В GUI можно — там шорткаты обрабатывает оконный менеджер, а не TTY.

Итог

ASCII control characters — это слоёный пирог из трёх эпох: телеграфные корни 60-х, POSIX-терминалы 80-х и GUI-эмуляторы 2020-х, которые поверх всего этого ещё накручивают свою логику. Когда Ctrl-S внезапно замораживает экран, а Ctrl-T делает разные вещи в разных программах — это не баг, а побочный эффект 60 лет совместимости.

Оригинал: jvns.ca — ASCII control characters in my terminal. Также читайте на tproger: 7 новых TUI-инструментов для терминала и 15 полезных команд терминала macOS.