Nếu bạn dùng Linux hoặc Mac, mỗi khi mở terminal, bạn đang dùng một ứng dụng shell.
Shell là giao diện giúp bạn thực thi lệnh trong hệ thống.
Shell lưu trữ biến môi trường và có tính năng hữu ích như lịch sử lệnh và tự động hoàn thành.
Nếu bạn là người thích học cách mọi thứ hoạt động "bên dưới capo", bài viết này sẽ hoàn hảo cho bạn!
Shell hoạt động như thế nào?
Để xây dựng ứng dụng shell của riêng mình, hãy suy nghĩ về shell thực sự là gì:
Đầu tiên, có một prompt (nhắc), thường có thông tin thêm như user hiện tại và thư mục hiện tại, sau đó bạn gõ lệnh và khi nhấn enter, kết quả hiển thị trên màn hình.
Đúng, nghe khá cơ bản, nhưng có nhắc bạn nhớ gì không?
Nếu bạn nghĩ đến pry, bạn đúng!
Một shell về cơ bản là một REPL (Read-Eval-Print-Loop) cho hệ điều hành.
Biết điều đó, chúng ta có thể viết phiên bản đầu tiên của shell:
prompt = "> " print prompt while (input = gets.chomp) break if input == "exit" system(input) print prompt end
Điều này cho chúng ta một shell tối thiểu, nhưng chức năng. Chúng ta có thể cải thiện bằng thư viện mà nhiều ứng dụng REPL khác dùng.
Thư viện đó gọi là Readline.
Sử dụng thư viện Readline
Readline là một phần của Ruby Standard Library, nên không có gì để cài đặt, bạn chỉ cần require nó.
Một trong những lợi thế của Readline là nó có thể tự động giữ lịch sử lệnh cho chúng ta.
Nó cũng có thể lo việc in prompt và nhiều thứ khác.
Đây là phiên bản v2 của shell, lần này dùng Readline:
require 'readline'
while input = Readline.readline("> ", true)
break if input == "exit"
system(input)
end
Tuyệt vời, chúng ta loại bỏ hai puts cho prompt và giờ có quyền truy cập vào khả năng mạnh mẽ từ Readline. Ví dụ, chúng ta có thể dùng phím tắt để xóa từ (CTRL + W) hoặc tìm kiếm lịch sử (CTRL + R)!
Hãy thêm lệnh mới để in toàn bộ lịch sử:
require 'readline'
while input = Readline.readline("> ", true)
break if input == "exit"
puts Readline::HISTORY.to_a if input == "hist"
# Loại bỏ dòng trống khỏi lịch sử
Readline::HISTORY.pop if input == ""
system(input)
end
Thú vị: Nếu bạn thử mã này trong pry, bạn sẽ nhận được lịch sử lệnh của pry! Lý do là pry cũng dùng
Readline, vàReadline::HISTORYlà shared state.
Giờ bạn có thể gõ hist để lấy lịch sử lệnh 🙂
Thêm Auto-completion (Tự động hoàn thành)
Nhờ tính năng tự động hoàn thành của shell yêu thích, bạn sẽ tiết kiệm rất nhiều gõ phím. Readline làm cho việc tích hợp tính năng này vào shell rất dễ dàng.
Hãy bắt đầu bằng tự động hoàn thành lệnh từ lịch sử.
Ví dụ:
comp = proc { |s| Readline::HISTORY.grep(/^#{Regexp.escape(s)}/) }
Readline.completion_append_character = " "
Readline.completion_proc = comp
## phần còn lại của mã ở đây ##
Với mã này, bạn nên có thể tự động hoàn thành lệnh đã gõ trước bằng phím <tab>. Giờ hãy tiến xa hơn và thêm tự động hoàn thành thư mục.
Ví dụ:
comp = proc do |s|
directory_list = Dir.glob("#{s}*")
if directory_list.size > 0
directory_list
else
Readline::HISTORY.grep(/^#{Regexp.escape(s)}/)
end
end
Phương thức completion_proc trả về danh sách ứng viên có thể, trong trường hợp này chúng ta chỉ cần kiểm tra xem chuỗi gõ có phải một phần của tên thư mục bằng Dir.glob. Readline sẽ lo phần còn lại!
Triển khai phương thức System
Giờ bạn nên có một shell hoạt động, với lịch sử và tự động hoàn thành, không tệ cho 25 dòng code 🙂
Nhưng có điều tôi muốn đào sâu hơn, để bạn có insight về điều xảy ra bên hậu trường khi thực thi lệnh.
Điều này được thực hiện bởi phương thức system, trong C, phương thức này chỉ gửi lệnh đến /bin/sh, là ứng dụng shell. Hãy xem cách bạn có thể triển khai điều /bin/sh làm trong Ruby.
Lưu ý: Điều này chỉ hoạt động trên Linux / Mac 🙂
Phương thức system:
def system(command)
fork { exec(command) }
end
Điều xảy ra ở đây là fork tạo bản sao mới của tiến trình hiện tại, sau đó tiến trình này bị thay thế bởi lệnh chúng ta muốn chạy qua phương thức exec. Đây là pattern rất phổ biến trong lập trình Linux.
Nếu bạn không fork, tiến trình hiện tại bị thay thế, nghĩa là khi lệnh bạn chạy (ls, cd hoặc bất cứ gì) xong, chương trình Ruby của bạn sẽ kết thúc cùng nó.
Bạn có thể thấy điều đó xảy ra ở đây:
def system(command)
exec(command)
end
system('ls')
# Mã này sẽ không bao giờ chạy!
puts "after system"
Kết luận
Trong bài viết này, bạn đã học shell là giao diện kiểu REPL (nghĩ irb / pry) để tương tác với hệ thống. Bạn cũng học cách xây dựng shell riêng bằng thư viện mạnh Readline, cung cấp nhiều tính năng tích hợp như lịch sử và tự động hoàn thành (nhưng bạn phải định nghĩa cách nó hoạt động).
Và sau đó, bạn học về pattern fork + exec thường dùng trong dự án lập trình Linux.
Nếu bạn thích bài viết này, bạn có thể làm ơn chia sẻ với tất cả bạn Ruby? Nó sẽ giúp blog phát triển và nhiều người hơn sẽ học được 🙂