Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Ứng Dụng Command-Line Trong Ruby: Hướng Dẫn Thực Tế

Mặc dù vui viết các utility scripts nhỏ one-off, đôi khi bạn cần viết một command-line application thật sự. Một cái nhận arguments và chơi tốt với unix conventions cho input, output, error reporting, v.v.

May mắn thay, Ruby cho bạn tất cả building blocks cần để làm command-line applications dễ dàng. Trong bài này tôi hy vọng đi xa hơn cách tiếp cận điển hình "how to do X with gem Y", và thay vào đó làm broad overview của tất cả các mảnh ghép đi cùng nhau để làm first-rate command-line app.

Input: Environment Variables

Environment variables thường dùng cho short configuration values bạn muốn stick around một lúc. API keys là ví dụ tốt.

Setting Environment Variables

Users thường set environment variables qua shell như bash. Chúng có thể set cho session cụ thể:


$ AWS_ACCESS_KEY_ID=FOO

Chúng có thể set cho session hiện tại, cộng với bất kỳ bash session mới:


$ export AWS_ACCESS_KEY_ID=FOO

Hoặc set cho single program:


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

Reading Environment Variables

Bất kể user set environment variable như thế nào, bạn có thể đọc nó qua ENV hash của Ruby.


key_id = ENV["AWS_ACCESS_KEY_ID"]

Environment vars hoạt động dưới mũi như thế nào?

Mọi process có table of environment variables associated với nó. Khi nó spawn child processes, chúng get copy của table đó cộng với bất kỳ changes parent muốn làm.

Clear as mud?

Cho giải thích dễ đọc hơn, xem bài blog khác của tôi: The Rubyist's Guide to Environment Variables.

Command-line arguments

Command-line arguments là bất cứ gì bạn đặt sau program name khi chạy program trong terminal:


$ echo foo bar baz

Trong ví dụ trên "foo", "bar" và "baz" đều là command-line arguments.

Ví dụ realistic hơn có thể trông như:


$ honeybadger deploy -v --environment staging

Nhưng ngay cả ở đây, command-line arguments chỉ là text. Nếu chúng ta muốn --environment staging có nghĩa gì, chúng ta phải tự figure out.

Gửi command-line arguments

Chúng ta đã thấy arguments gửi như:


$ echo foo bar baz

Nhưng vì Ruby là interpreted language, bạn có thể cũng gặp thứ như:


$ ruby myprog.rb foo

Về mặt OS, bạn đang chạy program "ruby" và pass nó hai arguments.

May mắn thay, Ruby đủ thông minh để biết argument "foo" intended cho program của bạn, không phải cho Ruby. Vì vậy program sẽ thấy một argument, "foo".

Đọc command-line arguments

Command-line arguments được lưu trong array gọi là ARGV. Nó là global variable, nên bạn có thể access từ bất cứ đâu trong program.

Trong ví dụ dưới, chúng ta in mọi thứ trong ARGV. Thoải mái copy paste vào terminal và chơi một chút.


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

Đừng bị ném bởi ruby -e business. Tôi chỉ dùng nó ở đây vì nó cho tôi show program và kết quả chạy trong một dòng.

Parse command-line arguments

Command-line arguments chỉ là text. Nếu bạn muốn text có nghĩa, bạn sẽ phải parse nó. May mắn thay, có vài thư viện tốt giúp parsing.

Quá nhiều năm, một syntax somewhat standard cho command-line arguments đã nảy mọc. Nó trông như:


$ program -a --option foo

Style này cho bạn boolean flags như -h để display help. Nó cũng cho bạn specify options với values.

Giới thiệu OptionParser

Class OptionParser là part của Ruby standard library. Nó cho bạn cách dễ dàng parse options match style trên.

Hãy làm ứng dụng đơn giản nói hello. Nó sẽ cho bạn specify name qua command-line argument.


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 chạy, nó just works:


$ ruby hello.rb --name Starr
Hello Starr
$ ruby hello.rb -n Starr
Hello Starr

Thêm màn hình "help"

Thêm help feature cũng dễ như vậy. Chỉ cần cung cấp text, và thêm command 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 ta có thể xin help:


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

Typecasting arguments

Tất cả command-line arguments là strings, nhưng đôi khi bạn muốn user cho bạn number hoặc date. Làm conversion thủ công có thể tedious, nên OptionParser làm nó cho bạn.

Trong ví dụ dưới, tôi thêm option "count" cho user specify bao nhiêu lần program nên nói hello. Tôi bảo OptionParser cast sang Integer, mà nó làm.


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 chạy program tôi được greet nhiều lần:


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

Naming conventions

Một trong những aspect khó nhất của programming có thể là figure out đặt tên gì. Command-line arguments không ngoại lệ. Để giúp bạn, tôi compile bảng ngắn các arguments phổ biến và meanings:

