bash 可以写入自己的输入流吗?

bash 可以写入自己的输入流吗?

是否可以在互动中巴什shell 输入一个命令,该命令输出一些文本,以便它出现在下一个命令提示符处,就好像用户在该提示符处输入了该文本一样?

我希望能够有source一个脚本生成命令行并输出它,以便在脚本结束后提示返回时出现,以便用户可以选择在按下enter执行它之前对其进行编辑。

这可以通过以下方式实现,xdotool但仅当终端位于 X 窗口中并且仅在已安装时才有效。

[me@mybox] 100 $ xdotool type "ls -l"
[me@mybox] 101 $ ls -l  <--- cursor appears here!

仅使用 bash 可以完成此操作吗?

答案1

使用zsh,您可以print -z将一些文本放入行编辑器缓冲区中以供下一个提示使用:

print -z echo test

将启动行编辑器echo test,您可以在下一个提示时使用它进行编辑。

我认为bash没有类似的功能,但是在许多系统上,您可以使用以下命令填充终端设备输入缓冲区TIOCSTI ioctl()

perl -e 'require "sys/ioctl.ph"; ioctl(STDIN, &TIOCSTI, $_)
  for split "", join " ", @ARGV' echo test

将插入echo test到终端设备输入缓冲区中,就像从终端接收一样。

更便携的变体@mike的Terminology方法并且不会牺牲安全性的是向终端模拟器发送一个相当标准的query status report转义序列:<ESC>[5n终端总是回复(作为输入)as<ESC>[0n并将其绑定到您想要插入的字符串:

bind '"\e[0n": "echo test"'; printf '\e[5n'

如果在 GNU 内screen,您还可以执行以下操作:

screen -X stuff 'echo test'

现在,除了 TIOCSTI ioctl 方法之外,我们要求终端仿真器向我们发送一些字符串,就像键入的一样。如果该字符串出现在readlinebash的行编辑器)禁用终端本地回显之前,则将显示该字符串不是在 shell 提示符下,稍微弄乱了显示。

要解决这个问题,您可以稍微延迟向终端发送请求,以确保在 readline 禁用回显时响应到达。

bind '"\e[0n": "echo test"'; ((sleep 0.05;  printf '\e[5n') &)

(这里假设您sleep支持亚秒分辨率)。

理想情况下你想做类似的事情:

bind '"\e[0n": "echo test"'
stty -echo
printf '\e[5n'
wait-until-the-response-arrives
stty echo

但是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"

进一步阅读

@starfry 的回答它扩展了@mikeserv 和我自己给出的两个解决方案,并提供了一些更详细的信息。

答案2

这个答案是为了澄清我自己的理解而提供的,受到我之前的@StéphaneChazelas 和@mikeserv 的启发。

长话短说

  • 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, 一个 UNIX 系统调用输入/输出控制它有一个TIOCSTI可用于注入输入的命令。

钛奥克从 ”时间终端国际奥委会特尔“ 和性病从 ”S结尾时间终端输入”。

没有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保存):

Perl inject.pl

#!/usr/bin/perl
ioctl(STDIN, 0x5412, $_) for split "", join " ", @ARGV

您可以生成sys/ioctl.ph哪个定义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)

红宝石 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选项来抑制它。--tor选项--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'

替代方法

如果您的环境满足某些先决条件,那么您可能可以使用其他方法来注入输入。如果您处于桌面环境中xdo工具是一个X组织模拟鼠标和键盘活动的实用程序,但您的发行版默认情况下可能不包含它。你可以试试:

$ xdotool type ls

如果你使用多路复用器,终端多路复用器,那么你可以这样做:

$ tmux send-key -t session:pane ls

其中-t选择哪个会议窗格注射。GNU 屏幕其命令具有类似的功能stuff

$ screen -S session -p pane -X stuff ls

如果您的发行版包括控制台工具打包然后你可能会有一个像我们的示例一样writevt使用的命令。ioctl然而,大多数发行版已弃用此软件包,转而使用知识库缺少这个功能。

更新后的副本写vt.c可以使用 进行编译gcc -o writevt writevt.c

其他可能更适合某些用例的选项包括预计空的其设计目的是允许对交互式工具进行脚本化。

您还可以使用支持终端注入的 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)我们正在输入命令的交互式 shell,它的文件描述符将其连接到我们用来与其交互的终端应用程序(xterm或类似的)。

