Unix родился не на персональном компьютере. Его запрограммировали через терминал телетайпа.
Unix родился не на персональном компьютере. Его запрограммировали через терминал телетайпа.

Unix родился не на персональном компьютере. Его запрограммировали через терминал телетайпа.

Вы, вероятно, видели эмулятор Unix-терминала. Изображение ниже — приложение macOS Terminal. Таких приложений много, мое любимое — iTerm2 (мое тоже — прим. переводчика). В Linux пользователи предпочитают терминал GNOME или приложение KDE Konsole.

Отображение списка содержимого каталога Godot с помощью приложения Terminal.
Отображение списка содержимого каталога Godot с помощью приложения Terminal.

Более общим термином для этих интерфейсов является CLI (интерфейс командной строки). Мое первое знакомство с CLI на самом деле было не с системой Unix или Microsoft DOS, а на компьютере Amiga 1000 под управлением AmigaDOS в синем окне CLI.

Командная строка Amiga (CLI) под управлением AmigaDOS.
Командная строка Amiga (CLI) под управлением AmigaDOS.

Концепция CLI довольно универсальна, но эта статья будет посвящена интерфейсам командной строки (CLI) Unix. Потому что они доминируют в современных операционных системах, таких как Linux, macOS и даже Windows. Да, в Windows свои CLI DOS и PowerShell, но Unix CLI захватывает Windows через подсистему Windows для Linux (WSL).

Командная строка Unix может сбивать с толку начинающих. Людей путает разница между оболочкой, такой как Bourne Again Shell bash, компьютерным терминалом, таким как VT100, и эмулятором терминала, таким как Konsole или GNOME Terminal. Для большей путаницы мы затронем псевдотерминалы.

Исходя из всего этого, появляется несколько вопросов:

  1. Почему, черт возьми, так сложно сделать интерфейс командной строки?

  2. Мне вообще нужно знать что-нибудь из этого? Можно я просто останусь в блаженном невежестве?

Очень долго я не знал основ командной строки Unix, но использовал ее без особых проблем. Тем не менее, не понимая основы технологии, вы часто будете застревать с пониманием важных вещей.

У меня было подобное с системой управления версиями Git. Долгое время я пытался придерживаться только знания команд. Относился к Git только как пользователь — человек, который знал, какие команды вводить. Это не работало. Я очень расстроился и был готов полностью отказаться от Git. Мой друг умолял меня не сдаваться. И я приложил последнее усилие, прочитав книгу «Git под капотом». Внезапно все щелкнуло. После я выступил с докладом, основанным на этом опыте, — Making Sense of the Git Version Control System.  Он помог многим людям, которые боролись с Git.

С командной строкой Unix так же. Понимая основные концепции, вы сможете работать в командной строке эффективнее.

Почему Unix CLI так сложен

Если бы вы строили CLI с нуля, то можно было многое упростить. Он был бы с самого начала разработан для мыши, клавиатуры и окон. Copy-paste в Windows и Linux работал бы с Ctrl-C и Ctrl-V, как в любом другом приложении. Горячие клавиши для перехода по одному слову или строке за раз работали бы, как в любом другом текстовом редакторе. Вы могли бы легко переместить курсор между буквами с помощью мыши.

Тем не менее, вы не можете сделать ничего из этого в Unix-терминале. И почему существуют эти, казалось бы, случайные ограничения, неочевидно.

Проблема в том что большое количество утилит Unix, таких как ls, telnet, ftp, ed, awk, sed, grep, tar, gzip, были созданы для компьютеров, которых больше не существует. Люди не хотели бросать все ПО, на котором они выросли и которое привыкли использовать. Также никто не хотел переписывать все с нуля. В ходе эволюции Unix-железа и ПО были созданы различные эмуляции, чтобы заставить существующие утилиты (типа grep и tar) думать, что они все еще работают на древних Unix-машинах.

По той же причине мы используем x86-процессоры до сих пор. Архитектура набора инструкций довольно устарела. Она не похожа на микропроцессор, который мы могли бы спроектировать с нуля. Однако дизайн архитектуры эволюционировал и обогатился большим количеством функций — так она осталась актуальной. Современная архитектура набора инструкций ближе к ARM, используемому сегодня в Apple M1 и M2, или RISC-V.

То же самое в языках программирования. C++ превратился в чудовище. Никто не разработал бы сегодня ничего похожего на C++, если бы ему дали чистый лист. Язык изначально стал популярен, так как разработчики могли переиспользовать свой старый код C. Позже C++ остался актуальным, из-за добавления в него новых фичей, при сохранении обратной совместимости со старыми версиями C++.

