สามารถทุบตีเขียนในกระแสข้อมูลของตนเองได้หรือไม่


39

เป็นไปได้ในเปลือกbashแบบโต้ตอบเพื่อป้อนคำสั่งที่แสดงผลข้อความบางส่วนเพื่อให้ปรากฏที่พรอมต์คำสั่งถัดไปราวกับว่าผู้ใช้พิมพ์ข้อความนั้นในพรอมต์นั้นหรือไม่

ฉันต้องการให้sourceสคริปต์ที่จะสร้างบรรทัดคำสั่งและส่งออกเพื่อให้ปรากฏเมื่อพรอมต์ส่งกลับหลังจากสคริปต์สิ้นสุดลงเพื่อให้ผู้ใช้สามารถเลือกที่จะแก้ไขมันก่อนที่จะกดenterเพื่อรันมัน

สามารถทำได้ด้วยxdotoolแต่จะใช้ได้เฉพาะเมื่อเครื่องอยู่ในหน้าต่าง X และเมื่อติดตั้งแล้ว

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

สิ่งนี้สามารถทำได้โดยใช้ bash เท่านั้น?


ฉันคิดว่าสิ่งนี้ไม่ควรยากกับคาดหวังถ้าคุณสามารถทนกับมันได้ แต่ฉันจำไม่ได้พอที่จะโพสต์คำตอบที่แท้จริง
tripleee

คำตอบ:


39

ด้วย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ลงในอินพุตบัฟเฟอร์อุปกรณ์เทอร์มินัลราวกับว่าได้รับจากเทอร์มินัล

