module Backticks module CLI # Command-line parameter generator that relies on traditional *nix getopt # conventions. Getopt doesn't know about GNU conventions such as short and # long options; it doesn't know about abbreviations; it doesn't know about # conventions such as `--X` vs. `--no-X` or `-d` vs. `-D`. # # Although Getopt is simple, it has the tremendous advantage of being # compatible with a wide range of other schemes including GNU getopt-long, # golang flags, and most Java utilities. It's a great choice of default # CLI. module Getopt # Translate a series of positional and keyword arguments into command-line # line parameters consisting of words and options. # # Each positional argument can be a Hash, an Array, or another object. # They are handled as follows: # - Hash is translated to a sequence of options; see #options # - Array is appended to the command line as a sequence of words # - other objects are turned into a strong with #to_s and appended to the command line as a single word # # @return [Array] list of String words and options # # @example recursively find all text files # parameters('ls', l:true, R:true, '*.txt') => 'ls -l -R *.txt # # @example install your favorite gem # parameters('gem', 'install', no_document:true, 'backticks') def self.parameters(*cmd) argv = [] cmd.each do |item| case item when Array # list of words to append to argv argv.concat(item.map { |e| e.to_s }) when Hash # list of options to convert to CLI parameters argv.concat(options(item)) else # single word to append to argv argv << item.to_s end end argv end # Translate Ruby method parameters into command-line parameters using a # notation that is compatible with traditional Unix getopt. Command lines # generated by this method are also mostly compatible with the following: # - GNU getopt # - Ruby trollop gem # - Golang flags package # # This method accepts an unbounded set of keyword arguments (i.e. you can # pass it _any_ valid Ruby symbol as a kwarg). Each kwarg has a # value; the key/value pair is translated into a CLI option using the # following heuristic: # 1) Snake-case keys are hyphenated, e.g. :no_foo => "--no-foo" # 2) boolean values indicate a CLI flag; true includes the flag, false or nil omits it # 3) all other values indicate a CLI option that has a value. # 4) single character keys are passed as short options; {X: V} becomes "-X V" # 5) multi-character keys are passed as long options; {Xxx: V} becomes "--XXX=V" # # The generic translator doesn't know about short vs. long option names, # abbreviations, or the GNU "X vs. no-X" convention, so it does not # produce the most idiomatic or compact command line for a given program; # its output is, however, almost always valid for utilities that use # Unix-like parameters. # # @return [Array] list of String command-line options def self.options(**opts) flags = [] # Transform opts into getopt-style command line parameters; # append them to the command. opts.each do |kw, arg| if kw.length == 1 # short-form option e.g. "-a" or "-x hello" pre='-' eql=' ' else # long-form option e.g. "--ask" or "--extra=hello" kw = kw.to_s.gsub('_','-') pre='--' eql='=' end # options can be single- or multi-valued; normalize the processing # of both by "upconverting" single options to an array or values. if arg.kind_of?(Array) values = arg else values = [arg] end values.each do |arg| if arg == true # true: boolean flag flags << "#{pre}#{kw}" elsif arg # truthey: option that has a value flags << "#{pre}#{kw}#{eql}#{arg}" else # falsey: omit boolean flag end end end flags end end end end