เขียน 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 |
|---|---|
-a | All, Append |
-d | Debug mode, หรือ specify directory |
-e | Execute something, หรือ edit it |
-f | Specify a file, หรือ force an operation. |
-h | Help |
-m | Specify a message |
-o | Specify an output file or device |
-q | Quiet mode |
-v | Verbose mode. Print the current version |
-y | Say "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