Как обмануть команду, заставив её думать, что её вывод идет в терминал

Вопрос или проблема

Учитывая команду, которая изменяет свое поведение, когда ее вывод отправляется в терминал (например, создает цветной вывод), как можно перенаправить этот вывод в конвейере, сохранив измененное поведение? Должен же быть утилита для этого, о которой я не знаю.

Некоторые команды, такие как 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

Дополнительное чтение

Вы можете получить то, что вам нужно, используя 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, каждый из предоставленных методов может быть полезным в различных сценариях. Выбор метода зависит от ваших конкретных требований и доступного окружения.

Оцените материал
Добавить комментарий

Капча загружается...