Câu trả lời này được cung cấp để làm rõ sự hiểu biết của riêng tôi và được truyền cảm hứng bởi @ StéphaneChazelas và @mikeerv trước tôi.
TL; DR
- không thể làm điều này
bash
mà không cần sự trợ giúp từ bên ngoài;
- cách chính xác để làm điều này là với một đầu vào gửi
ioctl
nhưng
bash
giải pháp khả thi nhất sử dụng bind
.
Giải pháp dễ dàng
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash có hàm dựng sẵn được gọi là bind
cho phép thực thi lệnh shell khi nhận được chuỗi khóa. Về bản chất, đầu ra của lệnh shell được ghi vào bộ đệm đầu vào của shell.
$ bind '"\e[0n": "ls -l"'
Chuỗi khóa \e[0n
( <ESC>[0n
) là mã thoát ANSI Terminal mà thiết bị đầu cuối gửi để chỉ ra rằng nó đang hoạt động bình thường. Nó sẽ gửi cái này để đáp lại yêu cầu báo cáo trạng thái thiết bị được gửi dưới dạng <ESC>[5n
.
Bằng cách ràng buộc phản hồi với một echo
đầu ra văn bản cần tiêm, chúng ta có thể tiêm văn bản đó bất cứ khi nào chúng ta muốn bằng cách yêu cầu trạng thái thiết bị và điều đó được thực hiện bằng cách gửi một <ESC>[5n
chuỗi thoát.
printf '\e[5n'
Điều này hoạt động, và có lẽ là đủ để trả lời câu hỏi ban đầu vì không có công cụ nào khác được tham gia. Đó là thuần túy bash
nhưng dựa vào một thiết bị đầu cuối hoạt động tốt (thực tế tất cả là).
Nó để lại văn bản lặp lại trên dòng lệnh sẵn sàng để được sử dụng như thể nó đã được gõ. Nó có thể được nối thêm, chỉnh sửa và nhấn ENTER
khiến nó được thực thi.
Thêm vào \n
lệnh bị ràng buộc để nó được thực thi tự động.
Tuy nhiên, giải pháp này chỉ hoạt động trong thiết bị đầu cuối hiện tại (nằm trong phạm vi của câu hỏi ban đầu). Nó hoạt động từ một dấu nhắc tương tác hoặc từ một tập lệnh có nguồn gốc nhưng nó sẽ phát sinh lỗi nếu được sử dụng từ một khung con:
bind: warning: line editing not enabled
Giải pháp chính xác được mô tả tiếp theo là linh hoạt hơn nhưng nó dựa vào các lệnh bên ngoài.
Giải pháp đúng
Cách thích hợp để nhập dữ liệu đầu vào sử dụng tty_ioctl , một lệnh gọi hệ thống unix cho Điều khiển I / O có một TIOCSTI
lệnh có thể được sử dụng để tiêm đầu vào.
TIOC từ " T erminal IOC tl " và STI từ " S cuối T erminal tôi nput ".
Không có lệnh nào được xây dựng bash
cho việc này; làm như vậy đòi hỏi một lệnh bên ngoài. Không có lệnh như vậy trong bản phân phối GNU / Linux điển hình nhưng không khó để đạt được với một lập trình nhỏ. Đây là một hàm shell sử dụng perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Đây 0x5412
là mã cho TIOCSTI
lệnh.
TIOCSTI
là một hằng số được xác định trong các tệp tiêu đề C tiêu chuẩn có giá trị 0x5412
. Hãy thử grep -r TIOCSTI /usr/include
, hoặc nhìn vào /usr/include/asm-generic/ioctls.h
; nó được bao gồm trong các chương trình C gián tiếp bởi #include <sys/ioctl.h>
.
Sau đó bạn có thể làm:
$ inject ls -l
ls -l$ ls -l <- cursor here
Việc triển khai trong một số ngôn ngữ khác được hiển thị bên dưới (lưu trong một tệp và sau đó là chmod +x
nó):
Perl inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Bạn có thể tạo sys/ioctl.ph
định nghĩa TIOCSTI
thay vì sử dụng giá trị số. Xem tại đây
Con tră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)
Hồng ngọc inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
biên dịch với 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;
}
**! ** Có thêm ví dụ ở đây .
Sử dụng ioctl
để làm điều này hoạt động trong subshells. Nó cũng có thể tiêm vào các thiết bị đầu cuối khác như được giải thích tiếp theo.
Đưa nó đi xa hơn (kiểm soát các thiết bị đầu cuối khác)
Nó vượt quá phạm vi của câu hỏi ban đầu nhưng có thể đưa các ký tự vào một thiết bị đầu cuối khác, có các quyền phù hợp. Thông thường điều này có nghĩa là root
, nhưng xem bên dưới để biết những cách khác.
Mở rộng chương trình C được đưa ra ở trên để chấp nhận đối số dòng lệnh chỉ định tty của thiết bị đầu cuối khác cho phép tiêm vào thiết bị đầu cuối đó:
#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;
}
Nó cũng gửi một dòng mới theo mặc định, nhưng tương tự echo
, nó cung cấp một -n
tùy chọn để loại bỏ nó. Các --t
hoặc --tty
tùy chọn đòi hỏi một cuộc tranh cãi - sự tty
của nhà ga để được tiêm. Giá trị này có thể đạt được trong thiết bị đầu cuối đó:
$ tty
/dev/pts/20
Biên dịch nó với gcc -o inject inject.c
. Tiền tố văn bản được thêm vào --
nếu nó chứa bất kỳ dấu gạch nối nào để ngăn trình phân tích cú pháp đối số diễn giải sai các tùy chọn dòng lệnh. Xem ./inject --help
. Sử dụng nó như thế này:
$ inject --tty /dev/pts/22 -- ls -lrt
hoặc chỉ
$ inject -- ls -lrt
để tiêm thiết bị đầu cuối hiện tại.
Tiêm vào một thiết bị đầu cuối khác đòi hỏi các quyền hành chính có thể có được bằng cách:
- ban hành lệnh như
root
,
- sử dụng
sudo
,
- có
CAP_SYS_ADMIN
khả năng hoặc
- thiết lập thực thi
setuid
Để gán CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
Để gán setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Đầu ra sạch
Văn bản được chèn xuất hiện trước dấu nhắc như thể nó được gõ trước khi dấu nhắc xuất hiện (mà thực tế là nó) nhưng sau đó nó lại xuất hiện sau dấu nhắc.
Một cách để ẩn văn bản xuất hiện trước dấu nhắc là trả trước lời nhắc bằng trả về vận chuyển ( \r
không phải nguồn cấp dữ liệu) và xóa dòng hiện tại ( <ESC>[M
):
$ PS1="\r\e[M$PS1"
Tuy nhiên, điều này sẽ chỉ xóa dòng mà lời nhắc xuất hiện. Nếu văn bản được chèn bao gồm các dòng mới thì điều này sẽ không hoạt động như dự định.
Một giải pháp khác vô hiệu hóa tiếng vang của các ký tự được tiêm. Một trình bao bọc sử dụng stty
để làm điều này:
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"
đâu inject
là một trong những giải pháp được mô tả ở trên, hoặc được thay thế bởi printf '\e[5n'
.
Cách tiếp cận khác
Nếu môi trường của bạn đáp ứng các điều kiện tiên quyết nhất định thì bạn có thể có sẵn các phương pháp khác mà bạn có thể sử dụng để tiêm đầu vào. Nếu bạn đang ở trong môi trường máy tính để bàn thì xdotool là tiện ích X.Org mô phỏng hoạt động của chuột và bàn phím nhưng bản phân phối của bạn có thể không bao gồm nó theo mặc định. Bạn co thể thử:
$ xdotool type ls
Nếu bạn sử dụng tmux , bộ ghép kênh đầu cuối, thì bạn có thể làm điều này:
$ tmux send-key -t session:pane ls
trong đó -t
chọn phiên và khung để tiêm. GNU Screen có khả năng tương tự với stuff
lệnh của nó :
$ screen -S session -p pane -X stuff ls
Nếu bản phân phối của bạn bao gồm gói công cụ bảng điều khiển thì bạn có thể có một writevt
lệnh sử dụng ioctl
như ví dụ của chúng tôi. Tuy nhiên, hầu hết các bản phân phối đều không dùng gói này để ủng hộ kbd mà thiếu tính năng này.
Một bản sao cập nhật của writevt.c có thể được biên dịch bằng cách sử dụng gcc -o writevt writevt.c
.
Các tùy chọn khác có thể phù hợp với một số trường hợp sử dụng tốt hơn bao gồm cả mong đợi và sản phẩm nào được thiết kế để cho phép các công cụ tương tác được viết kịch bản.
Bạn cũng có thể sử dụng một vỏ hỗ trợ tiêm đầu cuối như zsh
có thể làm được print -z ls
.
Câu trả lời "Wow, thật thông minh ..."
Phương pháp được mô tả ở đây cũng được thảo luận ở đây và dựa trên phương pháp được thảo luận ở đây .
Chuyển hướng shell từ /dev/ptmx
một thiết bị đầu cuối giả mới:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Một công cụ nhỏ được viết bằng C sẽ mở khóa pseudoterminal master (ptm) và đưa ra tên của nô lệ giả (pts) cho đầu ra tiêu chuẩn của nó.
#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;
}
(lưu dưới dạng pts.c
và biên dịch với gcc -o pts pts.c
)
Khi chương trình được gọi với đầu vào tiêu chuẩn được đặt thành ptm, nó sẽ mở khóa các pts tương ứng và xuất tên của nó thành đầu ra tiêu chuẩn.
$ ./pts </dev/ptmx
/dev/pts/20
Hàm Unlockpt () mở khóa thiết bị giả phụ nô lệ tương ứng với mã giả chính được gọi bởi bộ mô tả tệp đã cho. Chương trình vượt qua mức 0 này là đầu vào tiêu chuẩn của chương trình .
Hàm ptsname () trả về tên của thiết bị giả phụ nô lệ tương ứng với bản gốc được mô tả bởi tệp mô tả tệp đã cho, một lần nữa chuyển về 0 cho đầu vào tiêu chuẩn của chương trình.
Một quá trình có thể được kết nối với pts. Trước tiên hãy lấy ptm (ở đây được gán cho mô tả tệp 3, mở đọc-ghi bởi <>
chuyển hướng).
exec 3<>/dev/ptmx
Sau đó bắt đầu quá trình:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Các quy trình được sinh ra bởi dòng lệnh này được minh họa rõ nhất với pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
Đầu ra liên quan đến shell hiện tại ( $$
) và PID ( -p
) và PGID ( -g
) của mỗi quá trình được hiển thị trong ngoặc đơn (PID,PGID)
.
Ở phần đầu của cây là bash(5203,5203)
lớp vỏ tương tác mà chúng ta đang gõ lệnh và các bộ mô tả tệp của nó kết nối nó với ứng dụng đầu cuối mà chúng ta đang sử dụng để tương tác với nó ( xterm
hoặc tương tự).
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Nhìn lại lệnh một lần nữa, tập hợp dấu ngoặc đơn đầu tiên đã khởi động một lớp con, bash(6524,6524)
) với bộ mô tả tệp 0 ( đầu vào tiêu chuẩn của nó ) được gán cho pts (được mở đọc-ghi <>
) , được trả về bởi một lớp con khác được thực thi ./pts <&3
để mở khóa pts liên kết với mô tả tập tin 3 (được tạo ở bước trước, exec 3<>/dev/ptmx
).
Bộ mô tả tập tin của subshell 3 được đóng ( 3>&-
) để ptm không thể truy cập được. Đầu vào tiêu chuẩn của nó (fd 0), là pts đã được mở đọc / ghi, được chuyển hướng (thực tế là fd được sao chép - >&0
) thành đầu ra tiêu chuẩn của nó (fd 1).
Điều này tạo ra một subshell với đầu vào và đầu ra tiêu chuẩn của nó được kết nối với pts. Nó có thể được gửi đầu vào bằng cách viết vào ptm và đầu ra của nó có thể được nhìn thấy bằng cách đọc từ ptm:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Subshell thực thi lệnh này:
setsid -c bash -i 2>&1 | tee log
Nó chạy bash(6527,6527)
ở -i
chế độ tương tác ( ) trong một phiên mới ( setsid -c
lưu ý rằng PID và PGID giống nhau). Lỗi tiêu chuẩn của nó được chuyển hướng đến đầu ra tiêu chuẩn của nó ( 2>&1
) và được truyền qua tee(6528,6524)
để nó được ghi vào một log
tệp cũng như các pts. Điều này đưa ra một cách khác để xem đầu ra của subshell:
$ tail -f log
Vì lớp con đang chạy bash
tương tác, nên nó có thể được gửi các lệnh để thực thi, như ví dụ này hiển thị các mô tả tệp của lớp con:
$ echo 'ls -l /dev/fd/' >&3
Đọc kết quả đầu ra của subshell ( tail -f log
hoặc cat <&3
) cho thấy:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Đầu vào tiêu chuẩn (fd 0) được kết nối với pts và cả đầu ra tiêu chuẩn (fd 1) và lỗi (fd 2) được kết nối với cùng một đường ống, kết nối với 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]
Và xem xét các mô tả tập tin của 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
Đầu ra tiêu chuẩn (fd 1) là pts: mọi thứ mà 'tee' ghi vào đầu ra tiêu chuẩn của nó được gửi lại cho ptm. Lỗi tiêu chuẩn (fd 2) là các điểm thuộc về thiết bị đầu cuối điều khiển.
Gói nó lên
Kịch bản sau đây sử dụng kỹ thuật được mô tả ở trên. Nó thiết lập một bash
phiên tương tác có thể được chèn bằng cách viết vào một mô tả tập tin. Nó có sẵn ở đây và tài liệu với lời giải thích.
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