Вопрос или проблема
Учитывая команду, которая изменяет свое поведение, когда ее вывод отправляется в терминал (например, создает цветной вывод), как можно перенаправить этот вывод в конвейере, сохранив измененное поведение? Должен же быть утилита для этого, о которой я не знаю.
Некоторые команды, такие как grep --color=always
, имеют опции, заставляющие их вести себя определенным образом, но вопрос в том, как обойти программы, которые полагаются исключительно на проверку своего файлового дескриптора вывода.
Если это важно, моя оболочка – bash
на Linux.
История инструментов
Вы не первый, кто хочет такой инструмент. Люди хотят такие инструменты уже 30 лет. И они существовали почти столько же времени.
Первые инструменты для этого рода вещей были пакетом “pty” Даниэля Дж. Бернштейна, который Рич Сальц описал как “нож Гинзу”, написанный Бернштейном в начале 1990-х, чтобы жульничать в nethack (прим.). Версия 4 пакета “pty” была опубликована в 1992 году в comp.sources.unix
(том 25, выпуски 127 по 135). Он все еще доступен в Интернете. Пол Викси на то время описал это:
Что я могу сказать? Он нарезает, рубит, моет посуду, выгуливает собаку. Он “просто работает”, что означает, что если вы следуете инструкциям, вы получите рабочий пакет без вырывания волос и скрежета зубов или других стандартных портировочных действий.
Позже Бернштейн обновил это, где-то на или до 1999-04-07, с пакетом “ptyget”, который он анонсировал:
Я собрал новый выделитель псевдотерминалов, ptyget. Альфа-версия доступна по адресу
ftp://koobera.math.uic.edu/pub/software/ptyget-0.50.tar.gz
. Есть рассылка ptyget; чтобы присоединиться, отправьте пустое сообщение на[email protected]
. Я разработал интерфейс ptyget с нуля. Он гораздо более модульный, чем pty; базовый интерфейс pty теперь разделен на три части:
ptyget
: крошечная, низкоуровневая программа – единственная программа setuid в пакете – которая выделяет новый псевдотерминал и передает его программе по вашему выборуptyspawn
: еще одна небольшая программа, которая запускает дочерний процесс под псевдотерминалом, ждет его завершения и следит за остановкамиptyio
: еще одна, лишь немного большая программа, которая перемещает данные взад и впередСтарый нож Гинзу
pty
теперь называетсяptybandage
, что является синонимом дляptyget ptyio -t ptyspawn
;pty -d
, для подключения сетевых программ к псевдотерминалам, теперь называетсяptyrun
, что является синонимом дляptyget ptyio ptyspawn
; иnobuf
является синонимом дляptyget ptyio -r ptyspawn -23x
. Я выделил функции управления сессиями в отдельный пакет.
Этот отдельный пакет стал пакетом “sess”.
“ptyget” является, кстати, примечательным тем, что иллюстрирует очень раннюю версию, и один из немногих опубликованных случаев, собственный никогда не опубликованный “redo” сборочной системы Бернштейна. dependon
является явным предшественником redo-ifchange
.
Использование
ptybandage
ptybandage
— это то, что люди обычно хотят в сеансе входа. Его основное применение заключается в том, чтобы заставить программы, чувствительные к тому, подключены ли их стандартные входы, выходы или ошибки к терминалам, работать так, даже если они фактически находятся в оболочных конвейерах или имеют свои стандартные файловые дескрипторы перенаправлены в файл.
Он принимает команду для выполнения (которая, конечно, должна быть корректной внешней командой) и выполняет ее таким образом, чтобы она думала, что ее стандартный ввод, вывод и ошибка подключены к терминалу, соединяя их с оригинальным стандартным вводом, выводом и ошибкой ptybandage
.
Он справляется с нюансами работы под оболками управления задачами, обеспечивая, чтобы символ STOP терминала останавливал не только ptybandage
, но и программу, работающую внутри терминала.
ptyrun
ptyrun
— это то, что люди обычно хотят в TCP сетевых серверах. Его основное применение состоит в удаленных средах выполнения, которые сами не настроили терминалы, запускающих программы, которые не работают должным образом, когда терминала нет.
Ожидается, что он не будет запущен под оболкой управления задачами, и если выполняемая команда получает сигнал остановки, она просто перезапускается.
Доступные наборы инструментов
Дру Нельсон публикует как “pty” версию 4, так и “ptyget”.
Пол Ярк публикует исправленную версию ptyget, которая пытается решить проблемы с ioctl’ами псевдотерминальных устройств, специфичными для операционной системы, в оригинале которых операционные системы уже не предоставляют.
Пакет исходных кодов nosh поставляется со скриптами-аналоги ptybandage
и ptyrun
, которые используют инструмент execline
Лорана Берко и собственные команды управления псевдотерминалом пакета nosh. Начиная с версии nosh 1.23, они доступны предварительно упакованными в пакете nosh-terminal-extras. (Ранее версии поставляли их только тем, кто собирал из исходников.)
Несколько примеров использования
Юрген Оскам использует ptybandage
на AIX, чтобы передать ввод из here-документа в программу, которая явно открывает и читает свой управляющий терминал для запроса пароля:
$ ptybandage dsmadmc <<EOF >uit.txt joskam password query session query process quit EOF
Энди Брэдфорд использует ptyrun
на OpenBSD под daemontools и ucspi-tcp, чтобы сделать интерактивную программу управления маршрутизатора bgplgsh
доступной через сеть, заставляя ее думать, что она общается с терминалом:
#!/bin/sh exec 2>&1 exec envuidgid rviews tcpserver -vDRHl0 0 23 ptyrun /usr/bin/bgplgsh
Дополнительное чтение
- Даниэль Дж. Бернштейн (1992-02-19). v25i127: Унифицированный интерфейс для псевдотерминальных устройств. comp.sources.unix. 25 (127).
- Даниэль Дж. Бернштейн (1991-10-04). Введение в управление сессиями. Бернштейн о TTY. JdeBP.
- Пол Ярк. ptyget. Программное обеспечение Пола Ярка.
- Джонатан де Бойн Поллард (2016). Инструментальный набор ptyget Даниэля Дж. Бернштейна. Программное обеспечение.
- Дру Нельсон. drudru/pty4. GitHub.
- Дру Нельсон. drudru/ptyget. GitHub.
- Лоран Берко. execline. программное обеспечение.
- Джонатан де Бойн Поллард (2016). Все программное обеспечение Даниэля Дж. Бернштейна в одном. Программное обеспечение.
- Джонатан де Бойн Поллард (2014). Пакет nosh. Программное обеспечение.
- Джонатан де Бойн Поллард (2012). Введение в
redo
. Часто задаваемые вопросы. - Рейк Флоётер.
bgplgsh
. 8. Руководства OpenBSD.
Вы можете получить то, что вам нужно, используя unbuffer
.
unbuffer
– это скрипт tcl
/ expect
. Посмотрите на исходный код, если хотите. Также обратите внимание на раздел CAVEATS в man.
Также обратите внимание, что он не выполняет алиасы, такие как:
alias ls="ls --color=auto"
если только вы не добавите трюк, как указано Стефаном Шазеласом:
Если вы выполните alias unbuffer="unbuffer "
(обратите внимание на пробел в конце), тогда алиасы будут расширяться после unbuffer
.
Вы можете использовать socat, чтобы запустить ваш процесс с подключенным pty, и заставить socat подключить другой конец pty к файлу. Что, насколько я понимаю, именно то, что вы спрашивали:
socat EXEC:"my-command",pty GOPEN:mylog.log
Этот метод заставит isatty
, вызываемую my-command
, вернуться true
, и процесс, который на это полагается, будет обманут, чтобы выводить управляющие коды. Обратите внимание, что некоторые процессы (в частности, grep
) также проверяют значение переменной окружения TERM
, так что возможно, вам нужно будет установить его на что-то разумное, например, "xterm"
.
Также есть хорошее решение, опубликованное здесь на Super User Кэрлом:
Скомпилируйте небольшую общую библиотеку:
echo "int isatty(int fd) { return 1; }" | gcc -O2 -fpic -shared -ldl -o isatty.so -xc -
Затем скажите вашей команде грузить этот isatty(3)
переопределение динамически:
LD_PRELOAD=./isatty.so mycommand
Это может не сработать для каждой команды, возможно, даже сломает некоторые непредсказуемым образом, но, вероятно, будет работать в большинстве случаев.
Как насчет использования script(1)
?
Например:
script -q -c 'ls -G' out_file
Сохраняет вывод ls
в out_file
с сохранением цветовых кодов.
Недовольный предложенными здесь решениями, я выпустил python. Она была эффективной. Это решение не требует прав setuid или какого-либо действительно безумного monkey-patching с общими библиотеками и LD_LIBRARY_PATH
. Сохраните этот скрипт где-нибудь в вашем PATH
. Не забудьте chmod +x
его. Предположим, вы сохраняете этот скрипт как pty
.
#!/usr/bin/env python
from sys import argv
import os
import signal
# У меня были проблемы с объектами File python на таком низком уровне, поэтому
# мы будем использовать целые числа, чтобы указать все файлы в этом скрипте.
stdin = 0
stdout = 1
stderr = 2
# Включите это, если передаете команду и аргументы fish, чтобы
# предотвратить применение каких-либо расширений.
#import re
#def fish_escape(args):
# def escape_one(arg):
# return "'" + re.sub(r"('|\\)", r'\\\1', arg) + "'"
# escaped_args = map(escape_one, args)
# return ' '.join(escaped_args)
if len(argv) < 2:
os.write(stderr,
b"""Трагически красивая часть хакерства, созданная, чтобы обмануть программы, такие как ls,
grep, rg и fd, заставляя их думать, что они действительно подключены к терминалу.
Его использование:
pty команда [arg1 arg2 ...]
Примеры:
pty ls --color -R | less -r
git log -p | pty rg <поисковые термины> | less -r
""")
exit(255)
# Мы не используем forkpty здесь, потому что это блокировало бы ^Cs от достижения
# дочернего процесса. И нам это не нужно.
ptm, pts = os.openpty()
pid = os.fork()
if pid == 0:
# Ребенок выполняет это.
# Чтобы получить желаемое поведение, нам нужно только заменить stdout процесса
# на pts. Все остальное должно оставаться на месте, чтобы такие вещи,
# как `ps -eF | pty rg python | less -r`, все еще работали так, как задумано.
os.dup2(pts, stdout)
# Это не как subprocess.call(). Это заменяет весь дочерний
# процесс argv[1:], что означает, что execvp не вернется! Ищите в интернете
# "fork exec" для получения дополнительной информации.
os.execvp(argv[1], argv[1:])
# Используйте это, если вызываете fish.
#os.execvp('fish', ['fish', '-c', fish_escape(argv[1:])])
# Родитель выполняет это.
# Если родитель не закроет конец slave, скрипт не сможет
# выйти. Следующее чтение на ptm после завершения дочернего процесса повиснет
# навсегда, потому что pts на самом деле все еще будет открыт.
os.close(pts)
# Вся группа процессов получает SIGINT, включая ребенка, поэтому нам не нужно
# реагировать на него. Мы узнаем, когда уйти, судя по тому, что делает ребенок.
signal.signal(signal.SIGINT, signal.SIG_IGN)
while True:
try:
chunk = os.read(ptm, 4096)
except OSError:
break
try:
os.write(stdout, chunk)
except BrokenPipeError:
# Это происходит, когда родитель перенаправляет вывод в другой процесс в
# конвейере, как в `pty ls --color -R | less -r`, и принимающий
# процесс завершается до завершения ребенка. Если принимающий
# процесс - less, это может произойти очень легко. Это происходит каждый раз,
# когда пользователь решает выйти из less, прежде чем он отобразил весь вывод. Итак,
# нам нужно остановить сейчас дочерний процесс.
os.kill(pid, signal.SIGTERM)
# Также закройте входы и выходы ребенка, на случай, если он
# заблокирован на них и не может отреагировать на SIGTERM.
os.close(ptm)
break
wait_pid, status = os.waitpid(pid, 0)
exit(status >> 8)
Основываясь на @Ответе Амир, вот скрипт, который генерирует и затем включает библиотеку во время выполнения:
#!/bin/bash
set -euo pipefail
function clean_up {
trap - EXIT # Восстановить обработчик по умолчанию, чтобы избежать рекурсии
[[ -e "${isatty_so:-}" ]] && rm "$isatty_so"
}
# shellcheck disable=2154 ## err ссылается, но не назначен
trap 'err=$?; clean_up; exit $err' EXIT HUP INT TERM
isatty_so=$(mktemp --tmpdir "$(basename "$0")".XXXXX.isatty.so)
echo "int isatty(int fd) { return 1; }" \
| gcc -O2 -fpic -shared -ldl -o "$isatty_so" -xc -
# Позволить пользователю SH=/bin/zsh faketty mycommand
"${SH:-$SHELL}" -c 'eval $@' - LD_PRELOAD="$isatty_so" "$@"
Другой вариант – pipetty
, один из утилит colorized-logs, который вызывается как обертка, например unbuffer
, т.е. pipetty command [ args… ]
, например:
pipetty ls --color=auto
Ответ или решение
Чтобы обмануть команду, заставив её думать, что её вывод отправляется в терминал (что может вызывать, к примеру, использование цветного вывода), существует несколько эффективных подходов. Это может быть полезным в ситуациях, когда команды изменяют своё поведение в зависимости от того, подключён ли стандартный вывод к терминалу или нет. Я рассмотрю несколько утилит и методов, которые могут помочь решить эту задачу в оболочке Bash на Linux.
Использование unbuffer
Одним из простых решений является утилита unbuffer
, которая является скриптом на tcl
/expect
. Она позволяет запускать команды и обрабатывает вывод так, чтобы команды думали, что они выводят данные в терминал. Например, вы можете использовать её следующим образом:
unbuffer команду
Однако стоит отметить, что unbuffer
может не обрабатывать алиасы. Для их работы можно использовать хитрость:
alias unbuffer="unbuffer "
Это добавит пробел в конец, позволяя оболочке Bash расширять алиасы.
Использование script
Также можно использовать утилиту script
. Эта команда записывает сессию и может быть использована для захвата вывода команд с сохранением форматирования:
script -q -c 'команда' output_file
Это создаст output_file
, содержащий вывод команды с сохранением всех управляющих символов.
Псевдотерминалы через socat
Другим мощным инструментом является socat
, который позволяет создать псевдотерминал и взаимодействовать с ним. Например, вы можете использовать следующую команду:
socat EXEC:"my-command",pty GOPEN:mylog.log
В этом случае my-command
будет думать, что она работает с терминалом, а результаты её работы будут записаны в mylog.log
.
Патчинг isatty
Если у вас есть доступ к кодированию, можно создать небольшую библиотеку для замены функции isatty
. Например, выполните следующие команды:
echo "int isatty(int fd) { return 1; }" | gcc -O2 -fpic -shared -ldl -o isatty.so -xc -
Затем исполняйте команды с использованием:
LD_PRELOAD=./isatty.so mycommand
Это обманет вашу команду так, что она будет считать, что её стандартный вывод подключён к терминалу.
Скрипт на Python
Ещё одним интересным способом является использование скрипта на Python. Ниже приведён пример простого скрипта, который можно использовать для обмана программы, чтобы она думала, что пишет в терминал. Сохраните его как pty.py
и сделайте исполняемым:
#!/usr/bin/env python
import os
import sys
import signal
# Создание псевдотерминала
ptm, pts = os.openpty()
pid = os.fork()
if pid == 0:
# Дочерний процесс
os.dup2(pts, sys.stdout.fileno())
os.dup2(pts, sys.stderr.fileno())
os.execvp(sys.argv[1], sys.argv[1:])
else:
# Родительский процесс
os.close(pts)
while True:
try:
chunk = os.read(ptm, 4096)
os.write(sys.stdout.fileno(), chunk)
except OSError:
break
os.waitpid(pid, 0)
Затем можно запустить команду следующим образом:
./pty.py команда
Заключение
Как видно, существует множество способов обмануть команду, чтобы она думала, что вывод идёт в терминал. Будь то через использование утилит, таких как unbuffer
, script
, socat
, или через программирование собственных решений на Python
и C
, каждый из предоставленных методов может быть полезным в различных сценариях. Выбор метода зависит от ваших конкретных требований и доступного окружения.