Cette réponse est fournie à titre de clarification de ma propre compréhension et est inspirée par @ StéphaneChazelas et @mikeserv avant moi.
TL; DR
- il n'est pas possible de le faire
bashsans aide extérieure;
- la bonne façon de faire est avec une entrée de terminal d'envoi,
ioctl mais
- la
bashsolution la plus facile à utiliser utilise bind.
La solution facile
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash a un shell intégré appelé bindqui permet à une commande shell d’être exécutée lorsqu’une séquence de touches est reçue. En substance, la sortie de la commande shell est écrite dans le tampon d'entrée du shell.
$ bind '"\e[0n": "ls -l"'
La séquence de touches \e[0n( <ESC>[0n) est un code d'échappement de terminal ANSI envoyé par un terminal pour indiquer qu'il fonctionne normalement. Il envoie ceci en réponse à une demande de rapport d'état de périphérique qui est envoyée en tant que <ESC>[5n.
En liant la réponse à une echosortie du texte à injecter, nous pouvons injecter ce texte à tout moment en demandant l'état du périphérique, et cela en envoyant une <ESC>[5nséquence d'échappement.
printf '\e[5n'
Cela fonctionne et est probablement suffisant pour répondre à la question initiale car aucun autre outil n'est impliqué. C'est pur bashmais repose sur un terminal qui se comporte bien (pratiquement tous le sont).
Il laisse le texte renvoyé sur la ligne de commande prêt à être utilisé comme s'il avait été saisi. Vous pouvez l'ajouter, le modifier et appuyer sur ENTERpour l'exécuter.
Ajoutez \nà la commande liée pour l'exécuter automatiquement.
Cependant, cette solution ne fonctionne que dans le terminal actuel (ce qui relève de la question d'origine). Cela fonctionne à partir d'une invite interactive ou d'un script source mais il génère une erreur s'il est utilisé depuis un sous-shell:
bind: warning: line editing not enabled
La solution correcte décrite ci-après est plus flexible, mais elle repose sur des commandes externes.
La bonne solution
La méthode appropriée pour injecter une entrée utilise tty_ioctl , un appel système Unix pour le contrôle d'E / S comportant une TIOCSTIcommande permettant d'injecter une entrée.
TIOC de " T erminal CIO tl " et STI de " S fin T erminal I nput ".
Il n'y a pas de commande intégrée bashpour cela; cela nécessite une commande externe. Il n’existe pas de commande de ce type dans la distribution GNU / Linux typique, mais ce n’est pas difficile à réaliser avec un peu de programmation. Voici une fonction shell qui utilise perl:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Voici 0x5412le code de la TIOCSTIcommande.
TIOCSTIest une constante définie dans les fichiers d'en-tête C standard avec la valeur 0x5412. Essayez grep -r TIOCSTI /usr/include, ou regardez dedans /usr/include/asm-generic/ioctls.h; il est inclus dans les programmes C indirectement par #include <sys/ioctl.h>.
Vous pouvez alors faire:
$ inject ls -l
ls -l$ ls -l <- cursor here
Les implémentations dans d’autres langues sont présentées ci-dessous (sauvegardées dans un fichier puis dans un fichier chmod +x):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Vous pouvez générer sys/ioctl.phce qui définit TIOCSTIau lieu d'utiliser la valeur numérique. Voir ici
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)
Rubis inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
compiler avec 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;
}
**! ** Il y a d'autres exemples ici .
Utiliser ioctlpour faire cela fonctionne dans les sous-coques. Il peut également s'injecter dans d'autres terminaux, comme expliqué ci-après.
Aller plus loin (contrôler d'autres terminaux)
Cela dépasse le cadre de la question initiale, mais il est possible d'injecter des caractères dans un autre terminal, sous réserve de disposer des autorisations appropriées. Normalement, cela signifie être root, mais voir ci-dessous pour d'autres moyens.
L'extension du programme C donné ci-dessus pour accepter un argument de ligne de commande spécifiant le terminal d'un autre terminal permet d'injecter dans ce terminal:
#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;
}
Il envoie également une nouvelle ligne par défaut, mais, similaire à echo, il offre une -noption pour le supprimer. L' option --tou --ttynécessite un argument - ttyle terminal à injecter. La valeur pour cela peut être obtenue dans ce terminal:
$ tty
/dev/pts/20
Compilez-le avec gcc -o inject inject.c. Préfixez le texte à injecter --s'il contient des traits d'union afin d'éviter que l'analyseur d'arguments interprète mal les options de ligne de commande. Voir ./inject --help. Utilisez-le comme ceci:
$ inject --tty /dev/pts/22 -- ls -lrt
ou juste
$ inject -- ls -lrt
injecter le terminal courant.
L'injection dans un autre terminal nécessite des droits d'administration pouvant être obtenus par:
- émettre la commande en tant que
root,
- en utilisant
sudo,
- avoir la
CAP_SYS_ADMINcapacité ou
- mettre l'exécutable
setuid
Pour assigner CAP_SYS_ADMIN:
$ sudo setcap cap_sys_admin+ep inject
Pour assigner setuid:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Sortie propre
Le texte injecté apparaît avant l'invite comme s'il avait été tapé avant l'invite (ce qui était le cas), mais il réapparaît ensuite après l'invite.
Une façon de masquer le texte qui apparaît avant l'invite consiste à l'ajouter à la fin avec un retour à la \rligne ( pas de saut de ligne) et à effacer la ligne actuelle ( <ESC>[M):
$ PS1="\r\e[M$PS1"
Cependant, cela effacera uniquement la ligne sur laquelle l'invite apparaît. Si le texte injecté inclut des nouvelles lignes, cela ne fonctionnera pas comme prévu.
Une autre solution désactive l'écho des caractères injectés. Un wrapper utilise sttypour faire ceci:
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"
où injectest l’une des solutions décrites ci-dessus ou remplacée par printf '\e[5n'.
Approches alternatives
Si votre environnement remplit certaines conditions préalables, vous pouvez disposer d'autres méthodes que vous pouvez utiliser pour injecter des entrées. Si vous êtes dans un environnement de bureau, xdotool est un utilitaire X.Org qui simule l’activité de la souris et du clavier, mais votre distribution risque de ne pas l’inclure par défaut. Tu peux essayer:
$ xdotool type ls
Si vous utilisez tmux , le multiplexeur de terminaux, vous pouvez procéder comme suit :
$ tmux send-key -t session:pane ls
où -tsélectionne la session et le volet à injecter. GNU Screen a une capacité similaire avec sa stuffcommande:
$ screen -S session -p pane -X stuff ls
Si votre distribution inclut le paquet console-tools , alors vous pouvez avoir une writevtcommande qui utilise ioctlcomme nos exemples. Cependant, la plupart des distributions ont déconseillé ce package au profit de kbd, qui manque de cette fonctionnalité.
Une copie mise à jour de writevt.c peut être compilée avec gcc -o writevt writevt.c.
Parmi les autres options pouvant convenir à certains cas d'utilisation, on peut citer les options expect et empty, conçues pour permettre la scriptage d'outils interactifs.
Vous pouvez également utiliser un shell qui prend en charge l’injection terminale, comme zshce que vous pouvez faire print -z ls.
La réponse "Wow, c'est intelligent ..."
La méthode décrite ici est également discutée ici et repose sur la méthode discutée ici .
Une redirection de shell à partir d' /dev/ptmxun nouveau pseudo-terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Un petit outil écrit en C qui déverrouille le maître pseudoterminal (ptm) et renvoie le nom de l'esclave pseudoterminal (pts) sur sa sortie 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;
}
(enregistrer sous pts.cet compiler avec gcc -o pts pts.c)
Lorsque le programme est appelé avec son entrée standard définie sur un ptm, il déverrouille les points correspondants et affiche son nom sur la sortie standard.
$ ./pts </dev/ptmx
/dev/pts/20
La fonction unlockpt () déverrouille le périphérique pseudoterminal esclave correspondant au pseudoterminal maître auquel le descripteur de fichier donné fait référence. Le programme passe ceci à zéro, ce qui correspond à l' entrée standard du programme .
La fonction ptsname () renvoie le nom du périphérique pseudoterminal esclave correspondant au maître référencé par le descripteur de fichier donné, en passant à nouveau à zéro pour l'entrée standard du programme.
Un processus peut être connecté aux pts. Commencez par obtenir un ptm (ici, il est assigné au descripteur de fichier 3, ouvert en lecture-écriture par la <>redirection).
exec 3<>/dev/ptmx
Puis démarrez le processus:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Les processus générés par cette ligne de commande sont mieux illustrés avec pstree:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
La sortie est relative au shell actuel ( $$) et les PID ( -p) et PGID ( -g) de chaque processus sont indiqués entre parenthèses (PID,PGID).
Le bash(5203,5203)shell interactif dans lequel nous tapons les commandes et ses descripteurs de fichier le connectent au terminal que nous utilisons pour interagir avec lui ( xterm, ou similaire).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
En regardant à nouveau la commande, le premier ensemble de parenthèses a démarré un sous-shell, bash(6524,6524)avec son descripteur de fichier 0 (son entrée standard ) attribué aux pts (ouvert en lecture-écriture <>), renvoyé par un autre sous-shell ayant exécuté ./pts <&3le déverrouillage du fichier. pts associés au descripteur de fichier 3 (créé à l'étape précédente exec 3<>/dev/ptmx).
Le descripteur de fichier 3 du sous-shell est fermé ( 3>&-), de sorte que le gestionnaire de fichiers n'est pas accessible. Son entrée standard (fd 0), qui correspond aux points ouverts en lecture / écriture, est redirigée (en réalité, le fd est copié - >&0) vers sa sortie standard (fd 1).
Cela crée un sous-shell avec son entrée et sa sortie standard connectées aux pts. Il peut être envoyé en entrée en écrivant sur le ptm et sa sortie peut être vue en lisant à partir du ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Le sous-shell exécute cette commande:
setsid -c bash -i 2>&1 | tee log
Il s'exécute bash(6527,6527)en -imode interactif ( ) dans une nouvelle session ( setsid -cnotez que le PID et le PGID sont identiques). Son erreur standard est redirigée vers sa sortie standard ( 2>&1) et acheminée via: tee(6528,6524)il est donc écrit dans un logfichier ainsi que dans les pts. Cela donne une autre façon de voir la sortie du sous-shell:
$ tail -f log
Le sous-shell s'exécutant de manière bashinteractive, il est possible d'envoyer des commandes à exécuter, comme dans cet exemple qui affiche les descripteurs de fichier du sous-shell:
$ echo 'ls -l /dev/fd/' >&3
La lecture de la sortie du sous-shell ( tail -f logou cat <&3) révèle:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
L'entrée standard (fd 0) est connectée aux pts et la sortie standard (fd 1) et l'erreur (fd 2) sont connectées au même tuyau, celui qui se connecte à 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]
Et un regard sur les descripteurs de fichier de 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
La sortie standard (fd 1) correspond aux points: tout ce qui est écrit en “tee” sur sa sortie standard est renvoyé à la ptm. Erreur standard (fd 2) correspond aux points appartenant au terminal de contrôle.
Envelopper
Le script suivant utilise la technique décrite ci-dessus. Il configure une bashsession interactive qui peut être injectée en écrivant dans un descripteur de fichier. Il est disponible ici et documenté avec des explications.
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