Computer >> Máy Tính >  >> Lập trình >> Ruby

Viết ứng dụng dòng lệnh trong Ruby

Mặc dù rất thú vị khi viết một đoạn mã tiện ích nhỏ, nhưng đôi khi bạn cần phải viết một ứng dụng dòng lệnh thực sự trung thực với Chúa. Một trong đó nhận các đối số và chơi độc đáo với các quy ước unix cho đầu vào, đầu ra, báo cáo lỗi, v.v.

May mắn thay, Ruby cung cấp cho bạn tất cả các khối xây dựng bạn cần để ứng dụng dòng lệnh khá dễ dàng. Trong bài đăng này, tôi hy vọng sẽ vượt ra ngoài cách tiếp cận "cách thực hiện X với viên ngọc Y" điển hình và thay vào đó làm một cái nhìn tổng quan về tất cả các phần kết hợp với nhau để tạo ra một ứng dụng dòng lệnh hạng nhất.

Đầu vào:Biến môi trường

Các biến môi trường thường được sử dụng cho các giá trị cấu hình ngắn mà bạn muốn duy trì một thời gian. Khóa API là một ví dụ điển hình.

Cài đặt biến môi trường

Người dùng thường thiết lập các biến môi trường thông qua một trình bao như bash. Họ có thể đặt chúng cho một phiên cụ thể:

$ AWS_ACCESS_KEY_ID=FOO

Họ có thể đặt chúng cho phiên hiện tại, cộng với bất kỳ phiên bash mới nào:

$ export AWS_ACCESS_KEY_ID=FOO

Hoặc họ có thể đặt chúng cho một chương trình duy nhất:

$ env AWS_ACCESS_KEY_ID=FOO aws s3 mb s3://mybucket

Đọc các biến môi trường

Bất kể người dùng đặt biến môi trường như thế nào, bạn có thể đọc nó qua ENV của Ruby băm.

key_id = ENV["AWS_ACCESS_KEY_ID"]

Môi trường hoạt động ẩn như thế nào?

Mọi quy trình đều có một bảng các biến môi trường được liên kết với nó. Khi nó sinh ra các quy trình con, chúng sẽ nhận được một bản sao của bảng đó cùng với bất kỳ thay đổi nào mà cha mẹ muốn thực hiện.

Trong như bùn?

Để có lời giải thích dễ đọc hơn, hãy xem bài đăng trên blog khác của tôi:Hướng dẫn về các biến môi trường của Rubyist.

Đối số dòng lệnh

Đối số dòng lệnh là bất kỳ thứ gì bạn đặt sau tên chương trình khi chạy chương trình trong terminal:

$ echo foo bar baz

Trong ví dụ trên "foo", "bar" và "baz" đều là đối số dòng lệnh.

Một ví dụ thực tế hơn có thể trông như thế này:

$ honeybadger deploy -v --environment staging

Nhưng ngay cả ở đây, các đối số dòng lệnh cũng chỉ là văn bản. Nếu chúng ta muốn --environment staging đến nghĩa là bất cứ điều gì, chúng ta phải tự tìm ra điều đó.

Gửi đối số dòng lệnh

Chúng tôi đã thấy các đối số được gửi như thế này:

$ echo foo bar baz

Nhưng vì Ruby là một ngôn ngữ thông dịch, bạn cũng có thể gặp những thứ như sau:

$ ruby myprog.rb foo

Về hệ điều hành, bạn đang chạy một chương trình có tên "ruby" và chuyển cho nó hai đối số.

May mắn thay, Ruby đủ thông minh để biết rằng đối số "foo" là dành cho chương trình của bạn, không phải cho chính Ruby. Vì vậy, chương trình của bạn sẽ thấy một đối số, "foo" .

Đọc đối số dòng lệnh

Các đối số dòng lệnh được lưu trữ trong một mảng có tên là ARGV . Đó là một biến toàn cục, vì vậy bạn có thể truy cập nó từ bất kỳ đâu trong chương trình của mình.

Trong ví dụ dưới đây, chúng tôi đang in mọi thứ trong ARGV . Vui lòng sao chép và dán nó vào thiết bị đầu cuối của bạn và chơi với nó một chút.