Таким образом, понимание любой широко используемой сегодня технологии сродни археологическим раскопкам. Это необходимо для понимания процесса инженерии и проектирования программ. Я работал над инструментами 3D-моделирования, написанными на C++, с 1992 года. Часто понимание архитектуры и дизайна приходит не путем изучения инженерных компромиссов, а благодаря урокам истории от старожилов.

Это может показаться пустой тратой времени, но я гарантирую, что история появления терминалов Unix поможет вам понять, почему система работает так, как она работает.

История Unix-терминала 

Цель этого раздела — помочь вам понять, как терминал, оболочка и эмулятор терминала связаны друг с другом.

Оригинальные мэйнфреймы Unix не имели электронных дисплеев, к которым мы привыкли. Это были не персональные компьютеры, а огромные коробки, предназначенные для нескольких пользователей. На столе у них вместо компьютеров было то, что называется телетайпом, известным также как телепринтер, телетайпер или TTY.

Эти машины функционировали как пишущая машинка и телеграф, скрученные в одно целое. У них не было памяти, микропроцессоров или чего-то подобного. По сути это было электромеханическое устройство. Вы печатали на нем буквы, которые отображались на бумаге, как на обычной печатной машинке. Ключевое отличие заключалось в том, что введенные буквы отправлялись по последовательному кабелю (RS-232) на Unix-компьютер.

Linux работает на телетайпе 1930-х годов. Результаты команд оболочки печатаются на физической бумаге.
Linux работает на телетайпе 1930-х годов. Результаты команд оболочки печатаются на физической бумаге.

Чтобы понять связь между компьютерами Unix и телетайпами, проведем аналогию с современным веб-миром. Google, Amazon и другие имеют множество серверов, обслуживающих веб. Пользователи могут подключаться к этим компьютерам через интернет со своих смартфонов, планшетов, ноутбуков и настольных компьютеров — через веб-браузеры, такие как Firefox, Chrome или Safari. В конечном счете у вас есть несколько пользователей, подключающихся к одному и тому же компьютеру через сетевые кабели. Сервер отвечает на запросы веб-страницами.

В Unix-мэйнфрейме все примитивнее. Вместо смартфона вы подключаетесь через электромеханическое устройство — телетайп, которое даже не настоящий компьютер. Телетайп не может выполнять расчеты или запускать программное обеспечение. Все, что он может сделать, — зарегистрировать буквы, которые вы нажимаете, и отправить их по последовательному кабелю на Unix-мэйнфрейм. Мейнфрейм отвечает, отправляя буквы обратно по последовательному кабелю. Они выбиваются на бумаге телетайп-подобной-машинкой по мере поступления.

Есть несколько заметных отличий от нашего современного интернет-мира. В сетях пересылают данные пакетами, такими как TCP/IP. Даже скромный USB-кабель фактически пересылает пакеты. Пакет — это автономный маленький кусок данных. Вы можете думать о пакете как о письме: он сообщает, откуда он, куда идет, и содержит некоторые данные. Таким образом, данные от нескольких разных клиентов могут перемещаться по одному и тому же сетевому кабелю на сервер. При получении данных по одному и тому же кабелю сервер отличает пакеты от различных устройств: смартфонов, десктопов и ноутбуков.

Сравнение Unix-терминалов и современных веб-клиентов.
Сравнение Unix-терминалов и современных веб-клиентов.

Unix-компьютер работал не так. Каждому телетайпу нужен отдельный последовательный кабель. Он не отправляет данные пакетами. Он просто отправляет двоичные знаки, представляющие клавиши, которые вы нажимаете, как только вы их нажимаете. Это простая система.

На самом деле телетайпные машины были сделаны не для компьютеров. Они были изобретены на замену телеграфа. Вместо постукиваний телеграфа и прослушивания звуковых сигналов на другом конце появилась роскошь печати целых текстов. Не нужно изучать азбуку Морзе.

В 1970-х годах телетайпы превратились в более современные электронные варианты, такие как VT100. Вы можете подумать, что это монитор и клавиатура, объединенные в одно устройство. Следовательно, вы можете подумать, что старый Unix-мейнфрейм — компьютер, который позволял подключать несколько экранов и клавиатур. Каждая комбинация клавиатуры и экрана служила разным пользователям.

Терминал VT100. Не ПК. Всего лишь соединяется с компьютером.
Терминал VT100. Не ПК. Всего лишь соединяется с компьютером.

