提供此答案是为了阐明我自己的理解,并且受到@StéphaneChazelas和@mikeserv的启发。
TL; DR
bash
没有外部帮助就不可能做到这一点;
- 正确的方法是使用发送终端输入,
ioctl
但是
- 最可行的
bash
解决方案使用bind
。
简单的解决方案
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash有一个称为shell的内置bind
函数,当接收到一个键序列时,它允许执行shell命令。本质上,shell命令的输出被写入到shell的输入缓冲区。
$ bind '"\e[0n": "ls -l"'
按键序列\e[0n
(<ESC>[0n
)是终端发送的ANSI终端转义码,以指示其正常运行。它发送此消息以响应设备状态报告请求,该请求以形式发送<ESC>[5n
。
通过将响应绑定到echo
输出要注入的文本的,我们可以在任何时候通过请求设备状态来注入文本,这可以通过发送<ESC>[5n
转义序列来完成。
printf '\e[5n'
这有效,并且可能足以回答原始问题,因为不涉及其他工具。它是纯净的,bash
但依赖行为良好的终端(实际上都是)。
它将回显的文本留在命令行上,使其可以像已被键入一样使用。可以附加,编辑和按下ENTER
它来执行它。
添加\n
到绑定命令以使其自动执行。
但是,此解决方案仅在当前终端中有效(这在原始问题的范围内)。它可以从交互式提示或源脚本运行,但是如果从子shell中使用,则会引发错误:
bind: warning: line editing not enabled
接下来描述的正确解决方案更加灵活,但是它依赖于外部命令。
正确的解决方案
注入输入的正确方法是使用tty_ioctl,它是I / O Control的unix系统调用,具有TIOCSTI
可用于注入输入的命令。
TIOC从“牛逼端子IOC TL ”和 STI从“小号结束牛逼端子我 NPUT ”。
没有内置的命令bash
。这样做需要外部命令。在典型的GNU / Linux发行版中没有这样的命令,但是通过少量编程就不难实现。这是一个使用的shell函数perl
:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
这0x5412
是TIOCSTI
命令的代码。
TIOCSTI
是在标准C头文件中定义的常量,值为0x5412
。尝试grep -r TIOCSTI /usr/include
或看看/usr/include/asm-generic/ioctls.h
; 它由间接包含在C程序中#include <sys/ioctl.h>
。
然后,您可以执行以下操作:
$ inject ls -l
ls -l$ ls -l <- cursor here
下面显示了一些其他语言的实现(先保存在文件中,然后保存在文件中chmod +x
):
佩尔 inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
您可以生成sys/ioctl.ph
定义TIOCSTI
而不是使用数字值。看这里
蟒蛇 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)
红宝石 inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
C inject.c
用...编译 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;
}
**!**有进一步的例子在这里。
用ioctl
做在子shell这样的作品。它也可以注入到其他终端,如下所述。
更进一步(控制其他终端)
这超出了原始问题的范围,但是可以在具有适当权限的情况下将字符注入另一个终端。通常,这意味着root
,但是请参见下文以了解其他方式。
扩展上面给出的C程序以接受指定另一个终端的tty的命令行参数,可以注入该终端:
#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;
}
默认情况下,它还会发送一个换行符,但类似于echo
,它提供了一个-n
禁止换行的选项。的--t
或--tty
选项需要一个参数-该tty
终端的待注射。可以在该终端中获取此值:
$ tty
/dev/pts/20
用编译gcc -o inject inject.c
。--
如果文本包含任何连字符,请在文本前加上前缀,以防止参数解析器误解命令行选项。请参阅./inject --help
。像这样使用它:
$ inject --tty /dev/pts/22 -- ls -lrt
要不就
$ inject -- ls -lrt
注入当前端子。
注入另一个终端需要管理权限,可以通过以下方式获得该管理权限:
- 发出命令
root
,
- 使用
sudo
,
- 有
CAP_SYS_ADMIN
能力或
- 设置可执行文件
setuid
分配CAP_SYS_ADMIN
:
$ sudo setcap cap_sys_admin+ep inject
分配setuid
:
$ sudo chown root:root inject
$ sudo chmod u+s inject
清洁输出
插入的文本显示在提示的前面,就像在提示出现之前键入(实际上是输入)一样,但是在提示之后再次出现。
隐藏提示前面出现的文本的一种方法是在提示前面加上回车符(\r
不是换行符)并清除当前行(<ESC>[M
):
$ PS1="\r\e[M$PS1"
但是,这只会清除出现提示的行。如果注入的文本包含换行符,则该文本将无法正常工作。
另一种解决方案是禁用回显注入的字符。包装器用于stty
执行以下操作:
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"
其中inject
是上述描述的解决方案中的一个,或取代printf '\e[5n'
。
替代方法
如果您的环境满足某些先决条件,则可以使用其他方法来注入输入。如果您在桌面环境中,则xdotool是一个X.Org实用程序,它可以模拟鼠标和键盘的活动,但是您的发行版可能默认不包含它。你可以试试:
$ xdotool type ls
如果使用终端复用器tmux,则可以执行以下操作:
$ tmux send-key -t session:pane ls
在此处-t
选择要注入的会话和窗格。GNU Screen具有与其stuff
命令类似的功能:
$ screen -S session -p pane -X stuff ls
如果您的发行版包含console-tools软件包,那么您可能会writevt
使用ioctl
与我们的示例类似的命令。但是,大多数发行版不赞成使用此软件包,而推荐使用缺少此功能的kbd。
可以使用编译writevt.c的更新副本gcc -o writevt writevt.c
。
可能更适合某些用例的其他选项包括Expect和Empty,它们旨在允许编写交互式工具脚本。
您也可以使用支持终端注入的shell zsh
来做到这一点print -z ls
。
“哇,这很聪明...”答案
这里描述的方法也在这里讨论,并且以这里讨论的方法为基础。
从shell重定向将/dev/ptmx
获得一个新的伪终端:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
一个用C编写的小工具,用于解锁伪终端主设备(ptm),并将伪终端从设备的名称(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;
}
(另存为pts.c
并使用编译gcc -o pts pts.c
)
在将程序的标准输入设置为ptm的情况下调用程序时,它将解锁相应的pts,并将其名称输出到标准输出。
$ ./pts </dev/ptmx
/dev/pts/20
可以将进程连接到pt。首先获得一个ptm(此处已分配给文件描述符3,由<>
重定向以读写方式打开)。
exec 3<>/dev/ptmx
然后开始该过程:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
用以下命令最好地说明了此命令行产生的过程pstree
:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)─┬─bash(6524,6524)─┬─bash(6527,6527)
│ └─tee(6528,6524)
└─pstree(6815,6815)
输出是相对于当前shell($$
)的,括号中显示了每个进程的PID(-p
)和PGID(-g
)(PID,PGID)
。
在树的顶部是bash(5203,5203)
,我们在其中键入命令的交互式外壳,其文件描述符将其连接到我们用来与之交互的终端应用程序(xterm
或类似的)。
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
再次查看该命令,第一组括号启动了一个子外壳,bash(6524,6524)
其文件描述符0(其标准输入)被分配给pts(以读写方式打开<>
),而另一个子外壳通过执行该操作./pts <&3
来解锁该子外壳。与文件描述符3相关联的pts(在上一步中创建exec 3<>/dev/ptmx
)。
子外壳的文件描述符3关闭(3>&-
),因此无法访问ptm。它的标准输入(fd 0)是已读/写打开的pt,将被重定向(实际上fd已复制- >&0
)到其标准输出(fd 1)。
这将创建一个子外壳,其标准输入和输出连接到pts。可以通过写入ptm来发送输入,通过读取ptm可以看到其输出:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
子shell执行以下命令:
setsid -c bash -i 2>&1 | tee log
它bash(6527,6527)
在-i
新会话中以交互()模式运行(setsid -c
请注意,PID和PGID相同)。它的标准错误被重定向到其标准输出(2>&1
),并通过管道进行tee(6528,6524)
传递,因此将其写入log
文件和pts中。这提供了另一种查看子shell输出的方法:
$ tail -f log
由于子外壳程序是bash
交互式运行的,因此可以向其发送命令来执行,如显示子外壳程序的文件描述符的示例所示:
$ echo 'ls -l /dev/fd/' >&3
读取subshell的输出(tail -f log
或cat <&3
)显示:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
标准输入(fd 0)连接到pts,标准输出(fd 1)和错误(fd 2)都连接到同一条管道,该管道连接到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]
再看一下文件描述符 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
标准输出(fd 1)是pts:“ tee”写入其标准输出的任何内容都会发送回ptm。标准错误(fd 2)是属于控制终端的点。
包起来
下面的脚本使用上述技术。它建立了一个交互式bash
会话,可以通过写入文件描述符来注入该会话。它在这里可用,并附有说明文档。
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