Ця відповідь подається як роз'яснення мого власного розуміння і натхненна переді мною @ 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
послідовності запуску.
printf '\e[5n'
Це працює і, ймовірно, достатньо для відповіді на початкове запитання, оскільки жодних інших інструментів не задіяно. Це чисто, bash
але покладається на добре поводиться термінал (практично всі є).
Він залишає відлунне текст у командному рядку готовим до використання так, як ніби він був введений. Він може бути доданий, відредагований і натискання ENTER
викликає його виконання.
Додайте \n
до зв'язаної команди, щоб вона виконувалася автоматично.
Однак це рішення працює лише в поточному терміналі (що знаходиться в межах початкового питання). Він працює з інтерактивного підказки або з скрипту, який використовується з джерела, але він викликає помилку, якщо використовується з додаткової оболонки:
bind: warning: line editing not enabled
Далі описане правильне рішення є більш гнучким, але воно покладається на зовнішні команди.
Правильне рішення
Правильний спосіб введення вводу використовує tty_ioctl , системний виклик unix для управління введення-виводу, який має TIOCSTI
команду, яку можна використовувати для введення вводу.
TIOC від " T erminal IOC tl " і STI від " S end T erminal I nput ".
Для цього немає вбудованої команди bash
; для цього потрібна зовнішня команда. У типовому дистрибутиві GNU / Linux не існує такої команди, але досягти її з невеликим програмуванням не складно. Ось функція оболонки, яка використовує perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Ось 0x5412
код для TIOCSTI
команди.
TIOCSTI
- константа, визначена в стандартних файлах заголовка C зі значенням 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
або --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 має аналогічні можливості зі своєю stuff
командою:
$ screen -S session -p pane -X stuff ls
Якщо ваш дистрибутив включає пакет консольних інструментів, то у вас може бути 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 3>&-
підкатегорії закритий ( ), щоб ptm був недоступний для нього. Її стандартний вхід (fd 0), який є пунктами, які були відкриті для читання / запису, переспрямовується (насправді fd скопіюється - >&0
) на його стандартний вихід (fd 1).
Це створює додаткову оболонку зі стандартним входом і виходом, підключеним до очок. Він може бути надісланий введенням, записавши на ptm, а його вихід можна побачити, прочитавши з ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Підрозділ виконує цю команду:
setsid -c bash -i 2>&1 | tee log
Він працює bash(6527,6527)
в інтерактивному ( -i
) режимі в новому сеансі ( setsid -c
зауважте, PID і PGID однакові). Його стандартна помилка переспрямовується на його стандартний вихід ( 2>&1
) і передається через tee(6528,6524)
так, що вона записується у log
файл, а також у бали. Це дає ще один спосіб бачити вихідний пакет:
$ tail -f log
Оскільки нижня оболонка працює в bash
інтерактивному режимі, їй можна надіслати команди для виконання, як цей приклад, який відображає дескриптори файлів підкашлю:
$ echo 'ls -l /dev/fd/' >&3
Читання результатів ( tail -f log
або cat <&3
) підскладок виявляє:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Стандартний вхід (fd 0) підключений до pts, і обидва стандартні виходи (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