Эти терминалы не обязательно подключались к мэйнфреймам Unix. Могли и ко многим другим системам, таким как VAX — это довольно универсальные устройства. Когда компьютеры Unix уменьшились и стали настольными компьютерами для отдельных пользователей — ПК, отношения между терминалами и компьютерами изменились. Unix получил оконную систему под названием X Window System, в которой вы запускаете остальное ПО. Один из типов приложений, которые вы могли запустить, — эмуляторы терминалов. Ранний вариант назывался просто xterm.

Эмуляторы терминалов притворяются старыми физическими терминалами, такими как VT100. Базовая система Unix по-прежнему думает, что вы печатаете на физическом терминале, подключенном последовательным кабелем. 

Но как именно вы обманываете Unix, заставляя его  считать окно эмулятора терминала «настоящим» терминалом? Это следующий вопрос, который мы исследуем.

Файлы устройств Unix, TTY и PTY

В Unix файловая система больше похожа на пространство имен для различных видов ресурсов операционной системы. Это не просто файлы на диске. Большинство файлов сопоставляются с файлами на жестком диске. Однако в Unix файлы также могут представлять собой физические устройства, такие как клавиатура, мышь, аудиокарта, жесткий диск, дискета, последовательный порт с модемом или терминалом.

Вы можете посмотреть эти устройства в директории /dev. Эти файлы позволяют взаимодействовать с драйверами, которые в свою очередь работают с физическими устройствами. Драйвер — это ПО, позволяющее физическому устройству выглядеть в ОС как файл в директории /dev. В давние времена Unix взаимодействовал с каждым последовательным интерфейсом (портом) через файлы /dev/ttyS0, /dev/ttyS1, /dev/ttyS2 и т.д. (S означает «serial port» — последовательный порт).

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

Презентация соединения как простого файла в ОС зарекомендовало себя отличной абстракцией. Поставщики Unix могли писать новые драйверы для подключения к терминалу, и они работали так же. Unix-программы могли писать и читать файлы /dev/tty, не зная, что они сейчас соответствуют окнам в графическом интерфейсе. Unix выжил во многом благодаря ранее созданным могучим абстракциям.

Unix-пользователи подключаются физическими терминалами через TTY-устройства и запускают различные команды типа telnet, rsh, rcp и rlogin (смотри r-commands). Все они подразумевают подключение к другой Unix-машине. Несколько Unix-машин могут быть соединены через TCP/IP-сеть. Скажем, пользователь Боб сидит за старым телетайпом и подключается через последовательный порт к мейнфрейму Asterix. На нем он запускает программу telnet для подключения к другой Unix-машине Obelix, которая подключена к Asterix через TCP/IP-сеть.

Удаленный запуск команды ls на другом мэйнфрейме Unix, отличном от того, к которому подключен терминал Боба.
Удаленный запуск команды ls на другом мэйнфрейме Unix, отличном от того, к которому подключен терминал Боба.

Боб на удаленной машине Obelix выполняет разные команды типа ls. Каким образом ls понимает куда отправлять вывод? Ни одно из TTY устройств не подойдет, так как они соответствуют физическим портам, а Боб подключен через TCP/IP соединение.

Решение — псевдотерминал (pseudoterminal), сокращенно PTY. Они так же представляются файлами в /dev, создаваемыми на лету сетевыми приложениями. В реальности эмуляторы терминалов функционируют как псевдотерминалы. Они также не являются физическими устройствами, а вы также можете создавать сколько угодно окон терминалов. Например в macOS используются названия /dev/ttys000, /dev/ttys001 и т.д. Они создаются при запуске новых терминальных окон и вкладок.

В macOS вы можете проверить, с каким TTY-устройством взаимодействует окно эмулятора терминала, выполнив команду tty.

❯ tty/dev/ttys000

Вы можете запустить команду process info с параметром -a, чтобы получить список всех используемых устройств TTY и информацию о том, какие программы на них запущены. В выводе видно, что сразу после создания ttys000 выполнена команда login -fp erikengheim.

❯ ps -a

PID   TTY        TIME    CMD

25797 ttys000    0:00.02 login -fp erikengheim

25798 ttys000    0:00.10 -fish

25898 ttys000    0:00.00 ps -a

25849 ttys001    0:00.02 login -fp erikengheim

25850 ttys001    0:00.09 -fish

25897 ttys001    0:00.00 nc -l 1234

