require 'gli/command.rb' require 'gli/command_line_token.rb' require 'gli/copy_options_to_aliases.rb' require 'gli/exceptions.rb' require 'gli/flag.rb' require 'gli/options.rb' require 'gli/switch.rb' require 'gli_version.rb' require 'support/help.rb' require 'support/rdoc.rb' require 'support/initconfig.rb' require 'etc' # A means to define and parse a command line interface that works as # Git's does, in that you specify global options, a command name, command # specific options, and then command arguments. module GLI extend self include CopyOptionsToAliases @@program_name = $0.split(/\//)[-1] @@post_block = nil @@pre_block = nil @@error_block = nil @@config_file = nil @@use_openstruct = false @@version = nil @@stderr = $stderr @@program_desc = nil @@skips_pre = false @@skips_post = false # Override the device of stderr; exposed only for testing def error_device=(e) #:nodoc: @@stderr = e end # Reset the GLI module internal data structures; mostly useful for testing def reset # :nodoc: switches.clear flags.clear commands.clear @@version = nil @@config_file = nil @@use_openstruct = false @@prog_desc = nil clear_nexts end # Describe the next switch, flag, or command. This should be a # short, one-line description # # +description+:: A String of the short descripiton of the switch, flag, or command following def desc(description); @@next_desc = description; end # Describe the overall application/programm. This should be a one-sentence summary # of what your program does that will appear in the help output. # # +description+:: A String of the short description of your program's purpose def program_desc(description=nil) if description @@program_desc = description end @@program_desc end # Use this if the following command should not have the pre block executed. # By default, the pre block is executed before each command and can result in # aborting the call. Using this will avoid that behavior for the following command def skips_pre @@skips_pre = true end # Use this if the following command should not have the post block executed. # By default, the post block is executed after each command. # Using this will avoid that behavior for the following command def skips_post @@skips_post = true end # Provide a longer, more detailed description. This # will be reformatted and wrapped to fit in the terminal's columns # # +long_desc+:: A String that is s longer description of the switch, flag, or command following. def long_desc(long_desc); @@next_long_desc = long_desc; end # Describe the argument name of the next flag. It's important to keep # this VERY short and, ideally, without any spaces (see Example). # # +name+:: A String that *briefly* describes the argument given to the following command or flag. # # Example: # desc 'Set the filename' # arg_name 'file_name' # flag [:f,:filename] # # Produces: # -f, --filename=file_name Set the filename def arg_name(name); @@next_arg_name = name; end # set the default value of the next flag # # +val+:: A String reprensenting the default value to be used for the following flag if the user doesn't specify one # and, when using a config file, the config also doesn't specify one def default_value(val); @@next_default_value = val; end # Create a flag, which is a switch that takes an argument # # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names # and aliases for this flag. # # Example: # # desc 'Set the filename' # flag [:f,:filename,'file-name'] # # Produces: # # -f, --filename, --file-name=arg Set the filename def flag(*names) names = [names].flatten verify_unused(names,flags,switches,"in global options") flag = Flag.new(names,@@next_desc,@@next_arg_name,@@next_default_value,@@next_long_desc) flags[flag.name] = flag clear_nexts end # Create a switch, which is a command line flag that takes no arguments (thus, it _switches_ something on) # # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names # and aliases for this switch. def switch(*names) names = [names].flatten verify_unused(names,flags,switches,"in global options") switch = Switch.new(names,@@next_desc,@@next_long_desc) switches[switch.name] = switch clear_nexts end # Sets that this app uses a config file as well as the name of the config file. # # +filename+:: A String representing the path to the file to use for the config file. If it's an absolute # path, this is treated as the path to the file. If it's *not*, it's treated as relative to the user's home # directory as produced by File.expand_path('~'). def config_file(filename) if filename =~ /^\// @@config_file = filename else @@config_file = File.join(File.expand_path('~'),filename) end commands[:initconfig] = InitConfig.new(@@config_file) @@config_file end # Define a new command. This takes a block that will be given an instance of the Command that was created. # You then may call methods on this object to define aspects of that Command. # # +names+:: a String or Symbol, or an Array of String or Symbol that represent all the different names and aliases for this command. # def command(*names) command = Command.new([names].flatten,@@next_desc,@@next_arg_name,@@next_long_desc,@@skips_pre,@@skips_post) commands[command.name] = command yield command clear_nexts end # Define a block to run after command line arguments are parsed # but before any command is run. If this block raises an exception # the command specified will not be executed. # The block will receive the global-options,command,options, and arguments # If this block evaluates to true, the program will proceed; otherwise # the program will end immediately def pre(&a_proc) @@pre_block = a_proc end # Define a block to run after the command was executed, only # if there was not an error. # The block will receive the global-options,command,options, and arguments def post(&a_proc) @@post_block = a_proc end # Define a block to run if an error occurs. # The block will receive any Exception that was caught. # It should evaluate to false to avoid the built-in error handling (which basically just # prints out a message). GLI uses a variety of exceptions that you can use to find out what # errors might've occurred during command-line parsing: # * GLI::CustomExit # * GLI::UnknownCommandArgument # * GLI::UnknownGlobalArgument # * GLI::UnknownCommand # * GLI::BadCommandLine def on_error(&a_proc) @@error_block = a_proc end # Indicate the version of your application # # +version+:: String containing the version of your application. def version(version) @@version = version end # Call this with +true+ will cause the +global_options+ and # +options+ passed to your code to be wrapped in # Options, which is a subclass of +OpenStruct+ that adds # [] and []= methods. # # +use_openstruct+:: a Boolean indicating if we should use OpenStruct instead of Hashes def use_openstruct(use_openstruct) @@use_openstruct = use_openstruct end # Runs whatever command is needed based on the arguments. # # +args+:: the command line ARGV array # # Returns a number that would be a reasonable exit code def run(args) #:nodoc: rdoc = RDocCommand.new commands[:rdoc] = rdoc if !commands[:rdoc] commands[:help] = DefaultHelpCommand.new(@@version,rdoc) if !commands[:help] exit_code = 0 begin config = parse_config override_defaults_based_on_config(config) global_options,command,options,arguments = parse_options(args) copy_options_to_aliased_versions(global_options,command,options) global_options = convert_to_openstruct?(global_options) options = convert_to_openstruct?(options) if proceed?(global_options,command,options,arguments) command = commands[:help] if !command command.execute(global_options,options,arguments) if !command.skips_post && @@post_block @@post_block.call(global_options,command,options,arguments) end end rescue Exception => ex @@stderr.puts error_message(ex) if regular_error_handling?(ex) exit_code = if ex.respond_to? :exit_code ex.exit_code else -2 end raise ex if ENV['GLI_DEBUG'] == 'true' end exit_code end # True if we should proceed with executing the command; this calls # the pre block if it's defined def proceed?(global_options,command,options,arguments) #:nodoc: if command && command.skips_pre true elsif @@pre_block @@pre_block.call(global_options,command,options,arguments) else true end end # Returns true if we should proceed with GLI's basic error handling. # This calls the error block if the user provided one def regular_error_handling?(ex) #:nodoc: if @@error_block @@error_block.call(ex) else true end end # Returns a String of the error message to show the user # +ex+:: The exception we caught that launched the error handling routines def error_message(ex) #:nodoc: msg = "error: #{ex.message}" case ex when UnknownCommand msg += ". Use '#{program_name} help' for a list of commands" when UnknownCommandArgument msg += ". Use '#{program_name} help #{ex.command.name}' for a list of command options" when UnknownGlobalArgument msg += ". Use '#{program_name} help' for a list of global options" end msg end # Simpler means of exiting with a custom exit code. This will # raise a CustomExit with the given message and exit code, which will ultimatley # cause your application to exit with the given exit_code as its exit status def exit_now!(message,exit_code) raise CustomExit.new(message,exit_code) end # Set or get the name of the program, if you don't want the default (which is # the name of the command line program). This # is only used currently in the help and rdoc commands. # # +override+:: A String that represents the name of the program to use, other than the default. # # Returns the current program name, as a String def program_name(override=nil) if override @@program_name = override end @@program_name end alias :d :desc alias :f :flag alias :s :switch alias :c :command # Possibly returns a copy of the passed-in Hash as an instance of GLI::Option. # By default, it will *not*. However by putting use_openstruct true # in your CLI definition, it will def convert_to_openstruct?(options) # :nodoc: @@use_openstruct ? Options.new(options) : options end # Copies all options in both global_options and options to keys for the aliases of those flags. # For example, if a flag works with either -f or --flag, this will copy the value from [:f] to [:flag] # to allow the user to access the options by any alias def copy_options_to_aliased_versions(global_options,command,options) # :nodoc: copy_options_to_aliases(global_options) command.copy_options_to_aliases(options) end def parse_config # :nodoc: return nil if @@config_file.nil? require 'yaml' if File.exist?(@@config_file) File.open(@@config_file) { |file| YAML::load(file) } else {} end end # Returns an array of four values: # * global options (as a Hash) # * Command # * command options (as a Hash) # * arguments (as an Array) def parse_options(args) # :nodoc: global_options,command,options,arguments = parse_options_helper(args.clone,Hash.new,nil,Hash.new,Array.new) flags.each { |name,flag| global_options[name] = flag.default_value if !global_options[name] } command.flags.each { |name,flag| options[name] = flag.default_value if !options[name] } return [global_options,command,options,arguments] end # Finds the index of the first non-flag # argument or -1 if there wasn't one. def find_non_flag_index(args) # :nodoc: args.each_with_index do |item,index| return index if item =~ /^[^\-]/ return index-1 if item =~ /^\-\-$/ end -1 end def flag_switch_index(args) args.each_with_index do |item,index| return index if item =~ /^[\-]/ end -1 end def clear_nexts # :nodoc: @@next_desc = nil @@next_arg_name = nil @@next_default_value = nil @@next_long_desc = nil @@skips_pre = false @@skips_post = false end clear_nexts def flags # :nodoc: @@flags ||= {} end def switches # :nodoc: @@switches ||= {} end def commands # :nodoc: @@commands ||= {} end # Recursive helper for parsing command line options # args:: the arguments that have yet to be processed # global_options:: the global options hash # command:: the Command that has been identified (or nil if not identified yet) # command_options:: options for Command # arguments:: the arguments for Command # # This works by finding the first non-switch/flag argument, and taking that sublist and trying to pick out # flags and switches. After this is done, one of the following is true: # * the sublist is empty - in this case, go again, as there might be more flags to parse # * the sublist has a flag left in it - unknown flag; we bail # * the sublist has a non-flag left in it - this is the command (or the start of the arguments list) # # This sort of does the same thing in two phases; in the first phase, the command hasn't been identified, so # we are looking for global switches and flags, ending when we get the command. # # Once the command has been found, we start looking for command-specific flags and switches. # When those have been found, we know the rest of the argument list is arguments for the command def parse_options_helper(args,global_options,command,command_options,arguments) # :nodoc: non_flag_i = find_non_flag_index(args) all_flags = false if non_flag_i == 0 # no flags if !command command_name = args.shift command = find_command(command_name) raise UnknownCommand.new("Unknown command '#{command_name}'") if !command return parse_options_helper(args, global_options, command, Hash.new, arguments) elsif((index = flag_switch_index(args)) >= 0) try_me = args[0..index-1] rest = args[index..args.length] new_args = rest + try_me return parse_options_helper(new_args, global_options, command, Hash.new, arguments) else return global_options,command,command_options,arguments + args end elsif non_flag_i == -1 all_flags = true end try_me = args[0..non_flag_i] rest = args[(non_flag_i+1)..args.length] if all_flags try_me = args rest = [] end # Suck up whatever options we can switch_hash = switches flag_hash = flags options = global_options if command switch_hash = command.switches flag_hash = command.flags options = command_options end switch_hash.each do |name,switch| value = switch.get_value!(try_me) options[name] = value if !options[name] end flag_hash.each do |name,flag| value = flag.get_value!(try_me) # So, there's a case where the first time we request the value for a flag, # we get the default and not the user-provided value. The next time we request # it, we want to override it with the real value. # HOWEVER, sometimes this happens in reverse, so we want to err on taking the # user-provided, non-default value where possible. if value if options[name] options[name] = value if options[name] == flag.default_value else options[name] = value end end end if try_me.empty? return [global_options,command,command_options,arguments] if rest.empty? # If we have no more options we've parsed them all # and rest may have more return parse_options_helper(rest,global_options,command,command_options,arguments) else if command check = rest check = rest + try_me if all_flags check.each() do |arg| if arg =~ /^\-\-$/ try_me.delete arg break end raise UnknownCommandArgument.new("Unknown option #{arg}",command) if arg =~ /^\-/ end return [global_options,command,command_options,try_me + rest] else # Now we have our command name command_name = try_me.shift raise UnknownGlobalArgument.new("Unknown option #{command_name}") if command_name =~ /^\-/ command = find_command(command_name) raise UnknownCommand.new("Unknown command '#{command_name}'") if !command return parse_options_helper(rest, global_options, command, Hash.new, arguments) end end end def find_command(name) # :nodoc: sym = name.to_sym return commands[name.to_sym] if commands[sym] commands.each do |command_name,command| return command if (command.aliases && command.aliases.include?(sym)) end nil end # Checks that the names passed in have not been used in another flag or option def verify_unused(names,flags,switches,context) # :nodoc: names.each do |name| verify_unused_in_option(name,flags,"flag",context) verify_unused_in_option(name,switches,"switch",context) end end private def verify_unused_in_option(name,option_like,type,context) # :nodoc: raise ArgumentError.new("#{name} has already been specified as a #{type} #{context}") if option_like[name] option_like.each do |one_option_name,one_option| if one_option.aliases raise ArgumentError.new("#{name} has already been specified as an alias of #{type} #{one_option_name} #{context}") if one_option.aliases.include? name end end end # Sets the default values for flags based on the configuration def override_defaults_based_on_config(config) config ||= {} config['commands'] ||= {} override_default(flags,config) override_default(switches,config) commands.each do |command_name,command| command_config = config['commands'][command_name] || {} override_default(command.flags,command_config) override_default(command.switches,command_config) end end def override_default(tokens,config) tokens.each do |name,token| token.default_value=config[name] if config[name] end end end