cliqr 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/README.md +97 -71
  4. data/examples/README.md +12 -0
  5. data/examples/hbase +58 -0
  6. data/examples/my-command +63 -0
  7. data/examples/numbers +55 -0
  8. data/examples/vagrant +90 -0
  9. data/lib/cliqr.rb +17 -2
  10. data/lib/cliqr/argument_validation/argument_type_validator.rb +2 -2
  11. data/lib/cliqr/argument_validation/validator.rb +1 -1
  12. data/lib/cliqr/cli/argument_operator.rb +44 -0
  13. data/lib/cliqr/cli/argument_operator_context.rb +20 -0
  14. data/lib/cliqr/cli/command.rb +1 -1
  15. data/lib/cliqr/cli/command_context.rb +93 -12
  16. data/lib/cliqr/cli/command_runner_factory.rb +2 -2
  17. data/lib/cliqr/cli/config.rb +301 -33
  18. data/lib/cliqr/cli/executor.rb +14 -9
  19. data/lib/cliqr/cli/interface.rb +22 -7
  20. data/lib/cliqr/cli/router.rb +6 -2
  21. data/lib/cliqr/cli/shell_command.rb +69 -0
  22. data/lib/cliqr/cli/usage_builder.rb +185 -0
  23. data/lib/cliqr/config_validation/validator_factory.rb +59 -5
  24. data/lib/cliqr/error.rb +10 -4
  25. data/lib/cliqr/parser/action_token.rb +23 -0
  26. data/lib/cliqr/parser/argument_parser.rb +1 -1
  27. data/lib/cliqr/parser/argument_token.rb +1 -4
  28. data/lib/cliqr/parser/argument_tree_walker.rb +40 -8
  29. data/lib/cliqr/parser/option_token.rb +2 -1
  30. data/lib/cliqr/parser/parsed_input.rb +21 -2
  31. data/lib/cliqr/parser/parsed_input_builder.rb +11 -7
  32. data/lib/cliqr/parser/token.rb +3 -9
  33. data/lib/cliqr/parser/token_factory.rb +1 -1
  34. data/lib/cliqr/util.rb +135 -0
  35. data/lib/cliqr/version.rb +1 -1
  36. data/spec/argument_parser_spec_helper.rb +15 -0
  37. data/spec/config/action_config_validator_spec.rb +146 -0
  38. data/spec/config/config_finalize_spec.rb +1 -1
  39. data/spec/config/config_validator_spec.rb +29 -19
  40. data/spec/config/option_config_validator_spec.rb +13 -13
  41. data/spec/dsl/interface_spec.rb +1 -168
  42. data/spec/dsl/usage_spec.rb +705 -0
  43. data/spec/executor/action_executor_spec.rb +205 -0
  44. data/spec/executor/executor_spec.rb +405 -17
  45. data/spec/executor/help_executor_spec.rb +424 -0
  46. data/spec/executor/shell_executor_spec.rb +233 -0
  47. data/spec/fixtures/action_reader_command.rb +12 -0
  48. data/spec/fixtures/csv_argument_operator.rb +8 -0
  49. data/spec/fixtures/test_option_type_checker_command.rb +8 -0
  50. data/spec/parser/action_argument_parser_spec.rb +113 -0
  51. data/spec/parser/argument_parser_spec.rb +37 -44
  52. data/spec/spec_helper.rb +1 -0
  53. data/spec/validation/action_argument_validator_spec.rb +50 -0
  54. data/spec/validation/{argument_validation_spec.rb → command_argument_validation_spec.rb} +36 -18
  55. data/spec/validation/error_spec.rb +1 -1
  56. data/tasks/rdoc.rake +16 -0
  57. data/tasks/rubucop.rake +14 -0
  58. data/tasks/yard.rake +21 -0
  59. data/templates/usage.erb +39 -0
  60. metadata +48 -11
@@ -55,7 +55,7 @@ module Cliqr
55
55
  class ValidationError < CliqrError; end
56
56
 
57
57
  # Encapsulates the error that gets thrown during a command execution
58
- class CommandRuntimeException < CliqrError; end
58
+ class CommandRuntimeError < CliqrError; end
59
59
 
60
60
  # Error to signify that a command's runner is not available
