I tend to talk about how simple command line applications are. Usually, I say something like, "they read from standard in and write to standard out and standard error, that's it." Then, I draw a picture like:
But then I started thinking that's not actually the end of the story. Command line applications read from standard in and write to standard error and standard out, and...
- read configuration files (from any number of places like
/etc
,~
, or.
) - read environment variables
- read command line switches
- read command line flags
- read command line arguments
- set the exit status
A command line application ends up looking more like:
Which is objectively more complex than simply stdin, stdout, and stderr.
I use command line applications all day. Over time I've developed an opinion and intuition about them:
- standard in - usually data, not arguments (unless I use
xargs
) - standard out - the product, preferably columnated data
- standard error - for logging cruft or error messages if something goes wrong
- the exit status - sacred, either 0 for success or 1 for failure (or something else for some other type of failure)
- arguments - important but read somehow differently than standard in
- flags - like arguments but less important
- switches - like flags but are either on or off
- environment variables - sort of like arguments but there's some kind of philosophical difference between them
- configuration files - kind of like environment variables in the sense that you can control switches or flags, but you can keep them in version control and generally use them to prevent having to use frustratingly combinations of switches and flags, or show everyone how cool your dotfiles are
These guidelines tend to work with various command line tools I use. However, when it comes to making them for other people to use, I'd like to have a reference.
For example, the following is a command line application written with Ruby:
#!/usr/bin/env ruby
# somecli
require 'optparse'
require 'yaml'
options = {}
etc_config = File.join('etc', 'somecli')
if File.exist? etc_config
options.merge! YAML.load_file(etc_config)
end
home_config = File.join(ENV['HOME'], '.somecli')
if File.exist? home_config
options.merge! YAML.load_file(home_config)
end
current_working_directory_config = '.somecli'
if File.exist? current_working_directory_config
options.merge! YAML.load_file(current_working_directory_config)
end
OptionParser.new do |opts|
opts.on("-s", "--[no-]switch") do |s|
options[:switch] = s
end
opts.on("-a", "--[no-]another-switch") do |as|
options[:'another-switch'] = as
end
opts.on("-y", "--[no-]yet-another-switch") do |yas|
options[:'yet-another-switch'] = yas
end
opts.on("-y", "--[no-]even-yet-another-switch") do |eyas|
options[:'even-yet-another-switch'] = eyas
end
opts.on("--flag FLAG") do |f|
options[:flag] = f
end
end.parse!
puts "ARGV=#{ARGV.inspect}"
puts "options=#{options.inspect}"
puts "ENV['cats']=#{ENV['cats'].inspect}"
unless STDIN.tty?
puts "STDIN.read=#{STDIN.read.inspect}"
end
$stderr.puts "stderr: hello world!"
$stdout.puts "stdout: hello world!"
exit 0
When run it looks like
echo -n foo bar baz | ./somecli -s -f flap jacks; echo $?
ARGV=["jacks"]
options={:"another-switch"=>true, :"even-yet-another-switch"=>true, :switch=>true, :flag=>"flap"}
ENV['cats']="flapjacks"
STDIN.read="foo bar baz"
stderr: hello world!
stdout: hello world!
0
I'd like to have a link in the README to a more official document that guided it's input and output conventions and design decisions.
I know POSIX exists for deciding which common utility programs are installed on a system, except I'm wondering 2 things when it comes to building command line applications:
- Are there any other common ways for getting input into a command line program that I missed?
- Does authoritative documentation exist for conventions around how to organize the various inputs and outputs?