Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Capture STDOUT, STDERR และ Exit Status ของ Shell Commands ใน Ruby

tl;dr อยากรัน shell command จาก Ruby capture stdout, stderr และ return status ดู Open3.capture3 method อยาก process stdout/stderr data แบบ streaming ดู Open3.popen3

So Many Bad Choices

มี 492 วิธี execute shell commands จาก ruby แต่ละอันทำงานต่างกันนิดหน่อย คุณน่าจะเคยใช้ approach หนึ่งในนี้ back-ticks (``) เป็น go-to ของผม

exec("echo 'hello world'") # exits from ruby, then runs the command system('echo', 'hello world') # returns the status code sh('echo', 'hello world') # returns the status code `echo "hello world"` # returns stdout %x[echo 'hello world'] # returns stdout 

แต่วิธีเหล่านี้จำกัดมาก สมมติต้อง capture ไม่แค่ stdout แต่ stderr ด้วย เหนื่อยแล้ว หรืออยาก process stdout data แบบ streaming ไม่ใช่ทั้งหมดตอนจบ command? เหนื่อย

มีอีกออปชัน ให้ run commands asynchronously ให้ stdout, stderr, exit codes, และ PIDs มาดู!

Open3

Open3 module แปลกชื่ออยู่ใน Ruby standard library ทำอะไร?

Open3 grants you access to stdout, stderr, exit codes and a thread to wait for the child process when running another program. You can specify various attributes, redirections, current directory, etc., of the program in the same way as for Process.spawn. (_Source: [Open3 Docs](https://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html))_

ไม่เคยใช้? ไม่เคยได้ยิน? เพราะมันไม่ดู friendly lol ชื่อฟังแล้ว像 C มากกว่า Ruby และ documentation ค่อนข้าง hardcore neck-beard แต่ลองใช้แล้วไม่ intimidating เท่าที่คิด

capture3

ถ้ามีวิธีง่าย capture stdout, stderr AND status code? มีจริง ถ้าไม่มีเวลาอ่านต่อแค่รู้ว่า用 capture3 method แล้วเรียบร้อย

ตัวอย่าง อยากได้ list files ใน current directory รัน ls

Back-tick syntax:

puts(`ls`) 

กับ capture3:

require 'open3' stdout, stderr, status = Open3.capture3("ls") 

รัน command ให้ stdout และ stderr เป็น strings ไม่มุ่ยไม่ฟัซ

Security

โดยทั่วไปไม่อยากให้ users รัน arbitrary commands บน web server นั่นเป็นไอเดียแย่ identify #{ params[:filename] }

Open3 หลีกเลี่ยงปัญหาแยก commands จาก data ทำงานเหมือน system method

Open3.capture3("identify", params[:filename], other_unsafe_params) 

popen3

ภายใต้ capture3 ใช้ method powerful กว่า popen3 ทำงานต่างจาก system()

长这样:

require 'open3' Open3.popen3("ls") do |stdout, stderr, status, thread| puts stdout.read end 

คล้าย open/read file เคยเห็น code แบบนี้:

File.open("my/file/path", "r") do |f| puts f.read end 

Pipes

Open3 stdout/stderr เป็น pipes พฤติกรรม像 file buffers ต้อง close ตอนจบ ใช้ block syntax (มี non-block syntax แต่ต้อง manual close stdout, stderr)

read method รอจน pipes closed คืนค่า แต่ pipes รองรับ reading lines as they become available สมมติ shell command ใช้เวลาจักวิน พิมพ์ status message ไป stderr อยาก capture แสดง users

Capture stderr ทีละบรรทัด:

require 'open3' Open3.popen3("sleep 2; ls") do |stdout, stderr, status, thread| while line=stderr.gets do puts(line) end end 

Threads

เหลือ argument หนึ่งคือ thread

thread argument ให้ reference ruby thread ที่รอ command จบ Command ไม่ได้รันใน thread รันใน separate process thread แค่ watch process รอจบ

ได้ useful data จาก thread reference:

  • thread.pid - process id ของ shell command ต้องการเวลา OS-level operations against process นั้น

  • thread.status - exit status ของ process 1 หรือ 0 สำหรับ success/failure

Caveats

จาก Open3 docs:

You should be careful to avoid deadlocks. Since pipes are fixed length buffers,[::popen3](https://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen3)(“prog”) {|i, o, e, t| o.read } deadlocks if the program generates too much output on stderr. You should read stdout and stderr simultaneously (using threads or IO.select). However, if you don't need stderr output, you can use [::popen2](https://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen2). If merged stdout and stderr output is not a problem, you can use [::popen2e](https://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-popen2e). If you really need stdout and stderr output as separate strings, you can consider [::capture3](https://ruby-doc.org/stdlib-2.1.0/libdoc/open3/rdoc/Open3.html#method-c-capture3).