$ ls -l /dev/fd/
lrwx------ 0 -> /dev/pts/3
lrwx------ 1 -> /dev/pts/3
lrwx------ 2 -> /dev/pts/3

再次查看该命令,第一组括号启动了一个子 shell, bash(6524,6524)),其文件描述符为 0(其标准输入<>)被分配给由另一个子 shell 返回的 pts(以读写方式打开, ),该子 shell 执行./pts <&3解锁与文件描述符 3 关联的 pts(在上一步中创建,exec 3<>/dev/ptmx)。

子 shell 的文件描述符 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

子 shell 执行此命令:

setsid -c bash -i 2>&1 | tee log

它在新会话中bash(6527,6527)以交互 ( ) 模式运行(请注意 PID 和 PGID 相同)。它的标准错误被重定向到其标准输出 ( ) 并通过管道传输,因此它会写入文件和 pts。这提供了另一种查看子 shell 输出的方法:-isetsid -c2>&1tee(6528,6524)log

$ tail -f log

由于子 shell 是以bash交互方式运行的,因此可以向它发送命令来执行,例如显示子 shell 的文件描述符的示例:

$ echo 'ls -l /dev/fd/' >&3

读取 subshel​​l 的输出(tail -f logcat <&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

答案3

这取决于你的意思bash仅有的。如果您指的是单个交互式bash会话,那么答案几乎肯定是。这是因为,即使您ls -l在任何规范终端上的命令行中输入命令,bash也不会意识到这一点 - 并且bash当时甚至没有参与其中。

相反,到目前为止发生的事情是内核的 tty 线路规则已缓冲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'

这将在xtermw/中工作allowwindowOps资源集。它首先将图标/窗口名称保存在堆栈上,然后将终端的图标字符串设置为^Umy command然后请求终端将该名称插入输入队列,最后将其重置为保存的值。对于在 正确的配置中运行的交互式bashshell来说,它应该是隐形的,但这可能是一个坏主意。xterm请参阅下面 Stéphane 的评论。

printf不过,这是我在机器上运行带有不同转义序列的位后拍摄的术语终端的图片。对于printf我输入的命令中的每个换行符CTRL+V然后CTRL+J然后按下ENTER钥匙。之后我什么也没输入,但是,正如你所看到的,终端注入了my command进入线路纪律的输入队列:

术语注入

真正做到这一点的方法是使用嵌套 pty。这就是如何screentmux类似的工作 - 顺便说一句,这两者都可以让你实现这一目标。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

这绝不是可移植的,但如果有适当的打开权限,应该可以在大多数 Linux 系统上工作/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

...当在 GNU 系统上运行时(或者任何其他带有标准 C 编译器也可以从 stdin 读取的编译器),将写出一个名为的小型可执行二进制文件pts,该二进制文件将在其标准输入上运行该unlockpt()函数,并将刚刚解锁的 pty 设备的名称写入其标准输出。我在工作时写的...我如何获得这个 pty,我可以用它做什么?

不管怎样,上面的代码所做的就是bash在当前 tty 下面的 pty 层中运行一个 shell。bash被告知将所有输出写入从属 pty,并且当前 tty 被配置为既不配置-echo其输入也不缓冲它,而是传递它(大多) rawto cat,将其复制到bash.与此同时,另一个后台cat复制所有从属输出到当前 tty。

在大多数情况下,上述配置是完全无用的 - 只是多余的,基本上 -除了我们bash在 上发布了自己的 pty master fd 副本<>9。这意味着bash可以通过简单的重定向自由写入自己的输入流。所bash要做的就是:

echo echo hey >&9

……自言自语。

这是另一张图片:

在此输入图像描述

答案4

天哪,我们错过了bash 内置的一个简单解决方案:该read命令有一个选项-i ...,与 一起使用时-e,将文本推入输入缓冲区。从手册页:

-我 文本

如果使用 readline 读取该行,则在编辑开始之前将文本放入编辑缓冲区中。

因此,创建一个小的 bash 函数或 shell 脚本,将命令呈现给用户,并运行或评估他们的回复:

domycmd(){ read -e -i "$*"; eval "$REPLY"; }

毫无疑问,这使用了 ioctl(,TIOCSTI,),它已经存在了 32 年多了,因为它已经存在于2.9BSD ioctl.h

相关内容