simple-cli 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1fdfd698a5315891e4d5dd96e5d3ce07fc075d17d5ced10eb06a9cb5733b4a8a
4
- data.tar.gz: 9000f43f3dfceb572aae61b8907216c3dd7c9c4c468374587f56d5c433d84ecd
3
+ metadata.gz: 43c838ad9921bef80717cd3a61c33d68e794adb84799b7e764cc8f23e8fc30db
4
+ data.tar.gz: a3266ec697b770e68add5bbe8511db6253aa9453e4bde36cf86cddecc5577bff
5
5
  SHA512:
6
- metadata.gz: 82f219944dc7ffecac5cae4975749732a6741ee676f5ffd8b4ca862c48717d20a56dfc363dbf7c486e5cb9d48ba376eecb67a7185557547da7824bd201fd524f
7
- data.tar.gz: ee581a7eb68e35c9cc8a2560538f709a1c9ce8a90457338c14ef9446875563f6e9f9f4012844c5ada1f96486ef0945e9917cdda0434972b078c9dc22c5f2c9d1
6
+ metadata.gz: 198ffc917415a4221f1b2de93ff32f42e55dfb480b0bd8f51b2d191277ac3206c801d0cc14ada5bc6c18e2aad38447ddf93a1b303f0254e9f28e2821907f654b
7
+ data.tar.gz: cc2f55d0a681d4376eeff4b76f27f40199e3248b53736fe8f1061307328764477e212cff2e429a01c1dc28b7f214e0eb19f8839acc30e5e65d0d8196115c4c2d
data/.rubocop.yml CHANGED
@@ -22,6 +22,9 @@ Style/StringLiterals:
22
22
  EnforcedStyle: double_quotes
23
23
  ConsistentQuotesInMultiline: false
24
24
 
25
+ Style/StringLiteralsInInterpolation:
26
+ EnforcedStyle: double_quotes
27
+
25
28
  Style/ClassAndModuleChildren:
26
29
  Enabled: false
27
30
 
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in {gemname}.gemspec
4
4
  gemspec
5
+
6
+ # gem "simple-service", path: "../simple-service"
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.3
data/lib/simple/cli.rb CHANGED
@@ -1,29 +1,115 @@
1
+ # rubocop:disable Metrics/AbcSize
2
+ # rubocop:disable Metrics/CyclomaticComplexity
3
+ # rubocop:disable Metrics/MethodLength
4
+ # rubocop:disable Metrics/PerceivedComplexity
5
+
1
6
  module Simple; end
2
7
  module Simple::CLI; end
3
8
 
4
- require_relative "cli/pp"
5
-
6
- require_relative "cli/helpers"
9
+ require_relative "cli/default_options"
7
10
  require_relative "cli/runner"
11
+ require_relative "cli/helper"
8
12
  require_relative "cli/adapter"
9
13
  require_relative "cli/logger"
14
+ require_relative "cli/on_exception"
15
+ require_relative "cli/helpers"
16
+
17
+ require "simple/service"
10
18
 
11
19
  module Simple::CLI
12
20
  extend ::Simple::CLI::Logger
13
21
 
22
+ # It is not strictly necessary to include this module into another module
23
+ # (the "target module") to be able to run the target module via the command
24
+ # line. It is sufficient to just include ::Simple::Service, which turns
25
+ # the target into a service module, and then
26
+ #
27
+ # However, just including Simple::CLI gives you access to the Simple::CLI::Helpers
28
+ # module as well.
14
29
  def self.included(base)
15
- base.extend(::Simple::CLI::Adapter)
30
+ base.include(::Simple::Service)
16
31
  base.include(::Simple::CLI::Helpers)
17
32
  end
18
33
 
19
- # Simple::CLI.run! is called from Runner.run. It is called with a method
20
- # name, which is derived from the command passed in via the command line,
21
- # and parsed arguments.
34
+ # Runs the service with the current command line arguments.
22
35
  #
