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

เชี่ยวชาญ Command-Line Applications ใน Ruby: คู่มือปฏิบัติจริง

เขียน one-off utility scripts สนุก แต่บางครั้งต้องเขียน command-line application จริงๆ ที่รับ arguments และทำงานร่วมกับ unix conventions สำหรับ input, output, error reporting ฯลฯ

โชคดี Ruby ให้ building blocks ทั้งหมดทำ command-line apps ได้ค่อนข้างง่าย บทความนี้ขอไป beyond typical "how to do X with gem Y" approach แทนจะ broad overview ของทุกชิ้นส่วนที่รวมกันเป็น first-rate command-line app

Input: Environment Variables

Environment variables ใช้สำหรับ short configuration values ที่อยากคงไว้นาน API keys ตัวอย่างดี

ตั้งค่า Environment Variables

Users ตั้ง environment variables ผ่าน shell เช่น bash ตั้งสำหรับ session เฉพาะ:

$ AWS_ACCESS_KEY_ID=FOO

ตั้งสำหรับ current session + new bash sessions:

$ export AWS_ACCESS_KEY_ID=FOO

หรือตั้งสำหรับ single program:

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

อ่าน Environment Variables

ไม่ว่า user ตั้งยังไง อ่านได้ผ่าน Ruby ENV hash

key_id = ENV["AWS_ACCESS_KEY_ID"]

Environment vars ทำงานยังไงเบื้องหลัง?

ทุก process มี table of environment variables เมื่อ spawn child processes ได้ copy ของ table พร้อม changes ที่ parent ต้องการ

Clear as mud?

อธิบายอ่านง่ายกว่า: บล็อกอื่นของผม The Rubyist's Guide to Environment Variables

Command-line arguments

Command-line arguments คืออะไรก็ตามหลัง program name ตอนรันใน terminal:

$ echo foo bar baz

ตัวอย่าง "foo", "bar", "baz" เป็น command-line arguments ทั้งหมด

ตัวอย่าง realistic:

$ honeybadger deploy -v --environment staging

แต่นี่ก็แค่ text หากอยากให้ --environment staging หมายถึง อะไร ต้อง figure out เอง

ส่ง Command-line arguments

เห็นส่ง arguments แบบนี้:

$ echo foo bar baz

แต่ Ruby interpreted language อาจเจอแบบนี้:

$ ruby myprog.rb foo

OS มองว่า run program "ruby" ส่งสอง arguments

โชคดี Ruby พอจะรู้ว่า argument "foo" meant สำหรับโปรแกรมคุณ ไม่ใช่ Ruby เอง โปรแกรมคุณเห็นหนึ่ง argument "foo"

อ่าน Command-line arguments

Command-line arguments เก็บใน array ARGV global variable เข้าถึงได้จากที่ไหนในโปรแกรมก็ได้

ตัวอย่าง print ทุกอย่างใน ARGV ลอง copy paste ลง terminal เล่นดู

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

อย่าสับสน ruby -e ใช้เพราะแสดงโปรแกรมและผลรันในบรรทัดเดียว

Parse Command-line arguments

Command-line arguments แค่ text อยากให้ text หมายถึง อะไร ต้อง parse เอง โชคดีมี libraries ดีหลายตัวช่วย parsing

ตลอดปี syntax มาตรฐานสำหรับ command-line arguments พัฒนาขึ้น คล้ายนี้:

$ program -a --option foo

Style นี้ให้ boolean flags อย่าง -h แสดง help และระบุ options พร้อม values

Introducing OptionParser

OptionParser class ใน Ruby standard library ให้ easy way parse options match style ข้างบน

ทำ simple app พูด hello ให้ระบุ name ผ่าน command-line argument

require 'optparse'
options = {}
OptionParser.new do |parser|
  # เมื่อเจอ -n หรือ --name พร้อม argument เก็บ argument
  parser.on("-n", "--name NAME", "The name of the person to greet.") do |v|
    options[:name] = v
  end
end.parse!

# ตอนนี้ใช้ options hash อย่างไรก็ได้
puts "Hello #{ options[:name] }" if options[:name]

รันก็ works:

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

เพิ่ม "help" screen

เพิ่ม help feature ง่ายเท่าเดิม ให้ text และเพิ่ม command -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!

ขอ 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

Command-line arguments ทั้งหมดเป็น strings แต่บ้างอยากให้ user ให้ number หรือ date การแปลงมือ tedious OptionParser ทำให้

ตัวอย่างเพิ่ม "count" option ให้ user ระบุว่าพูด hello กี่ครั้ง บอก OptionParser cast เป็น Integer มันทำให้

OptionParser.new do |parser|
  ...
  # Note the `Integer` arg. บอก parser cast value เป็น int
  # อาจ用 `Float`, `Date`, หรือ 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

รันได้ greeted หลายครั้ง:

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

Naming conventions

