require 'optparse' require 'fig/command/optionerror' require 'fig/package' require 'fig/packagedescriptor' require 'fig/statement/archive' require 'fig/statement/include' require 'fig/statement/path' require 'fig/statement/resource' require 'fig/statement/set' module Fig; end class Fig::Command; end # Command-line processing. class Fig::Command::Options USAGE = <<-EOF Usage: fig [...] [DESCRIPTOR] [--update | --update-if-missing] [-- COMMAND] fig [...] [DESCRIPTOR] [--update | --update-if-missing] [--command-extra-args VALUES] fig {--publish | --publish-local} DESCRIPTOR [--resource PATH] [--archive PATH] [--include DESCRIPTOR] [--override DESCRIPTOR] [--force] [...] fig --clean DESCRIPTOR [...] fig --get VARIABLE [DESCRIPTOR] [...] fig --list-configs [DESCRIPTOR] [...] fig --list-dependencies [--list-tree] [--list-all-configs] [DESCRIPTOR] [...] fig --list-variables [--list-tree] [--list-all-configs] [DESCRIPTOR] [...] fig {--list-local | --list-remote} [...] fig {--version | --help} A DESCRIPTOR looks like <package name>[/<version>][:<config>] e.g. "foo", "foo/1.2.3", and "foo/1.2.3:default". Whether ":<config>" and "/<version>" are required or allowed is dependent upon what your are doing. Standard options (represented as "[...]" above): [--set VARIABLE=VALUE] [--append VARIABLE=VALUE] [--file PATH] [--no-file] [--config CONFIG] [--login] [--log-level LEVEL] [--log-config PATH] [--figrc PATH] [--no-figrc] [--suppress-warning-include-statement-missing-version] Environment variables: FIG_REMOTE_URL (required), FIG_HOME (path to local repository cache, defaults to $HOME/.fighome). EOF LOG_LEVELS = %w[ off fatal error warn info debug all ] LOG_ALIASES = { 'warning' => 'warn' } attr_reader :shell_command attr_reader :command_extra_argv attr_reader :descriptor attr_reader :exit_code def initialize(argv) process_command_line(argv) end def archives() return @options[:archives] end def clean?() return @options[:clean] end def config() return @options[:config] end def figrc() return @options[:figrc] end def force?() return @options[:force] end def get() return @options[:get] end def help?() return @options[:help] end def home() return @options[:home] end def listing() return @options[:listing] end def list_tree?() return @options[:list_tree] end def list_all_configs?() return @options[:list_all_configs] end def log_config() return @options[:log_config] end def login?() return @options[:login] end def log_level() return @options[:log_level] end def no_figrc?() return @options[:no_figrc] end def environment_statements() return @options[:environment_statements] end def package_definition_file() return @options[:package_definition_file] end def publish?() return @options[:publish] end def publish_local?() return @options[:publish_local] end def publishing?() return publish? || publish_local? end def resources() return @options[:resources] end def suppress_warning_include_statement_missing_version?() return @options[:suppress_warning_include_statement_missing_version] end def update?() return @options[:update] end def update_if_missing?() return @options[:update_if_missing] end def updating?() return update? || update_if_missing? end def version?() return @options[:version] end # Answers whether we should reset the environment to nothing, sort of like # the standardized environment that cron(1) creates. At present, we're only # setting this when we're listing variables. One could imagine allowing this # to be set by a command-line option in general; if we do this, the # Environment class will need to be changed to support deletion of values # from ENV. def reset_environment?() return listing() == :variables end # This needs to be public for efficient use of custom command.rb wrappers. def strip_shell_command(argv) argv.each_with_index do |arg, i| terminating_option = nil case arg when '--' terminating_option = arg @shell_command = argv[(i+1)..-1] when '--command-extra-args' terminating_option = arg @command_extra_argv = argv[(i+1)..-1] end if terminating_option argv.slice!(i..-1) break end end return end # This needs to be public for efficient use of custom command.rb wrappers. def help() puts @help_message puts <<-'END_MESSAGE' -- end of Fig options; anything after this is used as a command to run --command-extra-args end of Fig options; anything after this is appended to the end of a "command" statement in a "config" block. END_MESSAGE return 0 end private # Note that OptionParser insist that the regex match the entire value, not # just matches the regex in general. In effect, OptionParser is wrapping the # regex with "\A" and "\z". STARTS_WITH_NON_HYPHEN = %r< \A [^-] .* >x ARGUMENT_DESCRIPTION = { '--set' => Fig::Statement::Set::ARGUMENT_DESCRIPTION, '--append' => Fig::Statement::Path::ARGUMENT_DESCRIPTION } def process_command_line(argv) argv = argv.clone strip_shell_command(argv) @switches = [] @options = {} @options[:home] = ENV['FIG_HOME'] || File.expand_path('~/.fighome') parser = new_parser() @help_message = parser.help begin parser.parse!(argv) rescue OptionParser::InvalidArgument => error raise_invalid_argument(error.args[0], error.args[1]) rescue OptionParser::MissingArgument => error raise_missing_argument(error.args[0]) rescue OptionParser::InvalidOption => error raise Fig::Command::OptionError.new( "Unknown option #{error.args[0]}.\n\n#{USAGE}" ) rescue OptionParser::ParseError => error raise Fig::Command::OptionError.new(error.to_s) end if not exit_code.nil? return end if argv.size > 1 $stderr.puts %q<Extra arguments. Should only have a package/version after all other options. Had "> + argv.join(%q<", ">) + %q<" left over.> @exit_code = 1 return end derive_primary_descriptor(argv.first) return end def raise_missing_argument(option) raise Fig::Command::OptionError.new( "Please provide a value for #{option}." ) end def raise_invalid_argument(option, value) # *sigh* OptionParser does not raise MissingArgument for the case of an # option with a required value being followed by another option. It # assigns the next option as the value instead. E.g. for # # fig --set --get FOO # # it assigns "--get" as the value of the "--set" option. switch_strings = (@switches.collect {|switch| [switch.short, switch.long]}).flatten if switch_strings.any? {|string| string == value} raise_missing_argument(option) end description = ARGUMENT_DESCRIPTION[option] if description.nil? description = '' else description = ' ' + description end raise Fig::Command::OptionError.new( %Q<Invalid value for #{option}: "#{value}".#{description}> ) end def new_parser return OptionParser.new do |parser| set_up_queries(parser) set_up_commands(parser) set_up_package_configuration_source(parser) set_up_environment_statements(parser) set_up_package_contents_statements(parser) set_up_remote_repository_access(parser) set_up_program_configuration(parser) end end def set_up_queries(parser) parser.banner = "#{USAGE}\n" @switches << parser.define_tail( '-?', '-h','--help','display this help text' ) do @options[:help] = true end @switches << parser.define_tail('-v', '--version', 'print Fig version') do @options[:version] = true end @switches << parser.define( '-g', '--get VARIABLE', STARTS_WITH_NON_HYPHEN, 'print value of environment variable VARIABLE' ) do |get| @options[:get] = get end set_up_listings(parser) return end def set_up_listings(parser) option_mapping = { :local_packages => [ '--list-local', '--list', 'list packages in $FIG_HOME' ], :configs => ['--list-configs', 'list configurations'], :dependencies => ['--list-dependencies', 'list package dependencies, recursively'], :variables => [ '--list-variables', 'list all variables defined/used by package and its dependencies' ], :remote_packages => ['--list-remote', 'list packages in remote repo'] } option_mapping.each_pair do | type, specification | @switches << parser.define(*specification) do if @options[:listing] options_string = ( option_mapping.values.collect {|specification| specification[0]} ).join(', ') $stderr.puts "Can only specify one of #{options_string}." @exit_code = 1 else @options[:listing] = type end end end @switches << parser.define( '--list-tree', 'for listings, output a tree instead of a list' ) do @options[:list_tree] = true end @switches << parser.define( '--list-all-configs', 'for listings, follow all configurations of the base package' ) do @options[:list_all_configs] = true end return end def set_up_commands(parser) @switches << parser.define('--clean', 'remove package from $FIG_HOME') do @options[:clean] = true end @switches << parser.define( '--publish', 'install package in $FIG_HOME and in remote repo' ) do |publish| @options[:publish] = true end @switches << parser.define( '--publish-local', 'install package only in $FIG_HOME' ) do |publish_local| @options[:publish_local] = true end return end FILE_OPTION_VALUE_PATTERN = %r< \A (?: - # Solely a hyphen, to allow for stdin | [^-] .* # or anything not starting with a hyphen. ) \z >x def set_up_package_configuration_source(parser) @switches << parser.define( '-c', '--config CONFIG', STARTS_WITH_NON_HYPHEN, %q<apply configuration CONFIG, default is "default"> ) do |config| @options[:config] = config end @options[:package_definition_file] = nil @switches << parser.define( '--file FILE', FILE_OPTION_VALUE_PATTERN, %q<read Fig file FILE. Use '-' for stdin. See also --no-file> ) do |path| @options[:package_definition_file] = path end @switches << parser.define( '--no-file', 'ignore package.fig file in current directory' ) do |path| @options[:package_definition_file] = :none end return end def set_up_environment_statements(parser) @options[:environment_statements] = [] @switches << parser.define( '-p', '--append VARIABLE=VALUE', STARTS_WITH_NON_HYPHEN, 'append (actually, prepend) VALUE to PATH-like environment variable VARIABLE' ) do |name_value| @options[:environment_statements] << new_variable_statement('--append', name_value, Fig::Statement::Path) end @switches << parser.define( '-s', '--set VARIABLE=VALUE', STARTS_WITH_NON_HYPHEN, 'set environment variable VARIABLE to VALUE' ) do |name_value| @options[:environment_statements] << new_variable_statement('--set', name_value, Fig::Statement::Set) end @switches << parser.define( '-i', '--include DESCRIPTOR', STARTS_WITH_NON_HYPHEN, 'include package/version:config specified in DESCRIPTOR in environment' ) do |descriptor_string| statement = Fig::Statement::Include.new( nil, '--include option', Fig::Statement::Include.parse_descriptor( descriptor_string, :validation_context => ' given in a --include option' ), nil ) # We've never allowed versionless includes from the command-line. Hooray! statement.complain_if_version_missing() @options[:environment_statements] << statement end @switches << parser.define( '--override DESCRIPTOR', STARTS_WITH_NON_HYPHEN, 'dictate version of package as specified in DESCRIPTOR' ) do |descriptor_string| descriptor = Fig::Statement::Override.parse_descriptor( descriptor_string, :validation_context => ' given in a --override option' ) statement = Fig::Statement::Override.new( nil, '--override option', descriptor.name, descriptor.version ) @options[:environment_statements] << statement end return end def set_up_package_contents_statements(parser) @options[:archives] = [] @switches << parser.define( '--archive PATH', STARTS_WITH_NON_HYPHEN, 'include PATH archive in package (when using --publish)' ) do |path| @options[:archives] << Fig::Statement::Archive.new(nil, '--archive option', path) end @options[:resources] =[] @switches << parser.define( '--resource PATH', STARTS_WITH_NON_HYPHEN, 'include PATH resource in package (when using --publish)' ) do |path| @options[:resources] << Fig::Statement::Resource.new(nil, '--resource option', path) end return end def set_up_remote_repository_access(parser) @switches << parser.define( '-u', '--update', 'check remote repo for updates and download to $FIG_HOME as necessary' ) do @options[:update] = true end @switches << parser.define( '-m', '--update-if-missing', 'check remote repo for updates only if package missing from $FIG_HOME' ) do @options[:update_if_missing] = true end @switches << parser.define( '-l', '--login', 'login to remote repo as a non-anonymous user' ) do @options[:login] = true end @options[:force] = nil @switches << parser.define( '--force', 'force-overwrite existing version of a package to the remote repo' ) do |force| @options[:force] = force end return end def set_up_program_configuration(parser) @switches << parser.define( '--figrc PATH', STARTS_WITH_NON_HYPHEN, 'add PATH to configuration used for Fig' ) do |path| @options[:figrc] = path end @switches << parser.define('--no-figrc', 'ignore ~/.figrc') { @options[:no_figrc] = true } @switches << parser.define( '--log-config PATH', STARTS_WITH_NON_HYPHEN, 'use PATH file as configuration for Log4r' ) do |path| @options[:log_config] = path end level_list = LOG_LEVELS.join(', ') @switches << parser.define( '--log-level LEVEL', LOG_LEVELS, LOG_ALIASES, 'set logging level to LEVEL', " (#{level_list})" ) do |log_level| @options[:log_level] = log_level end @switches << parser.define( '--suppress-warning-include-statement-missing-version', %q<don't complain about "include package" without a version> ) do @options[:suppress_warning_include_statement_missing_version] = true end return end def new_variable_statement(option, name_value, statement_class) variable, value = statement_class.parse_name_value(name_value) { raise_invalid_argument(option, name_value) } return statement_class.new(nil, "#{option} option", variable, value) end # This will be the base package, unless we're publishing (in which case it's # the name to publish to. def derive_primary_descriptor(raw_string) return if raw_string.nil? @descriptor = Fig::PackageDescriptor.parse( raw_string, :name => :required, :version => :required, :validation_context => ' specified on command line' ) if @descriptor.config && config() $stderr.puts \ %Q<Cannot specify both --config and a config in the descriptor "#{raw_string}".> @exit_code = 1 end return end end