Jawaban ini diberikan sebagai klarifikasi dari pemahaman saya sendiri dan terinspirasi oleh @ StéphaneChazelas dan @mikeserv sebelum saya.
TL; DR
- tidak mungkin melakukan ini
bash
tanpa bantuan eksternal;
- cara yang benar untuk melakukan ini adalah dengan input terminal kirim
ioctl
tetapi
bash
menggunakan solusi termudah yang bisa diterapkan bind
.
Solusi mudah
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash memiliki shell builtin yang disebut bind
yang memungkinkan perintah shell dieksekusi ketika urutan kunci diterima. Intinya, output dari perintah shell ditulis ke buffer input shell.
$ bind '"\e[0n": "ls -l"'
Urutan kunci \e[0n
( <ESC>[0n
) adalah kode pelarian Terminal ANSI yang dikirim terminal untuk menunjukkan bahwa ia berfungsi secara normal. Ini mengirimkan ini sebagai tanggapan terhadap permintaan laporan status perangkat yang dikirim sebagai <ESC>[5n
.
Dengan mengikat respons ke echo
output teks yang akan disuntikkan, kita dapat menyuntikkan teks itu kapan pun kita inginkan dengan meminta status perangkat dan itu dilakukan dengan mengirimkan <ESC>[5n
urutan pelarian.
printf '\e[5n'
Ini berfungsi, dan mungkin cukup untuk menjawab pertanyaan awal karena tidak ada alat lain yang terlibat. Ini murni bash
tetapi bergantung pada terminal yang berperilaku baik (praktis semuanya).
Ini meninggalkan teks yang digaungkan pada baris perintah siap digunakan seolah-olah telah diketik. Itu dapat ditambahkan, diedit, dan ditekan ENTER
menyebabkannya dieksekusi.
Tambahkan \n
ke perintah terikat untuk menjalankannya secara otomatis.
Namun, solusi ini hanya berfungsi di terminal saat ini (yang berada dalam ruang lingkup pertanyaan awal). Ini berfungsi dari prompt interaktif atau dari skrip bersumber tetapi itu menimbulkan kesalahan jika digunakan dari subkulit:
bind: warning: line editing not enabled
Solusi yang benar yang dijelaskan selanjutnya lebih fleksibel tetapi bergantung pada perintah eksternal.
Solusi yang benar
Cara yang tepat untuk menyuntikkan input menggunakan tty_ioctl , panggilan sistem unix untuk I / O Control yang memiliki TIOCSTI
perintah yang dapat digunakan untuk menyuntikkan input.
TIOC dari " T erminal IOC tl " dan STI dari " S end T erminal I nput ".
Tidak ada perintah bash
untuk ini; melakukannya memerlukan perintah eksternal. Tidak ada perintah seperti itu dalam distribusi GNU / Linux yang khas tetapi tidak sulit untuk dicapai dengan sedikit pemrograman. Inilah fungsi shell yang menggunakan perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Di sini, 0x5412
adalah kode untuk TIOCSTI
perintah.
TIOCSTI
adalah konstanta yang didefinisikan dalam file header C standar dengan nilai 0x5412
. Coba grep -r TIOCSTI /usr/include
, atau lihat /usr/include/asm-generic/ioctls.h
; itu termasuk dalam program C secara tidak langsung oleh #include <sys/ioctl.h>
.
Anda kemudian dapat melakukan:
$ inject ls -l
ls -l$ ls -l <- cursor here
Implementasi dalam beberapa bahasa lain ditunjukkan di bawah ini (simpan dalam file dan kemudian chmod +x
):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Anda dapat membuat sys/ioctl.ph
yang menentukan TIOCSTI
alih-alih menggunakan nilai numerik. Lihat di sini
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
kompilasi dengan 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;
}
**! ** Ada contoh lebih lanjut di sini .
Menggunakan ioctl
untuk melakukan ini bekerja dalam subkulit. Itu juga dapat menyuntikkan ke terminal lain seperti yang dijelaskan selanjutnya.
Melangkah lebih jauh (mengendalikan terminal lain)
Itu di luar ruang lingkup pertanyaan asli tetapi dimungkinkan untuk menyuntikkan karakter ke terminal lain, tunduk pada izin yang sesuai. Biasanya ini berarti ada root
, tetapi lihat di bawah untuk cara lain.
Memperluas program C yang diberikan di atas untuk menerima argumen baris perintah yang menentukan tty terminal lain memungkinkan injeksi ke terminal itu:
#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;
}
Ini juga mengirimkan baris baru secara default tetapi, mirip dengan echo
, itu memberikan -n
opsi untuk menekannya. The --t
atau --tty
opsi membutuhkan sebuah argumen - yang tty
dari terminal harus disuntikkan. Nilai untuk ini dapat diperoleh di terminal itu:
$ tty
/dev/pts/20
Kompilasi dengan gcc -o inject inject.c
. Awali teks yang akan disuntikkan --
jika berisi tanda hubung untuk mencegah parser argumen salah mengartikan opsi baris perintah. Lihat ./inject --help
. Gunakan seperti ini:
$ inject --tty /dev/pts/22 -- ls -lrt
atau hanya
$ inject -- ls -lrt
untuk menyuntikkan terminal saat ini.
Menyuntikkan ke terminal lain membutuhkan hak administratif yang dapat diperoleh dengan:
- mengeluarkan perintah sebagai
root
,
- menggunakan
sudo
,
- memiliki
CAP_SYS_ADMIN
kemampuan atau
- mengatur executable
setuid
Untuk menetapkan CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Untuk menetapkan setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Bersihkan keluaran
Teks yang disuntikkan muncul di depan prompt seolah-olah itu diketik sebelum prompt muncul (yang, pada dasarnya, itu) tetapi kemudian muncul lagi setelah prompt.
Salah satu cara untuk menyembunyikan teks yang muncul di depan prompt adalah dengan mengawali prompt dengan carriage return ( \r
bukan feed baris) dan menghapus baris saat ini ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Namun, ini hanya akan menghapus garis di mana prompt muncul. Jika teks yang disuntikkan termasuk baris baru maka ini tidak akan berfungsi sebagaimana dimaksud.
Solusi lain menonaktifkan gema karakter yang disuntikkan. Pembungkus menggunakan stty
untuk melakukan ini:
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"
di mana inject
salah satu solusi yang dijelaskan di atas, atau diganti oleh printf '\e[5n'
.
Pendekatan alternatif
Jika lingkungan Anda memenuhi prasyarat tertentu maka Anda mungkin memiliki metode lain yang tersedia yang dapat Anda gunakan untuk menyuntikkan input. Jika Anda berada di lingkungan desktop maka xdotool adalah utilitas X.Org yang mensimulasikan aktivitas mouse dan keyboard tetapi distro Anda mungkin tidak memasukkannya secara default. Anda dapat mencoba:
$ xdotool type ls
Jika Anda menggunakan tmux , terminal multiplexer, maka Anda dapat melakukan ini:
$ tmux send-key -t session:pane ls
di mana -t
memilih sesi dan panel mana yang akan disuntikkan. Layar GNU memiliki kemampuan serupa dengan stuff
perintahnya:
$ screen -S session -p pane -X stuff ls
Jika distro Anda menyertakan paket konsol-alat maka Anda mungkin memiliki writevt
perintah yang menggunakan ioctl
seperti contoh kami. Sebagian besar distro telah menolak paket ini karena mendukung kbd yang tidak memiliki fitur ini.
Salinan writevt.c yang diperbarui dapat dikompilasi menggunakan gcc -o writevt writevt.c
.
Opsi lain yang mungkin cocok dengan beberapa kasus penggunaan lebih baik termasuk harapan dan kosong yang dirancang untuk memungkinkan alat interaktif yang akan dituliskan.
Anda juga bisa menggunakan shell yang mendukung injeksi terminal seperti zsh
yang bisa dilakukan print -z ls
.
Jawaban "Wow, itu pintar ..."
Metode yang dijelaskan di sini juga dibahas di sini dan didasarkan pada metode yang dibahas di sini .
Pengalihan shell dari /dev/ptmx
mendapatkan terminal semu baru:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Sebuah alat kecil yang ditulis dalam C yang membuka master pseudoterminal (ptm) dan menampilkan nama pseudoterminal slave (pts) ke output standarnya.
#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;
}
(simpan sebagai pts.c
dan kompilasi dengan gcc -o pts pts.c
)
Ketika program dipanggil dengan input standarnya diatur ke ptm, ia membuka kunci yang sesuai dan menampilkan namanya menjadi output standar.
$ ./pts </dev/ptmx
/dev/pts/20
Fungsi unlockpt () membuka perangkat pseudoterminal slave yang sesuai dengan master pseudoterminal yang dirujuk oleh deskriptor file yang diberikan. Program melewatkan ini sebagai nol yang merupakan input standar program .
Fungsi ptsname () mengembalikan nama perangkat pseudoterminal slave yang sesuai dengan master yang dirujuk oleh deskriptor file yang diberikan, sekali lagi melewati nol untuk input standar program.
Suatu proses dapat dihubungkan ke Poin. Pertama mendapatkan ptm (di sini ditugaskan untuk file deskriptor 3, dibuka baca-tulis oleh <>
redirect).
exec 3<>/dev/ptmx
Kemudian mulailah prosesnya:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Proses yang dihasilkan oleh command-line ini paling baik digambarkan dengan pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
Outputnya relatif terhadap shell saat ini ( $$
) dan PID ( -p
) dan PGID ( -g
) dari setiap proses ditunjukkan dalam tanda kurung (PID,PGID)
.
Di kepala pohon adalah bash(5203,5203)
, shell interaktif yang sedang kita ketikkan perintah, dan deskriptor file menghubungkannya ke aplikasi terminal yang kita gunakan untuk berinteraksi dengannya ( xterm
, atau serupa).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Melihat perintah lagi, set kurung pertama memulai subkulit, bash(6524,6524)
) dengan deskriptor file 0 ( input standarnya ) ditugaskan ke pts (yang dibuka baca-tulis, <>
) seperti yang dikembalikan oleh subkulit lain yang dijalankan ./pts <&3
untuk membuka kunci Poin yang terkait dengan deskriptor file 3 (dibuat pada langkah sebelumnya, exec 3<>/dev/ptmx
).
Deskriptor file subshell 3 ditutup ( 3>&-
) sehingga ptm tidak dapat diakses. Input standarnya (fd 0), yang merupakan pts yang dibuka baca / tulis, dialihkan (sebenarnya fd disalin - >&0
) ke output standarnya (fd 1).
Ini menciptakan subkulit dengan input dan output standar yang terhubung ke Poin. Ini dapat dikirim input dengan menulis ke ptm dan hasilnya dapat dilihat dengan membaca dari ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Subshell menjalankan perintah ini:
setsid -c bash -i 2>&1 | tee log
Ini berjalan bash(6527,6527)
dalam -i
mode interaktif ( ) dalam sesi baru ( setsid -c
, perhatikan PID dan PGID adalah sama). Kesalahan standarnya dialihkan ke output standar ( 2>&1
) dan disalurkan melalui tee(6528,6524)
sehingga ditulis ke log
file dan juga ke pts. Ini memberi cara lain untuk melihat output subshell:
$ tail -f log
Karena subkulit berjalan secara bash
interaktif, maka dapat dikirim perintah untuk dieksekusi, seperti contoh ini yang menampilkan deskriptor file subkulit:
$ echo 'ls -l /dev/fd/' >&3
Membaca output subkulit ( tail -f log
atau cat <&3
) mengungkapkan:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Input standar (fd 0) terhubung ke pts dan output standar (fd 1) dan kesalahan (fd 2) terhubung ke pipa yang sama, yang terhubung ke 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]
Dan lihat file deskriptor dari 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
Output Standar (fd 1) adalah pts: apa pun yang 'tee' tulis ke output standarnya dikirim kembali ke ptm. Kesalahan Standar (fd 2) adalah poin yang dimiliki oleh terminal pengendali.
Membungkusnya
Script berikut menggunakan teknik yang dijelaskan di atas. Ini mengatur bash
sesi interaktif yang dapat disuntikkan dengan menulis ke deskriptor file. Ini tersedia di sini dan didokumentasikan dengan penjelasan.
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