รูปแบบพกพามากขึ้นใน@ ไมค์ของTerminologyวิธีการและที่ไม่ได้เสียสละรักษาความปลอดภัยจะส่งจำลอง terminal มาตรฐานเป็นธรรมquery status reportลำดับหนี: <ESC>[5nซึ่งขั้วคงเส้นคงวาตอบ (เพื่อเป็น input) ในฐานะ<ESC>[0nและผูกที่สตริงที่คุณต้องการแทรก:

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

ถ้าภายใน GNU screenคุณสามารถทำได้:

screen -X stuff 'echo test'

ตอนนี้ยกเว้นวิธี TIOCSTI ioctl เรากำลังขอให้ terminal emulator ส่งสตริงให้เราราวกับพิมพ์ หากสตริงนั้นมาก่อนreadline(ตัวbashแก้ไขบรรทัด) ปิดใช้งานเทอร์มินัลเอคโค่แล้วสตริงนั้นจะไม่ปรากฏขึ้นที่เชลล์พรอมต์ทำให้จอแสดงผลสกปรกเล็กน้อย

ในการหลีกเลี่ยงปัญหานั้นคุณสามารถหน่วงเวลาการส่งคำขอไปยังเครื่องเทอร์มินัลเล็กน้อยเพื่อให้แน่ใจว่าการตอบสนองมาถึงเมื่อ 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 และตัวฉันเองพร้อมกับข้อมูลรายละเอียดเพิ่มเติมเล็กน้อย


ฉันคิดว่าbind '"\e[0n": "echo test"'; printf '\e[5n'อาจเป็นคำตอบเดียวที่ฉันกำลังมองหา มันใช้งานได้สำหรับฉัน อย่างไรก็ตามฉันได้รับการ^[[0nพิมพ์ก่อนพรอมต์เช่นกัน ฉันค้นพบสิ่งนี้เกิดขึ้นเมื่อ$PS1มี subshell คุณสามารถทำซ้ำได้โดยทำPS1='$(:)'ก่อนคำสั่ง bind ทำไมจะเกิดขึ้นและสามารถทำอะไรกับมันได้บ้าง
starfry

แม้ว่าทุกอย่างในคำตอบนี้จะถูกต้อง แต่คำถามก็คือทุบตีไม่ใช่ zsh บางครั้งเราไม่มีทางเลือกว่าเชลล์จะใช้อะไร
Falsenames

@Falsenames เฉพาะย่อหน้าแรกสำหรับ zsh ส่วนที่เหลือเป็นทั้งผู้ไม่เชื่อเรื่องพระเจ้าหรือทุบตีเฉพาะ คำถามและคำตอบไม่จำเป็นต้องมีประโยชน์สำหรับการทุบตีผู้ใช้เท่านั้น
Stéphane Chazelas

1
@starfry ดูเหมือนว่าบางทีคุณสามารถใส่คำ\rว่าหัวไว้ได้$PS1ไหม? ที่ควรจะทำงานถ้า$PS1นานพอ ถ้าไม่ใส่แล้ว^[[Mมี
mikeserv

@ mikeserv - rทำเคล็ดลับ มันไม่ได้ป้องกันเอาท์พุท แต่มันถูกเขียนทับก่อนที่ตาจะเห็น ฉันเดาว่า^[[Mจะลบบรรทัดเพื่อล้างข้อความที่ถูกฉีดในกรณีที่มันนานกว่าพรอมต์ ใช่ไหม (ฉันหามันไม่เจอใน ANSI escape list ที่ฉันมี)
starfry

23

คำตอบนี้ให้ไว้เพื่อชี้แจงความเข้าใจของฉันเองและได้รับแรงบันดาลใจจาก @ StéphaneChazelasและ @mikeserv ก่อนฉัน

TL; DR

  • มันเป็นไปไม่ได้ที่จะทำสิ่งนี้bashโดยปราศจากความช่วยเหลือจากภายนอก
  • วิธีที่ถูกต้องที่จะทำคือมีการป้อนข้อมูลสถานีส่ง ioctlแต่
  • ที่ง่ายที่สุดที่สามารถทำงานได้ใช้วิธีการแก้ปัญหาbashbind

ทางออกที่ง่าย

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"

ที่เป็นหนึ่งในโซลูชั่นที่อธิบายไว้ข้างต้นหรือแทนที่ด้วยinjectprintf '\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สามารถรวบรวมโดยใช้

ตัวเลือกอื่น ๆ ที่อาจเหมาะสมกับกรณีการใช้งานบางอย่างรวมถึงความคาดหวังและว่างเปล่าซึ่งได้รับการออกแบบมาเพื่ออนุญาตให้มีการใช้เครื่องมือการโต้ตอบ

นอกจากนี้คุณยังสามารถใช้เปลือกที่สนับสนุนขั้วฉีดเช่นที่สามารถทำได้zshprint -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

20

มันขึ้นอยู่กับสิ่งที่คุณหมายถึงโดยเฉพาะbash ถ้าคุณหมายถึงเป็นหนึ่งเดียวโต้ตอบbashเซสชั่นแล้วคำตอบคือเกือบแน่นอนไม่มี และนี่เป็นเพราะแม้ว่าคุณจะป้อนคำสั่งเช่นls -lที่บรรทัดคำสั่งบนเทอร์มินัลที่ยอมรับแล้วbashก็ยังไม่ได้ตระหนักถึงมันและbashยังไม่ได้มีส่วนร่วมในจุดนั้น

แต่สิ่งที่เกิดขึ้นจนถึงจุดนั้นก็คือการที่บรรทัด tty ของเคอร์เนลมีบัฟเฟอร์และstty echod อินพุตของผู้ใช้ไปยังหน้าจอเท่านั้น มันวูบวาบอินพุตที่ส่งไปยังผู้อ่าน - bashในกรณีตัวอย่างของคุณ - บรรทัดต่อบรรทัด - และโดยทั่วไปแปล\returns เป็น\newlines ในระบบ Unix ด้วย - และbashไม่ใช่ - และสคริปต์ของคุณไม่สามารถรู้ได้ ป้อนข้อมูลทั้งหมดจนกว่าผู้ใช้กดENTERปุ่ม

ตอนนี้มีการแก้ไขอยู่บ้าง ที่แข็งแกร่งที่สุดไม่ใช่การทำงานจริง ๆ และเกี่ยวข้องกับการใช้กระบวนการหลายอย่างหรือโปรแกรมที่เขียนขึ้นเป็นพิเศษเพื่อป้อนข้อมูลตามลำดับซ่อนระเบียบวินัยของ-echoผู้ใช้และเขียนลงบนหน้าจอตามความเหมาะสมในการตีความอินพุต พิเศษเมื่อจำเป็น นี่อาจเป็นเรื่องยากที่จะทำได้ดีเพราะมันหมายถึงการเขียนกฎการตีความซึ่งสามารถจัดการอินพุต char โดย char เมื่อมันมาถึงและเขียนมันออกมาพร้อมกันโดยไม่มีข้อผิดพลาดเพื่อจำลองสิ่งที่ผู้ใช้ทั่วไปคาดหวังในสถานการณ์นั้น ด้วยเหตุผลนี้อาจเป็นไปได้ว่าเทอร์มินัลแบบอินเทอร์แอคทีฟ i / o ไม่ค่อยเข้าใจกันมากนัก - โอกาสที่ยากไม่ใช่หนึ่งที่ให้ยืมตัวเองเพื่อสอบสวนต่อไป

การแก้ไขอื่นอาจเกี่ยวข้องกับเทอร์มินัลอีมูเลเตอร์ คุณบอกว่าปัญหาสำหรับคุณคือการพึ่งพา X และxdotoolอื่น ๆ ในกรณีเช่นนี้การแก้ไขที่ฉันกำลังจะเสนออาจมีปัญหาที่คล้ายกัน แต่ฉันจะดำเนินการต่อด้วยสิ่งเดียวกัน

printf  '\33[22;1t\33]1;%b\33\\\33[20t\33[23;0t' \
        '\025my command'

ที่จะทำงานในxtermw / allowwindowOpsชุดทรัพยากร โดยจะบันทึกชื่อไอคอน / หน้าต่างไว้บนสแต็กจากนั้นตั้งค่าไอคอนสตริงของเทอร์มินัลเพื่อ^Umy commandร้องขอให้เทอร์มินัลฉีดชื่อนั้นลงในคิวป้อนข้อมูลและรีเซ็ตเป็นค่าที่บันทึกไว้ล่าสุด มันควรจะทำงานอย่างล่องหนสำหรับbashเชลล์แบบอินเทอร์แอคทีฟที่ทำงานxterm ด้วย w / การตั้งค่าที่ถูกต้อง แต่อาจเป็นความคิดที่ไม่ดี โปรดดูความคิดเห็นของStéphaneด้านล่าง

อย่างไรก็ตามที่นี่เป็นรูปภาพที่ฉันถ่ายจากเทอร์มินัลของฉันหลังจากเรียกใช้printfบิตที่มีลำดับการหลบหนีที่แตกต่างกันในเครื่องของฉัน สำหรับแต่ละบรรทัดใหม่ในprintfคำสั่งฉันพิมพ์CTRL+Vแล้วCTRL+Jและหลังจากนั้นกดENTERคีย์ ฉันไม่ได้พิมพ์อะไรเลยหลังจากนั้น แต่อย่างที่คุณเห็นเทอร์มินัลฉีดmy commandเข้าไปในคิวอินพุตของระเบียบวินัยบรรทัดสำหรับฉัน:

term_inject

วิธีที่แท้จริงในการทำเช่นนี้คือ w / a ที่ซ้อนกัน 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

นั่นคือไม่ได้หมายความว่าแบบพกพา /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()ฟังก์ชั่นใน stdin และเขียนไปยัง stdout ของมัน ชื่อของอุปกรณ์ pty ที่เพิ่งปลดล็อค ฉันเขียนมันเมื่อทำงาน ... ฉันจะมาได้อย่างไรโดย pty นี้และฉันจะทำอย่างไรกับมัน? .

อย่างไรก็ตามสิ่งที่รหัสบิตด้านบนไม่ทำงานbashเชลล์ในชั้น pty a ภายใต้ tty ปัจจุบัน bashจะบอกให้เขียนออกทั้งหมดเพื่อ Pty ทาสและปัจจุบัน TTY มีการกำหนดค่าทั้งไม่ให้-echoปัจจัยการผลิตไม่ให้ buffer มัน แต่จะผ่านมัน(ส่วนใหญ่) rawไปซึ่งสำเนามันไปcat bashและในขณะที่อีกอันที่มีพื้นหลังจะcatคัดลอกเอาต์พุตทาสทั้งหมดไปยัง tty ปัจจุบัน

ส่วนใหญ่การตั้งค่าดังกล่าวข้างต้นจะไร้ประโยชน์โดยสิ้นเชิง - เพียงแค่ซ้ำซ้อนพื้น - ยกเว้นว่าเราจะเปิดตัวbashพร้อมสำเนาของนาย Pty ตัวเอง fd <>9บน ซึ่งหมายความว่าbashสามารถเขียนลงในสตรีมอินพุตของตนเองได้อย่างอิสระด้วยการเปลี่ยนเส้นทางแบบง่าย สิ่งที่bashต้องทำคือ:

echo echo hey >&9

... เพื่อคุยกับตัวเอง

นี่คือภาพอื่น:

ป้อนคำอธิบายรูปภาพที่นี่


2
เทอร์มินัลใดที่คุณสามารถจัดการให้ทำงานได้ สิ่งนั้นกำลังถูกล่วงละเมิดในสมัยก่อนและควรปิดการใช้งานโดยปริยายในปัจจุบัน ด้วยxtermคุณยังสามารถสอบถามชื่อไอคอนที่มีแต่ถ้ากำหนดค่าด้วย\e[20t allowWindowOps: true
Stéphane Chazelas

นั่นคือCVE-2003-0063
Stéphane Chazelas

@ StéphaneChazelasที่ทำงานในคำศัพท์ แต่ฉันค่อนข้างแน่ใจว่ามันยังทำงานในสถานี gnome ใน terminal kde (ฉันลืมชื่อของมันและฉันคิดว่ามีการหลบหนีที่แตกต่างกัน)และตามที่คุณพูดว่า w / xtermw / เหมาะสม การตั้งค่า แม้ว่าเหมาะสมกับ xterm แล้วคุณสามารถอ่านและเขียนบัฟเฟอร์การคัดลอก / วางได้ดังนั้นมันจึงง่ายขึ้นกว่านี้ฉันคิดว่า Xterm ยังมี escape sequences สำหรับการเปลี่ยนแปลง / ส่งผลกระทบต่อคำอธิบายคำศัพท์นั้นเอง
mikeserv

ฉันไม่สามารถทำงานในสิ่งใดนอกจากคำศัพท์ (ซึ่ง btw มีช่องโหว่อื่นที่คล้ายคลึงกันหลายแห่ง) CVE นั้นมีอายุมากกว่า 12 ปีและค่อนข้างเป็นที่รู้จักกันดีฉันจะแปลกใจถ้ามีผู้จำลองเทอร์มินัลหลักมีช่องโหว่เดียวกัน โปรดทราบว่าด้วย xterm นั่นคือ\e[20t(ไม่ใช่\e]1;?\a)
Stéphane Chazelas


8

ถึงแม้ว่าioctl(,TIOCSTI,) คำตอบ ของStéphane Chazelas คือคำตอบที่ถูกต้องบางคนอาจจะมีความสุขกับคำตอบเพียงเล็กน้อย แต่น่าสนใจเพียงแค่กดคำสั่งลงในสแต็กประวัติ คำสั่ง

$ history -s "ls -l"
$ echo "move up 1 line in history to get command to run"

สิ่งนี้สามารถกลายเป็นสคริปต์อย่างง่ายซึ่งมีประวัติ 1 บรรทัดของตนเอง:

#!/bin/bash
history -s "ls -l"
read -e -p "move up 1 line: "
eval "$REPLY"

read -eเปิดใช้งานการแก้ไข readline ของอินพุต-pเป็นพรอมต์


ที่จะทำงานในฟังก์ชั่นเชลล์หรือถ้าสคริปต์มีที่มา ( . foo.shหรือ `แหล่งที่มา foo.sh แทนการทำงานใน subshell.) วิธีการที่น่าสนใจ การแฮ็คที่คล้ายกันซึ่งต้องการการแก้ไขบริบทของการเรียก shell จะเป็นการตั้งค่าความสมบูรณ์แบบกำหนดเองที่ขยายบรรทัดว่างไปยังบางสิ่งจากนั้นเรียกคืนตัวจัดการความสมบูรณ์แบบเก่า
Peter Cordes

@PeterCordes คุณถูกต้อง ฉันรับคำถามด้วยเช่นกัน แต่ฉันได้เพิ่มตัวอย่างสคริปต์ง่าย ๆ ที่สามารถใช้งานได้
meuh

@mikeserv เฮ้มันเป็นแค่ทางออกง่ายๆที่อาจเป็นประโยชน์กับบางคน คุณยังสามารถลบevalถ้าคุณมีคำสั่งง่ายๆที่จะแก้ไขได้โดยไม่ต้องเปลี่ยนเส้นทางท่อและอื่น ๆ
meuh

1

โอ้คำพูดของเราเราพลาดวิธีง่ายๆในการทุบตี : readคำสั่งมีตัวเลือก-i ...ซึ่งเมื่อใช้กับ-eผลักข้อความลงในบัฟเฟอร์อินพุต จากหน้าคน:

ข้อความ-i

หากกำลังใช้ readline เพื่ออ่านบรรทัดข้อความจะถูกวางในบัฟเฟอร์การแก้ไขก่อนที่จะเริ่มการแก้ไข

ดังนั้นสร้างฟังก์ชั่นทุบตีขนาดเล็กหรือเชลล์สคริปต์ซึ่งใช้คำสั่งเพื่อนำเสนอให้กับผู้ใช้และเรียกใช้หรือประเมินการตอบกลับของพวกเขา:

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

นี้ไม่ต้องสงสัยเลยใช้ IOCTL (TIOCSTI) ซึ่งได้รับรอบกว่า 32 ปีขณะที่มันมีอยู่แล้วใน2.9BSD ioctl.h


1
สิ่งที่น่าสนใจที่มีเอฟเฟกต์คล้าย ๆ กัน แต่มันไม่ได้แทรกเข้าสู่พรอมต์
starfry

ในความคิดที่ 2 คุณคิดถูก bash ไม่จำเป็นต้อง TIOCSTI เพราะมันทำทุกอย่างด้วยตัวเอง
meuh
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.