Esta respuesta se proporciona como una aclaración de mi propia comprensión y está inspirada en @ StéphaneChazelas y @mikeserv antes que yo.
TL; DR
- no es posible hacer esto
bash
sin ayuda externa;
- la forma correcta de hacerlo es con una entrada de terminal de envío
ioctl
pero
- La
bash
solución más fácil de usar bind
.
La solución fácil
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash tiene un shell incorporado llamado bind
que permite ejecutar un comando de shell cuando se recibe una secuencia de teclas. En esencia, la salida del comando de shell se escribe en el búfer de entrada del shell.
$ bind '"\e[0n": "ls -l"'
La secuencia de teclas \e[0n
( <ESC>[0n
) es un código de escape de Terminal ANSI que un terminal envía para indicar que está funcionando normalmente. Envía esto en respuesta a una solicitud de informe de estado del dispositivo que se envía como <ESC>[5n
.
Al vincular la respuesta a un echo
mensaje que genera el texto a inyectar, podemos inyectar ese texto siempre que lo solicitemos solicitando el estado del dispositivo y eso se hace enviando una <ESC>[5n
secuencia de escape.
printf '\e[5n'
Esto funciona y probablemente sea suficiente para responder la pregunta original porque no hay otras herramientas involucradas. Es puro bash
pero se basa en un terminal que se comporta bien (prácticamente todos lo son).
Deja el texto repetido en la línea de comando listo para ser utilizado como si hubiera sido escrito. Se puede agregar, editar y presionar ENTER
hace que se ejecute.
Agregue \n
al comando enlazado para que se ejecute automáticamente.
Sin embargo, esta solución solo funciona en el terminal actual (que está dentro del alcance de la pregunta original). Funciona desde una solicitud interactiva o desde un script de origen, pero genera un error si se usa desde una subshell:
bind: warning: line editing not enabled
La solución correcta que se describe a continuación es más flexible, pero se basa en comandos externos.
La solución correcta
La forma correcta de inyectar entrada usa tty_ioctl , una llamada al sistema de Unix para el control de E / S que tiene un TIOCSTI
comando que se puede usar para inyectar entrada.
TIOC de " T erminal IOC tl " y STI de " S end T erminal I nput ".
No hay un comando incorporado bash
para esto; hacerlo requiere un comando externo. No existe dicho comando en la distribución típica de GNU / Linux, pero no es difícil de lograr con un poco de programación. Aquí hay una función de shell que usa perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Aquí 0x5412
está el código para el TIOCSTI
comando.
TIOCSTI
es una constante definida en los archivos de cabecera C estándar con el valor 0x5412
. Probar grep -r TIOCSTI /usr/include
o mirar hacia adentro /usr/include/asm-generic/ioctls.h
; está incluido en los programas C indirectamente por#include <sys/ioctl.h>
.
Entonces puedes hacer:
$ inject ls -l
ls -l$ ls -l <- cursor here
Las implementaciones en algunos otros idiomas se muestran a continuación (guarde en un archivo y luego chmod +x
):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Puede generar lo sys/ioctl.ph
que define en TIOCSTI
lugar de usar el valor numérico. Ver aquí
Pitón 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)
Rubí inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
do inject.c
compilar 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;
}
**! ** Hay más ejemplos aquí .
Usar ioctl
para hacer esto funciona en subcapas. También puede inyectarse en otros terminales como se explica a continuación.
Llevándolo más lejos (controlando otros terminales)
Está más allá del alcance de la pregunta original, pero es posible inyectar caracteres en otro terminal, sujeto a tener los permisos apropiados. Normalmente esto significa serroot
, pero vea a continuación otras formas.
Extender el programa C dado anteriormente para aceptar un argumento de línea de comandos que especifique tty de otro terminal permite inyectar en ese 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;
}
También envía una nueva línea de forma predeterminada, pero, de forma similar echo
, proporciona una -n
opción para suprimirla. La opción --t
o --tty
requiere un argumento: el tty
del terminal a inyectar. El valor para esto se puede obtener en ese terminal:
$ tty
/dev/pts/20
Compilarlo con gcc -o inject inject.c
. Prefije el texto para inyectar --
si contiene guiones para evitar que el analizador de argumentos malinterprete las opciones de la línea de comandos. Ver ./inject --help
. Úselo así:
$ inject --tty /dev/pts/22 -- ls -lrt
o solo
$ inject -- ls -lrt
para inyectar el terminal actual.
Inyectar en otro terminal requiere derechos administrativos que se pueden obtener mediante:
- emitiendo el comando como
root
,
- utilizando
sudo
,
- teniendo la
CAP_SYS_ADMIN
capacidad o
- configurar el ejecutable
setuid
Para asignar CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Para asignar setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Salida limpia
El texto inyectado aparece antes de la solicitud como si se hubiera escrito antes de que apareciera la solicitud (que, en efecto, lo fue) pero luego vuelve a aparecer después de la solicitud.
Una forma de ocultar el texto que aparece antes de la solicitud es anteponer la solicitud con un retorno de carro ( \r
no salto de línea) y borrar la línea actual ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Sin embargo, esto solo borrará la línea en la que aparece el mensaje. Si el texto inyectado incluye nuevas líneas, entonces esto no funcionará según lo previsto.
Otra solución deshabilita el eco de los caracteres inyectados. Un contenedor utiliza stty
para hacer esto:
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"
donde inject
es una de las soluciones descritas anteriormente, o reemplazada por printf '\e[5n'
.
Aproximaciones alternativas
Si su entorno cumple con ciertos requisitos previos, entonces puede tener otros métodos disponibles que puede usar para inyectar entradas. Si está en un entorno de escritorio, entonces xdotool es una utilidad X.Org que simula la actividad del mouse y el teclado, pero es posible que su distribución no lo incluya de manera predeterminada. Puedes probar:
$ xdotool type ls
Si usa tmux , el multiplexor terminal, puede hacer esto:
$ tmux send-key -t session:pane ls
donde -t
selecciona qué sesión y panel inyectar. GNU Screen tiene una capacidad similar con su stuff
comando:
$ screen -S session -p pane -X stuff ls
Si su distribución incluye el paquete de herramientas de la consola, entonces puede tener un writevt
comando que se usa ioctl
como nuestros ejemplos. Sin embargo, la mayoría de las distribuciones han desaprobado este paquete a favor de kbd que carece de esta característica.
Se puede compilar una copia actualizada de writevt.c usando gcc -o writevt writevt.c
.
Otras opciones que pueden adaptarse mejor a algunos casos de uso incluyen esperar y vaciar, que están diseñadas para permitir que las herramientas interactivas sean programadas.
También podría usar un shell que admita la inyección de terminal, como lo zsh
que puede hacer print -z ls
.
La respuesta "Wow, eso es inteligente ..."
El método descrito aquí también se discute aquí y se basa en el método discutido aquí .
Un redireccionamiento de shell /dev/ptmx
obtiene un nuevo pseudo-terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Una pequeña herramienta escrita en C que desbloquea el pseudoterminal master (ptm) y muestra el nombre del pseudoterminal slave (pts) en su salida estándar.
#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;
}
(guardar como pts.c
y compilar con gcc -o pts pts.c
)
Cuando se llama al programa con su entrada estándar establecida en un ptm, desbloquea los pts correspondientes y envía su nombre a la salida estándar.
$ ./pts </dev/ptmx
/dev/pts/20
La función unlockpt () desbloquea el dispositivo pseudoterminal esclavo correspondiente al pseudoterminal maestro al que se refiere el descriptor de archivo dado. El programa pasa esto a cero, que es la entrada estándar del programa .
La función ptsname () devuelve el nombre del dispositivo pseudoterminal esclavo correspondiente al maestro al que hace referencia el descriptor de archivo dado, pasando nuevamente cero para la entrada estándar del programa.
Se puede conectar un proceso a los pts. Primero obtenga un ptm (aquí se asigna al descriptor de archivo 3, abierto lectura-escritura por la <>
redirección).
exec 3<>/dev/ptmx
Luego comienza el proceso:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Los procesos generados por esta línea de comandos se ilustran mejor con pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
El resultado es relativo al shell actual ( $$
) y el PID ( -p
) y el PGID ( -g
) de cada proceso se muestran entre paréntesis (PID,PGID)
.
En la parte superior del árbol se encuentra bash(5203,5203)
el shell interactivo en el que estamos escribiendo comandos, y sus descriptores de archivos lo conectan a la aplicación de terminal que estamos usando para interactuar con él ( xterm
o similar).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Al mirar el comando nuevamente, el primer conjunto de paréntesis comenzó una subshell, bash(6524,6524)
con su descriptor de archivo 0 (su entrada estándar ) asignado a los pts (que se abre lectura-escritura <>
) , como lo devuelve otra subshell que se ejecutó ./pts <&3
para desbloquear el pts asociados con el descriptor de archivo 3 (creado en el paso anterior exec 3<>/dev/ptmx
).
El descriptor de archivo de la subshell 3 está cerrado ( 3>&-
) para que el ptm no sea accesible para él. Su entrada estándar (fd 0), que es el pts que se abrió lectura / escritura, se redirige (en realidad, se copia la fd >&0
) a su salida estándar (fd 1).
Esto crea una subshell con su entrada y salida estándar conectadas a los pts. Se puede enviar entrada escribiendo al ptm y su salida se puede ver leyendo desde el ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
La subshell ejecuta este comando:
setsid -c bash -i 2>&1 | tee log
Se ejecuta bash(6527,6527)
en modo interactivo ( -i
) en una nueva sesión ( setsid -c
tenga en cuenta que el PID y el PGID son los mismos). Su error estándar se redirige a su salida estándar ( 2>&1
) y se canaliza tee(6528,6524)
para que se escriba en un log
archivo y en los pts. Esto le da otra forma de ver la salida del subshell:
$ tail -f log
Debido a que la subshell se ejecuta de forma bash
interactiva, se pueden enviar comandos para ejecutar, como este ejemplo que muestra los descriptores de archivo de la subshell:
$ echo 'ls -l /dev/fd/' >&3
La lectura de la salida del subshell ( tail -f log
o cat <&3
) revela:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
La entrada estándar (fd 0) está conectada a los pts y tanto la salida estándar (fd 1) como el error (fd 2) están conectados a la misma tubería, la que se conecta 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]
Y un vistazo a los descriptores de archivo 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 salida estándar (fd 1) es el pts: todo lo que 'tee' escribe en su salida estándar se envía de vuelta al ptm. Error estándar (fd 2) son los pts que pertenecen al terminal de control.
Envolviendolo
El siguiente script utiliza la técnica descrita anteriormente. Configura una bash
sesión interactiva que puede inyectarse escribiendo en un descriptor de archivo. Está disponible aquí y está documentado con explicaciones.
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