Этот ответ является разъяснением моего собственного понимания и вдохновлен @ StéphaneChazelas и @mikeserv до меня.
TL; DR
- это невозможно сделать
bash
без посторонней помощи;
- правильный способ сделать это с помощью входного терминала,
ioctl
но
- самое простое из возможных
bash
решений bind
.
Простое решение
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash имеет встроенную оболочку, bind
которая позволяет выполнять команду оболочки при получении последовательности клавиш. По сути, выходные данные команды оболочки записываются во входной буфер оболочки.
$ bind '"\e[0n": "ls -l"'
Последовательность клавиш \e[0n
( <ESC>[0n
) - это управляющий код терминала ANSI, который терминал отправляет, чтобы указать, что он функционирует нормально. Это отправляется в ответ на запрос отчета о состоянии устройства, который отправляется как <ESC>[5n
.
Связав ответ с echo
выводом текста для вставки, мы можем внедрить этот текст, когда захотим, запросив состояние устройства, и это делается путем отправки <ESC>[5n
escape-последовательности.
printf '\e[5n'
Это работает, и, вероятно, достаточно, чтобы ответить на первоначальный вопрос, потому что никакие другие инструменты не участвуют. Это чисто, bash
но полагается на хорошо ведущий себя терминал (практически все).
Он оставляет отображаемый текст в командной строке готовым для использования, как если бы он был напечатан. Это может быть добавлено, отредактировано, и нажатие ENTER
заставляет это быть выполненным.
Добавьте \n
к связанной команде, чтобы она выполнялась автоматически.
Однако это решение работает только в текущем терминале (что находится в рамках исходного вопроса). Он работает из интерактивного приглашения или из сценария с источником, но вызывает ошибку при использовании из подоболочки:
bind: warning: line editing not enabled
Правильное решение, описанное ниже, является более гибким, но оно опирается на внешние команды.
Правильное решение
Правильный способ ввода ввода использует tty_ioctl , системный вызов unix для управления вводом / выводом, в котором есть TIOCSTI
команда, которую можно использовать для ввода ввода.
TIOC из « Т erminal МОК ТЛ » и ИППП от « S конца T erminal я Nput ».
Для этого нет встроенной команды bash
; для этого требуется внешняя команда. В типичном дистрибутиве GNU / Linux такой команды нет, но это не сложно сделать с помощью небольшого программирования. Вот функция оболочки, которая использует perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Вот 0x5412
код для TIOCSTI
команды.
TIOCSTI
это константа , определенная в стандартных заголовочных файлов С со значением 0x5412
. Попробуй grep -r TIOCSTI /usr/include
или посмотри /usr/include/asm-generic/ioctls.h
; он включен в программы на C косвенным путем #include <sys/ioctl.h>
.
Затем вы можете сделать:
$ inject ls -l
ls -l$ ls -l <- cursor here
Реализации на некоторых других языках показаны ниже (сохраните в файле, а затем в chmod +x
нем):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Вы можете создать, sys/ioctl.ph
который определяет TIOCSTI
вместо использования числового значения. Смотри здесь
питон inject.py
#!/usr/bin/python
import fcntl, sys, termios
del sys.argv[0]
for c in ' '.join(sys.argv):
fcntl.ioctl(sys.stdin, termios.TIOCSTI, c)
Рубин inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
С inject.c
компилировать с gcc -o inject inject.c
#include <sys/ioctl.h>
int main(int argc, char *argv[])
{
int a,c;
for (a=1, c=0; a< argc; c=0 )
{
while (argv[a][c])
ioctl(0, TIOCSTI, &argv[a][c++]);
if (++a < argc) ioctl(0, TIOCSTI," ");
}
return 0;
}
**! ** Есть другие примеры здесь .
Использование ioctl
для этого работает в подоболочках. Он также может вводить в другие терминалы, как объяснено далее.
Идем дальше (контролируем другие терминалы)
Это выходит за рамки оригинального вопроса, но можно вводить символы в другой терминал при условии наличия соответствующих разрешений. Обычно это означает быть root
, но о других путях смотрите ниже.
Расширение приведенной выше программы на C для приема аргумента командной строки, указывающего tty другого терминала, позволяет ввести этот терминал:
#include <stdlib.h>
#include <argp.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
const char *argp_program_version ="inject - see https://unix.stackexchange.com/q/213799";
static char doc[] = "inject - write to terminal input stream";
static struct argp_option options[] = {
{ "tty", 't', "TTY", 0, "target tty (defaults to current)"},
{ "nonl", 'n', 0, 0, "do not output the trailing newline"},
{ 0 }
};
struct arguments
{
int fd, nl, next;
};
static error_t parse_opt(int key, char *arg, struct argp_state *state) {
struct arguments *arguments = state->input;
switch (key)
{
case 't': arguments->fd = open(arg, O_WRONLY|O_NONBLOCK);
if (arguments->fd > 0)
break;
else
return EINVAL;
case 'n': arguments->nl = 0; break;
case ARGP_KEY_ARGS: arguments->next = state->next; return 0;
default: return ARGP_ERR_UNKNOWN;
}
return 0;
}
static struct argp argp = { options, parse_opt, 0, doc };
static struct arguments arguments;
static void inject(char c)
{
ioctl(arguments.fd, TIOCSTI, &c);
}
int main(int argc, char *argv[])
{
arguments.fd=0;
arguments.nl='\n';
if (argp_parse (&argp, argc, argv, 0, 0, &arguments))
{
perror("Error");
exit(errno);
}
int a,c;
for (a=arguments.next, c=0; a< argc; c=0 )
{
while (argv[a][c])
inject (argv[a][c++]);
if (++a < argc) inject(' ');
}
if (arguments.nl) inject(arguments.nl);
return 0;
}
Он также отправляет новую строку по умолчанию, но, аналогично echo
, он предоставляет -n
опцию для ее подавления. Опция --t
or --tty
требует аргумент - tty
терминала, который будет введен. Значение для этого можно получить в этом терминале:
$ tty
/dev/pts/20
Скомпилируйте это с gcc -o inject inject.c
. Добавьте к тексту префикс, --
если он содержит дефисы, чтобы синтаксический анализатор не мог правильно интерпретировать параметры командной строки. См ./inject --help
. Используйте это так:
$ inject --tty /dev/pts/22 -- ls -lrt
или просто
$ inject -- ls -lrt
ввести текущий терминал.
Внедрение в другой терминал требует административных прав, которые могут быть получены:
- выдача команды как
root
,
- используя
sudo
,
- имея
CAP_SYS_ADMIN
возможность или
- установка исполняемого файла
setuid
Назначить CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Назначить setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Чистый вывод
Введенный текст появляется перед подсказкой, как если бы он был напечатан до появления подсказки (что, по сути, так и было), но затем снова появляется после подсказки.
Один из способов скрыть текст, который появляется перед приглашением, состоит в том, чтобы добавить к приглашению возврат каретки ( \r
не перевод строки) и очистить текущую строку ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Однако это только очистит строку, на которой появляется подсказка. Если введенный текст содержит символы новой строки, это не будет работать так, как задумано.
Другое решение отключает отображение введенных символов. Для этого используется оболочка stty
:
saved_settings=$(stty -g)
stty -echo -icanon min 1 time 0
inject echo line one
inject echo line two
until read -t0; do
sleep 0.02
done
stty "$saved_settings"
где inject
одно из решений, описанных выше, или заменено на printf '\e[5n'
.
Альтернативные подходы
Если ваша среда отвечает определенным предварительным условиям, у вас могут быть другие доступные методы, которые вы можете использовать для ввода данных. Если вы работаете в среде рабочего стола, то xdotool - это утилита X.Org, которая имитирует работу мыши и клавиатуры, но ваш дистрибутив может не включать ее по умолчанию. Можешь попробовать:
$ xdotool type ls
Если вы используете tmux , терминальный мультиплексор, то вы можете сделать это:
$ tmux send-key -t session:pane ls
где -t
выбирает, какой сеанс и панель для вставки. GNU Screen имеет аналогичную возможность с его stuff
командой:
$ screen -S session -p pane -X stuff ls
Если ваш дистрибутив включает в себя пакет console-tools , то у вас может быть writevt
команда, которая использует ioctl
как наши примеры. Однако большинство дистрибутивов не одобряют этот пакет в пользу kbd, в котором отсутствует эта функция.
Обновленная копия writevt.c может быть скомпилирована с использованием gcc -o writevt writevt.c
.
Другие варианты, которые могут лучше подходить для некоторых сценариев использования, включают ожидаемый и пустой, которые предназначены для сценариев интерактивных инструментов.
Вы также можете использовать оболочку, которая поддерживает инъекцию терминала, например, zsh
которая может это сделать print -z ls
.
Ответ "Ого, это умно ..."
Описанный здесь метод также обсуждается здесь и основывается на методе, обсуждаемом здесь .
Перенаправление оболочки от /dev/ptmx
получает новый псевдо-терминал:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Небольшой инструмент, написанный на C, который разблокирует мастер псевдотерминала (ptm) и выводит имя ведомого псевдотерминала (pts) на его стандартный вывод.
#include <stdio.h>
int main(int argc, char *argv[]) {
if(unlockpt(0)) return 2;
char *ptsname(int fd);
printf("%s\n",ptsname(0));
return argc - 1;
}
(сохранить как pts.c
и скомпилировать с gcc -o pts pts.c
)
Когда программа вызывается со стандартным входом, установленным в ptm, она разблокирует соответствующие точки и выводит свое имя в стандартный вывод.
$ ./pts </dev/ptmx
/dev/pts/20
Функция unlockpt () разблокирует подчиненное псевдотерминальное устройство, соответствующее главному псевдотерминалу, на который ссылается данный файловый дескриптор. Программа передает это как ноль, который является стандартным вводом программы .
Функция ptsname () возвращает имя подчиненного псевдотерминального устройства, соответствующего мастеру, указанному в данном дескрипторе файла, снова передавая ноль для стандартного ввода программы.
Процесс может быть связан с очками. Сначала получите ptm (здесь он назначен файловому дескриптору 3, открытому для чтения-записи <>
перенаправлением).
exec 3<>/dev/ptmx
Затем запустите процесс:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Процессы, порожденные этой командной строкой, лучше всего иллюстрируются с помощью pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
Выходные данные относятся к текущей оболочке ( $$
), а PID ( -p
) и PGID ( -g
) каждого процесса показаны в скобках (PID,PGID)
.
Во главе дерева находится bash(5203,5203)
интерактивная оболочка, в которую мы вводим команды, и ее файловые дескрипторы связывают ее с терминальным приложением, которое мы используем для взаимодействия с ней ( xterm
или аналогичным образом).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Посмотрев на команду еще раз, первый набор скобок запустил подоболочку, bash(6524,6524)
) с ее файловым дескриптором 0 (его стандартный ввод ), назначенным для pts (который открыт для чтения-записи <>
), как это было возвращено другой подоболочкой, которая была выполнена ./pts <&3
для разблокировки pts, связанные с файловым дескриптором 3 (созданным на предыдущем шаге exec 3<>/dev/ptmx
).
Файловый дескриптор 3>&-
subshell 3 закрыт ( ), поэтому ptm для него недоступен. Его стандартный ввод (fd 0), который является pts, который был открыт для чтения / записи, перенаправляется (фактически, fd копируется - >&0
) на стандартный вывод (fd 1).
Это создает подоболочку со стандартным вводом и выводом, соединенным с очками. Он может отправить входные данные, написав в ptm, а его вывод можно увидеть, прочитав из ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Subshell выполняет эту команду:
setsid -c bash -i 2>&1 | tee log
Он запускается bash(6527,6527)
в интерактивном -i
режиме ( ) в новом сеансе ( setsid -c
обратите внимание, что PID и PGID одинаковы). Его стандартная ошибка перенаправляется в стандартный вывод ( 2>&1
) и передается по каналу, tee(6528,6524)
поэтому записывается в log
файл, а также в pts. Это дает еще один способ увидеть результат подоболочки:
$ tail -f log
Поскольку подоболочка работает в bash
интерактивном режиме, ей можно отправлять команды для выполнения, как в этом примере, который отображает дескрипторы файлов подоболочки:
$ echo 'ls -l /dev/fd/' >&3
Чтение вывода subshell ( tail -f log
или cat <&3
) показывает:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Стандартный вход (fd 0) подключен к точкам, и оба стандартных выхода (fd 1) и ошибка (fd 2) подключены к одной и той же трубе, которая подключается к tee
:
$ (find /proc -type l | xargs ls -l | fgrep 'pipe:[116261]') 2>/dev/null
l-wx------ /proc/6527/fd/1 -> pipe:[116261]
l-wx------ /proc/6527/fd/2 -> pipe:[116261]
lr-x------ /proc/6528/fd/0 -> pipe:[116261]
И посмотрите на файловые дескрипторы tee
$ ls -l /proc/6528/fd/
lr-x------ 0 -> pipe:[116261]
lrwx------ 1 -> /dev/pts/17
lrwx------ 2 -> /dev/pts/3
l-wx------ 3 -> /home/myuser/work/log
Стандартный вывод (fd 1) - это число: все, что 'tee' записывает в свой стандартный вывод, отправляется обратно в ptm. Стандартная ошибка (fd 2) - это точки, принадлежащие управляющему терминалу.
Завершение
Следующий скрипт использует технику, описанную выше. Он устанавливает интерактивный bash
сеанс, который может быть введен путем записи в файловый дескриптор. Это доступно здесь и задокументировано с объяснениями.
sh -cm 'cat <&9 &cat >&9|( ### copy to/from host/slave
trap " stty $(stty -g ### save/restore stty settings on exit
stty -echo raw) ### host: no echo and raw-mode
kill -1 0" EXIT ### send a -HUP to host pgrp on EXIT
<>"$($pts <&9)" >&0 2>&1\
setsid -wc -- bash) <&1 ### point bash <0,1,2> at slave and setsid bash
' -- 9<>/dev/ptmx 2>/dev/null ### open pty master on <>9