Questa risposta è fornita come chiarimento della mia comprensione ed è ispirata da @ StéphaneChazelas e @mikeserv prima di me.
TL; DR
- non è possibile farlo
bashsenza un aiuto esterno;
- il modo corretto per farlo è con un input del terminale di invio
ioctl ma
- la
bashsoluzione praticabile più semplice utilizza bind.
La soluzione semplice
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash ha un builtin shell chiamato bindche permette di eseguire un comando shell quando viene ricevuta una sequenza di tasti. In sostanza, l'output del comando shell viene scritto nel buffer di input della shell.
$ bind '"\e[0n": "ls -l"'
La sequenza di tasti \e[0n( <ESC>[0n) è un codice di escape del terminale ANSI che un terminale invia per indicare che funziona normalmente. Invia questo in risposta a una richiesta di rapporto sullo stato del dispositivo che viene inviata come <ESC>[5n.
Associando la risposta a un messaggio echoche genera il testo da iniettare, possiamo iniettare quel testo ogni volta che lo desideriamo, richiedendo lo stato del dispositivo e facendo ciò inviando una <ESC>[5nsequenza di escape.
printf '\e[5n'
Funziona ed è probabilmente sufficiente per rispondere alla domanda originale perché non sono coinvolti altri strumenti. È puro bashma si basa su un terminale ben educato (praticamente tutti lo sono).
Lascia il testo echo sulla riga di comando pronto per essere utilizzato come se fosse stato digitato. Può essere aggiunto, modificato e premendo ENTERprovoca l'esecuzione.
Aggiungi \nal comando associato per eseguirlo automaticamente.
Tuttavia, questa soluzione funziona solo nel terminale corrente (che rientra nell'ambito della domanda originale). Funziona da un prompt interattivo o da uno script di origine ma genera un errore se utilizzato da una subshell:
bind: warning: line editing not enabled
La soluzione corretta descritta di seguito è più flessibile ma si basa su comandi esterni.
La soluzione corretta
Il modo corretto di iniettare l'input utilizza tty_ioctl , una chiamata di sistema unix per il controllo I / O che ha un TIOCSTIcomando che può essere utilizzato per iniettare l'input.
TIOC da " T erminal IOC tl " e STI da " S end T erminal I nput ".
Non esiste un comando incorporato bashper questo; farlo richiede un comando esterno. Non esiste un comando simile nella tipica distribuzione GNU / Linux, ma non è difficile da ottenere con un po 'di programmazione. Ecco una funzione shell che utilizza perl:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Ecco 0x5412il codice per il TIOCSTIcomando.
TIOCSTIè una costante definita nei file di intestazione C standard con il valore 0x5412. Prova grep -r TIOCSTI /usr/includeo guarda dentro /usr/include/asm-generic/ioctls.h; è incluso nei programmi C indirettamente #include <sys/ioctl.h>.
È quindi possibile fare:
$ inject ls -l
ls -l$ ls -l <- cursor here
Le implementazioni in alcune altre lingue sono mostrate di seguito (salvare in un file e quindi chmod +x):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
È possibile generare sys/ioctl.phquale definisce TIOCSTIinvece di utilizzare il valore numerico. Vedi qui
Pitone 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)
Rubino inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
compilare con 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;
}
**! ** Ci sono altri esempi qui .
Usare ioctlper fare questo funziona in subshells. Può anche essere iniettato in altri terminali come spiegato di seguito.
Portarlo oltre (controllare altri terminali)
È al di là dell'ambito della domanda originale ma è possibile iniettare caratteri in un altro terminale, a condizione che dispongano delle autorizzazioni appropriate. Normalmente questo significa essere root, ma vedi sotto per altri modi.
L'estensione del programma C sopra indicato per accettare un argomento della riga di comando che specifica il tty di un altro terminale consente l'iniezione a quel terminale:
#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;
}
Invia anche una nuova riga per impostazione predefinita ma, analogamente a echo, fornisce -nun'opzione per eliminarla. L' opzione --to --ttyrichiede un argomento: l' ttyiniezione del terminale. Il valore per questo può essere ottenuto in quel terminale:
$ tty
/dev/pts/20
Compilalo con gcc -o inject inject.c. Prefisso il testo da iniettare --se contiene trattini per impedire all'analizzatore di argomenti di interpretare erroneamente le opzioni della riga di comando. Vedere ./inject --help. Usalo in questo modo:
$ inject --tty /dev/pts/22 -- ls -lrt
o solo
$ inject -- ls -lrt
per iniettare il terminale corrente.
L'iniezione in un altro terminale richiede diritti amministrativi che possono essere ottenuti da:
- impartendo il comando come
root,
- usando
sudo,
- avere la
CAP_SYS_ADMINcapacità o
- impostazione dell'eseguibile
setuid
Per assegnare CAP_SYS_ADMIN:
$ sudo setcap cap_sys_admin+ep inject
Per assegnare setuid:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Uscita pulita
Il testo iniettato appare prima del prompt come se fosse stato digitato prima che apparisse (che, in effetti, lo era) ma poi riapparirà dopo il prompt.
Un modo per nascondere il testo che appare davanti al prompt è anteporre il prompt con un ritorno a capo ( \rnon avanzamento riga) e cancellare la riga corrente ( <ESC>[M):
$ PS1="\r\e[M$PS1"
Tuttavia, questo cancellerà solo la linea su cui appare il prompt. Se il testo iniettato include nuove righe, questo non funzionerà come previsto.
Un'altra soluzione disabilita l'eco dei caratteri iniettati. Un wrapper usa sttyper fare questo:
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"
dove injectè una delle soluzioni sopra descritte o sostituita da printf '\e[5n'.
Approcci alternativi
Se l'ambiente soddisfa determinati prerequisiti, è possibile che siano disponibili altri metodi che è possibile utilizzare per iniettare l'input. Se ti trovi in un ambiente desktop, xdotool è un'utilità X.Org che simula l'attività del mouse e della tastiera, ma la distribuzione potrebbe non includerla per impostazione predefinita. Puoi provare:
$ xdotool type ls
Se usi tmux , il multiplexer terminale, puoi farlo:
$ tmux send-key -t session:pane ls
dove -tseleziona la sessione e il riquadro da iniettare. Lo schermo GNU ha una capacità simile con il suo stuffcomando:
$ screen -S session -p pane -X stuff ls
Se la tua distribuzione include il pacchetto console-tools , potresti avere un writevtcomando che usa ioctlcome i nostri esempi. La maggior parte delle distro, tuttavia, ha deprecato questo pacchetto a favore di kbd che manca di questa funzionalità.
È possibile compilare una copia aggiornata di writevt.c utilizzando gcc -o writevt writevt.c.
Altre opzioni che potrebbero adattarsi meglio ad alcuni casi d'uso includono prevedono e svuota, progettate per consentire lo script di strumenti interattivi.
È inoltre possibile utilizzare una shell che supporti l'iniezione terminale come ciò zshche può fare print -z ls.
La risposta "Wow, che intelligente ..."
Il metodo qui descritto è anche discusso qui e si basa sul metodo discusso qui .
Un reindirizzamento della shell da /dev/ptmxottiene un nuovo pseudo-terminale:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Un piccolo strumento scritto in C che sblocca il master pseudoterminal (ptm) e restituisce il nome dello slave pseudoterminal (pts) alla sua uscita standard.
#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;
}
(salva come pts.ce compila con gcc -o pts pts.c)
Quando il programma viene chiamato con il suo input standard impostato su un ptm sblocca i punti corrispondenti e invia il suo nome all'output standard.
$ ./pts </dev/ptmx
/dev/pts/20
La funzione unlockpt () sblocca il dispositivo pseudoterminale slave corrispondente allo pseudoterminale principale a cui fa riferimento il descrittore di file specificato. Il programma passa questo come zero, che è l' input standard del programma .
La funzione ptsname () restituisce il nome del dispositivo pseudoterminale slave corrispondente al master a cui fa riferimento il descrittore di file specificato, passando di nuovo zero per l'input standard del programma.
Un processo può essere collegato ai punti. Prima ottieni un ptm (qui è assegnato al descrittore di file 3, aperto in lettura-scrittura dal <>reindirizzamento).
exec 3<>/dev/ptmx
Quindi avviare il processo:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
I processi generati da questa riga di comando sono meglio illustrati con pstree:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
L'output è relativo all'attuale shell ( $$) e il PID ( -p) e PGID ( -g) di ciascun processo sono mostrati tra parentesi (PID,PGID).
Alla testa dell'albero c'è bash(5203,5203)la shell interattiva in cui stiamo digitando i comandi e i suoi descrittori di file lo collegano all'applicazione terminale che stiamo usando per interagire con esso ( xtermo simili).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Guardando di nuovo il comando, il primo set di parentesi ha avviato una subshell bash(6524,6524)) , con il suo descrittore di file 0 (il suo input standard ) assegnato ai pts (che è aperto in lettura-scrittura <>) , come restituito da un'altra subshell eseguita ./pts <&3per sbloccare il pts associati al descrittore di file 3 (creato nel passaggio precedente, exec 3<>/dev/ptmx).
Il descrittore di file 3 della subshell è chiuso ( 3>&-) in modo che il ptm non sia accessibile ad esso. Il suo input standard (fd 0), che è il pts che è stato aperto in lettura / scrittura, viene reindirizzato (in realtà il fd viene copiato - >&0) al suo output standard (fd 1).
Questo crea una subshell con i suoi input e output standard collegati ai punti. Può essere inviato input scrivendo al ptm e il suo output può essere visto leggendo dal ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
La subshell esegue questo comando:
setsid -c bash -i 2>&1 | tee log
Funziona bash(6527,6527)in -imodalità interattiva ( ) in una nuova sessione ( setsid -cnotare che PID e PGID sono uguali). Il suo errore standard viene reindirizzato al suo output standard ( 2>&1) e reindirizzato via in tee(6528,6524)modo che sia scritto su un logfile e sui pts. Questo offre un altro modo per vedere l'output della subshell:
$ tail -f log
Poiché la subshell è in esecuzione in modo bashinterattivo, è possibile inviare comandi da eseguire, come in questo esempio che mostra i descrittori di file della subshell:
$ echo 'ls -l /dev/fd/' >&3
La lettura dell'output ( tail -f logo cat <&3) della subshell rivela:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
L'ingresso standard (fd 0) è collegato ai punti e sia l'uscita standard (fd 1) sia l'errore (fd 2) sono collegati alla stessa pipe, quella che si collega a 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]
E uno sguardo ai descrittori di file di 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
L'output standard (fd 1) è il pts: tutto ciò che "tee" scrive nel suo output standard viene rinviato al ptm. Errore standard (fd 2) sono i punti appartenenti al terminale di controllo.
Avvolgendolo
Il seguente script utilizza la tecnica sopra descritta. Imposta una bashsessione interattiva che può essere iniettata scrivendo in un descrittore di file. È disponibile qui e documentato con spiegazioni.
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