61
61
  class UnknownCommandRunnerException < CliqrError; end
@@ -69,16 +69,22 @@ module Cliqr
69
69
  # Indicates that a option has multiple values in the command line
70
70
  class MultipleOptionValues < CliqrError; end
71
71
 
72
- # Raised if two options are defined with same long or short name
72
+ # Raised if more than one option is defined with same long or short name
73
73
  class DuplicateOptions < CliqrError; end
74
74
 
75
- # Raised if an option is not defined properly
76
- class InvalidArgumentError < CliqrError; end
75
+ # Raised if multiple actions are defined with same name at the same nesting level
76
+ class DuplicateActions < CliqrError; end
77
77
 
78
78
  # Raised if an argument does not conform to the option's type
79
79
  class IllegalArgumentError < CliqrError; end
80
80
 
81
81
  # Indicates that a unknown validator type is being used in a class
82
82
  class UnknownValidatorType < CliqrError; end
83
+
84
+ # Indicates that a unknown action was invoked
85
+ class UnknownActionError < CliqrError; end
86
+
87
+ # Raised when a command is executed that is not supposed to run
88
+ class IllegalCommandError < CliqrError; end
83
89
  end
84
90
  end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ module Cliqr
4
+ module Parser
5
+ # Token handler for parsing action commands
6
+ #
7
+ # @api private
8
+ class ActionToken < Token
9
+ # Create a new action token
10
+ def initialize(_name)
11
+ super(arg)
12
+ end
13
+
14
+ # This token is valid if argument is non-empty
15
+ #
16
+ # @return [Boolean] <tt>true</tt> if token's argument is non-empty
17
+ def valid?
18
+ return false if arg.nil?
19
+ !arg.empty?
20
+ end
21
+ end
22
+ end
23
+ end
@@ -12,7 +12,7 @@ module Cliqr
12
12
  # @param [Cliqr::CLI::Config] config Command line configuration
13
13
  # @param [Array<String>] args An array of arguments from command line
14
14
  #
15
- # @return [Hash] Parsed hash of command linet arguments
15
+ # @return [Hash] Parsed action config and hash of command line arguments
16
16
  def self.parse(config, args)
17
17
  tree_walker = ArgumentTreeWalker.new(config)
18
18
  tree_walker.walk(args)
@@ -16,10 +16,7 @@ module Cliqr
16
16
  # @return [Boolean] <tt>true</tt> if token's argument is non-empty
17
17
  def valid?
18
18
  return false if arg.nil?
19
-
20
- return !arg.empty? if @arg.respond_to?(:empty?)
21
-
22
- true
19
+ !arg.empty?
23
20
  end
24
21
  end
25
22
  end
@@ -20,19 +20,38 @@ module Cliqr
20
20
 
21
21
  # Parse the arguments and generate tokens by iterating over command line arguments
22
22
  #
23
- # @param [Array<String>] args List of arguments that needs to parsed
23
+ # @param [Array<String>] raw_args List of arguments that needs to parsed
24
24
  #
25
- # @return [Hash] Parsed hash of command line arguments
26
- def walk(args)
27
- input_builder = ParsedInputBuilder.new(@config)
28
- token_factory = TokenFactory.new(@config)
25
+ # @return [Array] Action config and parsed hash of command line arguments
26
+ def walk(raw_args)
27
+ action_config, args = parse_action(raw_args)
28
+ input_builder = ParsedInputBuilder.new(@config, action_config)
29
+ token_factory = TokenFactory.new(action_config)
29
30
  token = token_factory.get_token
30
31
  args.each do |arg|
31
32
  token = handle_argument(arg, token, token_factory, input_builder)
32
33
  end
33
- fail Cliqr::Error::OptionValueMissing, "a value must be defined for argument \"#{token.arg}\"" \
34
- if token.active?
35
- input_builder.build
34
+ fail Cliqr::Error::OptionValueMissing, \
35
+ "a value must be defined for argument \"#{token.arg}\"" if token.active?
36
+ ensure_default_action(action_config, input_builder)
37
+ end
38
+
39
+ private
40
+
41
+ # Parse the action from the list of arguments
42
+ #
43
+ # @return [Cliqr::CLI::Config] Configuration of the command invoked and remaining arguments
44
+ def parse_action(raw_args)
45
+ args = []
46
+ action_config = @config
47
+ raw_args.each do |arg|
48
+ if action_config.action?(arg)
49
+ action_config = action_config.action(arg)
50
+ else
51
+ args.push(arg)
52
+ end
53
+ end
54
+ [action_config, args]
36
55
  end
