คำตอบนี้ให้ไว้เพื่อชี้แจงความเข้าใจของฉันเองและได้รับแรงบันดาลใจจาก @ 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
ไปยังคำสั่ง bound เพื่อให้ดำเนินการโดยอัตโนมัติ
อย่างไรก็ตามวิธีนี้ใช้งานได้ในเทอร์มินัลปัจจุบันเท่านั้น (ซึ่งอยู่ภายในขอบเขตของคำถามเดิม) ใช้งานได้จากพรอมต์แบบอินเทอร์แอคทีฟหรือจากสคริปต์ที่มาแต่จะทำให้เกิดข้อผิดพลาดหากใช้จาก subshell:
bind: warning: line editing not enabled
โซลูชันที่ถูกต้องที่อธิบายไว้ถัดไปนั้นมีความยืดหยุ่นมากกว่า แต่ต้องอาศัยคำสั่งภายนอก
ทางออกที่ถูกต้อง
วิธีที่เหมาะสมในการฉีดอินพุตใช้tty_ioctlการเรียกใช้ระบบยูนิกซ์สำหรับการควบคุม I / Oที่มีTIOCSTI
คำสั่งที่สามารถใช้ในการฉีดอินพุต
TIOCจาก " T erminal IOC tl " และ STIจาก " S end 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
แทนการใช้ค่าตัวเลข ดูที่นี่
หลาม 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
เพื่อทำสิ่งนี้ใช้ได้ใน subshells นอกจากนี้ยังสามารถฉีดเข้าไปในเทอร์มินัลอื่นตามที่อธิบายไว้ต่อไป
นำไปใช้เพิ่มเติม (การควบคุมเทอร์มินัลอื่น)
มันอยู่นอกเหนือขอบเขตของคำถามเดิม แต่สามารถฉีดอักขระลงในเทอร์มินัลอื่นโดยขึ้นอยู่กับการอนุญาตที่เหมาะสม ปกติแล้วนี่หมายถึงการเป็นอยู่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
ตัวเลือกที่ต้องมีการโต้แย้ง - The 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ที่จำลองการทำงานของเมาส์และคีย์บอร์ด แต่ distro ของคุณอาจไม่รวมถึงค่าเริ่มต้น คุณสามารถลอง:
$ xdotool type ls
หากคุณใช้tmuxเทอร์มินัลมัลติเพล็กเซอร์แล้วคุณสามารถทำได้:
$ tmux send-key -t session:pane ls
โดยที่-t
เลือกเซสชันและบานหน้าต่างที่จะฉีด หน้าจอ GNUมีความสามารถคล้ายกันกับstuff
คำสั่ง:
$ screen -S session -p pane -X stuff ls
หาก distro ของคุณมีแพ็คเกจเครื่องมือคอนโซลคุณอาจมีwritevt
คำสั่งที่ใช้ioctl
เช่นตัวอย่างของเรา อย่างไรก็ตาม distros ส่วนใหญ่มีการเลิกใช้แพคเกจนี้ในความโปรดปรานของkbdซึ่งขาดคุณสมบัตินี้
สำเนาปรับปรุงของwritevt.cgcc -o writevt writevt.c
สามารถรวบรวมโดยใช้
ตัวเลือกอื่น ๆ ที่อาจเหมาะสมกับกรณีการใช้งานบางอย่างรวมถึงความคาดหวังและว่างเปล่าซึ่งได้รับการออกแบบมาเพื่ออนุญาตให้มีการใช้เครื่องมือการโต้ตอบ
นอกจากนี้คุณยังสามารถใช้เปลือกที่สนับสนุนขั้วฉีดเช่นที่สามารถทำได้zsh
print -z ls
คำตอบ "ว้าวช่างฉลาด ... "
วิธีการอธิบายที่นี่ยังเป็นที่กล่าวถึงที่นี่และสร้างวิธีการที่กล่าวถึงที่นี่
การเปลี่ยนเส้นทางเชลล์จาก/dev/ptmx
รับ pseudo-terminal ใหม่:
$ $ ls /dev/pts; ls /dev/pts </dev/ptmx
0 1 2 ptmx
0 1 2 3 ptmx
เครื่องมือเล็ก ๆ ที่เขียนใน C ที่ปลดล็อกต้นแบบ pseudoterminal (ptm) และส่งออกชื่อของ pseudoterminal slave (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
unlockpt () ฟังก์ชั่นปลดล็อคทาสอุปกรณ์ pseudoterminal ที่สอดคล้องกับ pseudoterminal ต้นแบบที่อ้างถึงโดยอธิบายไฟล์ที่กำหนด โปรแกรมผ่านนี้เป็นศูนย์ซึ่งเป็นโปรแกรมเข้ามาตรฐาน
ptsname () ผลตอบแทนที่ฟังก์ชั่นชื่อของอุปกรณ์ pseudoterminal ทาสที่สอดคล้องกับหลักที่อ้างถึงโดยอธิบายไฟล์ที่ได้รับอีกครั้งผ่านศูนย์สำหรับการป้อนข้อมูลมาตรฐานของโปรแกรม
กระบวนการสามารถเชื่อมต่อกับ pts ได้ อันดับแรกรับ ptm (ที่นี่ถูกกำหนดให้กับ file descriptor 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)
เอาท์พุทจะสัมพันธ์กับเปลือกปัจจุบัน ( $$
) และ 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
เมื่อดูที่คำสั่งอีกครั้งชุดแรกของวงเล็บจะเริ่ม subshell, bash(6524,6524)
) พร้อมกับ file descriptor 0 ( อินพุตมาตรฐาน ) ที่กำหนดให้กับ pts (ซึ่งถูกเปิดอ่าน - เขียน<>
) ซึ่งส่งกลับโดย subshell อื่นที่ดำเนินการ./pts <&3
เพื่อปลดล็อก pts ที่เชื่อมโยงกับ file descriptor 3 (สร้างในขั้นตอนก่อนหน้าexec 3<>/dev/ptmx
)
ไฟล์อธิบายของ subshell 3 ถูกปิด ( 3>&-
) เพื่อให้ ptm ไม่สามารถเข้าถึงได้ อินพุตมาตรฐาน (fd 0) ซึ่งเป็น pts ที่เปิดอ่าน / เขียนถูกเปลี่ยนเส้นทาง (ที่จริงแล้ว fd จะถูกคัดลอก - >&0
) ไปยังเอาต์พุตมาตรฐาน (fd 1)
สิ่งนี้สร้างเชลล์ย่อยที่มีอินพุตและเอาต์พุตมาตรฐานเชื่อมต่อกับ 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 นี่เป็นอีกวิธีหนึ่งในการดูผลลัพธ์ของ subshell:
$ tail -f log
เนื่องจาก subshell ทำงานbash
แบบโต้ตอบจึงสามารถส่งคำสั่งเพื่อดำเนินการเช่นตัวอย่างนี้ซึ่งแสดงไฟล์ descriptors ของ subshell:
$ 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 Standard Error (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