Ta odpowiedź ma na celu wyjaśnienie mojego rozumienia i jest inspirowana przez @ StéphaneChazelas i @mikeserv przede mną.
TL; DR
- nie jest to możliwe
bash
bez pomocy zewnętrznej;
- poprawnym sposobem na to jest wejście terminala wysyłania,
ioctl
ale
- najłatwiejsze w użyciu
bash
rozwiązanie bind
.
Proste rozwiązanie
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash ma wbudowaną powłokę o nazwie, bind
która pozwala na wykonanie polecenia powłoki po otrzymaniu sekwencji klawiszy. Zasadniczo dane wyjściowe polecenia powłoki są zapisywane w buforze wejściowym powłoki.
$ bind '"\e[0n": "ls -l"'
Sekwencja klawiszy \e[0n
( <ESC>[0n
) jest kodem ucieczki terminala ANSI, który terminal wysyła w celu wskazania, że działa normalnie. Wysyła to w odpowiedzi na żądanie raportu o stanie urządzenia, które jest wysyłane jako <ESC>[5n
.
Wiążąc odpowiedź na wartość echo
wyjściową tekstu do wstrzyknięcia, możemy wstrzyknąć ten tekst w dowolnym momencie, żądając statusu urządzenia, a to odbywa się poprzez wysłanie <ESC>[5n
sekwencji specjalnej.
printf '\e[5n'
To działa i prawdopodobnie wystarczy, aby odpowiedzieć na pierwotne pytanie, ponieważ nie są w to zaangażowane żadne inne narzędzia. Jest czysty, bash
ale opiera się na dobrze zachowującym się terminalu (praktycznie wszyscy są).
Pozostawia echem tekst w wierszu poleceń gotowy do użycia, tak jakby został wpisany. Można go dodawać, edytować i naciskać, co ENTER
powoduje jego wykonanie.
Dodaj \n
do powiązanego polecenia, aby było ono wykonywane automatycznie.
Jednak to rozwiązanie działa tylko w bieżącym terminalu (co wchodzi w zakres pierwotnego pytania). Działa z interaktywnego monitu lub ze skryptu źródłowego, ale powoduje błąd, jeśli jest używany z podpowłoki:
bind: warning: line editing not enabled
Prawidłowe rozwiązanie opisane poniżej jest bardziej elastyczne, ale opiera się na zewnętrznych poleceniach.
Prawidłowe rozwiązanie
Właściwy sposób wstrzykiwania danych wejściowych wykorzystuje tty_ioctl , systemowe wywołanie systemu Unix dla I / O Control, które ma TIOCSTI
polecenie, którego można użyć do wprowadzenia danych wejściowych.
TIOC z " T erminal MKOl tl " i STI z " S końca T erminal I nPrzenieś ".
Nie ma w bash
tym wbudowanego polecenia ; wykonanie tego wymaga zewnętrznego polecenia. W typowej dystrybucji GNU / Linux nie ma takiego polecenia, ale nie jest to trudne do wykonania przy odrobinie programowania. Oto funkcja powłoki, która używa perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Oto 0x5412
kod TIOCSTI
polecenia.
TIOCSTI
jest stałą zdefiniowaną w standardowych plikach nagłówka C z wartością 0x5412
. Spróbuj grep -r TIOCSTI /usr/include
lub zajrzyj do środka /usr/include/asm-generic/ioctls.h
; jest zawarty w programach C pośrednio przez #include <sys/ioctl.h>
.
Następnie możesz:
$ inject ls -l
ls -l$ ls -l <- cursor here
Implementacje w niektórych innych językach pokazano poniżej (zapisz w pliku, a następnie chmod +x
):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Możesz wygenerować, sys/ioctl.ph
który definiuje TIOCSTI
zamiast używać wartości liczbowej. Zobacz tutaj
Pyton 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)
Rubin inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
do inject.c
Połącz z 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;
}
**! ** Istnieją dalsze przykłady tutaj .
Używanie ioctl
do tego działa w podpowłokach. Może również wstrzykiwać do innych zacisków, jak wyjaśniono poniżej.
Dalszy rozwój (sterowanie innymi terminalami)
To wykracza poza zakres pierwotnego pytania, ale możliwe jest wstrzykiwanie znaków do innego terminala, pod warunkiem posiadania odpowiednich uprawnień. Zwykle oznacza to bycie root
, ale zobacz poniżej inne sposoby.
Rozszerzenie programu C podanego powyżej, aby zaakceptować argument wiersza poleceń określający tty innego terminala, umożliwia wstrzyknięcie do tego terminala:
#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;
}
Domyślnie wysyła również nowy wiersz, ale podobnie jak echo
umożliwia -n
opcję jego zniesienia. Opcja --t
lub --tty
wymaga argumentu - tty
terminalu, który ma zostać wstrzyknięty. Wartość tego można uzyskać w tym terminalu:
$ tty
/dev/pts/20
Skompiluj to z gcc -o inject inject.c
. Prefiks tekstu do wstrzyknięcia, --
jeśli zawiera on łączniki, aby zapobiec błędnej interpretacji parsera argumentów opcji wiersza polecenia. Zobaczyć ./inject --help
. Użyj tego w ten sposób:
$ inject --tty /dev/pts/22 -- ls -lrt
Lub tylko
$ inject -- ls -lrt
wstrzyknąć aktualny terminal.
Wstrzyknięcie do innego terminalu wymaga uprawnień administracyjnych, które można uzyskać poprzez:
- wydanie polecenia jako
root
,
- za pomocą
sudo
,
- posiadający
CAP_SYS_ADMIN
zdolność lub
- ustawianie pliku wykonywalnego
setuid
Aby przypisać CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Aby przypisać setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Czysta wydajność
Wstrzyknięty tekst pojawia się przed monitem, tak jakby został wpisany przed wyświetleniem monitu (który w rzeczywistości był), ale potem pojawia się ponownie po monicie.
Jednym ze sposobów ukrycia tekstu wyświetlanego przed pytaniem jest dodanie znaku zachęty ze znakiem powrotu karetki ( \r
bez podawania wiersza) i wyczyszczenie bieżącej linii ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Spowoduje to jednak wyczyszczenie tylko linii, w której pojawia się monit. Jeśli wstrzyknięty tekst zawiera znaki nowej linii, nie będzie to działać zgodnie z przeznaczeniem.
Inne rozwiązanie wyłącza echo wstrzykiwanych znaków. Opakowanie używa stty
do tego celu:
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"
gdzie inject
jest jedno z opisanych powyżej rozwiązań lub zastąpione przez printf '\e[5n'
.
Alternatywne podejścia
Jeśli twoje środowisko spełnia pewne wymagania wstępne, możesz mieć inne dostępne metody, których możesz użyć do wstrzyknięcia danych wejściowych. Jeśli pracujesz w środowisku komputerowym, xdotool to narzędzie X.Org , które symuluje działanie myszy i klawiatury, ale twoja dystrybucja może nie zawierać go domyślnie. Możesz spróbować:
$ xdotool type ls
Jeśli używasz multipleksera terminalowego tmux , możesz to zrobić:
$ tmux send-key -t session:pane ls
gdzie -t
wybiera sesję i panel do wstrzyknięcia. GNU Screen ma podobne możliwości ze swoim stuff
poleceniem:
$ screen -S session -p pane -X stuff ls
Jeśli twoja dystrybucja zawiera pakiet narzędzi konsolowych , możesz mieć writevt
komendę, która używa ioctl
podobnie jak nasze przykłady. Jednak większość dystrybucji zdezaktualizowała ten pakiet na rzecz kbd, który nie ma tej funkcji.
Zaktualizowaną kopię pliku writevt.c można skompilować za pomocą gcc -o writevt writevt.c
.
Inne opcje, które mogą lepiej pasować do niektórych przypadków użycia, obejmują oczekiwanie i puste, które zostały zaprojektowane w celu umożliwienia skryptowania interaktywnych narzędzi.
Możesz także użyć powłoki, która obsługuje iniekcję terminalną, na przykład zsh
co może zrobić print -z ls
.
Odpowiedź „Wow, to sprytne ...”
Metoda opisana tutaj jest także omawiane tutaj i opiera się na metodzie omówione tutaj .
Przekierowanie powłoki z /dev/ptmx
dostaje nowy pseudo-terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Małe narzędzie napisane w C, które odblokowuje pseudoterminal master (ptm) i wysyła nazwę pseudoterminalu slave (pts) na standardowe wyjście.
#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;
}
(zapisz jako pts.c
i skompiluj z gcc -o pts pts.c
)
Kiedy program jest wywoływany ze standardowym wejściem ustawionym na ptm, odblokowuje odpowiednie punkty i wypisuje swoją nazwę na standardowe wyjście.
$ ./pts </dev/ptmx
/dev/pts/20
Funkcja unlockpt () odblokowuje pseudoterminal podrzędny odpowiadający pseudoterminalowi głównemu, do którego odwołuje się dany deskryptor pliku. Program przyjmuje to jako zero, co jest standardowym wejściem programu .
Funkcja ptsname () zwraca nazwę urządzenia pseudoterminalnego slave odpowiadającego urządzeniu nadrzędnemu określonemu przez dany deskryptor pliku, ponownie przekazując zero dla standardowego wejścia programu.
Proces można podłączyć do pkt. Najpierw pobierz ptm (tutaj jest przypisany do deskryptora pliku 3, otwarty odczyt-zapis przez <>
przekierowanie).
exec 3<>/dev/ptmx
Następnie rozpocznij proces:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Procesy powstałe w tym wierszu poleceń najlepiej ilustrują pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
Dane wyjściowe odnoszą się do bieżącej powłoki ( $$
), a PID ( -p
) i PGID ( -g
) każdego procesu są pokazane w nawiasach (PID,PGID)
.
Na czele drzewa znajduje bash(5203,5203)
się interaktywna powłoka, w której wpisujemy polecenia, a jej deskryptory plików łączą ją z aplikacją terminalową, z której korzystamy do interakcji ( xterm
lub podobnej).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Ponownie patrząc na komendę, pierwszy zestaw nawiasów uruchomił podpowłokę bash(6524,6524)
), a deskryptor pliku 0 ( standardowe wejście ) jest przypisany do pts (który jest otwierany do odczytu i zapisu <>
), jak zwrócony przez inną podpowłokę wykonaną w ./pts <&3
celu odblokowania pts skojarzone z deskryptorem pliku 3 (utworzonym w poprzednim kroku, exec 3<>/dev/ptmx
).
Deskryptor pliku podpowłoki 3 jest zamknięty ( 3>&-
), więc ptm nie jest dla niego dostępny. Jego standardowe wejście (fd 0), czyli pts, które zostały otwarte do odczytu / zapisu, jest przekierowywane (tak naprawdę fd jest kopiowane - >&0
) na standardowe wyjście (fd 1).
To tworzy podpowłokę ze standardowym wejściem i wyjściem podłączonym do pts. Można go przesłać, pisząc do ptm, a jego wynik można zobaczyć, czytając z ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Podshell wykonuje następujące polecenie:
setsid -c bash -i 2>&1 | tee log
Działa bash(6527,6527)
w -i
trybie interaktywnym ( ) w nowej sesji ( setsid -c
zauważ, że PID i PGID są takie same). Standardowy błąd jest przekierowywany do standardowego wyjścia ( 2>&1
) i przesyłany potokowo, tee(6528,6524)
dzięki czemu jest zapisywany zarówno w log
pliku, jak i w pts. To daje inny sposób, aby zobaczyć dane wyjściowe podpowłoki:
$ tail -f log
Ponieważ podpowłoka działa bash
interaktywnie, można jej wysyłać polecenia do wykonania, tak jak w tym przykładzie, który wyświetla deskryptory plików podpowłoki:
$ echo 'ls -l /dev/fd/' >&3
Czytanie wyników ( tail -f log
lub cat <&3
) podpowłoki ujawnia:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Standardowe wejście (fd 0) jest podłączone do pts, a oba standardowe wyjście (fd 1) i błąd (fd 2) są podłączone do tej samej rury, tej, która łączy się z 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]
I spojrzenie na deskryptory plików 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
Standardowe wyjście (fd 1) to pts: wszystko, co „tee” zapisuje na standardowe wyjście, jest wysyłane z powrotem do ptm. Błąd standardowy (fd 2) to punkty należące do terminala sterującego.
Podsumowując
Poniższy skrypt używa techniki opisanej powyżej. Konfiguruje bash
sesję interaktywną, którą można wstrzyknąć, pisząc do deskryptora pliku. Jest dostępny tutaj i udokumentowany objaśnieniami.
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