37
56
 
38
57
  # Handle the next argument in the context of the current token
@@ -50,6 +69,19 @@ module Cliqr
50
69
 
51
70
  token
52
71
  end
72
+
73
+ # Make sure default options are processed by overriding action
74
+ #
75
+ # @return [Array] Action config and parsed hash of command line arguments
76
+ def ensure_default_action(action_config, input_builder)
77
+ parsed_input = input_builder.build
78
+ default_action_name = parsed_input.default_action(action_config)
79
+ unless default_action_name.nil?
80
+ action_config = action_config.action(default_action_name)
81
+ parsed_input.remove_option(default_action_name)
82
+ end
83
+ [action_config, parsed_input]
84
+ end
53
85
  end
54
86
  end
55
87
  end
@@ -46,7 +46,8 @@ module Cliqr
46
46
 
47
47
  # Collect this token's name and value into a input builder
48
48
  #
49
- # @param [Cliqr::Parser::ParsedInputBuilder] input_builder A builder to prepare parsed arguments
49
+ # @param [Cliqr::Parser::ParsedInputBuilder] input_builder A builder to prepare parsed
50
+ # arguments
50
51
  #
51
52
  # @return [Cliqr::Parser::ParsedInputBuilder] The updated input builder instance
52
53
  def collect(input_builder)
@@ -25,7 +25,7 @@ module Cliqr
25
25
  @command = parsed_arguments[:command]
26
26
 
27
27
  @options = Hash[parsed_arguments[:options].collect \
28
- { |option| [option[:name], option[:value]] }]\
28
+ { |option| [option[:name].to_s, option[:value]] }]\
29
29
  if parsed_arguments.key?(:options)
30
30
 
31
31
  @arguments = parsed_arguments[:arguments]
@@ -37,7 +37,14 @@ module Cliqr
37
37
  #
38
38
  # @return [String]
39
39
  def option(name)
40
- @options[name]
40
+ @options[name.to_s]
41
+ end
42
+
43
+ # Remove a option
44
+ #
45
+ # @return [Object] Option's original value
46
+ def remove_option(name)
47
+ @options.delete(name.to_s)
41
48
  end
42
49
 
43
50
  # Test equality with another object
@@ -55,6 +62,18 @@ module Cliqr
55
62
  def ==(other)
56
63
  eql?(other)
57
64
  end
65
+
66
+ # Get name of default action if present as option
67
+ #
68
+ # @return [Symbol] Name of the default action or <tt>nil</tt> if not present
69
+ def default_action(action_config)
70
+ if option('help') && action_config.help?
71
+ return :help
72
+ elsif option('version') && action_config.version?
73
+ return :version
74
+ end
75
+ nil
76
+ end
58
77
  end
59
78
  end
60
79
  end
@@ -14,8 +14,10 @@ module Cliqr
14
14
  # @param [Cliqr::CLI::Config] config Configuration settings for the command line interface
15
15
  #
16
16
  # @return [Cliqr::CLI::Parser::ParsedInputBuilder]
17
- def initialize(config)
17
+ def initialize(config, action_config)
18
18
  @config = config
19
+ @action_config = action_config
20
+ @actions = []
19
21
  @options = []
20
22
  @option_names = Set.new
21
23
  @arguments = []
@@ -23,7 +25,8 @@ module Cliqr
23
25
 
24
26
  # Add a new parsed option token from the list of options
25
27
  #
26
- # @param [Cliqr::CLI::Parser::OptionToken] token A parsed option token from command line arguments
28
+ # @param [Cliqr::CLI::Parser::OptionToken] token A parsed option token from command line
29
+ # arguments
27
30
  #
28
31
  # @return [Cliqr::Parser::ParsedInputBuilder] Updated input builder
29
32
  def add_option_token(token)
@@ -37,11 +40,11 @@ module Cliqr
37
40
 
