Shellopts
ShellOpts is a simple Linux command line parsing libray that covers most modern use
cases incl. sub-commands. Options and commands are specified using a
getopt(1)-like string that is interpreted by the library to process the command
line
Usage
Program that accepts the options -a or --all, --count, --file, and -v or
--verbose. The usage definition expects --count to have an optional integer
argument, --file to have a mandatory argument, and allows -v and
--verbose to be repeated:
# Define options
USAGE = "a,all count=#? file= +v,verbose -- FILE..."
# Define default values
all = false
count = nil
file = nil
verbosity_level = 0
# Process command line and return remaining non-option arguments
args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
case opt
when '-a', '--all'; all = true
when '--count'; count = arg || 42
when '--file'; file = arg # never nil
when '-v, '--verbose'; verbosity_level += 1
else
fail "Internal Error: Unmatched option: '#{opt}'"
end
end
# Process remaining command line arguments
args.each { |arg| ... }
Note that the else clause catches legal but unhandled options; it is not an
user error. It typically happens because of a missing or misspelled option name
in the when clauses
If there is an error in the command line options, the program will exit with status 1 and print an error message and a short usage description on standard error
Processing
ShellOpts.process compiles a usage definition string into a grammar and use
that to parse the command line. If given a block, the block is called with a
name/value pair for each option or command and return a list of the remaining
non-option arguments
args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
case opt
when ...
end
end
This calls the block for each option in the same order as on the command line
and return the remaining non-option args. It also sets up the ShellOpts.error and
ShellOpts.fail methods. Please note that you need to call ShellOpts.reset
if you want to process another command line
If ShellOpts.process is called without a block it returns a
ShellOpts::ShellOpts object. It can be used to process more than one command
line at a time and to inspect the grammar and AST
shellopts = ShellOpts.process(USAGE, ARGV) # Returns a ShellOpts::ShellOpts object
shellopts.each { |opt, val| ... } # Access options
args = shellopts.args # Access remaining arguments
shellopts.error "Something went wrong" # Emit an error message and exit
Usage string
A usage string, typically named USAGE, is a list of option and command
definitions separated by whitespace. It can span multiple lines. A double
dash (--) marks the end of the definition, anything after that is not
interpreted but copied verbatim in error messages
The general syntax is
options { command options } [ "--" anything ]
Options
An option is defined by a list of comma-separated names optionally prefixed by a
+ and/or followed by a = and a set of flags. The syntax is
[ "+" ] name-list [ "=" [ "#" | "$" ] [ label ] [ "?" ] ]
Flags
There are the following flags:
| Flag | Effect |
|---|---|
| + | Repeated option (prefix) |
| = | Argument. Mandatory unless ? is also used |
| # | Integer argument |
| $ | Floating point argument |
| ? | Optional argument |
Repeated options
Options are unique by default and the user will get an error if an option is
used more than once. You can tell the parser to allow several instances of the
same option by prefixing the option names with a +. A typical use case is to
let the user repeat a 'verbose' option to increase verbosity: +v,verbose
allows -vvv or --verbose --verbose --verbose. ShellOpts::process yields
an entry for each usage so should handle repeated options like this
verbosity_level = 0
args = ShellOpts.process(USAGE, ARGV) do |opt, arg|
case opt
when '-v', '--verbose'; verbosity_level += 1
# other options
end
end
Option names
Option names are a comma-separated list of names. Names can consist of one or more ASCII letters (a-zA-Z), digits, underscores ('_') and dashes ('-'). A name can't start with a dash, though
Names that are one character long are considered 'short options' and are prefixed with a single dash on the command line (eg. '-a'). Names with two or more characters are 'long options' and are used with two dashes (eg. '--all'). Note that short and long names handles arguments differently
Examples:
a # -a
all # --all
a,all # -a or --all
r,R,recursive # -r, -R, or --recursive
Option argument
An option that takes an an argument is declared with a = after the name list.
By default the type of an option is a String but a integer argument can be
specified by the # flag and a float argument by the $ flag.
You can label a option value that will be used in help texts and error
messages. A usage string like file=FILE will be displayed as --file=FILE
and file=FILE? like --file[=FILE]. If no label is given, INT will be used
for integer arguments, FLOAT for floating point, and else ARG
Arguments are mandatory by default but can be made optional by suffixing a ?
Commands
Sub-commands (like git clone) are defined by a name (or a dot-separated list
of names) followed by an exclamation mark. All options following a command are
local to that command. It is not possible to 'reset' this behaviour so global
options should always come before the first command. Nested commands are
specified using a dot-separated "path" to the nested sub-command
Examples
g,global clone! t,template=
g,global clone! t,template= clone.list! v,verbose
The last example could be called like program -g clone list -v. You may split
the usage string to improve readability:
g,global
clone! t,template=
clone.list! v,verbose
Command processing
Commands are treated like options but with a value that is an array of options (and sub-commands) to the command:
USAGE = "a cmd! b c"
args = ShellOpts.process(USAGE, ARGV) { |opt,val|
case opt
when '-a'; # Handle -a
when 'cmd'
opt.each { |opt, val|
case opt
when '-b'; # Handle -b
when '-c'; # Handle -c
end
}
end
}
Parsing
Parsing of the command line follows the UNIX traditions for short and long
options. Short options are one letter long and prefixed by a -. Short options
can be grouped so that -abc is the same as -a -b -c. Long options are
prefixed with a -- and can't be grouped
Mandatory arguments to short options can be separated by a whitespace (-f
/path/to/file) but optional arguments needs to come immediately after the
option: -f/path/to/file. Long options also allow a space separator for
mandatory arguments but use = to separate the option from optional arguments:
--file=/path/to/file
Examples
f= # -farg or -f arg
f=? # -farg
file= # --file=arg or --file arg
file=? # --file=arg
Error handling
If the command line is invalid, it's a user error and the program exits with status 1 and prints an error message on STDERR
If there is an error in the usage string, ShellOpts raises a
ShellOpts::CompileError. Note that this exception signals an error by the
application developer and shouldn't be catched. If there is an internal error
in the library, a ShellOpts::InternalError is raised and you should look for a
newer version of ShellOpts or file a bug-report
All ShellOpt exceptions derive from ShellOpt::Error
Error handling methods
ShellOpts provides two methods that can be used by the application to
generate error messages in the style of ShellOpts: ShellOpts.error and
ShellOpts.fail. Both write an error message on STDERR and terminates the
program with status 1.
error is intended to respond to user errors (like giving a file name that
doesn't exist) and prints a short usage summary to remind the user:
<PROGRAM>: <MESSAGE>
Usage: <PROGRAM> <USAGE>
The usage string is a prettyfied version of the usage definition given to ShellOpts
fail is used to report that something is wrong with the assumptions about the
system (eg. disk full) and omits the usage summary
<PROGRAM>: <MESSAGE>
The methods are defined as instance methods on ShellOpts::ShellOpts and as
class methods on ShellOpts. They can also be included in the global scope by
include ShellOpts::Utils
Usage string
The error handling methods prints a prettified version of the usage string
given to ShellOpts.parse. The usage string can be overridden by assigning to
ShellOpts.usage. A typical use case is when you want to split the usage
description over multiple lines:
USAGE="long-and-complex-usage-string"
ShellOpts.usage = <<~EOD
usage explanation
split over
multiple lines
EOD
Note that this only affects the module-level ShellOpts.error method and not
object-level ShellOpts::ShellOpts#error method. This is considered a bug and
will fixed at some point
Example
The rm(1) command could be implemented like this
require 'shellopts'
# Define options
USAGE = %{
f,force i I interactive=WHEN? r,R,recusive d,dir
one-file-system no-preserve-root preserve-root
v,verbose help version
}
# Define defaults
force = false
prompt = false
prompt_once = false
interactive = false
interactive_when = nil
recursive = false
remove_empty_dirs = false
one_file_system = false
preserve_root = true
verbose = false
# Process command line
args = ShellOpts.process(USAGE, ARGV) { |opt, val|
case opt
when '-f', '--force'; force = true
when '-i'; prompt = true
when '-I'; prompt_once = true
when '--interactive'; interactive = true; interactive_when = val
when '-r', '-R', '--recursive'; recursive = true
when '-d', '--dir'; remove_empty_dirs = true
when '--one-file-system'; one_file_system = true
when '--preserve-root'; preserve_root = true
when '--no-preserve-root'; preserve_root = false
when '--verbose'; verbose = true
when '--help'; print_help; exit
when '--version'; puts VERSION; exit
end
end
# Remaining arguments are files or directories
files = args
See also
Installation
To install in your gem repository:
$ gem install shellopts
To add it as a dependency for an executable add this line to your application's
Gemfile. Use exact version match as ShellOpts is still in development:
gem 'shellopts', 'x.y.z'
If you're developing a library, you should add the dependency to the *.gemfile instead:
spec.add_dependency 'shellopts', 'x.y.z'
Development
After checking out the repo, run bin/setup to install dependencies. Then, run
rake spec to run the tests. You can also run bin/console for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To
release a new version, update the version number in version.rb, and then run
bundle exec rake release, which will create a git tag for the version, push
git commits and tags, and push the .gem file to
rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/shellopts.