tl;dr Muốn chạy shell command từ Ruby và capture stdout, stderr, return status, hãy dùng Open3.capture3. Muốn xử lý stdout/stderr streaming, dùng Open3.popen3.
Nhiều lựa chọn "kém"
Có đúng 492 cách chạy shell command từ Ruby, mỗi cái hoạt động hơi khác. Chắc bạn đã dùng một trong các cách dưới. Cách tôi hay dùng: back-ticks (``).
exec("echo 'hello world'") # thoát Ruby, rồi chạy command
system('echo', 'hello world') # trả về status code
sh('echo', 'hello world') # trả về status code
`echo "hello world"` # trả về stdout
%x[echo 'hello world'] # trả về stdout
Nhưng các cách này hạn chế. Cần capture không chỉ stdout mà cả stderr? Hết cách. Muốn xử lý stdout streaming, không đợi command xong? Hết cách.
Có lựa chọn khác. Cho chạy command async, cho stdout, stderr, exit code, PID. Xem ngay!
Open3
Module `open3` kỳ lạ là một phần của thư viện chuẩn Ruby. Nó làm gì?
Open3 cấp quyền truy cập stdout, stderr, exit code và thread chờ child process khi chạy chương trình khác. Bạn chỉ định các attribute, redirection, current directory, v.v., giống như `Process.spawn`. (Nguồn: Open3 Docs)
Chưa dùng? Chưa nghe? Tôi đoán vì nó không "friendly". Tên nghe như C hơn Ruby. Docs khá "neck-beard". Nhưng thử một lần, bạn thấy nó không đáng sợ.
capture3
Nếu có cách dễ capture stdout, stderr VÀ status code? Có. Nếu không đọc hết bài, chỉ nhớ dùng method `capture3` xong.
Ví dụ: muốn list file thư mục hiện tại. Chạy lệnh `ls`.
Dùng back-tick:
puts(`ls`)
Với `capture3`:
require 'open3'
stdout, stderr, status = Open3.capture3("ls")
Chạy command, cho stdout/stderr dạng string. No muss no fuss.
Security (Bảo mật)
Thường không muốn user chạy command tùy ý trên web server. Đó là lý do code như `identify #{ params[:filename] }` cực kỳ tồi.
Open3 tránh vấn đề này bằng cách tách command khỏi data. Hoạt động như method `system`.
Open3.capture3("identify", params[:filename], other_unsafe_params)
popen3
Bên dưới, `capture3` dùng method mạnh hơn `popen3`. Method này hoạt động khác `system()` quen thuộc.
Trông như sau:
require 'open3'
Open3.popen3("ls") do |stdout, stderr, status, thread|
puts stdout.read
end
Hơi giống mở và đọc file. Chắc bạn đã thấy code kiểu:
File.open("my/file/path", "r") do |f|
puts f.read
end
Pipes (Ống dẫn)
Với Open3, stdout/stderr là pipes, hành xử như file buffer. Và như file, cần close khi xong. Đó là lý do block syntax. (Có non-block syntax, nhưng phải tự gọi close trên stdout, stderr.)
Method `read` chờ pipes close mới trả giá trị. Nhưng pipes cũng hỗ trợ đọc line khi available. Giả sử shell command chạy vài giây, in status message ra stderr. Muốn capture hiển thị cho user.
Cách capture stderr line by line:
require 'open3'
Open3.popen3("sleep 2; ls") do |stdout, stderr, status, thread|
while line = stderr.gets
puts line
end
end
Threads
Chưa nói về argument `thread`.
Argument `thread` cho reference đến ruby thread chờ command xong. Command không chạy trong thread. Nó chạy trong process riêng biệt. Thread chỉ watch process và chờ xong.
Tuy nhiên từ thread reference lấy được data hữu ích:
- thread.pid — PID của shell command. Cần nếu muốn operation OS-level thêm trên process đó.
- thread.status — exit status của process. 1 hoặc 0 cho success/failure.
Caveats (Lưu ý)
Từ Open3 docs:
Cẩn tránh deadlock. Vì pipes fixed-length buffers,::popen3("prog") {|i, o, e, t| o.read } deadlock nếu chương trình sinh quá nhiều output trên stderr. Nên đọc stdout và stderr đồng thời (dùng threads hoặc IO.select). Tuy nhiên, nếu không cần stderr output, dùng::popen2. Nếu merge stdout/stderr không vấn đề, dùng::popen2e. Nếu thực sự cần stdout/stderr tách biệt, cân nhắc::capture3.