$ ruby -e "puts ARGV.inspect" foo bar baz
["foo", "bar", "baz"]

Xin đừng để bị ném ra bởi ruby -e việc kinh doanh. Tôi chỉ sử dụng nó ở đây vì nó cho phép tôi hiển thị chương trình và kết quả của việc chạy nó trong một dòng duy nhất.

Phân tích cú pháp các đối số dòng lệnh

Đối số dòng lệnh chỉ là văn bản. Nếu bạn muốn văn bản thành có nghĩa là bất cứ thứ gì, bạn sẽ phải phân tích cú pháp nó. May mắn thay, có một số thư viện tốt có thể giúp bạn phân tích cú pháp.

Trong những năm qua, một cú pháp hơi tiêu chuẩn cho các đối số dòng lệnh đã xuất hiện. Nó trông giống như sau:

$ program -a --option foo

Kiểu này cho phép bạn có các cờ boolean như -h để hiển thị trợ giúp. Nó cũng cho phép bạn chỉ định các tùy chọn với các giá trị.

Giới thiệu OptionParser

OptionParser lớp là một phần của thư viện chuẩn của Ruby. Nó cung cấp cho bạn một cách dễ dàng để phân tích cú pháp các tùy chọn phù hợp với kiểu ở trên.

Hãy tạo một ứng dụng đơn giản nói xin chào. Nó sẽ cho phép bạn chỉ định tên thông qua đối số dòng lệnh.

require 'optparse'

# This will hold the options we parse
options = {}

OptionParser.new do |parser|

  # Whenever we see -n or --name, with an
  # argument, save the argument.
  parser.on("-n", "--name NAME", "The name of the person to greet.") do |v|
    options[:name] = v
  end
end.parse!

# Now we can use the options hash however we like.
puts "Hello #{ options[:name] }"  if options[:name]

Sau đó, khi tôi chạy nó, nó chỉ hoạt động:

$ ruby hello.rb --name Starr
Hello Starr

$ ruby hello.rb -n Starr
Hello Starr

Thêm màn hình "trợ giúp"

Thêm một tính năng trợ giúp cũng dễ dàng như vậy. Tất cả những gì chúng ta phải làm là cung cấp một số văn bản và thêm lệnh cho -h :

OptionParser.new do |parser|
  parser.banner = "Usage: hello.rb [options]"

  parser.on("-h", "--help", "Show this help message") do ||
    puts parser
  end

  ...
end.parse!

Bây giờ chúng tôi có thể yêu cầu trợ giúp:

$ ruby hello.rb -h
Usage: hello.rb [options]
    -h, --help                       Show this help message
    -n, --name NAME                  The name of the person to greet.

Đánh máy đối số

Tất cả các đối số dòng lệnh đều là chuỗi, nhưng đôi khi bạn muốn người dùng cung cấp cho mình một số hoặc ngày tháng. Thực hiện chuyển đổi bằng tay có thể tẻ nhạt, vì vậy OptionParser làm điều đó cho bạn.

Trong ví dụ dưới đây, tôi sẽ thêm một tùy chọn "đếm" cho phép người dùng chỉ định số lần chương trình sẽ nói lời chào. Tôi đang nói với OptionParser để truyền tới một Integer , mà nó thực hiện.

OptionParser.new do |parser|

  ...

  # Note the `Integer` arg. That tells the parser to cast the value to an int.
  # I could have used `Float`, `Date`, or a number of other types.
  parser.on("-c", "--count COUNT", Integer, "Repeat the message COUNT times") do |v|
    options[:count] = v
  end

end.parse!

if options[:name]
  options.fetch(:count, 1).times do
    puts "Hello #{ options[:name] }"
  end
end

Bây giờ, khi tôi chạy chương trình, tôi được chào nhiều lần:

$ ruby hello.rb -n Starr -c 5
Hello Starr
Hello Starr
Hello Starr
Hello Starr
Hello Starr

Quy ước đặt tên

