simple-cli 0.3.0 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
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