หนึ่งใน hardest aspects of programming คือตั้งชื่อ Command-line arguments ไม่ข้อยกเว้น ช่วยคุณ ผมรวบรวมตาราง common arguments และ meanings:

Flag Common Meanings
-aAll, Append
-dDebug mode, หรือ specify directory
-eExecute something, หรือ edit it
-fSpecify a file, หรือ force an operation.
-hHelp
-mSpecify a message
-oSpecify an output file or device
-qQuiet mode
-vVerbose mode. Print the current version
-ySay "yes" to any prompts

Alternatives to OptionParser

OptionParser ดีแต่มี limitations ความชัดเจน: ไม่รองรับ command-based syntaxes ที่ popular ล่าสุด:

$ myprog command subcommand -V

โชคดีมี ton option parsing libraries จะหาได้ที่ชอบ นี่สามตัวน่าสนใจ:

  • GLI - "Git-like Interface" command-line parser ทำ apps ที่เหมือน git single executable หลาย commands ได้ง่าย
  • CRI - Easy-to-use library สร้าง command-line tools รองรับ nested commands
  • Methadone - มี option parsing DSL ของตัว แต่ไปไกลกว่านั้น Setup directory structure, test suite, logging เหมือน Rails สำหรับ CLI

Input ข้อมูลเยอะด้วย STDIN

Command-line arguments ไม่เหมาะสำหรับ input ข้อมูลเยอะ ใช้ IO stream STDIN หนึ่งใน convenient IO streams มาก

Programs ส่วนใหญ่ได้ STDIN จาก OS โดยอัตโนมัติ เหมือน read-only file OS ใช้ส่ง data ให้ app ใช้รับ keyboard input จาก user แต่สำคัญกว่า: ใช้รับ data จาก programs อื่นผ่าน pipe

ใช้ STDIN เหมือน read-only file ตัวอย่าง pipe text เข้า STDIN ของโปรแกรม ใช้ read fetch

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

ใน Ruby ใช้ Enumerable features บน IO objects STDIN ไม่ข้อยกเว้น ทำ tricks ได้:

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

Output ผลลัพธ์ไป STDOUT

STDOUT write-only IO stream OS assign ให้ program

เขียน STDOUT ส่ง text ไป terminal user redirect output ไป file หรือ pipe ไป program อื่น

ใช้ STDOUT เหมือน write-only file:

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

และ puts และ print ทั้งคู่ output ไป STDOUT

ส่งข้อมูลสถานะไป STDERR

STDERR write-only IO stream อีกตัว ไม่ใช่สำหรับ general output แต่สำหรับ status messages — ไม่ให้รบกวน real output

STDERR ปกติแสดงใน terminal user แม้ redirect STDOUT ไป file หรือ program อื่น

ตัวอย่าง curl fetch webpage output content ไป STDOUT แสดง progress info ไป 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

เขียน STDERR จากโปรแกรมเองง่าย ใช้เหมือน IO object:

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

ตกแต่งให้สวย (Hitting it with the pretty stick)

ตัวอย่างที่เห็นไม่อาจได้ design awards แต่ไม่มีเหตุทำให้ command-line apps น่าเกลียด หรือ non-interactive

ถ้าตัดสินใจ soup up app มี gems เยี่ยมมากช่วย heavy lifting นี่สามตัวที่ชอบ:

  • highline - Library ยอดเยี่ยมลดงาน gathering, validating, typecasting user input
  • command_line_reporter - ทำ ASCII progress reports ง่าย
  • paint - ใส่ ANSI color codes ให้ข้อความน่าเบื่อสีสันได้ง่าย

ดู exhaustive list มากกว่า

Exit Status

ถ้าโปรแกรม exit with error ควรบอก OS ผ่าน exit code ทำให้ bash code แบบนี้ได้:

$ prog1 && prog2

ไม่คุ้น bash && operator มันแค่ "run prog2 only if prog1 exited successfully"

Ruby exit "success" code by default และ "failure" code on exception ส่วนอื่นขึ้นกับคุณ implement โชคดีง่าย:

# Nonzero argument to `exit` counts as failure
exit(1)

รายละเอียดเพิ่ม: บล็อกอื่น How to exit a Ruby program

ตั้งชื่อ Process Name

ถ้า command-line program รันนานพอ ควรให้คนรู้ว่ามันอะไรตอน list system processes ปกติ Ruby program process name ประกอบจากทุกอย่างที่พิมพ์รัน ถ้า ruby myapp -v นั่นคือ process name

ถ้า program มี arguments เยอะ อ่านยาก อาจตั้ง friendly process name แบบนี้:

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

ถ้ารู้สึก fancy ใช้ process title ให้ user info ว่าทำอะไรอยู่ตอนนั้น

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

รายละเอียดเพิ่ม: โพสต์ How to change the process name of your Ruby script as shown by top and ps