Bash peut-il écrire dans son propre flux d'entrée?

39

Est-il possible dans un shell bash interactif d'entrer une commande qui génère du texte afin qu'il apparaisse à la prochaine invite de commande, comme si l'utilisateur avait saisi ce texte à cette invite?

Je veux pouvoir sourcecréer un script qui générera une ligne de commande et la affichera afin qu'elle apparaisse lorsque l'invite revient après la fin du script, de sorte que l'utilisateur puisse éventuellement le modifier avant d'appuyer sur enterpour l'exécuter.

Cela peut être réalisé avec, xdotoolmais cela ne fonctionne que lorsque le terminal est dans une fenêtre X et uniquement s’il est installé.

[me@mybox] 100 $ xdotool type "ls -l"
[me@mybox] 101 $ ls -l  <--- cursor appears here!

Cela peut-il être fait en utilisant uniquement bash?

starfry
la source
Je pense que cela ne devrait pas être difficile avec Expect, si vous pouvez le tolérer et le laisser conduire à une sous-coque; mais je ne m'en souviens pas assez pour poster une réponse réelle.
triplee

Réponses:

40

Avec zsh, vous pouvez utiliser print -zdu texte dans la mémoire tampon de l’éditeur de lignes pour la prochaine invite:

print -z echo test

amorcerait l’éditeur de ligne avec echo testlequel vous pourrez éditer à l’invite suivante.

Je ne pense pas qu’il bashexiste une fonctionnalité similaire, mais sur de nombreux systèmes, vous pouvez amorcer le tampon d’entrée du terminal avec TIOCSTI ioctl():

perl -e 'require "sys/ioctl.ph"; ioctl(STDIN, &TIOCSTI, $_)
  for split "", join " ", @ARGV' echo test

Insérerait echo testdans le tampon d'entrée du terminal, comme s'il avait été reçu du terminal.

Une variante plus portable de l' Terminologyapproche de @ mike et qui ne sacrifie pas la sécurité consisterait à envoyer à l'émulateur de terminal une query status reportséquence d'échappement assez standard : les <ESC>[5nterminaux répondent invariablement (en tant qu'entrée) <ESC>[0net sont liés à la chaîne que vous souhaitez insérer:

bind '"\e[0n": "echo test"'; printf '\e[5n'

Si screenvous êtes dans GNU , vous pouvez également faire:

screen -X stuff 'echo test'

Désormais, à l'exception de l'approche TIOCSTI ioctl, nous demandons à l'émulateur de terminal de nous envoyer une chaîne comme si elle était typée. Si cette chaîne précède readline( bashl'éditeur de ligne) a désactivé l'écho local du terminal, cette chaîne ne sera pas affichée à l'invite du shell, ce qui perturbera légèrement l'affichage.

Pour contourner ce problème, vous pouvez retarder légèrement l'envoi de la demande au terminal afin de vous assurer que la réponse parvient lorsque l'écho a été désactivé par readline.

bind '"\e[0n": "echo test"'; ((sleep 0.05;  printf '\e[5n') &)

(ici en supposant que votre sleepsupporte une résolution inférieure à la seconde).

Idéalement, vous voudriez faire quelque chose comme:

bind '"\e[0n": "echo test"'
stty -echo
printf '\e[5n'
wait-until-the-response-arrives
stty echo

Cependant bash(contrairement à zsh) ne supporte pas un tel wait-until-the-response-arrivesqui ne lit pas la réponse.

Cependant, il a une has-the-response-arrived-yetfonctionnalité avec read -t0:

bind '"\e[0n": "echo test"'
saved_settings=$(stty -g)
stty -echo -icanon min 1 time 0
printf '\e[5n'
until read -t0; do
  sleep 0.02
done
stty "$saved_settings"

Lectures complémentaires

Voir la réponse de @ starfry qui développe les deux solutions proposées par @mikeserv et moi-même avec quelques informations plus détaillées.