38
41
  # Add a argument to the list of parsed arguments
39
42
  #
40
- # @param [String] arg Argument value
43
+ # @param [Cliqr::CLI::Parser::ArgumentToken] token Argument token
41
44
  #
42
45
  # @return [Cliqr::Parser::ParsedInputBuilder] Updated input builder
43
- def add_argument(arg)
44
- @arguments.push(arg)
46
+ def add_argument_token(token)
47
+ @arguments.push(token.arg) if token.valid?
45
48
  self
46
49
  end
47
50
 
@@ -49,7 +52,8 @@ module Cliqr
49
52
  #
50
53
  # @return [Cliqr::Parser::ParsedInput] Parsed arguments wrapper
51
54
  def build
52
- ParsedInput.new(:command => @config.basename,
55
+ ParsedInput.new(:command => @config.name,
56
+ :actions => @actions,
53
57
  :options => @options,
54
58
  :arguments => @arguments)
55
59
  end
@@ -62,7 +66,7 @@ module Cliqr
62
66
  #
63
67
  # @return [Set<String>] Current list of option names
64
68
  def add_option_name(token)
65
- option_config = @config.option(token.name)
69
+ option_config = @action_config.option(token.name)
66
70
  old_config = @option_names.add?(option_config.name)
67
71
  fail Cliqr::Error::MultipleOptionValues,
68
72
  "multiple values for option \"#{token.arg}\"" if old_config.nil?
@@ -27,20 +27,14 @@ module Cliqr
27
27
  @active
28
28
  end
29
29
 
30
- # This token is never valid
31
- #
32
- # @return [Boolean] Always <tt>false</tt>
33
- def valid?
34
- false
35
- end
36
-
37
30
  # Collect this token's argument into a input builder
38
31
  #
39
- # @param [Cliqr::Parser::ParsedInputBuilder] input_builder A builder to prepare parsed arguments
32
+ # @param [Cliqr::Parser::ParsedInputBuilder] input_builder A builder to prepare parsed
33
+ # arguments
40
34
  #
41
35
  # @return [Cliqr::Parser::ParsedInputBuilder] The updated input builder instance
42
36
  def collect(input_builder)
43
- input_builder.add_argument(@arg)
37
+ input_builder.add_argument_token(self)
44
38
  end
45
39
 
46
40
  protected
@@ -35,7 +35,7 @@ module Cliqr
35
35
  option_config = get_option_config(Regexp.last_match(2), arg)
36
36
  build_token(option_config, arg)
37
37
  else
38
- fail Cliqr::Error::InvalidArgumentError, "invalid command argument \"#{arg}\"" \
38
+ fail Cliqr::Error::IllegalArgumentError, "invalid command argument \"#{arg}\"" \
39
39
  unless @config.arguments?
40
40
  ArgumentToken.new(arg)
41
41
  end
@@ -0,0 +1,135 @@
1
+ # encoding: utf-8
2
+
3
+ require 'cliqr/cli/shell_command'
4
+
5
+ module Cliqr
6
+ # Utility methods
7
+ #
8
+ # @api private
9
+ class Util
10
+ # Ensure that a variable is a instance object not a class type
11
+ #
12
+ # @return [Object]
13
+ def self.ensure_instance(obj)
14
+ return obj.new if obj.class == Class
15
+ obj
16
+ end
17
+
18
+ # Build a help action for a parent config
19
+ #
20
+ # @return [Cliqr::CLI::Config] New action config
21
+ def self.build_help_action(config)
22
+ cli = Cliqr.interface do
23
+ name 'help'
24
+ description "The help action for command \"#{config.command}\" which provides details " \
25
+ 'and usage information on how to use the command.'
26
+ handler Util.help_action_handler(config)
27
+ help :disable if config.help?
28
+ shell :disable
29
+ end
30
+ cli.config
31
+ end
32
+
33
+ # Build a help option for a parent config
34
+ #
35
+ # @return [Cliqr::CLI::OptionConfig] New option config
36
+ def self.build_help_option(config)
37
+ Cliqr::CLI::OptionConfig.new.tap do |option_config|
38
+ option_config.name = 'help'
39
+ option_config.short = 'h'
40
+ option_config.description = "Get helpful information for action \"#{config.command}\" " \
41
+ 'along with its usage information.'
42
+ option_config.type = Cliqr::CLI::BOOLEAN_ARGUMENT_TYPE
43
+ option_config.operator = Cliqr::CLI::ArgumentOperator::DEFAULT_ARGUMENT_OPERATOR
44
+ option_config.finalize
45
+ end
46
+ end
47
+
48
+ # Build a version action for a parent config
49
+ #
50
+ # @return [Cliqr::CLI::Config] New action config
51
+ def self.build_version_action(config)
52
+ cli = Cliqr.interface do
53
+ name 'version'
54
+ description "Get version information for command \"#{config.command}\"."
55
+ handler do
56
+ puts config.version
57
+ end
58
+ shell :disable
59
+ end
60
+ cli.config
61
+ end
62
+
63
+ # Build a version option for a parent config
64
+ #
65
+ # @return [Cliqr::CLI::OptionConfig] New option config
66
+ def self.build_version_option(config)
67
+ Cliqr::CLI::OptionConfig.new.tap do |option_config|
68
+ option_config.name = 'version'
69
+ option_config.short = 'v'
70
+ option_config.description = "Get version information for command \"#{config.command}\"."
71
+ option_config.type = Cliqr::CLI::BOOLEAN_ARGUMENT_TYPE
72
+ option_config.operator = Cliqr::CLI::ArgumentOperator::DEFAULT_ARGUMENT_OPERATOR
73
+ option_config.finalize
74
+ end
75
+ end
76
+
77
+ # Action handler for help action
78
+ #
79
+ # @return [Proc]
80
+ def self.help_action_handler(config)
81
+ proc do
82
+ fail Cliqr::Error::IllegalArgumentError,
83
+ "too many arguments for \"#{command}\" command" if arguments.length > 1
84
+ action_config = arguments.length == 0 ? config : config.action(arguments.first)
85
+ puts Cliqr::CLI::UsageBuilder.build(action_config)
86
+ end
87
+ end
88
+
89
+ # Build a shell action for a parent config
90
+ #
91
+ # @return [Cliqr::CLI::Config] New action config
92
+ def self.build_shell_action(config)
93
+ cli = Cliqr.interface do
94
+ name 'shell'
95
+ description "Execute a shell in the context of \"#{config.command}\" command."
96
+ handler Cliqr::CLI::ShellCommand
97
+ shell :disable
98
+ end
99
+ cli.config
100
+ end
101
+
102
+ # Sanitize raw command line arguments
103
+ #
104
+ # @return [Array<String>]
105
+ def self.sanitize_args(args, config = nil)
106
+ sanitized = []
107
+ if args.is_a?(Array)
108
+ args.each { |arg| sanitized.concat(sanitize_args(arg)) }
109
+ elsif args.is_a?(String)
110
+ sanitized = args.split(' ')
111
+ end
112
+ remove_base_command(sanitized, config)
113
+ end
114
+
115
+ # Remove base command form sanitized list of arguments
116
+ #
117
+ # @return [Array<String>]
118
+ def self.remove_base_command(sanitized, config)
119
+ if !config.nil? && sanitized[0] == config.root.name.to_s
120
+ sanitized.drop(1)
121
+ else
122
+ sanitized
123
+ end
124
+ end
125
+
126
+ # Get handler that forwards command to the help action
127
+ #
128
+ # @return [Proc]
129
+ def self.forward_to_help_handler
130
+ proc do
131
+ forward "#{command} help"
132
+ end
133
+ end
134
+ end
135
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Versioned gem
4
4
  module Cliqr
5
- VERSION = '1.1.0'
5
+ VERSION = '1.2.0'
6
6
  end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ require 'cliqr/parser/argument_parser'
6
+ require 'cliqr/parser/parsed_input'
7
+
8
+ require 'fixtures/test_command'
9
+
10
+ def assert_results(config, args, expected_result, expected_config = nil)
11
+ expected_config ||= config
12
+ action_config, actual_parsed_input = Cliqr::Parser.parse(config, args)
13
+ expect(actual_parsed_input).to eq(expected_result)
14
+ expect(action_config).to eq(expected_config)
15
+ end