При запуске программ в Unix создаются так называемые процессы. Процесс — это репрезентация запущенной программы в ОС. Процесс может запускать дочерние процессы. Например, процесс login запустил процесс оболочки fish, который в свою очередь запустил процесс ps -a, который и сделал вывод.

Параллельно во втором открытом окне /dev/ttys001 появился ряд других процессов. Начало то же самое, но я решил запустить программу NetCat для прослушивания соединений на порту 1234.

Это означает, что я могу отправлять и получать сообщения через NetCat, подключившись по сети к порту 1234 на localhost. Все, что он сделает, это переадресует данные на псевдотерминал /dev/ttys001, в котором запущен NetCat, и прослушает порт 1234. Проверим, как все это работает. Создадим два отдельных окна терминала:

  • Сервер — запустим в этом окне nc -l 1234 

  • Клиент — подключимся командой telnet localhost 1234

Можно написать сообщение в окне клиента и увидеть, как оно появится в окне сервера:

❯ telnet localhost 1234

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

hello world

На стороне сервера увидим:

❯ nc -l 1234

hello world

Если вы подключаетесь с другого компьютера, то надо использовать telnet. Но, так как у нас все локально, можно просто писать и читать напрямую /dev/ttys001.  Для выхода из telnet нажмите Ctrl-]. Это вызовет приглашение telnet для ввода его команд, далее выполним quit:

❯ telnet localhost 1234

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

^]

telnet> quit

Connection closed.

❯

Давайте перезапустим NetCat, чтобы сделать наш маленький чит. Во-первых, мы выполним команду tty, чтобы убедиться в номере TTY. На macOS это /dev/ttys001. Если вы на Linux, возможно, получите что-то другое.

❯ tty

/dev/ttys001

❯ nc -l 1234

Переключимся на второй терминал и попробуем отправить текст в /dev/ttys001 или другой ваш TTY.

❯ echo we are cheating > /dev/ttys001

В другом окне появится «we are cheating».

❯ nc -l 1234

we are cheating

Аналогично можно читать из TTY как из файла командой cat. В этом случае cat будет молча висеть в консоли, пока вы не отправите сообщение.

Использование NetCat для демонстрации работы TTY
Использование NetCat для демонстрации работы TTY

Можно сообщить об окончании коммуникации отправкой Ctrl-D через NetCat. Команда cat на другом конце интерпретирует это как окончание и закроется. Технически это соответствует EOF (End-of-File).

❯ nc -l 1234

we are cheating

A message from NetCat

С другой стороны:

❯ cat /dev/ttys001

A message from NetCat

Это сработает с любой программой, читающей и пишущей в файл. Например, так же будут работать такие текстовые редакторы, как vim и emacs. При сохранении файла они напишут в TTY и вы увидите редактированный текст в окне NetCat.

Последовательный порт через USB-кабели

Эмулировать последовательный порт для сохранения обратной совместимости может быть полезно во многих контекстах. Выглядит так, будто окно терминала подключено по кабелю RS-232 к оболочке Unix с использованием псевдотерминалов. На Mac они представлены файлами /dev/ttys. Это все происходит на программном уровне. Можно использовать тот же трюк с железом, которое не является последовательным интерфейсом.

Для чего это может пригодиться? Немногие современные компьютеры оснащены последовательными портами RS-232, вместо них USB-порты. USB — гораздо более сложный протокол, больше похожий на сетевое соединение, передающее пакеты данных, а не отдельные символы, как старые добрые Unix TTY.

Сравнение разъемов RS-232 и USB.
Сравнение разъемов RS-232 и USB.

Программно эмулируя интерфейс последовательного соединения, мы можем написать код для связи с микроконтроллером, таким как Arduino, как если бы мы подключились к нему через старомодный порт RS-232. А на плате Arduino есть разъем USB, аппаратное обеспечение которого преобразует USB в последовательное соединение. Таким образом, микроконтроллер AVR на плате Arduino считает, что он подключен по последовательному кабелю. То есть мы написали код для последовательной связи на обеих сторонах.

Плата Arduino UNO с портом USB-B. На плате аппаратный конвертер USB-to-serial.
Плата Arduino UNO с портом USB-B. На плате аппаратный конвертер USB-to-serial.

При подключении Arduino в ваш ПК или Mac по USB появится файл  /dev/cu.usbserial-10 (точное имя может отличаться). Посмотреть такие файлы можно командой ls /dev/cu*. Почему имя /dev/cu* вместо /dev/tty*? В прежние времена это были Dial-up модемы. Вы могли, сидя дома, подключаться к Unix-мейнфрейму своим терминалом через такой модем. А TTY-устройства /dev/tty* были для людей в здании с мэйнфреймами и подключавшихся напрямую.