Stéphane Chazelas
la source
Je pense que bind '"\e[0n": "echo test"'; printf '\e[5n'probablement la réponse uniquement bash que je cherche. Ça marche pour moi. Cependant, je suis également ^[[0nimprimé avant mon invite. J'ai découvert que c'est causé quand $PS1contient un sous-shell. Vous pouvez le reproduire en le faisant PS1='$(:)'avant la commande bind. Pourquoi cela arriverait-il et peut-on faire quelque chose à ce sujet?
starfry
Bien que tout dans cette réponse soit correct, la question était pour bash, pas zsh. Parfois, nous n'avons pas le choix du shell à utiliser.
Falsenames
@Falsenames seul le premier paragraphe est pour zsh. Le reste est soit agnostique, soit spécifique à Bash. Les questions et réponses ne doivent pas être utiles uniquement aux utilisateurs de bash.
Stéphane Chazelas
1
@starfry semble que vous pourriez peut-être mettre un \rchiffre à la tête de $PS1? Cela devrait fonctionner si $PS1est assez long. Si non, alors mettez- ^[[My.
mikeserv
@mikeserv - rfait le tour. Cela n'empêche évidemment pas la sortie, elle est simplement écrasée avant que l'œil ne la voie. J'imagine que ^[[Mla ligne efface le texte injecté au cas où il serait plus long que l'invite. Est-ce exact (je ne l'ai pas trouvée dans la liste d'échappement ANSI que j'ai)?
starfry
25

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"

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

-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
starfry
la source
Avec la bind '"\e[0n": "ls -l"'; printf '\e[5n'solution la plus simple , après tout, le résultat de la sortie sera ls -légalement ^[[0naffiché sur le terminal une fois que j'aurai appuyé sur la touche Entrée ls -l. Des idées comment "cacher" s'il vous plaît? Merci.
Ali
1
J'ai présenté une solution qui donne l'effet recherché: dans la section des résultats en clair de ma réponse, je suggère d'ajouter un retour à l'invite pour masquer le texte trop épais. J'ai essayé PS1="\r\e[M$PS1"avant de faire bind '"\e[0n": "ls -l"'; printf '\e[5n'et cela a donné l'effet que vous décrivez.
starfry
Merci! J'ai totalement raté ce point.
Ali
20

Cela dépend de ce que vous entendez par bashseulement . Si vous voulez parler d'une seule bashsession interactive , la réponse est presque certainement non . Et c’est parce que même lorsque vous entrez une commande comme ls -lsur la ligne de commande de n’importe quel terminal canonique, alors bashn’en êtes même pas conscient - et bashn’est même pas impliqué à ce stade.

Au lieu de cela, ce qui s’est passé jusqu’à présent, c’est que la discipline de ligne du noyau a été mise en mémoire tampon et que stty echol’utilisateur n’a saisi que l’écran. Il efface cette entrée pour son lecteur - bash, dans votre cas, - ligne par ligne - et traduit généralement les \rrésumés en \nlignes électroniques sur les systèmes Unix - et il en bashest de même - et votre script source ne peut donc en être informé - il est conscient qu'il en existe entrer du tout jusqu'à ce que l'utilisateur appuie sur la ENTERtouche.

Maintenant, il y a quelques solutions de rechange. Le plus robuste n'est en réalité pas une solution de contournement et implique l'utilisation de plusieurs processus ou programmes spécialement écrits pour séquencer les entrées, masquer la discipline de lignes -echoà l'utilisateur et n'écrire que sur l'écran ce qui est jugé approprié pendant l'interprétation des entrées. spécialement lorsque nécessaire. Cela peut être difficile à faire, car cela implique d’écrire des règles d’interprétation capables de gérer une entrée arbitraire, caractère par caractère, et de les écrire simultanément sans erreur afin de simuler les attentes de l’utilisateur moyen dans ce scénario. C’est pour cette raison, probablement, que les entrées / sorties de terminaux interactifs sont si rarement bien comprises - une perspective difficile n’est pas celle qui se prête à une investigation plus poussée pour la plupart.

Une autre solution pourrait impliquer l’émulateur de terminal. Vous dites qu'un problème pour vous est une dépendance à X et à xdotool. Dans ce cas, une solution de ce genre que je suis sur le point de proposer pourrait poser des problèmes similaires, mais je vais continuer avec la même chose.

printf  '\33[22;1t\33]1;%b\33\\\33[20t\33[23;0t' \
        '\025my command'

Cela fonctionne dans un xtermw / l' allowwindowOpsensemble des ressources. Il enregistre d’abord les noms d’icône / de fenêtre sur une pile, puis configure la chaîne d’icônes du terminal pour demander ^Umy commandensuite au terminal d’injecter ce nom dans la file d’entrée, puis le réinitialise aux valeurs enregistrées. Cela devrait fonctionner de manière invisible pour les bashshells interactifs exécutés dans une xterm configuration correcte, mais c'est probablement une mauvaise idée. Veuillez voir les commentaires de Stéphane ci-dessous.

Voici cependant une photo de mon terminal Terminology que j'ai prise après avoir exécuté le printfbit avec une séquence d'échappement différente sur ma machine. Pour chaque nouvelle ligne de la printfcommande, j'ai tapé CTRL+Vpuis CTRL+J, puis appuyé sur la ENTERtouche. Je n'ai rien saisi après, mais, comme vous pouvez le constater, le terminal s'est injecté my commanddans la file d'attente de saisie de la discipline de ligne:

term_inject

La vraie façon de faire est d'utiliser un pty imbriqué. C’est comment screenet tmuxun travail similaire - les deux, à propos, peuvent vous rendre cela possible. xtermvient en fait avec un petit programme appelé luitqui peut également rendre cela possible. Ce n'est pas facile, cependant.

Voici une façon dont vous pourriez:

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

Ce n'est en aucun cas portable, mais devrait fonctionner sur la plupart des systèmes Linux avec les autorisations appropriées pour l'ouverture /dev/ptmx. Mon utilisateur est dans le ttygroupe, ce qui est suffisant sur mon système. Vous aurez aussi besoin de ...

<<\C cc -xc - -o 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;
}
C

... qui, lorsqu'il est exécuté sur un système GNU (ou tout autre avec un compilateur C standard pouvant également lire stdin) , écrira un petit binaire exécutable nommé ptsqui exécutera la unlockpt()fonction sur son stdin et écrira sur son stdout nom du périphérique pty qu'il vient de déverrouiller. Je l'ai écrit en travaillant sur ... Comment puis-je venir par ce pty et que puis-je faire avec? .

Quoi qu'il en soit, le bit de code ci-dessus exécute un bashshell dans un pty un calque situé sous le tty actuel. bashest dit d'écrire toutes les sorties sur le pty esclave, et le tty actuel est configuré non pas pour -echoson entrée ni pour le mettre en tampon, mais plutôt pour le transmettre (principalement) raw à cat, qui le copie bash. Et pendant ce temps-là, l’arrière-plan catcopie toutes les sorties d’esclaves vers le tty actuel.

Pour la plupart, la configuration ci-dessus serait totalement inutile - simplement redondante, en gros - sauf que nous lançons bashavec une copie de son propre fichier pty master fd on <>9. Cela signifie que vous bashpouvez écrire librement dans son propre flux d’entrée avec une simple redirection. Tout ce qu'il bashfaut faire c'est:

echo echo hey >&9

... se parler.

Voici une autre image:

entrez la description de l'image ici

Mikeserv
la source
2
Quels terminaux avez-vous réussi à faire fonctionner cela? Ce genre de chose a été maltraité dans le passé et devrait être désactivé par défaut de nos jours. Avec xterm, vous pouvez toujours interroger le titre de l'icône avec, \e[20tmais seulement s'il est configuré avec allowWindowOps: true.
Stéphane Chazelas
C'est CVE-2003-0063
Stéphane Chazelas le
@ StéphaneChazelas qui fonctionne en terminologie, mais je suis à peu près sûr que cela fonctionne également en terminal gnome, dans le terminal kde (j'oublie son nom et je pense qu'il y a une évasion différente) , et comme vous le dites, w / xtermw / the approprié config. W / an xterm proprement dit, vous pouvez lire et écrire le tampon copier / coller et cela devient donc plus simple, je pense. Xterm a aussi des séquences d'échappement pour changer / affecter la description du terme lui-même.
mikeserv
Je ne peux obtenir que cela fonctionne dans autre chose que dans la terminologie (qui contient plusieurs autres vulnérabilités similaires). CVE ayant plus de 12 ans et qui est relativement bien connu, je serais surpris de savoir si l'un des émulateurs de terminaux principaux présente la même vulnérabilité. Notez qu'avec xterm, c'est \e[20t(pas \e]1;?\a)
Stéphane Chazelas le
8

Bien que la ioctl(,TIOCSTI,) réponse de Stéphane Chazelas soit, bien sûr, la bonne réponse, certaines personnes pourraient être assez satisfaites de cette réponse partielle mais triviale: il suffit de placer la commande dans la pile d’historique, l’utilisateur peut alors déplacer une ligne vers le haut pour rechercher le commander.

$ history -s "ls -l"
$ echo "move up 1 line in history to get command to run"

Cela peut devenir un script simple, qui a son propre historique en 1 ligne:

#!/bin/bash
history -s "ls -l"
read -e -p "move up 1 line: "
eval "$REPLY"

read -eactive l'édition en lecture de l'entrée, -pest une invite.

meuh
la source
Cela ne fonctionnera que dans les fonctions du shell, ou si le script a été généré ( . foo.shou `source foo.sh, au lieu d'être exécuté dans un sous-shell.) Une approche intéressante, cependant. Un hack similaire qui nécessite de modifier le contexte du shell appelant serait de définir un achèvement personnalisé qui étendrait la ligne vide à quelque chose, puis restaurerait l'ancien gestionnaire d'achèvement.
Peter Cordes
@PeterCordes vous avez raison. Je prenais la question trop littéralement. Mais j'ai ajouté un exemple de script simple qui pourrait fonctionner.
meuh
@mikeserv Hey, c'est juste une solution simple qui peut être utile à certaines personnes. Vous pouvez même supprimer le evalsi vous avez des commandes simples à éditer, sans tuyaux ni redirection, etc.
meuh
1

Oh, ma parole, nous avons manqué une solution simple intégrée à bash : la readcommande a une option -i ...qui, lorsqu'elle est utilisée avec -e, insère du texte dans le tampon de saisie. De la page de manuel:

-i texte

Si readline est utilisé pour lire la ligne, le texte est placé dans le tampon d'édition avant le début de l'édition.

Créez donc une petite fonction bash ou un script shell qui prend la commande à présenter à l'utilisateur et exécute ou évalue sa réponse:

domycmd(){ read -e -i "$*"; eval "$REPLY"; }

Ceci utilise sans doute l'ioctl (, TIOCSTI), utilisé depuis plus de 32 ans, comme il existait déjà dans 2.9BSD ioctl.h .

meuh
la source
1
Intéressant avec un effet similaire, mais cela n’injecte pas dans l’invite.
starfry
à la 2e pensée, vous avez raison. bash n’a pas besoin de TIOCSTI car il fait tout l’E / S elle-même.
Meuh