Một trong những khía cạnh khó nhất của lập trình có thể là tìm ra cái gì để đặt tên cho mọi thứ. Đối số dòng lệnh cũng không ngoại lệ. Để giúp bạn, tôi đã biên soạn một bảng ngắn các lập luận phổ biến và ý nghĩa của chúng:

Flag Ý nghĩa chung
-a Tất cả, Nối
-d Chế độ gỡ lỗi hoặc chỉ định thư mục
-e Thực thi một cái gì đó hoặc chỉnh sửa nó
-f Chỉ định một tệp hoặc buộc thực hiện một thao tác.
-h Trợ giúp
-m Chỉ định một thông báo
-o Chỉ định tệp đầu ra hoặc thiết bị
-q Chế độ yên lặng
-v Chế độ chi tiết. In phiên bản hiện tại
-y Nói "có" với bất kỳ lời nhắc nào

Lựa chọn thay thế cho OptionParser

OptionParser là tốt, nhưng nó có những hạn chế của nó. Rõ ràng nhất, nó không hỗ trợ các cú pháp dựa trên lệnh đã trở nên phổ biến trong những năm gần đây:

$ myprog command subcommand -V

May mắn thay, có một tấn của các thư viện phân tích cú pháp tùy chọn ngoài đó. Tôi chắc chắn rằng bạn sẽ có thể tìm thấy một cái mà bạn thích. Dưới đây là ba cái nhìn khá thú vị:

  • GLI - Trình phân tích cú pháp dòng lệnh "Git-like Interface" cho phép bạn dễ dàng tạo các ứng dụng, như git, là một ứng dụng thực thi duy nhất với nhiều lệnh.
  • CRI - Một thư viện dễ sử dụng để xây dựng các công cụ dòng lệnh với sự hỗ trợ cho các lệnh lồng nhau.
  • Methadone - Methadone cung cấp tùy chọn phân tích cú pháp DSL của riêng mình, nhưng nó còn đi xa hơn thế. Nó sẽ thiết lập cấu trúc thư mục cho bạn, một bộ thử nghiệm và ghi nhật ký. Nó giống như Rails cho CLI của bạn.

Nhập lượng dữ liệu lớn hơn bằng STDIN

Đối số dòng lệnh không phù hợp để nhập lượng dữ liệu lớn hơn. Vì vậy, bạn sẽ muốn sử dụng luồng IO. Và STDIN là một trong những luồng IO thuận tiện nhất hiện có.

Hầu hết các chương trình sẽ tự động được hệ điều hành gán một STDIN. Nó giống như một tệp chỉ đọc mà Hệ điều hành sử dụng để gửi dữ liệu ứng dụng của bạn. Nó có thể được sử dụng để lấy đầu vào bàn phím từ người dùng. Nhưng quan trọng hơn, nó có thể được sử dụng để nhận dữ liệu từ các chương trình khác qua đường ống.

Bạn sử dụng STDIN giống như bạn sử dụng tệp chỉ đọc. Đây, tôi đang đưa một số văn bản vào STDIN của chương trình của tôi. Sau đó, tôi sử dụng read để tìm nạp nó.

$ echo "hello world" | ruby -e "puts STDIN.read.upcase"
HELLO WORLD

Trong Ruby, bạn có thể sử dụng Enumerable các tính năng trên các đối tượng IO. Và STDIN cũng không ngoại lệ. Điều đó có nghĩa là bạn có thể làm tất cả các loại thủ thuật:

# Get the first 20 lines
STDIN.first(20)

# Convert to integers and reject odds
STDIN.map(&:to_i).reject(&:odd)

...etc

Xuất kết quả thành STDOUT

STDOUT là luồng IO chỉ ghi mà hệ điều hành gán cho chương trình của bạn.

Bằng cách viết tới STDOUT, bạn có thể gửi văn bản tới thiết bị đầu cuối của người dùng. Người dùng có thể chuyển hướng đầu ra đến một tệp hoặc chuyển nó vào một chương trình khác.

Bạn sẽ sử dụng STDOUT giống như bạn sử dụng bất kỳ tệp chỉ ghi nào khác:

STDOUT.write("Hi!\n")
# hi

Và tất nhiên, putsprint cả hai đầu ra cho STDOUT.