Есть ключевое отличие в использовании управляющего сигнала DTR (Data Terminal Ready) на порту RS-232. Программа будет ждать, пока не появится сигнал DTR. А телетайп отправит сигнал DTR, показывая, что он готов к общению.

Для модемов это работает немного по-другому: сначала вы даете модему команды о том, куда и как подключиться. В этом случае DTR используется для сигнализации об установленном соединении. Модемы используют устройство CU.

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

Несмотря на долгое повествование, я до сих пор не объяснил, почему эмуляторы терминалов довольно сложны и могут быть настроены для эмуляции многочисленных терминалов, таких как VT100, xterm, ANSI, rxvt и других.

Коды управления и эмуляторы терминалов

Если бы все, что терминал должен отправлять и получать, было видимыми символами, то все было бы очень просто. Но дело в том, что нам нужно гораздо больше, чем видимые символы. При отправке текста в терминал нужен способ сказать ему перейти в начало строки или начать новую строку. При наличии цветных картриджей вам нужен способ сказать ему изменить цвет печати на бумаге.

Пользователь может захотеть отредактировать или изменить текст. Таким образом, мы должны иметь возможность перемещать курсор вперед, назад, вверх и вниз, перейти на следующую страницу, стереть строку и так далее. Чтобы представить все эти «невидимые» символы, разработали контрольные коды. Различные терминалы используют разные коды управления, поэтому не все они совместимы друг с другом.