Flag Common Meanings
-a All, Append
-d Debug mode, hoặc specify directory
-e Execute something, hoặc edit it
-f Specify a file, hoặc force an operation.
-h Help
-m Specify a message
-o Specify an output file or device
-q Quiet mode
-v Verbose mode. Print current version
-y Say "yes" to any prompts

Alternatives to OptionParser

OptionParser nice, nhưng nó có limitations. Rõ ràng nhất, nó không support command-based syntaxes phổ biến gần đây:


$ myprog command subcommand -V

May mắn thay, có ton option parsing libraries. Tôi chắc bạn sẽ tìm được cái bạn thích. Đây là ba trông thú vị:

  • GLI - "Git-like Interface" command-line parser cho phép dễ dàng làm applications mà, như git, là single executable với multiple commands.
  • CRI - Easy-to-use library cho building command-line tools với support nested commands.
  • Methadone - Methadone cung cấp own option parsing DSL, nhưng đi way beyond that. Nó sẽ setup directory structure, test suite và logging. Nó như Rails cho CLI của bạn.

Input larger amounts of data với STDIN

Command-line arguments không suitable cho input larger amounts of data. Cho đó, bạn muốn dùng IO stream. Và STDIN là một trong những IO streams convenient nhất.

Hầu hết programs tự động được assigned STDIN bởi OS. Nó giống read-only file OS dùng để gửi app data. Nó có thể dùng để get keyboard input từ user. Nhưng quan trọng hơn, nó có thể dùng để receive data từ programs khác qua pipe.

Bạn dùng STDIN giống như dùng read-only file. Ở đây, tôi pipe text vào STDIN của program. Sau đó dùng method read để fetch nó.


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

Trong Ruby, bạn có thể dùng Enumerable features trên IO objects. Và STDIN không ngoại lệ. Nghĩa là bạn có thể làm tất cả các tricks:


# Get first 20 lines
STDIN.first(20)
# Convert to integers và reject odds
STDIN.map(&:to_i).reject(&:odd)
...etc

Output results to STDOUT

STDOUT là write-only IO stream OS assign cho program.

Bằng cách viết vào STDOUT, bạn có thể gửi text đến terminal của user. User có thể redirect output đến file, hoặc pipe vào program khác.

Bạn sẽ dùng STDOUT giống như dùng bất kỳ write-only file nào:


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

Và tất nhiên, puts và print đều output ra STDOUT.

Gửi thông tin trạng thái đến STDERR

STDERR là write-only IO stream khác. Nó không phải cho general output. Nó specifically cho status messages — để chúng không cản trở real output.

STDERR thường sẽ hiển thị trong terminal của user, ngay cả khi họ redirect STDOUT đến file hoặc program khác.

Trong ví dụ dưới chúng ta dùng curl fetch webpage. Nó output content của webpage ra STDOUT, và hiển thị progress information ra 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ết vào STDERR từ program của bạn dễ dàng. Chỉ treat nó như IO object nó là:


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

Đấm với cây đẹp

Các ví dụ chúng ta thấy cho đến nay không win design awards. Nhưng không lý do command-line apps nên xấu, hoặc non-interactive.

Nếu bạn quyết định soup up app, có ton gems tuyệt vời làm heavy lifting. Đây là ba tôi thích:

  • highline - Highline là thư viện tuyệt vời lấy đi nhiều work khỏi gathering, validating, và typecasting user input.
  • command_line_reporter - Làm dễ dàng generate ASCII progress reports.
  • paint - Cho phép dễ dàng thêm ANSI color codes để colorize boring old text.

Xem list exhaustive hơn

Exit Status

Nếu program thoát với error, bạn nên báo cho OS bằng exit code. Đó là thứ làm bash code như sau work:


$ prog1 && prog2

Nếu bạn không quen với bash's && operator, nó đơn giản nghĩa là "run prog2 chỉ nếu prog1 exited successfully."

Ruby exit với "success" code mặc định, và "failure" code on exception. Mọi thứ khác là up to you. May mắn thay, nó dễ dàng:


# Bất kỳ nonzero argument nào cho `exit` counts as failure
exit(1)

Cho thêm info, xem bài khác của tôi How to exit a Ruby program.

Setting the process name

Nếu command-line program sẽ chạy một lúc, quan trọng là người ta biết nó là gì khi list system processes. Bình thường, Ruby programs process name bao gồm mọi thứ bạn type để chạy program. Vì vậy nếu bạn type ruby myapp -v, đó là tên process.

Nếu program có nhiều arguments, điều này có thể unreadable. Vì vậy bạn có thể muốn set friendly process name hơn. Bạn làm như sau:


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

Nếu bạn fancy, bạn có thể dùng process title để cho user info về process đang làm gì tại moment nào.


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

Cho thêm info, xem bài: How to change the process name of your Ruby script as shown by top and ps