是否可以在交互式bash shell 中输入输出一些文本的命令,以便它出现在下一个命令提示符下,就好像用户在该提示符下输入了该文本一样?
我希望能够创建source一个脚本,该脚本将生成一个命令行并输出它,以便在脚本结束后提示返回时出现该脚本,以便用户可以在按下enter执行它之前选择对其进行编辑。
这可以通过xdotool但仅当终端在 X 窗口中并且安装时才有效。
[me@mybox] 100 $ xdotool type "ls -l"
[me@mybox] 101 $ ls -l <--- cursor appears here!
Run Code Online (Sandbox Code Playgroud)
这可以仅使用 bash 来完成吗?
Sté*_*las 48
使用zsh,您可以使用print -z将一些文本放入行编辑器缓冲区以供下一个提示:
print -z echo test
Run Code Online (Sandbox Code Playgroud)
将启动行编辑器echo test,您可以在下一个提示中使用该编辑器进行编辑。
我认为没有bash类似的功能,但是在许多系统上,您可以使用以下命令填充终端设备输入缓冲区TIOCSTI ioctl():
perl -e 'require "sys/ioctl.ph"; ioctl(STDIN, &TIOCSTI, $_)
for split "", join " ", @ARGV' echo test
Run Code Online (Sandbox Code Playgroud)
将echo test设备输入缓冲区插入终端,就好像从终端接收一样。
@mikeTerminology方法的一个更便携的变体并且不牺牲安全性是向终端模拟器发送一个相当标准的query status report转义序列:<ESC>[5n哪些终端总是回复(作为输入)<ESC>[0n并将其绑定到您要插入的字符串:
bind '"\e[0n": "echo test"'; printf '\e[5n'
Run Code Online (Sandbox Code Playgroud)
如果在 GNU 中screen,您还可以执行以下操作:
screen -X stuff 'echo test'
Run Code Online (Sandbox Code Playgroud)
现在,除了 TIOCSTI ioctl 方法,我们要求终端仿真器向我们发送一些字符串,就像输入的一样。如果该字符串到来之前readline(bash的行编辑器)具有禁用终端本地回声,则该字符串将显示不在shell提示,稍搞乱了显示。
要解决这个问题,您可以稍微延迟向终端发送请求,以确保在 readline 禁用回声时响应到达。
bind '"\e[0n": "echo test"'; ((sleep 0.05; printf '\e[5n') &)
Run Code Online (Sandbox Code Playgroud)
(这里假设您sleep支持亚秒级分辨率)。
理想情况下,您希望执行以下操作:
bind '"\e[0n": "echo test"'
stty -echo
printf '\e[5n'
wait-until-the-response-arrives
stty echo
Run Code Online (Sandbox Code Playgroud)
但是bash(与 相反zsh)不支持这样的wait-until-the-response-arrives不读取响应。
但是它有一个has-the-response-arrived-yet特点read -t0:
bind '"\e[0n": "echo test"'
saved_settings=$(stty -g)
stty -echo -icanon min 1 time 0
printf '\e[5n'
until read -t0; do
sleep 0.02
done
stty "$saved_settings"
Run Code Online (Sandbox Code Playgroud)
请参阅@starfry 的答案,该答案扩展了@mikeserv 和我自己提供的两个解决方案,并提供了一些更详细的信息。
sta*_*fry 31
提供此答案是为了澄清我自己的理解,并受到我之前的 @StéphaneChazelas 和 @mikeserv 的启发。
TL; 博士
bash没有外部帮助,这是不可能做到的;ioctl但是bash解决方案使用bind.
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bash 有一个 shell 内置调用bind,它允许在接收到一个键序列时执行一个 shell 命令。本质上,shell 命令的输出被写入 shell 的输入缓冲区。
$ bind '"\e[0n": "ls -l"'
Run Code Online (Sandbox Code Playgroud)
键序列\e[0n( <ESC>[0n) 是一个ANSI 终端转义码,终端发送该代码以指示其正常运行。它响应于发送该设备的状态报告的请求被发送作为<ESC>[5n。
通过将响应绑定到echo输出要注入的文本,我们可以随时通过请求设备状态来注入该文本,这是通过发送<ESC>[5n转义序列来完成的。
printf '\e[5n'
Run Code Online (Sandbox Code Playgroud)
这有效,并且可能足以回答原始问题,因为不涉及其他工具。它是纯粹的,bash但依赖于一个行为良好的终端(几乎都是)。
它使命令行上的回显文本准备好使用,就像已经输入一样。它可以被附加、编辑和按下ENTER使其被执行。
添加\n到绑定命令以使其自动执行。
但是,此解决方案仅适用于当前终端(在原始问题的范围内)。它从交互式提示或源脚本工作,但如果从子shell使用它会引发错误:
bind: warning: line editing not enabled
Run Code Online (Sandbox Code Playgroud)
下面描述的正确解决方案更灵活,但它依赖于外部命令。
注入输入的正确方法是使用tty_ioctl,这是一个用于I/O 控制的 unix 系统调用,它有一个TIOCSTI可用于注入输入的命令。
TIOC从“牛逼端子IOC TL ”和STI从“小号结束牛逼端子我NPUT ”。
没有bash为此内置命令;这样做需要一个外部命令。在典型的 GNU/Linux 发行版中没有这样的命令,但是通过一点编程来实现它并不困难。这是一个 shell 函数,它使用perl:
function inject() {
perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV' "$@"
}
Run Code Online (Sandbox Code Playgroud)
这0x5412是TIOCSTI命令的代码。
TIOCSTI是在标准 C 头文件中定义的常量,值为0x5412。尝试grep -r TIOCSTI /usr/include,或查看/usr/include/asm-generic/ioctls.h;它由 .c 间接包含在 C 程序中#include <sys/ioctl.h>。
然后你可以这样做:
$ inject ls -l
ls -l$ ls -l <- cursor here
Run Code Online (Sandbox Code Playgroud)
其他一些语言的实现如下所示(保存在文件中然后chmod +x它):
珀尔 inject.pl
#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV
Run Code Online (Sandbox Code Playgroud)
您可以生成sys/ioctl.phwhich 定义TIOCSTI而不是使用数值。看这里
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)
Run Code Online (Sandbox Code Playgroud)
红宝石 inject.rb
#!/usr/bin/ruby
ARGV.join(' ').split('').each { |c| $stdin.ioctl(0x5412,c) }
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)
**!**有进一步的例子在这里。
用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;
}
Run Code Online (Sandbox Code Playgroud)
它还默认发送一个换行符,但与 类似echo,它提供了一个-n选项来抑制它。的--t或--tty选项需要一个参数-该tty终端的待注射。可以在该终端中获得此值:
$ tty
/dev/pts/20
Run Code Online (Sandbox Code Playgroud)
用gcc -o inject inject.c. --如果文本包含任何连字符,则为要注入的文本添加前缀,以防止参数解析器误解命令行选项。见./inject --help。像这样使用它:
$ inject --tty /dev/pts/22 -- ls -lrt
Run Code Online (Sandbox Code Playgroud)
要不就
$ inject -- ls -lrt
Run Code Online (Sandbox Code Playgroud)
注入当前终端。
注入另一个终端需要可以通过以下方式获得的管理权限:
root,sudo,CAP_SYS_ADMIN能力或setuid分配CAP_SYS_ADMIN:
$ sudo setcap cap_sys_admin+ep inject
Run Code Online (Sandbox Code Playgroud)
分配setuid:
$ sudo chown root:root inject
$ sudo chmod u+s inject
Run Code Online (Sandbox Code Playgroud)
注入的文本出现在提示之前,就好像它是在提示出现之前输入的一样(实际上确实如此),但在提示之后再次出现。
隐藏出现在提示前的文本的一种方法是在提示前加上回车(\r不是换行)并清除当前行 ( <ESC>[M):
$ PS1="\r\e[M$PS1"
Run Code Online (Sandbox Code Playgroud)
但是,这只会清除出现提示的行。如果注入的文本包含换行符,那么这将无法按预期工作。
另一种解决方案禁用注入字符的回显。包装器用于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"
Run Code Online (Sandbox Code Playgroud)
其中inject是上述解决方案之一,或替换为printf '\e[5n'。
如果您的环境满足某些先决条件,那么您可能有其他方法可用于注入输入。如果您在桌面环境中,那么xdotool是一个模拟鼠标和键盘活动的X.Org实用程序,但您的发行版可能默认不包含它。你可以试试:
$ xdotool type ls
Run Code Online (Sandbox Code Playgroud)
如果您使用tmux,终端多路复用器,那么您可以这样做:
$ tmux send-key -t session:pane ls
Run Code Online (Sandbox Code Playgroud)
where-t选择要注入的会话和窗格。GNU Screen的stuff命令也有类似的功能:
$ screen -S session -p pane -X stuff ls
Run Code Online (Sandbox Code Playgroud)
如果您的发行版包含console-tools包,那么您可能有一个类似于我们示例的writevt命令ioctl。然而,大多数发行版都弃用了这个包,转而支持缺少此功能的kbd。
writevt.c的更新副本可以使用gcc -o writevt writevt.c.
其他可能更适合某些用例的选项包括expect和empty,它们旨在允许编写交互式工具的脚本。
您还可以使用支持终端注入的外壳,例如zshwhich can do print -z ls。
来自的 shell 重定向/dev/ptmx获得一个新的伪终端:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
Run Code Online (Sandbox Code Playgroud)
一个用 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;
}
Run Code Online (Sandbox Code Playgroud)
(另存为pts.c并编译gcc -o pts pts.c)
当程序在其标准输入设置为 ptm 的情况下被调用时,它会解锁相应的 pts 并将其名称输出到标准输出。
$ ./pts </dev/ptmx
/dev/pts/20
Run Code Online (Sandbox Code Playgroud)
所述unlockpt() 函数解锁对应于由给定的文件描述符称为主伪终端从伪终端设备。程序将此作为零传递,这是程序的标准输入。
所述ptsname()使用 函数返回相应于主从伪终端设备的名称提到了给定的文件描述符,再次通过零的程序的标准输入。
一个进程可以连接到 pts。首先获得一个 ptm(这里它被分配给文件描述符 3,由<>重定向打开读写)。
exec 3<>/dev/ptmx
Run Code Online (Sandbox Code Playgroud)
然后开始这个过程:
$ (setsid -c bash -i 2>&1 | tee log) <>"$(./pts <&3)" 3>&- >&0 &
Run Code Online (Sandbox Code Playgroud)
此命令行产生的进程最好用pstree以下内容说明:
$ pstree -pg -H $(jobs -p %+) $$
bash(5203,5203)???bash(6524,6524)???bash(6527,6527)
? ??tee(6528,6524)
??pstree(6815,6815)
Run Code Online (Sandbox Code Playgroud)
输出与当前 shell ( ) 相关,每个进程$$的 PID( -p) 和 PGID( -g) 显示在括号中(PID,PGID)。
树的头部是bash(5203,5203),我们在其中输入命令的交互式 shell,它的文件描述符将它连接到我们用来与之交互的终端应用程序(xterm或类似的)。
$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3
Run Code Online (Sandbox Code Playgroud)
再次查看该命令,第一组括号启动了一个子shell, bash(6524,6524)) 其文件描述符0(其标准输入)被分配给pts(以读写方式打开<>),这是由另一个执行./pts <&3解锁的子shell返回的与文件描述符 3 关联的 pts(在上一步中创建exec 3<>/dev/ptmx)。
子外壳的文件描述符 3 已关闭 ( 3>&-),因此无法访问 ptm。它的标准输入 (fd 0),即以读/写方式打开的 pts,被重定向(实际上 fd 被复制 - >&0)到其标准输出(fd 1)。
这将创建一个子shell,其标准输入和输出连接到pts。它可以通过写入 ptm 来发送输入,它的输出可以通过从 ptm 读取来查看:
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
Run Code Online (Sandbox Code Playgroud)
子shell执行这个命令:
setsid -c bash -i 2>&1 | tee log
Run Code Online (Sandbox Code Playgroud)
它bash(6527,6527)在-i新会话中以交互 ( ) 模式运行(setsid -c注意 PID 和 PGID 是相同的)。它的标准错误被重定向到它的标准输出 ( 2>&1) 并通过管道传输,tee(6528,6524)因此它被写入log文件和 pts。这提供了另一种查看子shell输出的方法:
$ tail -f log
Run Code Online (Sandbox Code Playgroud)
因为子shell是bash交互式运行的,所以可以发送命令来执行,就像这个显示子shell的文件描述符的例子:
$ echo 'ls -l /dev/fd/' >&3
Run Code Online (Sandbox Code Playgroud)
阅读子shell的输出(tail -f log或cat <&3)显示:
lrwx------ 0 -> /dev/pts/17
l-wx------ 1 -> pipe:[116261]
l-wx------ 2 -> pipe:[116261]
Run Code Online (Sandbox Code Playgroud)
标准输入 (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]
Run Code Online (Sandbox Code Playgroud)
并查看文件描述符 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
Run Code Online (Sandbox Code Playgroud)
标准输出 (fd 1) 是 pts:“tee”写入其标准输出的任何内容都将发送回 ptm。标准误差 (fd 2) 是属于控制终端的 pts。
包起来
以下脚本使用上述技术。它设置了一个交互式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
Run Code Online (Sandbox Code Playgroud)
mik*_*erv 20
这取决于您所说的bashonly是什么意思。如果您指的是单个交互式bash会话,那么答案几乎肯定是否定的。这是因为即使您ls -l在任何规范终端上的命令行中输入命令时,您甚至bash还没有意识到它 - 甚至那时甚至bash没有参与。
相反,到目前为止发生的事情是内核的 tty line-discipline 已经缓冲并且stty echo只将用户的输入发送到屏幕。它将该输入刷新到其阅读器 -bash在您的示例中 - 逐行 - 并且通常也将\returns转换为\nUnix 系统上的ewlines - 所以bash不是 - 所以你的源脚本也不能 - 意识到有任何输入,直到用户按下ENTER键。
现在,有一些变通方法。实际上,最健壮的根本不是解决方法,而是涉及使用多个进程或专门编写的程序来对输入进行排序,-echo对用户隐藏行规,并仅在解释输入时将判断为适当的内容写入屏幕特别需要的时候。这可能很难做好,因为这意味着编写解释规则,可以在任意输入字符到达时逐个字符处理,并同时将其写出而不会出错,以模拟普通用户在该场景中的期望。可能正是出于这个原因,交互式终端 i/o 很少被很好地理解 - 对大多数人来说,这种困难的前景并不适合进一步调查。
另一种解决方法可能涉及终端模拟器。你说你的问题是对 X 和 的依赖xdotool。在这种情况下,我即将提供的这种变通方法可能会遇到类似的问题,但我会继续进行下去。
printf '\33[22;1t\33]1;%b\33\\\33[20t\33[23;0t' \
'\025my command'
Run Code Online (Sandbox Code Playgroud)
这将在一个工作xterm瓦特/allowwindowOps资源集。它首先将图标/窗口名称保存在堆栈中,然后将终端的图标字符串设置为^Umy command然后请求终端将该名称注入输入队列,最后将其重置为保存的值。对于 在正确配置中bash运行的交互式shell,它应该可以无形地工作 -xterm但这可能是一个坏主意。请参阅下面 Stéphane 的评论。
不过,这里是我printf在我的机器上运行带有不同转义序列的位后拍摄的 Terminology 终端的照片。对于printf我输入的命令中的每个换行符,CTRL+V然后CTRL+J按下ENTER键。之后我什么也没输入,但是,正如你所看到的,终端my command为我注入了 line-discipline 的输入队列:

真正做到这一点的方法是使用嵌套的 pty。这是如何screen和tmux类似的工作 - 顺便说一下,这两者都可以使您成为可能。xterm实际上带有一个名为的小程序luit,它也可以使这成为可能。不过,这并不容易。
这是您可能的一种方法:
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
Run Code Online (Sandbox Code Playgroud)
这绝不是可移植的,但应该适用于大多数具有打开/dev/ptmx. 我的用户tty在我的系统上足够的组中。你还需要...
<<\C cc -xc - -o 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;
}
C
Run Code Online (Sandbox Code Playgroud)
...当在 GNU 系统(或任何其他带有也可以从 stdin 读取的标准 C 编译器)上运行时,将写出一个名为的小型可执行二进制文件,该二进制文件pts将unlockpt()在其 stdin 上运行该函数并将其写入其 stdout刚刚解锁的 pty 设备的名称。我在工作时写的……我是如何来到这个 pty 的,我可以用它做什么?.
无论如何,上面的代码所做的是bash在当前 tty 下一层的 pty 中运行一个shell。bash被告知将所有输出写入从属 pty,并且当前 tty 被配置为既不是-echo其输入也不是缓冲它,而是将其(主要) 传递raw给cat,然后将其复制到bash。一直以来,后台将cat所有从属输出复制到当前 tty。
在大多数情况下,上述配置将完全无用——基本上只是多余的——除了我们bash使用它自己的 pty master fd on 的副本启动<>9。这意味着bash可以通过简单的重定向自由地写入自己的输入流。所bash要做的就是:
echo echo hey >&9
Run Code Online (Sandbox Code Playgroud)
……自言自语。
这是另一张图片:

尽管
Stéphane Chazelas的ioctl(,TIOCSTI,) 答案当然是正确的答案,但有些人可能对这个部分但微不足道的答案感到满意:只需将命令推入历史堆栈,然后用户就可以在历史记录中移动 1 行以找到命令。
$ history -s "ls -l"
$ echo "move up 1 line in history to get command to run"
Run Code Online (Sandbox Code Playgroud)
这可以变成一个简单的脚本,它有自己的 1 行历史记录:
#!/bin/bash
history -s "ls -l"
read -e -p "move up 1 line: "
eval "$REPLY"
Run Code Online (Sandbox Code Playgroud)
read -e启用 readline 编辑输入,-p是一个提示。