Gửi thông tin trạng thái tới STDERR

STDERR là một luồng IO chỉ ghi khác. Nó không dành cho đầu ra chung. Nó dành riêng cho các thông báo trạng thái - vì vậy chúng không cản trở thông điệp thực đầu ra.

STDERR thường sẽ được hiển thị trong thiết bị đầu cuối của người dùng, ngay cả khi họ đang chuyển hướng STDOUT đến một tệp hoặc chương trình khác.

Trong ví dụ dưới đây, chúng tôi đang sử dụng curl để tìm nạp một trang web. Nó xuất nội dung của trang web thành STDOUT và hiển thị thông tin tiến trình cho STDERR:

$ curl "https://www.google.com/" > /tmp/g.html
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  151k    0  151k    0     0   277k      0 --:--:-- --:--:-- --:--:--  277k

Việc ghi vào STDERR từ các chương trình của riêng bạn thật dễ dàng. Chỉ cần đối xử với nó như đối tượng IO của nó:

STDERR.write("blah\n")
STDERR.puts("blah")

Đánh bằng chiếc gậy xinh xắn

Các ví dụ mà chúng tôi đã thấy cho đến nay sẽ không giành được bất kỳ giải thưởng thiết kế nào. Nhưng không có lý do gì mà các ứng dụng dòng lệnh phải xấu hoặc không tương tác.

Nếu bạn quyết định cải tiến ứng dụng của mình, có rất nhiều viên ngọc tuyệt vời để thực hiện một số công việc nặng nhọc. Đây là ba điều tôi thích:

  • highline - Highline là một thư viện tuyệt vời giúp bạn thực hiện rất nhiều công việc thu thập, xác thực và đánh máy thông tin đầu vào của người dùng.
  • command_line_reporter - Giúp dễ dàng tạo báo cáo tiến độ ASCII.
  • sơn - Cho phép bạn dễ dàng thêm mã màu ANSI để tô màu cho văn bản cũ nhàm chán của mình.

Xem danh sách đầy đủ hơn

Trạng thái thoát

Nếu chương trình của bạn thoát mà gặp lỗi, bạn nên thông báo cho HĐH bằng mã thoát. Đó là những gì làm cho mã bash như thế này có thể hoạt động:

$ prog1 && prog2

Nếu bạn không quen với && của bash , nó chỉ đơn giản có nghĩa là "chỉ chạy prog2 khi prog1 đã thoát thành công."

Ruby thoát với mã "thành công" theo mặc định và mã "thất bại" nếu ngoại lệ. Mọi thứ khác là tùy thuộc vào bạn để thực hiện. May mắn thay, nó dễ dàng:

# Any nonzero argument to `exit` counts as failure
exit(1)

Để biết thêm thông tin, hãy xem bài đăng khác của tôi Cách thoát khỏi chương trình Ruby.

Đặt tên quá trình

Nếu chương trình dòng lệnh của bạn sẽ chạy trong một thời gian ngắn, điều quan trọng là mọi người phải biết nó là gì khi họ liệt kê các quy trình hệ thống. Thông thường, tên tiến trình chương trình Ruby bao gồm mọi thứ mà bạn đã nhập để chạy chương trình. Vì vậy, nếu bạn đã nhập ruby myapp -v , đó là tên của quy trình.

Nếu chương trình của bạn có nhiều đối số, điều này có thể trở nên không đọc được. Vì vậy, bạn có thể muốn đặt một tên quy trình thân thiện hơn. Bạn có thể làm như vậy:

Process.setproctitle("My Awesome Command Line App")

Nếu bạn cảm thấy thích thú, bạn có thể sử dụng tiêu đề quy trình để cung cấp cho người dùng thông tin về những gì các quy trình đang thực hiện tại bất kỳ thời điểm nào.

Process.setproctitle("Mail Sender: initializing")
init(...)
Process.setproctitle("Mail Sender: connecting")
connect(...)
Process.setproctitle("Mail Sender: sending")
send(...)

Để biết thêm thông tin, hãy xem bài đăng của tôi:Cách thay đổi tên tiến trình của tập lệnh Ruby của bạn như được hiển thị bằng đầu và ps