23
- # The default implementation just calls the respective method on self.
24
- # Implementations might override this method to provide some before/after
25
- # functionality.
26
- def run!(command, *args)
27
- send(command, *args)
36
+ # The +service+ argument must match a simple-service service module. The CLI
37
+ # application's subcommands and their arguments are derived from the actions
38
+ # provided by the service module.
39
+ def self.run!(service, args: nil)
40
+ ::Simple::Service.verify_service!(service)
41
+
42
+ # prepare arguments: we always duplicate the args array, to make guarantee
43
+ # we don't interfere with the caller's view of the world.
44
+ args ||= ARGV
45
+ args = args.dup
46
+
47
+ logger.level = ::Logger::DEBUG
48
+
49
+ # Extract default options. This returns the command to run, the verbosity
50
+ # setting, and the help flag.
51
+ options = DefaultOptions.new(args)
52
+
53
+ # Set logger verbosity. This happens before anything else - this way
54
+ # any further step which raises an exception will have the correct log
55
+ # level applied during exception handling.
56
+ logger.level = options.log_level
57
+
58
+ # Validate the command. If this command is invalid this will print a short
59
+ # help message.
60
+ if options.command
61
+ unless H.action_for_command(service, options.command)
62
+ logger.error "Invalid command '#{options.command}'."
63
+ Helper.short_help!(service)
64
+ end
65
+ end
66
+
67
+ # Run help if requested.
68
+ if options.help?
69
+ if options.command
70
+ Helper.help_on_command! service, options.command, verbose: options.verbose?
71
+ else
72
+ Helper.help! service, verbose: options.verbose?
73
+ end
74
+ end
75
+
76
+ # Run help if command is missing..
77
+ unless options.command
78
+ Helper.short_help! service
79
+ end
80
+
81
+ # Run service.
82
+ Runner.run! service, options.command, *args, verbose: options.verbose?
83
+ rescue ::Simple::Service::ArgumentError
84
+ Helper.help_on_command! service, command, verbose: false
85
+ rescue StandardError => e
86
+ on_exception(e)
87
+ exit 3
88
+ end
89
+
90
+ module H
91
+ def self.action_for_command(service, command)
92
+ actions = ::Simple::Service.actions(service)
93
+
94
+ action_name = H.command_to_action(command)
95
+ return nil unless actions.key?(action_name)
96
+ actions[action_name]
97
+ end
98
+
99
+ def self.action_to_command(action_name)
100
+ raise "action_name must by a Symbol" unless action_name.is_a?(Symbol)
101
+
102
+ action_name.to_s.tr("_", ":")
103
+ end
104
+
105
+ def self.command_to_action(command)
106
+ raise "command must by a String" unless command.is_a?(String)
107
+
108
+ command.tr(":", "_").to_sym
109
+ end
110
+
111
+ def self.binary_name
112
+ $0.gsub(/.*\//, "")
113
+ end
28
114
  end
29
115
  end
@@ -1,17 +1,25 @@
1
+ # rubocop:disable Metrics/CyclomaticComplexity
2
+ # rubocop:disable Metrics/MethodLength
3
+ # rubocop:disable Metrics/PerceivedComplexity
4
+
1
5
  module Simple::CLI::Adapter
2
6
  # Run a Simple::CLI application
3
7
  #
4
8
  # This is usually called with as either
5
9
  #
6
10
  # - Application::CLI.run!: runs the Application's CLI with subcommand support.
7
- #
11
+ #
8
12
  # or
9
13
  #
10
14
  # - Application::CLI.run!("main"): runs the Application's CLI without subcommand support.
11
15
  #
12
- def run!(main_command = nil)
16
+ def run!(*argv)
17
+ if argv.length == 1 && argv != ARGV
18
+ main_command = *argv
19
+ end
20
+
13
21
  runner = Simple::CLI::Runner.new(self)
14
-
22
+
15
23
  if main_command && (ARGV.include?("--help") || ARGV.include?("-h"))
16
24
  runner.help(main_command)
17
25
  elsif main_command
@@ -0,0 +1,73 @@
1
+ require "logger"
2
+
3
+ module Simple::CLI
4
+ # A DefaultOptions object holds values for default options.
5
+ class DefaultOptions
6
+ # extract default CLI options and the "help" command. Returns a DefaultOptions object
7
+ def extract!(args:)
8
+ new args
9
+ end
10
+
11
+ # verbosity (one of ::Logger::WARN, ::Logger::INFO, ::Logger::DEBUG)
12
+ attr_reader :log_level
13
+
14
+ # returns true if we run in verbose mode.
15
+ def verbose?
16
+ log_level == ::Logger::DEBUG
17
+ end
18
+
19
+ # command
20
+ attr_reader :command
21
+
22
+ # The help flag. Is set when
23
+ #
24
+ # - running the "help" command
25
+ # - when a "-h" or "--help" CLI flag was given.
26
+ def help?
27
+ @help
28
+ end
29
+
30
+ private
31
+
32
+ LOG_LEVEL_FLAGS = {
33
+ "--verbose" => ::Logger::DEBUG,
34
+ "-v" => ::Logger::DEBUG,
35
+ "--quiet" => ::Logger::WARN,
36
+ "-q" => ::Logger::WARN,
37
+ default: ::Logger::INFO
38
+ }
39
+
40
+ HELP_FLAGS = {
41
+ "--help" => true,
42
+ "-h" => true,
43
+ default: false
44
+ }
45
+
46
+ def initialize(args)
47
+ @args = args
48
+
49
+ @log_level = extract_w_lookup!(LOG_LEVEL_FLAGS) # get -v/--verbose and -q7--quiet flags
50
+ @command = extract_command! # extract the command
51
+ if @command == "help"
52
+ @help = true
53
+ @command = extract_command!
54
+ else
55
+ @help = extract_w_lookup!(HELP_FLAGS) # extract --help flag
56
+ end
57
+ end
58
+
59
+ def extract_w_lookup!(hsh)
60
+ value = hsh[:default]
61
+ @args.reject! do |str|
62
+ next unless hsh.key?(str)
63
+ value = hsh[str]
64
+ end
65
+ value
66
+ end
67
+
68
+ def extract_command!
69
+ return nil if /^-/ =~ @args.first
70
+ @args.shift
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,9 @@
1
+ require_relative "helper/help_on_command"
2
+ require_relative "helper/help"
3
+ require_relative "helper/short_help"
4
+
5
+ module Simple::CLI
6
+ module Helper
7
+ extend self
8
+ end
9
+ end
@@ -0,0 +1,60 @@
1
+ module Simple::CLI
2
+ module Helper
3
+ def help!(service, verbose:)
4
+ STDERR.puts <<~MSG
5
+ #{H.binary_name} <command> [ options... ]
6
+
7
+ Commands:
8
+
9
+ #{format_usages usages(service, verbose: verbose), prefix: " "}
10
+
11
+ Default options and commands include:
12
+
13
+ #{format_usages default_usages(service, verbose: verbose), prefix: " "}
14
+
15
+ MSG
16
+
17
+ exit 2
18
+ end
19
+
20
+ private
21
+
22
+ def usages(service, verbose:)
23
+ actions = ::Simple::Service.actions(service).values
24
+ actions = actions.select(&:short_description) unless verbose
25
+ actions = actions.sort_by(&:name)
26
+
27
+ actions.map do |action|
28
+ [ action_usage(action), action.short_description ]
29
+ end
30
+ end
31
+
32
+ def default_usages(service, verbose:)
33
+ _ = service
34
+ _ = verbose
35
+
36
+ [
37
+ [ "#{H.binary_name} help [ <command> ]", "print help for all or a specific command" ],
38
+ [ "#{H.binary_name} help -v", "show help for internal commands as well"],
39
+ [ "#{H.binary_name} [ --verbose | -v ]", "run on DEBUG log level"],
40
+ [ "#{H.binary_name} [ --quiet | -q ]", "run on WARN log level"],
41
+ ]
42
+ end
43
+
44
+ def format_usages(ary, prefix:)
45
+ # each entry is an Array of one or two entries. The first entry is a command usage
46
+ # string, the second entry is the command's short_description.
47
+ max_cmd_length = ary.inject(45) do |max, (cmd, _description)|
48
+ cmd.length > max ? cmd.length : max
49
+ end
50
+
51
+ ary.map do |cmd, description|
52
+ if description
53
+ format("#{prefix}%-#{max_cmd_length}s # %s", cmd, description)
54
+ else
55
+ format("#{prefix}%-#{max_cmd_length}s", cmd)
56
+ end
57
+ end.join("\n")
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ # rubocop:disable Metrics/AbcSize
2
+ # rubocop:disable Metrics/CyclomaticComplexity
3
+ # rubocop:disable Metrics/MethodLength
4
+
5
+ module Simple::CLI
6
+ module Helper
7
+ def help_on_command!(service, command, verbose:)
8
+ action = H.action_for_command(service, command)
9
+
10
+ parts = [
11
+ action.short_description,
12
+ action_usage(action),
13
+ action.full_description,
14
+ ].compact
15
+
16
+ STDERR.puts <<~MSG
17
+ #{parts.join("\n\n")}
18
+
19
+ MSG
20
+
21
+ exit 2
22
+ end
23
+
24
+ private
25
+
26
+ # Used in command_help.rb and in help.rb
27
+ def action_usage(action)
28
+ args = action.parameters.reject(&:keyword?).map do |param|
29
+ case param.kind
30
+ when :req then "<#{param.name}>"
31
+ when :opt then "[ <#{param.name}> ]"
32
+ when :rest then "[ <#{param.name}> .. ]"
33
+ end
34
+ end.compact
35
+
36
+ options = action.parameters.select(&:keyword?).map do |param|
37
+ if param.required?
38
+ "--#{name}=<#{name}>"
39
+ else
40
+ case param.default_value
41
+ when false then "[ --#{param.name} ]"
42
+ when true then "[ --no-#{param.name} ]"
43
+ when nil then "[ --#{param.name}=<#{param.name}> ]"
44
+ else "[ --#{param.name}=#{param.default_value} ]"
45
+ end
46
+ end
47
+ end
48
+
49
+ help = "#{H.binary_name} #{H.action_to_command(action.name)}"
50
+ help << " #{options.join(" ")}" unless options.empty?
51
+ help << " #{args.join(" ")}" unless args.empty?
52
+ help
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ module Simple::CLI
2
+ module Helper
3
+ def short_help!(service)
4
+ # We check if we have only a few number of actions. In that case we just show the full help instead.
5
+ actions = ::Simple::Service.actions(service).values
6
+ actions, hidden_actions = actions.partition(&:short_description)
7
+
8
+ STDERR.puts <<~MSG
9
+ #{H.binary_name} <command> [ options... ]
10
+
11
+ MSG
12
+
13
+ subcommands = actions.map { |action| "'" + H.action_to_command(action.name) + "'" }
14
+ msg = "Subcommands include #{subcommands.sort.join(", ")}"
15
+ msg += " (and an additional #{hidden_actions.count} internal commands)"
16
+
17
+ STDERR.puts <<~MSG
18
+ #{msg}. Default options and commands include:
19
+
20
+ #{format_usages default_usages(service, verbose: false), prefix: " "}
21
+
22
+ MSG
23
+
24
+ exit 2
25
+ end
26
+ end
27
+ end
@@ -1,7 +1,6 @@
1
1
  require "open3"
2
2
 
3
- # Helpers are mixed into all CLI modules. They implement the following methods,
4
- # mostly to help with integrating external commands:
3
+ # This module defines various helpers that might be useful for many CLI applications.
5
4
  #
6
5
  # - sys
7
6
  # - sys!
@@ -9,6 +8,9 @@ require "open3"
9
8
  # - die!
10
9
  #
11
10
  module Simple::CLI::Helpers
11
+ # The methods here must be private, lest they not show up as subcommands.
12
+ private
13
+
12
14
  def die!(msg)
13
15
  STDERR.puts msg
14
16
  exit 1
@@ -12,7 +12,9 @@ module Simple::CLI::Logger
12
12
  end
13
13
 
14
14
  def logger=(logger)
15
+ old_log_level = logger.level
15
16
  @logger = Adapter.new(logger)
17
+ @logger.level = old_log_level
16
18
  end
17
19
 
18
20
  private
@@ -1,5 +1,6 @@
1
- # rubocop:disable Metrics/AbcSize
2
1
  # rubocop:disable Metrics/MethodLength
2
+ # rubocop:disable Metrics/ModuleLength
3
+ # rubocop:disable Metrics/AbcSize
3
4
  # rubocop:disable Metrics/CyclomaticComplexity
4
5
  # rubocop:disable Metrics/PerceivedComplexity
5
6
 
@@ -0,0 +1,23 @@
1
+ # rubocop:disable Metrics/AbcSize
2
+ # rubocop:disable Metrics/MethodLength
3
+
4
+ module Simple::CLI
5
+ def self.on_exception(e)
6
+ msg = e.message
7
+ msg += " (#{e.class.name})" unless $!.class.name == "RuntimeError"
8
+
9
+ logger.error msg
10
+
11
+ raise(e) if Simple::CLI.logger.level == ::Logger::DEBUG
12
+
13
+ logger.info do
14
+ backtrace = e.backtrace.reject { |l| l =~ /simple-cli/ }
15
+ "called from\n " + backtrace[0, 10].join("\n ")
16
+ end
17
+
18
+ verbosity_hint = "(Backtraces are currently silenced. Run with --verbose to see backtraces.)"
19
+ logger.warn verbosity_hint
20
+
21
+ exit 2
22
+ end
23
+ end
@@ -1,213 +1,41 @@
1
- # rubocop:disable Metrics/AbcSize
2
- # rubocop:disable Metrics/ClassLength
3
- # rubocop:disable Metrics/CyclomaticComplexity
4
- # rubocop:disable Metrics/MethodLength
5
- # rubocop:disable Metrics/PerceivedComplexity
1
+ module Simple::CLI
2
+ module Runner
3
+ extend self
6
4
 
7
- class Simple::CLI::Runner
8
- end
9
-
10
- require_relative "runner/command_help"
5
+ def run!(service, command, *args, verbose:)
6
+ _ = verbose
11
7
 
12
- # A Runner object manages running a CLI application module with a set
13
- # of string arguments (usually taken from ARGV)
14
- class Simple::CLI::Runner
15
- def self.run(app, *args)
16
- new(app).run(*args)
17
- end
18
-
19
- def initialize(app)
20
- @app = app
21
- end
22
-
23
- def extract_default_flags!(args)
24
- args.reject! do |arg|
25
- case arg
26
- when "--verbose", "-v" then logger.level = Logger::DEBUG
27
- when "--quiet", "-q" then logger.level = Logger::WARN
8
+ action_name = H.command_to_action(command)
9
+ Simple::Service.with_context do
10
+ flags = extract_flags!(args)
11
+ ::Simple::Service.invoke(service, action_name, *args, **flags)
28
12
  end
29
13
  end
30
- end
31
14
 
32
- attr_accessor :subcommand
15
+ private
33
16
 
34
- def help(command)
35
- if command
36
- run "help", command
37
- else
38
- run "help"
39
- end
40
- end
17
+ # extract options from the array. Note: simple-cli is the correct place
18
+ # for this, and not simple-service, because it deals with a conversion
19
+ # which is strictly related to command line applications, and simple-service
20
+ # doesn't have any knowledge of CLI applications.
21
+ #
22
+ # This returns a hash of flag values, as determined by a "--flagname[=<value>]"
23
+ # command line options, and removes all such options from the arg array.
24
+ def extract_flags!(args)
25
+ flags = {}
41
26
 
42
- def run(*args)
43
- extract_default_flags!(args)
27
+ args.reject! do |arg|
28
+ next false unless arg =~ /^--(no-)?([^=]+)(=(.+))?/
44
29
 
45
- @instance = Object.new.extend(@app)
46
- command_name = args.shift || help!
47
- command = string_to_command(command_name)
48
-
49
- if command == :help
50
- do_help!(*args)
51
- elsif commands.include?(command)
52
- self.subcommand = command
53
- @instance.run! command, *args_with_options(args)
54
- else
55
- help!
56
- end
57
- rescue StandardError => e
58
- on_exception(e)
59
- end
60
-
61
- def has_subcommands?
62
- commands.length > 1
63
- end
64
-
65
- def do_help!(subcommand = nil)
66
- if !subcommand
67
- help!
68
- else
69
- help_subcommand!(subcommand)
70
- end
71
- end
30
+ flag_name = $2.tr("-", "_")
31
+ flag_name = "no_#{flag_name}" if $4 && $1
32
+ value = $4 || ($1 ? false : true)
72
33
 
73
- def help_subcommand!(subcommand)
74
- edoc = CommandHelp.new(@app, string_to_command(subcommand))
75
-
76
- puts <<~MSG
77
- #{help_for_command(subcommand)}
78
-
79
- #{edoc.full}
80
- MSG
81
-
82
- unless has_subcommands?
83
-
84
- STDERR.puts <<~MSG
85
-
86
- Default options include:
87
-
88
- #{binary_name} [ --help | -h ] ... print this help
89
- #{binary_name} [ --verbose | -v ] ... run on DEBUG log level
90
- #{binary_name} [ --quiet | -q ] ... run on WARN log level
91
- MSG
92
- end
93
- exit 1
94
- end
95
-
96
- def logger
97
- Simple::CLI.logger
98
- end
99
-
100
- def on_exception(e)
101
- raise(e) if Simple::CLI.logger.level == Logger::DEBUG
102
-
103
- verbosity_hint = "Backtraces are currently silenced. Run with --verbose to see backtraces."
104
-
105
- case e
106
- when ArgumentError
107
- logger.error e.message
108
- logger.warn verbosity_hint
109
- if subcommand
110
- help_subcommand! subcommand
111
- else
112
- help!
34
+ flags[flag_name.to_sym] = value
35
+ true
113
36
  end
114
- else
115
- msg = e.message
116
- msg += " (#{e.class.name})" unless $!.class.name == "RuntimeError"
117
- logger.error msg
118
- logger.warn verbosity_hint
119
- exit 2
120
- end
121
- end
122
37
 
123
- def args_with_options(args)
124
- r = []
125
- options = {}
126
- while (arg = args.shift)
127
- case arg
128
- when /^--(.*)=(.*)/ then options[$1.to_sym] = $2
129
- when /^--no-(.*)/ then options[$1.to_sym] = false
130
- when /^--(.*)/ then options[$1.to_sym] = true
131
- else r << arg
132
- end
38
+ flags
133
39
  end
134
-
135
- r << options unless options.empty?
136
- r
137
- end
138
-
139
- def command_to_string(sym)
140
- sym.to_s.tr("_", ":")
141
- end
142
-
143
- def string_to_command(s)
144
- s.to_s.tr(":", "_").to_sym
145
- end
146
-
147
- def commands
148
- @app.public_instance_methods(false).grep(/^[_a-zA-Z0-9]+$/)
149
- end
150
-
151
- def help_for_command(sym)
152
- cmd = string_to_command(sym)
153
- CommandHelp.new(@app, cmd).interface(binary_name, cmd, include_subcommand: has_subcommands?)
154
- end
155
-
156
- def binary_name
157
- $0.gsub(/.*\//, "")
158
- end
159
-
160
- def help!
161
- # collect help information on individual comments; when not on DEBUG
162
- # level skipping the commands that don't jave a command help.
163
- command_helps = commands.inject({}) do |hsh, sym|
164
- edoc = CommandHelp.new(@app, sym)
165
- next hsh if !edoc.head && logger.level != ::Logger::DEBUG
166
-
167
- hsh.update sym => help_for_command(sym)
168
- end
169
-
170
- # build a lambda which prints a help line with nice formatting
171
- max_length = command_helps.values.map(&:length).max
172
- print_help_line = lambda do |cmd, description|
173
- if description
174
- STDERR.puts format(" %-#{max_length}s # %s", cmd, description)
175
- else
176
- STDERR.puts format(" %-#{max_length}s", cmd)
177
- end
178
- end
179
-
180
- # print help for commands
181
- STDERR.puts "Usage:\n\n"
182
-
183
- command_helps.keys.sort.each do |sym|
184
- command_help = command_helps[sym]
185
- edoc = CommandHelp.new(@app, sym)
186
- print_help_line.call command_help, edoc.head
187
- end
188
-
189
- # print help for default commands
190
-
191
- STDERR.puts <<~DOC
192
-
193
- Default options include:
194
-
195
- DOC
196
-
197
- print_help_line.call "#{binary_name} [ --verbose | -v ]", "run on DEBUG log level"
198
- print_help_line.call "#{binary_name} [ --quiet | -q ]", "run on WARN log level"
199
-
200
- STDERR.puts <<~DOC
201
-
202
- Other commands:
203
-
204
- DOC
205
-
206
- print_help_line.call "#{binary_name} help [ subcommand ]", "print help on a specific subcommand"
207
- print_help_line.call "#{binary_name} help -v", "show help for internal commands as well"
208
-
209
- STDERR.puts "\n"
210
-
211
- exit 1
212
40
  end
213
41
  end
data/simple-cli.gemspec CHANGED
@@ -5,11 +5,10 @@
5
5
 
6
6
  lib = File.expand_path('../lib', __FILE__)
7
7
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
8
- require 'simple/cli/version'
9
8
 
10
9
  Gem::Specification.new do |gem|
11
10
  gem.name = "simple-cli"
12
- gem.version = Simple::CLI::VERSION
11
+ gem.version = File.read("VERSION")
13
12
 
14
13
  gem.authors = [ "radiospiel", "mediapeers GmbH" ]
15
14
  gem.email = "eno@radiospiel.org"
@@ -28,6 +27,7 @@ Gem::Specification.new do |gem|
28
27
  gem.required_ruby_version = '~> 2.3'
29
28
 
30
29
  # optional gems (required by some of the parts)
30
+ gem.add_dependency "simple-service", "~> 0.1.2"
31
31
 
32
32
  # development gems
33
33
  gem.add_development_dependency 'rake', '~> 12'
data/spec/spec_helper.rb CHANGED
@@ -16,6 +16,5 @@ RSpec.configure do |config|
16
16
  config.run_all_when_everything_filtered = true
17
17
  config.filter_run focus: (ENV["CI"] != "true")
18
18
  config.expect_with(:rspec) { |c| c.syntax = :expect }
19
- config.include FactoryGirl::Syntax::Methods
20
19
  config.order = "random"
21
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - radiospiel
@@ -9,8 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2019-11-16 00:00:00.000000000 Z
12
+ date: 2019-11-29 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: simple-service
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: 0.1.2
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: 0.1.2
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: rake
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -77,18 +91,22 @@ files:
77
91
  - ".rubocop.yml"
78
92
  - Gemfile
79
93
  - Rakefile
94
+ - VERSION
80
95
  - bin/rake
81
96
  - lib/simple-cli.rb
82
97
  - lib/simple/cli.rb
83
98
  - lib/simple/cli/adapter.rb
99
+ - lib/simple/cli/default_options.rb
100
+ - lib/simple/cli/helper.rb
101
+ - lib/simple/cli/helper/help.rb
102
+ - lib/simple/cli/helper/help_on_command.rb
103
+ - lib/simple/cli/helper/short_help.rb
84
104
  - lib/simple/cli/helpers.rb
85
105
  - lib/simple/cli/logger.rb
86
106
  - lib/simple/cli/logger/adapter.rb
87
107
  - lib/simple/cli/logger/colored_logger.rb
88
- - lib/simple/cli/pp.rb
108
+ - lib/simple/cli/on_exception.rb
89
109
  - lib/simple/cli/runner.rb
90
- - lib/simple/cli/runner/command_help.rb
91
- - lib/simple/cli/runner/module_ex.rb
92
110
  - lib/simple/cli/version.rb
93
111
  - log/.gitkeep
94
112
  - simple-cli.gemspec
data/lib/simple/cli/pp.rb DELETED
@@ -1,12 +0,0 @@
1
- # rubocop:disable Lint/HandleExceptions
2
-
3
- require "pp"
4
-
5
- begin
6
- require "awesome_print"
7
-
8
- def pp(*args)
9
- ap(*args)
10
- end
11
- rescue LoadError
12
- end
@@ -1,120 +0,0 @@
1
- # rubocop:disable Metrics/AbcSize
2
- # rubocop:disable Metrics/CyclomaticComplexity
3
- # rubocop:disable Metrics/MethodLength
4
-
5
- require_relative "./module_ex"
6
-
7
- class Simple::CLI::Runner::CommandHelp
8
- def self.option_names(app, subcommand)
9
- new(app, subcommand).option_names
10
- rescue NameError
11
- []
12
- end
13
-
14
- def initialize(mod, method_id)
15
- raise(ArgumentError, "#{method_id.inspect} should be a Symbol") unless method_id.is_a?(Symbol)
16
-
17
- @method_id = method_id
18
- @method = mod.instance_method(@method_id)
19
- @method_parameters_ex = mod.method_parameters_ex(@method_id)
20
- end
21
-
22
- # First line of the help as read from the method comments.
23
- def head
24
- comments.first
25
- end
26
-
27
- # Full help as read from the method comments
28
- def full
29
- comments.join("\n") if comments.first
30
- end
31
-
32
- def option_names
33
- option_names = @method_parameters_ex.map do |mode, name, _|
34
- case mode
35
- when :key then name
36
- when :keyreq then name
37
- end
38
- end.compact
39
-
40
- option_names.map do |name|
41
- ["--#{name}", "--#{name}="]
42
- end.flatten
43
- end
44
-
45
- # A help string constructed from the commands method signature.
46
- def interface(binary_name, command_name, include_subcommand: false)
47
- args = @method_parameters_ex.map do |mode, name|
48
- case mode
49
- when :req then "<#{name}>"
50
- when :opt then "[ <#{name}> ]"
51
- when :rest then "[ <#{name}> .. ]"
52
- end
53
- end.compact
54
-
55
- options = @method_parameters_ex.map do |mode, name, default_value|
56
- case mode
57
- when :key then
58
- case default_value
59
- when false then "[ --#{name} ]"
60
- when true then "[ --no-#{name} ]"
61
- when nil then "[ --#{name}=<#{name}> ]"
62
- else "[ --#{name}=#{default_value} ]"
63
- end
64
- when :keyreq then
65
- "--#{name}=<#{name}>"
66
- end
67
- end.compact
68
-
69
- help = "#{binary_name}"
70
- help << " #{command_to_string(command_name)}" if include_subcommand
71
- help << " #{options.join(' ')}" unless options.empty?
72
- help << " #{args.join(' ')}" unless args.empty?
73
- help
74
- end
75
-
76
- private
77
-
78
- def command_to_string(s)
79
- s.to_s.tr("_", ":")
80
- end
81
-
82
- def comments
83
- @comments ||= begin
84
- file, line = @method.source_location
85
- extract_comments(from: parsed_source(file), before_line: line)
86
- end
87
- end
88
-
89
- # reads the source \a file and turns each non-comment into :code and each comment
90
- # into a string without the leading comment markup.
91
- def parsed_source(file)
92
- File.readlines(file).map do |line|
93
- case line
94
- when /^\s*# ?(.*)$/ then $1
95
- when /^\s*end/ then :end
96
- end
97
- end
98
- end
99
-
100
- def extract_comments(from:, before_line:)
101
- parsed_source = from
102
-
103
- # go down from before_line until we see a line which is either a comment
104
- # or an :end. Note that the line at before_line-1 should be the first
105
- # line of the method definition in question.
106
- last_line = before_line - 1
107
- last_line -= 1 while last_line >= 0 && !parsed_source[last_line]
108
-
109
- first_line = last_line
110
- first_line -= 1 while first_line >= 0 && parsed_source[first_line]
111
- first_line += 1
112
-
113
- comments = parsed_source[first_line..last_line]
114
- if comments.include?(:end)
115
- []
116
- else
117
- parsed_source[first_line..last_line]
118
- end
119
- end
120
- end
@@ -1,69 +0,0 @@
1
- # rubocop:disable Metrics/MethodLength
2
- # rubocop:disable Metrics/AbcSize
3
-
4
- class Module
5
- #
6
- # returns an array with entries like the following:
7
- #
8
- # [ :key, name, default_value ]
9
- # [ :keyreq, name [, nil ] ]
10
- # [ :req, name [, nil ] ]
11
- # [ :opt, name [, nil ] ]
12
- # [ :rest, name [, nil ] ]
13
- #
14
- def method_parameters_ex(method_id)
15
- method = instance_method(method_id)
16
- parameters = method.parameters
17
-
18
- # method parameters with a :key mode are optional keyword arguments. We only
19
- # support defaults for those - if there are none we abort here already.
20
- keys = parameters.map { |mode, name| name if mode == :key }.compact
21
- return parameters if keys.empty?
22
-
23
- # We are now doing a fake call to the method, with a minimal viable set of
24
- # arguments, to let the ruby runtime fill in default values for arguments.
25
- # We do not, however, let the call complete. Instead we use a TracePoint to
26
- # abort as soon as the method is called, and use the its binding to determine
27
- # the default values.
28
-
29
- fake_recipient = Object.new.extend(self)
30
- fake_call_args = minimal_arguments(method)
31
-
32
- trace_point = TracePoint.trace(:call) do |tp|
33
- throw :received_fake_call, tp.binding if tp.defined_class == self && tp.method_id == method_id
34
- end
35
-
36
- bnd = catch(:received_fake_call) do
37
- fake_recipient.send(method_id, *fake_call_args)
38
- end
39
-
40
- trace_point.disable
41
-
42
- # extract default values from the received binding, and merge with the
43
- # parameters array.
44
- default_values = keys.each_with_object({}) do |key_parameter, hsh|
45
- hsh[key_parameter] = bnd.local_variable_get(key_parameter)
46
- end
47
-
48
- parameters.map do |mode, name|
49
- [mode, name, default_values[name]]
50
- end
51
- end
52
-
53
- private
54
-
55
- # returns a minimal Array of arguments, which is suitable for a call to the method
56
- def minimal_arguments(method)
57
- # Build an arguments array with holds all required parameters. The actual
58
- # values for these arguments doesn't matter at all.
59
- args = method.parameters.select { |mode, _name| mode == :req }
60
-
61
- # Add a hash with all required keyword arguments
62
- required_keyword_args = method.parameters.each_with_object({}) do |(mode, name), hsh|
63
- hsh[name] = :anything if mode == :keyreq
64
- end
65
- args << required_keyword_args if required_keyword_args
66
-
67
- args
68
- end
69
- end