この回答は、私自身の理解の明確化として提供されており、私の前の@StéphaneChazelasと@mikeservに触発されています。
TL; DR
bash外部からの支援なしにこれを行うことはできません。
- これを行う正しい方法は、端末入力を送信 することです
ioctlが、
- 最も簡単な実行可能な
bashソリューションはを使用しbindます。
簡単な解決策
bind '"\e[0n": "ls -l"'; printf '\e[5n'
Bashにはシェルビルトインがありbind、キーシーケンスを受け取ったときにシェルコマンドを実行できます。本質的に、シェルコマンドの出力はシェルの入力バッファに書き込まれます。
$ bind '"\e[0n": "ls -l"'
キーシーケンス\e[0n(<ESC>[0n)は、端末が正常に機能していることを示すために送信するANSI端末エスケープコードです。として送信されるデバイスステータスレポート要求への応答でこれを送信し<ESC>[5nます。
echo挿入するテキストを出力するに応答をバインドすることにより、デバイスステータスを要求することにより、必要なときにいつでもテキストを挿入でき<ESC>[5nます。
printf '\e[5n'
これは機能し、おそらく他のツールが関与しないため、元の質問に答えるのにおそらく十分です。これは純粋ですbashが、正常に動作する端末に依存しています(実際にはすべてがそうです)。
コマンドラインにエコーされたテキストが入力されたかのように使用できる状態になります。追加、編集、押すENTERことができ、実行されます。
\nバインドされたコマンドに追加して、自動的に実行されるようにします。
ただし、このソリューションは現在の端末でのみ機能します(元の質問の範囲内です)。対話型プロンプトまたはソーススクリプトから機能しますが、サブシェルから使用するとエラーが発生します。
bind: warning: line editing not enabled
次に説明する正しいソリューションはより柔軟ですが、外部コマンドに依存しています。
正しい解決策
入力をインジェクトする適切な方法は、tty_ioctlを使用します。これは、入力をインジェクトするためにTIOCSTI使用できるコマンドを持つI / O Controlの UNIXシステムコールです。
TIOC "から T erminal IOCの TL "と STI "から Sの終わりT erminal I NPUT "。
このためのコマンドは組み込まれbashていません。そのためには、外部コマンドが必要です。典型的なGNU / Linuxディストリビューションにはこのようなコマンドはありませんが、少しのプログラミングで達成することは難しくありません。以下が使用するシェル関数です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を行うために使用すると、サブシェルで機能します。次に説明するように、他の端末に注入することもできます。
さらに進める(他の端末を制御する)
元の質問の範囲を超えていますが、適切な権限を持っていることを条件に、別の端末に文字を注入することは可能です。通常、これはを意味しますが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
きれいな出力
挿入されたテキストは、プロンプトが表示される前に入力されたかのようにプロンプトの前に表示されます(実際にはそうでした)が、プロンプトの後に再び表示されます。
プロンプトの前に表示されるテキストを非表示にする1つの方法は、プロンプトにキャリッジリターン(\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で、上記のソリューションの1つ、またはに置き換えられprintf '\e[5n'ます。
代替アプローチ
ご使用の環境が特定の前提条件を満たしている場合、入力を注入するために使用できる他の方法を使用できる場合があります。デスクトップ環境の場合、xdotoolはマウスとキーボードの動作をシミュレートするX.Orgユーティリティですが、ディストリビューションにはデフォルトで含まれていない場合があります。あなたが試すことができます:
$ xdotool type ls
端末マルチプレクサであるtmuxを使用する場合、これを行うことができます。
$ tmux send-key -t session:pane ls
whereは、注入-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があります。
また、可能性のある端末インジェクションをサポートするシェルを使用することもzshできますprint -z ls。
「うわー、それは賢い...」答え
ここで説明されたメソッドもここで議論されて、ここで議論されたメソッドに基づいています。
からのシェルリダイレクト/dev/ptmxは、新しい擬似端末を取得します。
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
擬似端末マスター(ptm)のロックを解除し、擬似端末スレーブ(pts)の名前を標準出力に出力する、Cで書かれた小さなツール。
#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
プロセスをPTSに接続できます。最初に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にはアクセスできません。読み取り/書き込みで開かれたPTSである標準入力(fd 0)は、>&0標準出力(fd 1)にリダイレクトされます(実際にはfdがコピーされます- )。
これにより、標準入力と出力がPTSに接続されたサブシェルが作成されます。ptmに書き込むことで入力を送信でき、ptmから読み取ることで出力を確認できます。
$ echo 'some input' >&3 # write to subshell
$ cat <&3 # read from subshell
サブシェルは次のコマンドを実行します。
setsid -c bash -i 2>&1 | tee log
新しいセッションbash(6527,6527)でインタラクティブ(-i)モードで実行されます(setsid -c、PIDとPGIDは同じであることに注意してください)。その標準エラーはその標準出力(2>&1)にリダイレクトされ、経由でパイプされるtee(6528,6524)ためlog、ptsだけでなくファイルにも書き込まれます。これにより、サブシェルの出力を確認する別の方法が提供されます。
$ tail -f log
サブシェルはbash対話的に実行されているため、サブシェルのファイル記述子を表示する次の例のように、実行するコマンドを送信できます。
$ echo 'ls -l /dev/fd/' >&3
サブシェルの出力(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)は、制御端末に属する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