Diese Antwort dient der Klarstellung meines eigenen Verständnisses und wurde von @ StéphaneChazelas und @mikeserv vor mir inspiriert.
TL; DR
bash
ohne fremde hilfe ist dies nicht möglich ;
- der richtige weg dies zu tun ist mit einem sendeterminal eingang
ioctl
aber
- die am einfachsten umsetzbare
bash
Lösung verwendet bind
.
Die einfache Lösung
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash hat eine eingebaute Shell bind
, die es ermöglicht, einen Shell-Befehl auszuführen , wenn eine Tastensequenz empfangen wird. Im Wesentlichen wird die Ausgabe des Shell-Befehls in den Eingabepuffer der Shell geschrieben.
$ bind '"\e[0n": "ls -l"'
Die Tastenfolge \e[0n
( <ESC>[0n
) ist ein ANSI-Terminal-Escape-Code , den ein Terminal sendet, um anzuzeigen, dass es normal funktioniert. Es sendet dies als Antwort auf eine Gerätestatusberichtsanforderung, die als gesendet wird <ESC>[5n
.
Indem echo
wir die Antwort an eine binden, die den einzufügenden Text ausgibt, können wir diesen Text jederzeit einfügen, indem wir den Gerätestatus abfragen. Dazu wird eine <ESC>[5n
Escape-Sequenz gesendet.
printf '\e[5n'
Dies funktioniert und reicht wahrscheinlich aus, um die ursprüngliche Frage zu beantworten, da keine anderen Tools beteiligt sind. Es ist pur bash
, setzt aber auf ein gut funktionierendes Terminal (praktisch alle).
Der wiedergegebene Text bleibt in der Befehlszeile zur Verwendung bereit, als ob er eingegeben worden wäre. Es kann angehängt, bearbeitet und durch Drücken ENTER
von ausgeführt werden.
Fügen Sie \n
den gebundenen Befehl hinzu, damit er automatisch ausgeführt wird.
Diese Lösung funktioniert jedoch nur im aktuellen Terminal (das im Rahmen der ursprünglichen Frage liegt). Es funktioniert über eine interaktive Eingabeaufforderung oder über ein bereitgestelltes Skript, löst jedoch einen Fehler aus, wenn es in einer Subshell verwendet wird:
bind: warning: line editing not enabled
Die richtige Lösung, die als nächstes beschrieben wird, ist flexibler, stützt sich jedoch auf externe Befehle.
Die richtige Lösung
Die richtige Methode zum Einfügen von Eingaben verwendet tty_ioctl , einen Unix-Systemaufruf für die E / A-Steuerung , der einen TIOCSTI
Befehl zum Einfügen von Eingaben enthält.
TIOC von " T erminal IOC tl " und STI von " S end T erminal I nput ".
Dafür ist kein Befehl eingebaut bash
. Dies erfordert einen externen Befehl. Es gibt keinen solchen Befehl in der typischen GNU / Linux-Distribution, aber es ist nicht schwierig, ihn mit ein wenig Programmierung zu erreichen. Hier ist eine Shell-Funktion, die Folgendes verwendet perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Hier 0x5412
ist der Code für den TIOCSTI
Befehl.
TIOCSTI
ist eine Konstante, die in den Standard-C-Header-Dateien mit dem Wert definiert ist 0x5412
. Versuchen Sie es grep -r TIOCSTI /usr/include
oder schauen Sie rein /usr/include/asm-generic/ioctls.h
; es ist indirekt in C-Programmen von enthalten #include <sys/ioctl.h>
.
Sie können dann Folgendes tun:
$ inject ls -l
ls -l$ ls -l <- cursor here
Implementierungen in einigen anderen Sprachen werden unten gezeigt (in einer Datei speichern und dann chmod +x
):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Sie können festlegen, sys/ioctl.ph
welche Definitionen TIOCSTI
anstelle des numerischen Werts verwendet werden sollen. Sehen Sie hier
Python 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) }
C inject.c
kompilieren mit 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;
}
**! ** Es gibt weitere Beispiele hier .
Unter Verwendung ioctl
dieses Werk in Subshells zu tun. Es kann auch in andere Terminals eingespeist werden, wie im Folgenden erläutert wird.
Weiterentwickeln (andere Terminals steuern)
Dies würde den Rahmen der ursprünglichen Frage sprengen, es ist jedoch möglich, Zeichen in ein anderes Terminal einzufügen, sofern die entsprechenden Berechtigungen vorliegen. Normalerweise bedeutet dies sein root
, aber siehe unten für andere Möglichkeiten.
Wenn Sie das oben angegebene C-Programm erweitern, um ein Befehlszeilenargument zu akzeptieren, das die tty eines anderen Terminals angibt, können Sie Folgendes in dieses Terminal einfügen:
#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;
}
Es sendet auch eine neue Zeile standardmäßig aber ähnlich echo
, es bietet eine -n
Möglichkeit , es zu unterdrücken. Die Option --t
oder --tty
erfordert ein Argument - das tty
des zu injizierenden Terminals. Der Wert hierfür kann in diesem Terminal abgerufen werden:
$ tty
/dev/pts/20
Kompiliere es mit gcc -o inject inject.c
. Stellen Sie dem einzufügenden Text ein Präfix voran, --
wenn er Bindestriche enthält, um zu verhindern, dass der Argument-Parser Befehlszeilenoptionen falsch interpretiert. Sehen ./inject --help
. Benutze es so:
$ inject --tty /dev/pts/22 -- ls -lrt
oder nur
$ inject -- ls -lrt
den Stromanschluss zu injizieren.
Für das Injizieren in ein anderes Terminal sind Administratorrechte erforderlich, die erhalten werden können durch:
- Ausgabe des Befehls als
root
,
- mit
sudo
,
- mit der
CAP_SYS_ADMIN
Fähigkeit oder
- Einstellen der ausführbaren Datei
setuid
Zuweisen CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Zuweisen setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Ausgabe reinigen
Eingefügter Text wird vor der Eingabeaufforderung angezeigt, als wäre er vor der Eingabeaufforderung eingegeben worden (was tatsächlich der Fall war), wird jedoch nach der Eingabeaufforderung erneut angezeigt.
Eine Möglichkeit, den Text vor der Eingabeaufforderung auszublenden, besteht darin, der Eingabeaufforderung einen \r
Zeilenumbruch ( kein Zeilenvorschub) voranzustellen und die aktuelle Zeile zu löschen ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Dadurch wird jedoch nur die Zeile gelöscht, in der die Eingabeaufforderung angezeigt wird. Wenn der eingefügte Text Zeilenumbrüche enthält, funktioniert dies nicht wie beabsichtigt.
Eine andere Lösung deaktiviert das Echo von eingefügten Zeichen. Ein Wrapper verwendet stty
dazu:
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"
wo inject
ist eine der oben beschriebenen lösungen oder ersetzt durch printf '\e[5n'
.
Alternative Ansätze
Wenn Ihre Umgebung bestimmte Voraussetzungen erfüllt, stehen Ihnen möglicherweise andere Methoden zur Verfügung, mit denen Sie Eingaben einfügen können. Wenn Sie sich in einer Desktop-Umgebung befinden, ist xdotool ein X.Org- Dienstprogramm, das Maus- und Tastaturaktivitäten simuliert, das Ihre Distribution jedoch möglicherweise nicht standardmäßig enthält. Du kannst es versuchen:
$ xdotool type ls
Wenn Sie den Terminal-Multiplexer tmux verwenden , können Sie dies tun:
$ tmux send-key -t session:pane ls
wo -t
wählt welche Sitzung und Bereich zu injizieren. GNU Screen hat eine ähnliche Fähigkeit mit seinem stuff
Befehl:
$ screen -S session -p pane -X stuff ls
Wenn Ihre Distribution das Paket console-tools enthält , haben Sie möglicherweise einen writevt
Befehl, der ioctl
wie in unseren Beispielen verwendet wird. Die meisten Distributionen haben dieses Paket jedoch zugunsten von kbd abgelehnt , dem diese Funktion fehlt.
Eine aktualisierte Kopie von writevt.c kann mit kompiliert werden gcc -o writevt writevt.c
.
Andere Optionen, die für einige Anwendungsfälle besser geeignet sind, sind Expect und Empty, mit denen interaktive Tools skriptgesteuert werden können.
Sie können auch eine Shell verwenden, die die Terminalinjektion unterstützt, wie zsh
dies möglich ist print -z ls
.
Die "Wow, das ist schlau ..." Antwort
Die hier beschriebene Methode wird auch hier besprochen und baut auf der hier besprochenen Methode auf .
Eine Shell-Umleitung von /dev/ptmx
erhält ein neues Pseudo-Terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Ein kleines in C geschriebenes Tool, das den Pseudoterminal-Master (ptm) entsperrt und den Namen des Pseudoterminal-Slaves (pts) an seine Standardausgabe ausgibt.
#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;
}
(speichern unter pts.c
und kompilieren mit gcc -o pts pts.c
)
Wenn das Programm mit einer Standardeingabe von ptm aufgerufen wird, entsperrt es die entsprechenden Punkte und gibt seinen Namen an die Standardausgabe aus.
$ ./pts </dev/ptmx
/dev/pts/20
Die Funktion unlockpt () entsperrt das Slave-Pseudoterminal, das dem Master-Pseudoterminal entspricht, auf das der angegebene Dateideskriptor verweist. Das Programm übergibt dies als Null, was die Standardeingabe des Programms ist .
Die Funktion ptsname () gibt den Namen des Slave-Pseudoterminals zurück, das dem Master entspricht, auf den der angegebene Dateideskriptor verweist, und übergibt für die Standardeingabe des Programms erneut Null.
Ein Prozess kann mit dem Punkt verbunden werden. Holen Sie sich zuerst ein ptm (hier ist es dem Dateideskriptor 3 zugeordnet, der vom <>
Redirect mit Lese- und Schreibzugriff geöffnet wird ).
exec 3<>/dev/ptmx
Dann starte den Prozess:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Die von dieser Befehlszeile ausgehenden Prozesse lassen sich am besten wie folgt veranschaulichen pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
Die Ausgabe ist relativ zur aktuellen Shell ( $$
) und die PID ( -p
) und PGID ( -g
) jedes Prozesses werden in Klammern angezeigt (PID,PGID)
.
Am Anfang des Baums befindet bash(5203,5203)
sich die interaktive Shell, in die wir Befehle eingeben, und ihre Dateideskriptoren verbinden sie mit der Terminalanwendung, die wir für die Interaktion mit ihr verwenden ( xterm
oder ähnlichem).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Bei nochmaliger Betrachtung des Befehls starteten die ersten Klammern eine Subshell, bash(6524,6524)
wobei der Dateideskriptor 0 (die Standardeingabe ) den PTS (die mit Lese- und Schreibzugriff geöffnet werden) zugewiesen wurde, die <>
von einer anderen Subshell zurückgegeben wurden, die ./pts <&3
zum Entsperren der PTS ausgeführt wurde Punkte, die dem Dateideskriptor 3 zugeordnet sind (der im vorherigen Schritt erstellt wurde exec 3<>/dev/ptmx
).
Der Dateideskriptor 3 3>&-
der Subshell ist geschlossen ( ), so dass der ptm nicht darauf zugreifen kann. Seine Standardeingabe (fd 0), die die Punkte darstellt, die beim Lesen / Schreiben geöffnet wurden, wird >&0
auf seine Standardausgabe (fd 1) umgeleitet (tatsächlich wird die fd - kopiert ).
Dadurch wird eine Subshell erstellt, deren Standardeingabe und -ausgabe mit den pts verbunden ist. Es kann eine Eingabe gesendet werden, indem in das ptm geschrieben wird, und die Ausgabe kann durch Lesen des ptm angezeigt werden:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Die Subshell führt diesen Befehl aus:
setsid -c bash -i 2>&1 | tee log
Es wird in einer neuen Sitzung bash(6527,6527)
im interaktiven ( -i
) Modus ausgeführt ( setsid -c
beachten Sie, dass PID und PGID identisch sind). Der Standardfehler wird auf die Standardausgabe ( 2>&1
) umgeleitet und über die Pipeline weitergeleitet, tee(6528,6524)
sodass er sowohl in eine log
Datei als auch in die Punkte geschrieben wird. Dies gibt eine andere Möglichkeit, die Ausgabe der Subshell zu sehen:
$ tail -f log
Da die Subshell bash
interaktiv ausgeführt wird, können Befehle zur Ausführung gesendet werden, wie in diesem Beispiel, in dem die Dateideskriptoren der Subshell angezeigt werden:
$ echo 'ls -l /dev/fd/' >&3
Das Lesen der Ausgabe ( tail -f log
oder cat <&3
) der Subshell zeigt:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Der Standardeingang (fd 0) ist mit dem Punkt verbunden, und sowohl der Standardausgang (fd 1) als auch der Fehler (fd 2) sind mit derselben Pipe verbunden, die mit Folgendem verbunden ist 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]
Und ein Blick auf die Dateideskriptoren von 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
Standardausgabe (fd 1) ist der Punkt: Alles, was 'tee' in seine Standardausgabe schreibt, wird an den Punkt zurückgesendet. Standardfehler (fd 2) sind die Punkte, die zum steuernden Terminal gehören.
Verpacken
Das folgende Skript verwendet die oben beschriebene Technik. Es wird eine interaktive bash
Sitzung eingerichtet, die durch Schreiben in einen Dateideskriptor eingefügt werden kann. Es ist hier verfügbar und mit Erläuterungen dokumentiert.
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