Esta resposta é fornecida como esclarecimento do meu próprio entendimento e é inspirada em @ StéphaneChazelas e @mikeserv antes de mim.
TL; DR
- não é possível fazer isso
bashsem ajuda externa;
- a maneira correta de fazer isso é com uma entrada do terminal de envio,
ioctl mas
- a
bashsolução viável mais fácil usa bind.
A solução fácil
bind '"\e[0n": "ls -l"'; printf '\e[5n'
O Bash possui um shell interno chamado bindque permite que um comando do shell seja executado quando uma sequência de teclas é recebida. Em essência, a saída do comando shell é gravada no buffer de entrada do shell.
$ bind '"\e[0n": "ls -l"'
A sequência de teclas \e[0n( <ESC>[0n) é um código de escape do terminal ANSI que um terminal envia para indicar que está funcionando normalmente. Ele envia isso em resposta a uma solicitação de relatório de status do dispositivo que é enviada como <ESC>[5n.
Ao vincular a resposta a uma echoque gera o texto a ser injetado, podemos injetar esse texto sempre que quisermos solicitando o status do dispositivo e isso é feito enviando uma <ESC>[5nsequência de escape.
printf '\e[5n'
Isso funciona e provavelmente é suficiente para responder à pergunta original porque não há outras ferramentas envolvidas. É puro, bashmas depende de um terminal com bom comportamento (praticamente todos são).
Ele deixa o texto ecoado na linha de comando pronto para ser usado como se tivesse sido digitado. Ele pode ser anexado, editado e pressionado ENTERfaz com que seja executado.
Adicione \nao comando ligado para que ele seja executado automaticamente.
No entanto, esta solução funciona apenas no terminal atual (que está dentro do escopo da pergunta original). Ele funciona a partir de um prompt interativo ou de um script de origem, mas gera um erro se usado em um subshell:
bind: warning: line editing not enabled
A solução correta descrita a seguir é mais flexível, mas depende de comandos externos.
A solução correta
A maneira correta de injetar entrada usa tty_ioctl , uma chamada de sistema unix para Controle de E / S que possui um TIOCSTIcomando que pode ser usado para injetar entrada.
TIOC de " T erminal COI tl " e STI de " S final T erminal I nput ".
Não há comando incorporado bashpara isso; fazer isso requer um comando externo. Não existe tal comando na distribuição típica do GNU / Linux, mas não é difícil de obter com um pouco de programação. Aqui está uma função shell que usa perl:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Aqui 0x5412está o código para o TIOCSTIcomando.
TIOCSTIé uma constante definida nos arquivos de cabeçalho C padrão com o valor 0x5412. Tente grep -r TIOCSTI /usr/includeou procure /usr/include/asm-generic/ioctls.h; está incluído nos programas C indiretamente por #include <sys/ioctl.h>.
Você pode então fazer:
$ inject ls -l
ls -l$ ls -l <- cursor here
Implementações em alguns outros idiomas são mostradas abaixo (salve em um arquivo e em seguida chmod +x):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Você pode gerar sys/ioctl.phquais define, em TIOCSTIvez de usar o valor numérico. Veja aqui
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)
Rubi inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
ajuntar com 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;
}
**! ** Existem outros exemplos aqui .
Usar ioctlpara fazer isso funciona em sub-conchas. Também pode injetar em outros terminais, conforme explicado a seguir.
Indo além (controlando outros terminais)
Está além do escopo da pergunta original, mas é possível injetar caracteres em outro terminal, sujeito às permissões apropriadas. Normalmente, isso significa ser root, mas veja abaixo outras maneiras.
Estender o programa C fornecido acima para aceitar um argumento de linha de comando especificando o tty de outro terminal permite injetar nesse 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;
}
Ele também envia uma nova linha por padrão, mas, semelhante a echo, fornece uma -nopção para suprimi-la. A opção --tou --ttyrequer um argumento - o ttydo terminal a ser injetado. O valor para isso pode ser obtido nesse terminal:
$ tty
/dev/pts/20
Compile com gcc -o inject inject.c. Prefixe o texto a ser injetado --se ele contiver hífens para impedir que o analisador de argumentos interprete mal as opções da linha de comando. Veja ./inject --help. Use-o assim:
$ inject --tty /dev/pts/22 -- ls -lrt
ou apenas
$ inject -- ls -lrt
para injetar o terminal atual.
A injeção em outro terminal requer direitos administrativos que podem ser obtidos por:
- emitindo o comando como
root,
- usando
sudo,
- ter a
CAP_SYS_ADMINcapacidade ou
- configurando o executável
setuid
Para atribuir CAP_SYS_ADMIN:
$ sudo setcap cap_sys_admin+ep inject
Para atribuir setuid:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Saída limpa
O texto injetado aparece antes do prompt como se tivesse sido digitado antes do prompt aparecer (o que, na verdade, era), mas depois aparece novamente após o prompt.
Uma maneira de ocultar o texto que aparece antes do prompt é anexá-lo com um retorno de carro ( \rsem avanço de linha) e limpar a linha atual ( <ESC>[M):
$ PS1="\r\e[M$PS1"
No entanto, isso limpará apenas a linha na qual o prompt aparece. Se o texto injetado incluir novas linhas, isso não funcionará como pretendido.
Outra solução desativa o eco dos caracteres injetados. Um wrapper usa sttypara fazer isso:
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"
onde injecté uma das soluções descritas acima ou substituída por printf '\e[5n'.
Abordagens alternativas
Se o seu ambiente atender a certos pré-requisitos, você poderá ter outros métodos disponíveis que podem ser usados para injetar entrada. Se você estiver em um ambiente de área de trabalho, o xdotool é um utilitário X.Org que simula a atividade do mouse e do teclado, mas sua distribuição não pode incluí-la por padrão. Podes tentar:
$ xdotool type ls
Se você usa tmux , o multiplexador de terminal, pode fazer o seguinte:
$ tmux send-key -t session:pane ls
onde -tseleciona qual sessão e painel injetar. O GNU Screen tem uma capacidade semelhante com seu stuffcomando:
$ screen -S session -p pane -X stuff ls
Se sua distribuição inclui o pacote console-tools , você pode ter um writevtcomando que usa ioctlcomo nossos exemplos. A maioria das distribuições, no entanto, obsoleta esse pacote em favor do kbd, que não possui esse recurso.
Uma cópia atualizada do writevt.c pode ser compilada usando gcc -o writevt writevt.c.
Outras opções que podem se encaixar melhor em alguns casos de uso incluem expect e empty, projetados para permitir que ferramentas interativas sejam scripts.
Você também pode usar um shell que suporte injeção terminal, como o zshque pode ser feito print -z ls.
A resposta "Uau, isso é inteligente ..."
O método descrito aqui também é discutido aqui e se baseia no método discutido aqui .
Um redirecionamento de shell /dev/ptmxobtém um novo pseudo-terminal:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Uma pequena ferramenta escrita em C que desbloqueia o mestre pseudoterminal (ptm) e gera o nome do escravo pseudoterminal (pts) em sua saída padrão.
#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;
}
(salve como pts.ce compile com gcc -o pts pts.c)
Quando o programa é chamado com sua entrada padrão definida como ptm, ele desbloqueia os pontos correspondentes e envia seu nome para a saída padrão.
$ ./pts </dev/ptmx
/dev/pts/20
A função unlockpt () desbloqueia o dispositivo pseudoterminal escravo correspondente ao pseudoterminal principal referido pelo descritor de arquivo fornecido. O programa passa como zero, que é a entrada padrão do programa .
A função ptsname () retorna o nome do dispositivo pseudoterminal escravo correspondente ao mestre referido pelo descritor de arquivo fornecido, passando novamente zero para a entrada padrão do programa.
Um processo pode ser conectado aos pontos. Primeiro, obtenha um ptm (aqui está atribuído ao descritor de arquivo 3, aberto, leitura e gravação pelo <>redirecionamento).
exec 3<>/dev/ptmx
Então inicie o processo:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Os processos gerados por esta linha de comando são melhor ilustrados com pstree:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
A saída é relativa ao shell atual ( $$) e o PID ( -p) e PGID ( -g) de cada processo são mostrados entre parênteses (PID,PGID).
No topo da árvore está bash(5203,5203)o shell interativo no qual estamos digitando comandos, e seus descritores de arquivos o conectam ao aplicativo de terminal que estamos usando para interagir com ele ( xtermou similar).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Observando o comando novamente, o primeiro conjunto de parênteses iniciou um subshell, bash(6524,6524)) com seu descritor de arquivo 0 (sua entrada padrão ) sendo atribuído aos pts (que é aberto para leitura e gravação <>), retornado por outro subshell que foi executado ./pts <&3para desbloquear o pontos associados ao descritor de arquivo 3 (criado na etapa anterior exec 3<>/dev/ptmx).
O descritor de arquivo 3 do subshell é fechado ( 3>&-) para que o ptm não esteja acessível a ele. Sua entrada padrão (fd 0), que é o pts que foi aberto para leitura / gravação, é redirecionada (na verdade o fd é copiado - >&0) para sua saída padrão (fd 1).
Isso cria um subshell com sua entrada e saída padrão conectada aos pts. Ele pode ser enviado de entrada escrevendo no ptm e sua saída pode ser vista lendo no ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
O subshell executa este comando:
setsid -c bash -i 2>&1 | tee log
É executado bash(6527,6527)no modo interativo ( -i) em uma nova sessão ( setsid -cobserve que o PID e o PGID são os mesmos). Seu erro padrão é redirecionado para sua saída padrão ( 2>&1) e transmitido por meio de tee(6528,6524)modo que seja gravado em um logarquivo e nos pts. Isso fornece outra maneira de ver a saída do subshell:
$ tail -f log
Como o subshell está em execução bashinterativamente, ele pode receber comandos para executar, como este exemplo, que exibe os descritores de arquivo do subshell:
$ echo 'ls -l /dev/fd/' >&3
A leitura da saída do subshell ( tail -f logou cat <&3) revela:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
A entrada padrão (fd 0) é conectada aos pontos e a saída padrão (fd 1) e o erro (fd 2) são conectados ao mesmo tubo, aquele 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]
E uma olhada nos descritores de arquivo 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
Saída padrão (fd 1) são os pontos: qualquer coisa que 'tee' grave na saída padrão é enviada de volta ao ptm. Erro padrão (fd 2) são os pontos pertencentes ao terminal de controle.
Embrulhando-o
O script a seguir usa a técnica descrita acima. Ele configura uma bashsessão interativa que pode ser injetada gravando em um descritor de arquivo. Está disponível aqui e documentado com explicações.
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