К счастью, появился стандарт ANSI, который большинство людей, работающих с терминалами, используют сегодня. Со многими вы, возможно, знакомы, если программировали на C или языке, вдохновленном C:

  • Line feed 0x0A \n. Перейти к следующей строке. Пишется \n в C коде. В стандартном Unix-терминале вы отправляете его в TTY хоткеем Ctrl-J.

  • Carriage Return 0x0D \r. Перейти к началу строки. В TTY отправка хоткеем Ctrl-M.

  • Backspace 0x08 \b. На один символ назад (влево), обычно с удалением буквы на этом месте. В TTY хоткей Ctrl-H.

  • Tab 0x09 \t. Сдвиг вправо на 8 мест. Хоткей Ctrl-I.

  • Vertical Tab 0x0B \v. Вертикальная табуляция — вниз на 1 строку.

  • Escape 0x1B \e or \x. Начать экранирование ввода (escape sequence) или выход из текущего режима работы. Хоткей Ctrl-[

  • EOT0x04. Конец передачи. Нажимая Ctrl-D, вы сообщаете терминалу об окончании ввода символов.Так же известно как EOF, так как вызывает условие  End-of-File на другом конце. Однако EOF — это все же условное состояние. В настоящих файлах в конце нет символа 0x04.

Но вы же не нажимаете Ctrl-J для ввода команд в терминале. Вместо этого используете клавишу Enter. Почему? 

Это часть сложности терминальных эмуляторов. Эмулятор терминала видит, что вы нажимаете клавишу Enter, Tab или Escape и транслирует их в комбинации клавиш, типа Ctrl-J и Ctrl-I. Иногда можно даже изменить это сопоставление. Вы можете посмотреть в своем TTY, как настроены различные контрольные символы (cchars):

❯ stty -a

speed 38400 baud; 15 rows; 69 columns;

lflags: icanon isig iexten echo echoe echok echoke -echonl echoctl

	-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo

	-extproc

iflags: -istrip icrnl -inlcr -igncr -ixon -ixoff ixany imaxbel iutf8

	-ignbrk brkint -inpck -ignpar -parmrk

oflags: opost onlcr -oxtabs -onocr -onlret

cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow

	-dtrflow -mdmbuf

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;

Escape-кодов так много, что у нас быстро закончились бы символы — в нашем распоряжении только 7 или 8 бит. Поэтому были созданы escape-коды. Они начинаются с клавиши Escape (код ASCII 0x1B) и позволяют использовать несколько символов для описания необходимой операции. Общий стандарт, используемый сегодня, называется escape-кодами ANSI — вот полезная шпаргалка.

Escape-коды позволяют много чего — например, переместить курсор в определенное положение, изменить цвета символов, очистить экран или даже выключить перенос. Редакторы текстовых режимов, такие как Emacs, Vim, Pico и Kakoune (удобный для пользователя Vim), широко используют escape-коды для создания своих текстовых интерфейсов. Некоторые примеры:

  • ESC[H — перемещение на домашнюю позицию (0, 0),

  • ESC[nA — перемещение курсора вверх на n линий,

  • ESC[n;mH — перемещение в положение (n, m),

  • ESC[0m — сбросить все режимы и цвета,

  • ESC[31m — сделать текст красным.

Вы можете запустить язык программирования, такой как Julia или Python, и попробовать эти escape-коды. Вот код Джулии, который меняет цвет текста на красный, пишет буквы, а затем выполняет сброс:

❯ julia

julia> println("\e[31m Hello world \e[0m")

Hello world

То же на Python, но чуть сложнее, так как вы не можете использовать escape-код \e :

❯ python

>>> print("\x1b[31m Hello world \x1b[0m")

Hello world

Можно провернуть то же самое прямо в Unix-терминале. Но не все оболочки это поддерживают. У меня не получилось в Z Shell zsh и Bourne Again Shell bash, но заработало в Fish shell fish. Главное — запомнить экранирование квадратной скобки.

❯ echo \e[31mhello\e[0m world

hello world

Терминалы, файловые дескрипторы и пайпы

Мы приближаемся к полной картине того, как работают терминалы. Есть важный недостающий момент: как команды и программы Unix направляют свои выходные данные на правильный терминал? Иногда программа работает удаленно и должна отправлять выходные данные через псевдотерминал по сети.

На деле Unix-программы еще более гибкие. Можно не только перенаправить вывод в другой терминал, но даже в файл. Например, выполнив ls, я получу список директорий отправленный в мой TTY. Но, если написать ls > foo.txt, список будет отправлен в файл foo.txt. Команда rev просто разворачивает все, что вы напишите (выход хоткеем Ctrl-D):

❯ rev

hello

olleh

world

dlrow

Но идеально сработает и ввод из файла вместо печати rev < foo.txt. В примере создадим файл с содержимым и скормим его в rev, используя символ перенаправления <.

❯ cat > hello.txt

hello

world  # Press Ctrl-D

❯ cat hello.txt

hello

world

❯ rev < hello.txt

olleh

dlrow

При запуске программы, например rev, вы создаете процесс (представление запущенной программы в памяти). Процесс Unix использует пронумерованные файловые дескрипторы для связи с файлами. Под капотом при открытии файла создается файловый дескриптор для ссылки на этот файл.

Процесс Unix имеет три стандартных файловых дескриптора с номерами 0, 1 и 2, которые создаются всегда:

  • стандартный вход — stdin

  • стандартный выход — stdout

  • стандартная ошибка — stderr

На деле вы можете найти их в /dev. Тут, вероятно, могут быть отличия между различными операционными системами Unix. Я опишу, как это работает в macOS. Каждый из этих файлов является ссылками на пронумерованные файловые дескрипторы (fd).

  • /dev/stdin — ссылка на /dev/fd/0

  • /dev/stdout — ссылка на /dev/fd/1

  • /dev/stderr — ссылка на /dev/fd/2

В примерах мы оставим stderr и сфокусируемся на stdin и stdout. Можно представить, что у процесса есть три разъема с названиями stdin, stdout и stderr, в которые можно подключать файлы или файлоподобные объекты. На картинках ниже показаны соединения с сокращенными названиями in и out.

Перенаправление ввода из файла.
Перенаправление ввода из файла.

По умолчанию /dev/stdin и /dev/stdout указывают на стандартный терминал /dev/tty, который указывает на псевдотерминал типа /dev/ttys001. Символами перенаправления  < и > мы заменяем дескрипторы процесса на необходимые.

Гениальное изобретение в этой абстракции — концепция трубы aka пайп. Труба немного похожа на файл, с одной особенностью: два процесса могут держать ее открытой одновременно. Один процесс пишет в нее, а другой читает из нее. (Еще это похоже на FIFO-буфер. Здесь и далее я буду использовать более привычный термин «пайп» вместо дословного “труба” — прим. переводчика) 

Создать пайп можно командой mkfifo. Создадим пайп tube:

❯ mkfifo tube

❯ file tube

tube: fifo (named pipe)

Мы создали запись в файловой системе под названием tube, которая останется там даже после перезагрузки. Однако данные, записанные и прочитанные из именованного канала, никогда не хранятся на диске, и обрабатываются в памяти. Давайте посмотрим, как это работает на практике.

Настройка связи через именованный пайп.
Настройка связи через именованный пайп.

Нам понадобится два окна терминала. В первом напишем rev < tube, что означает чтение из именованного пайпа. Во втором напишем cat > tube — все, что мы напишем в терминал, отправится в tube, и процесс rev сможет прочитать это из пайпа.

Чаще всего нам не нужны именованные пайпы и не нужно хранить их постоянно. Вполне достаточно создавать временные пайпы, чтобы изящно перенаправлять данные от одной команды в другую. Для создания временных пайпов используется символ |.

❯ echo hello | rev

olleh

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

Сложные комбинации пайпов позволяют собирать в цепочки функции нескольких команд. На примере рассмотрим цепочку из ls, head и sort:

❯ touch hotel golf foxtrot echo

❯ ls | head -3

echo

foxtrot

golf

❯ ls | head -3 | sort -r

golf

foxtrot

echo

Команда head -3 берет первые три строки, далее sort -r сортирует ввод в обратном порядке. Как stdout и stdin соединены пайпами, показано на картинке:

Пример объединения трех команд с использованием пайпов.
Пример объединения трех команд с использованием пайпов.

Примеры выше показывают всю мощь Unix по перенаправлению ввода и вывода в разные места, при этом программы понятия не имеют, что происходит. Команде ls не надо знать, происходит ли вывод в пайп, файл или в псевдотерминал, созданный telnet-клиентом. Может быть даже передача данных через USB-кабель, притворяющийся последовательным портом.

Таким образом, когда в программе выполняется простой код print («hello world») за сценой происходит немало:

  • Текст записан в файл /dev/stdout, но это просто ссылка на /dev/fd/1

  • /dev/fd/1 ссылается на /dev/tty — текущее TTY-устройство.

  • /dev/tty будет указывать на фактический псевдотерминал, такой как /dev/ttys001, который ссылается на фактическое окно эмулятора терминала.

Вот так «hello world» отправляется в правильное окно в вашем пользовательском интерфейсе. Конечно, можно перенаправить /dev/stdout, чтобы отправить текст куда то еще.

Терминал vs Оболочка

Unix-терминал — это просто труба, через которую мы прокачиваем символы в /dev/tty и обратно, плюс контрольные символы. Сам по себе терминал предоставляет не особо много фич.

Чтобы анализировать пользовательский ввод, интерпретировать его как команды для запуска команд и отображения результатов, нам нужен Unix Shell (оболочка). Оболочка управляет пользовательским приглашением (prompt), куда вы вводите команды.

Также оболочка управляет заданиями — например, позволяет переключаться между различными запущенными процессами, если один процесс займет много времени. Не все оболочки имеют один и тот же синтаксис. Во многом можно рассмотреть языки программирования (Python, Ruby и Julia) как оболочки. Однако эти среды программирования не очень подходят в качестве оболочек Unix, так как файлы, процессы, программы, каталоги и каналы не являются главенствующими объектами.

Оболочки Unix, например bash, не очень хороши как языки программирования, но хороши в управлении файлами и заданиями. На диаграмме ниже показано отношение между терминалами и оболочками.

На картинке терминалы показаны как физические устройства. В современном мире это, конечно, редкость. Обычно они существуют в виде эмуляторов терминалов, внутри какой-либо оконной системы.

Когда вы открываете окно эмулятора терминала, он запускает программу login, которая запускает оболочку. Она запустит одну из оболочек, перечисленных в файле /etc/shells, если вы используете macOS:

❯ cat /etc/shells

/bin/bash

/bin/csh

/bin/ksh

/bin/sh

/bin/tcsh

/bin/zsh

/usr/local/bin/fish

Можно заметить, как много оболочек было создано за много лет. В современной macOS Z Shell zsh стал стандартной оболочкой, хотя до этого и Linux, и macOS долгое время использовали Bourn Again Shell bash по умолчанию.

В давние времена программа login смотрела в файл /etc/passwd, чтобы залогинить пользователя. В нем программа проверяла наличие пользователя и корректность введенного пароля. Далее login запускала указанную оболочку. Для каждого пользователя в /etc/passwd есть строка вида:

tommy:x:1000:1000::/home/tommy:/bin/bash

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

Теперь мы используем команды для изменения конфигурации — например, оболочки. Для смены оболочки используйте команду chsh (CHange SHell). Вот как я изменил оболочку на fish:

❯ chsh -s /usr/local/bin/fish

На этом пока все. Невозможно охватить все о терминалах и оболочках Unix в одной статье, поэтому я буду добавлять связанные статьи по мере их написания.

Если интересуетесь историей IT, у нас есть тексты для вас:

→ История системного вызова chroot и его применение в современности

8 мифов о микропроцессорах RISC-V — ответ критикам

Что означает RISC и CISC?

Комментарии (15)


  1. sYB-Tyumen
    01.09.2022 14:39
    +3

    Проблема в том что большое количество утилит Unix, таких как lstelnetftpedawksedgreptargzip, были созданы для компьютеров, которых больше не существует.

    Ограничения, связанные с утилитами, скорее обусловлены тем, что все они работают с потоками данных. А концепция потока данных для современного пользователя неочевидна (хотя сейчас с появлением возможности стримить что угодно должна восприниматься легче, чем 10-15 лет назад, когда основной единицей обработки для пользователя был файл, если не "документ word").


    1. fortyseven Автор
      01.09.2022 15:15

      Все так, далее по тексту мысль раскрывается


  1. saboteur_kiev
    01.09.2022 15:16
    +1

    Терминал - это физическая часть интерфейса
    Оболочка (shell) - софтварная

    Я с этого все лекции по введению в Линукс начинаю. И что вывести на экран, если мы в винде запустили какой-нить putty, подключились по сети к серверу который находится в другом городе вместе с еще десятком пользователей - заставляет подумать что же такое "вывести на экран", и понять что stdout это немножко другое.

    А насчет "мы читим" - так для чата в пределах компа через tty/pts давно была придуманы команды write/mesg/wall и группа tty


    1. Wesha
      02.09.2022 08:44
      +2

      Я так объясняю: сначала у компьютера были один экран и одна клавиатура. Потом один умный человек сказал — "а почему одна, всё равно для компьютера это устройство ввода-вывода, давайте подключим две (а где две — там и пятнадцать), и тогда с компьютером будут работать много пользователей". Потом другой умный человек сказал — "а зачем их подключать шнуром — пусть /dev/ptyXXX делает вид, что оно и есть этот самый шнур — а как реально оно подключено, уже неважно", ну и так далее и так далее. Смысл в том, что начинаем с простого, а потом на следующем этапе нечто делает вид, что оно — это то простое, что было, когда этого нечто ещё не было.


  1. ugenk
    01.09.2022 18:01

    В bash и zsh echo встроенный, и он не поддерживает esc-последовательности. Надо использовать внешний /bin/echo -e


    1. selivanov_pavel
      02.09.2022 16:01
      +2

      $ bash -c 'help echo' | grep -- '-e'
      -e enable interpretation of the following backslash escapes
      `echo' interprets the following backslash-escaped characters:


  1. andreishe
    01.09.2022 20:43
    +1

    А как мне послать что-то в другой pty, чтобы оно там воспринялось как ввод (а не просто появилось на экране)?


    1. Tim777
      02.09.2022 09:22
      +1

      tmux send-keys


      1. andreishe
        03.09.2022 05:19

        Дык для этого поди ж tmux должен быть запущен на принимающей стороне?


    1. fortyseven Автор
      03.09.2022 10:11

      Через пайп сделать кат в другой терминал прямо по его имени или через < перенаправление. Кажется в статье как раз такой пример.


      1. andreishe
        03.09.2022 21:47
        +1

        Запись в файл терминала просто выводит записанное на экран. Мне надо, чтобы записанное было воспринято как ввод.

        Например, есть у нас два терминала: /dev/pts/0 и /dev/pts/1, в обоих запущен bash, оба просто ждут ввода. Если я в баше нулевого терминала запущу echo ls >/dev/pts/1, то строка ls просто появится во втором терминале. Bash ее не увидит и не выполнит.


        1. fortyseven Автор
          04.09.2022 08:26

          А, понятно. Только непонятно зачем.

          Ведь весь смысл терминалов, что за каждым сидит отдельный пользователь со своим вводом и выводом. Вам бы не понравилось если бы ваш терминал принимал ввод от другого пользователя.

          Скорее всего так можно сделать, но это ломает всю концепцию терминалов.


  1. bo4kare8
    02.09.2022 15:27

    не получилось в Z Shell zsh и Bourne Again Shell bash
    В bash:
    echo -e '\e[31mhello\e[0m world'
    В zsh:
    echo '\e[31mhello\e[0m world'


    1. fortyseven Автор
      02.09.2022 15:29

      В macos Terminal app


  1. bo4kare8
    02.09.2022 15:42
    +1

    я буду добавлять связанные статьи по мере их написания

    Супер! Ждём!