atli 0.1.9 → 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
data/lib/thor/command.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'semantic_logger'
2
- require_relative './completion/bash'
1
+ require 'shellwords'
2
+ require 'nrser'
3
3
 
4
4
  class Thor
5
5
  class Command < Struct.new( :name,
@@ -7,10 +7,11 @@ class Thor
7
7
  :long_description,
8
8
  :usage,
9
9
  :examples,
10
+ :arguments,
10
11
  :options,
11
12
  :ancestor_name )
12
- include SemanticLogger::Loggable
13
- include Thor::Completion::Bash::Command
13
+
14
+ include NRSER::Log::Mixin
14
15
 
15
16
  FILE_REGEXP = /^#{Regexp.escape(File.dirname(__FILE__))}/
16
17
 
@@ -19,6 +20,7 @@ class Thor
19
20
  long_description: nil,
20
21
  usage: nil,
21
22
  examples: [],
23
+ arguments: [],
22
24
  options: nil
23
25
  super \
24
26
  name.to_s,
@@ -26,6 +28,7 @@ class Thor
26
28
  long_description,
27
29
  usage,
28
30
  examples,
31
+ arguments,
29
32
  options || {}
30
33
  end
31
34
 
@@ -176,10 +179,11 @@ class Thor
176
179
  formatted ||= "".dup
177
180
 
178
181
  # Add usage with required arguments
179
- formatted << if klass && !klass.arguments.empty?
182
+ arguments = klass&.arguments( command: self ) || []
183
+ formatted << unless arguments.empty?
180
184
  usage.to_s.gsub(/^#{name}/) do |match|
181
185
  match << " " \
182
- << klass.arguments.map(&:usage).compact.join(" ")
186
+ << arguments.map(&:usage).compact.join(" ")
183
187
  end
184
188
  else
185
189
  usage.to_s
@@ -192,6 +196,47 @@ class Thor
192
196
  formatted.strip
193
197
  end
194
198
 
199
+
200
+ # The command's name as depicted in it's {#usage} message.
201
+ #
202
+ # We prefer this format when completing commands because it's how we
203
+ # depict the command to the user.
204
+ #
205
+ # @see Thor::Completion
206
+ # @see #names_by_format
207
+ #
208
+ # @return [String]
209
+ #
210
+ def usage_name
211
+ @usage_name ||= usage.shellsplit[0]
212
+ end
213
+
214
+
215
+ # The name formats we recognize for the command, in command completion
216
+ # resolution order.
217
+ #
218
+ # @note
219
+ # In reality, since input words have `-` replaced with `_` when finding
220
+ # their instances during execution,
221
+ #
222
+ # @return [Hash<Symbol, String>]
223
+ # Keys and values in order (and order matters when completing commands,
224
+ # first result takes priority):
225
+ #
226
+ # 1. `usage:` {#usage_name}
227
+ # 2. `method:` {#name}, which is the command's actual method name,
228
+ # and hence "underscored".
229
+ # 3. `dashed:` A dash-separated version of {#name}.
230
+ #
231
+ def names_by_format
232
+ @names_by_format ||= {
233
+ usage: usage_name,
234
+ method: name,
235
+ dashed: name.dasherize,
236
+ }.freeze
237
+ end
238
+
239
+
195
240
  protected
196
241
 
197
242
  def not_debugging?(instance)
@@ -1,27 +1,15 @@
1
1
  # encoding: UTF-8
2
2
  # frozen_string_literal: true
3
3
 
4
- # Requirements
5
- # =======================================================================
6
-
7
- # Stdlib
8
- # -----------------------------------------------------------------------
9
-
10
- # Deps
11
- # -----------------------------------------------------------------------
12
-
13
- require 'nrser'
14
- require 'nrser/labs/i8'
15
4
 
16
5
  # Project / Package
17
6
  # -----------------------------------------------------------------------
18
7
 
19
-
20
- # Refinements
21
- # =======================================================================
22
-
23
- require 'nrser/refinements/types'
24
- using NRSER::Types
8
+ require_relative './bash/argument_mixin'
9
+ require_relative './bash/command_mixin'
10
+ require_relative './bash/request'
11
+ require_relative './bash/subcmd'
12
+ require_relative './bash/thor_mixin'
25
13
 
26
14
 
27
15
  # Namespace
@@ -36,122 +24,79 @@ module Completion
36
24
 
37
25
  # Experimental support for Bash completions.
38
26
  #
27
+ # To enable, require this module and add the following to your entry-point
28
+ # {Thor} subclass:
29
+ #
30
+ # include Thor::Completion::Bash
31
+ #
32
+ # You should now have a `bash-complete` subcommand present. You can test
33
+ # this out with
34
+ #
35
+ # YOUR_EXE bash-complete help
36
+ #
37
+ # where `YOUR_EXE` is replaced with your executable name.
38
+ #
39
+ # You should see output like
40
+ #
41
+ # Commands:
42
+ # locd bash-complete complete -- CUR PREV CWORD SPLIT WORDS... # (...)
43
+ # locd bash-complete help [COMMAND] # (...)
44
+ # locd bash-complete setup # (...)
45
+ #
46
+ #
47
+ # Then, to hook your executable into Bash's `compelte` builtin:
48
+ #
49
+ # source <(YOUR_EXE bash-complete setup)
50
+ #
51
+ # where, again, `YOUR_EXE` is replaced with your executable name.
52
+ #
39
53
  module Bash
40
54
 
41
- Request = I8::Struct.new \
42
- cur: t.str,
43
- prev: t.str,
44
- cword: t.non_neg_int,
45
- split: t.bool,
46
- words: t.array( t.str )
47
-
48
- # Needs to be mixed in to {Thor}. It's all class methods at the moment.
55
+ # Hook to setup Bash complete on including {Thor} subclass.
49
56
  #
50
- # @todo
51
- # Deal with that {Thor::Group} thing? I never use it...
52
- #
53
- module Thor
54
-
55
- # Methods to be mixed as class methods in to {Thor}.
56
- #
57
- module ClassMethods
58
-
59
- # Get this class' {Thor::Command} instances, keyed by how their names will
60
- # appear in the UI (replace `_` with `-`).
61
- #
62
- # @param [Boolean] include_hidden:
63
- # When `true`, "hidden" commands will also be included.
64
- #
65
- # @return [Hash<String, Thor::Command>]
66
- #
67
- def all_commands_by_ui_name include_hidden: false
68
- all_commands.
69
- each_with_object( {} ) { |(name, cmd), hash|
70
- next if cmd.hidden? && !include_hidden
71
- hash[ name.tr( '_', '-' ) ] = cmd
72
- }
73
- end # .all_commands_by_ui_name
74
-
75
- #
76
- #
77
- def bash_complete comp_req:, index:
78
- # Find the command, if any
79
-
80
- logger.info __method__,
81
- comp_req: comp_req,
82
- index: index,
83
- word: comp_req.words[index]
84
-
85
- scan_index = index
86
-
87
- while (comp_req.words[scan_index] || '').start_with? '-'
88
- scan_index += 1
89
- end
90
-
91
- cmd_ui_name = comp_req.words[scan_index] || ''
92
-
93
- cmd = all_commands_by_ui_name[cmd_ui_name]
94
-
95
- if cmd.nil?
96
- return all_commands_by_ui_name.keys.select { |ui_name|
97
- ui_name.start_with? cmd_ui_name
98
- }
99
- end
100
-
101
- index = scan_index + 1
102
-
103
- # is it a subcommand?
104
- if subcommand_classes.key? cmd.name
105
- # It is, hand it off to there
106
- subcommand_classes[cmd.name].bash_complete \
107
- comp_req: comp_req,
108
- index: index
109
- else
110
- # It's a command, have that handle it
111
- cmd.bash_complete \
112
- comp_req: comp_req,
113
- index: index
114
- end
115
- end
116
-
117
- end # module ClassMethods
118
-
119
- # Hook to mix {ClassMethods} in on include.
120
- #
121
- def self.included base
122
- base.extend ClassMethods
123
- end
124
-
125
- end # module Thor
126
-
127
-
128
- # Methods that need to mixed in to {Thor::Command}.
57
+ # 1. Mixes {ThorMixin} into {Thor}.
58
+ # 2. Mixes {CommandMixin} into {Thor::Command}.
59
+ # 3. Creates a new subclass of {Subcmd} boun0d to `base` and adds that
60
+ # as `bash-complete` to `base` via {Thor.subcommand}.
61
+ #
62
+ # @param [Class<Thor>] base
63
+ # The class that inluded {Thor::Completion::Bash}. Should be a {Thor}
64
+ # subclass and be the main/entry command of the program, though neither
65
+ # of these are enforced.
129
66
  #
130
- module Command
67
+ # @return [nil]
68
+ # Totally side-effect based.
69
+ #
70
+ def self.included base
131
71
 
132
- def bash_complete comp_req:, index:
133
- # TODO Handle
134
- return [] if comp_req.split
135
-
136
- logger.info __method__,
137
- cmd_name: name,
138
- options: options
139
-
140
- options.
141
- each_with_object( [ '--help' ] ) { |(name, opt), results|
142
- ui_name = name.to_s.tr( '_', '-' )
143
-
144
- if opt.type == :boolean
145
- results << "--#{ ui_name }"
146
- results << "--no-#{ ui_name }"
147
- else
148
- results << "--#{ ui_name }="
149
- end
150
- }.
151
- select { |term| term.start_with? comp_req.cur }
72
+ unless Thor.include? ThorMixin
73
+ Thor.send :include, ThorMixin
74
+ end
75
+
76
+ unless Thor::Command.include? CommandMixin
77
+ Thor::Command.send :include, CommandMixin
78
+ end
79
+
80
+ unless Thor::Argument.include? ArgumentMixin
81
+ Thor::Argument.send :include, ArgumentMixin
152
82
  end
83
+
84
+ subcmd_class = Class.new Subcmd do
85
+ def self.target
86
+ @target
87
+ end
88
+ end
89
+
90
+ subcmd_class.instance_variable_set :@target, base
153
91
 
154
- end # module Command
92
+ # Install {Subcmd} as a subcommand
93
+ base.send :subcommand,
94
+ 'bash-complete',
95
+ subcmd_class,
96
+ desc: "Support for Bash command completion."
97
+
98
+ nil
99
+ end # #.included
155
100
 
156
101
  end # module Bash
157
102
 
@@ -0,0 +1,83 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Requirements
5
+ # =======================================================================
6
+
7
+ # Stdlib
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Deps
11
+ # -----------------------------------------------------------------------
12
+
13
+ require 'nrser'
14
+ require 'nrser/labs/i8'
15
+
16
+ # Project / Package
17
+ # -----------------------------------------------------------------------
18
+
19
+
20
+ # Refinements
21
+ # =======================================================================
22
+
23
+ require 'nrser/refinements/types'
24
+ using NRSER::Types
25
+
26
+
27
+ # Namespace
28
+ # =======================================================================
29
+
30
+ class Thor
31
+ module Completion
32
+ module Bash
33
+
34
+
35
+ # Definitions
36
+ # =======================================================================
37
+
38
+ # Methods mixed in to {Thor::Argument}.
39
+ #
40
+ module ArgumentMixin
41
+
42
+ def bash_complete request:, klass:
43
+ # logger.level = :trace
44
+
45
+ logger.trace "ENTERING #{ self.class }##{ __method__ }",
46
+ name: name,
47
+ complete: complete,
48
+ request: request,
49
+ klass: klass
50
+
51
+ unless complete
52
+ return [].tap { |results|
53
+ logger.trace "No `#complete` proc to call",
54
+ results: results
55
+ }
56
+ end
57
+
58
+ values = case complete.arity
59
+ when 0
60
+ complete.call
61
+ else
62
+ complete.call request: request, klass: klass, command: self
63
+ end
64
+
65
+ logger.trace "Got values", values: values
66
+
67
+ values.
68
+ select { |value| value.start_with? request.cur }.
69
+ tap { |results|
70
+ logger.trace "Selected values for argument #{ name }",
71
+ results: results
72
+ }
73
+ end
74
+
75
+ end # module ArgumentMixin
76
+
77
+
78
+ # /Namespace
79
+ # =======================================================================
80
+
81
+ end # module Bash
82
+ end # module Completion
83
+ end # class Thor
@@ -0,0 +1,236 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Requirements
5
+ # =======================================================================
6
+
7
+ # Stdlib
8
+ # -----------------------------------------------------------------------
9
+
10
+ # Deps
11
+ # -----------------------------------------------------------------------
12
+
13
+ require 'nrser'
14
+ require 'nrser/labs/i8'
15
+
16
+ # Project / Package
17
+ # -----------------------------------------------------------------------
18
+
19
+
20
+ # Refinements
21
+ # =======================================================================
22
+
23
+ require 'nrser/refinements/types'
24
+ using NRSER::Types
25
+
26
+
27
+ # Namespace
28
+ # =======================================================================
29
+
30
+ class Thor
31
+ module Completion
32
+ module Bash
33
+
34
+
35
+ # Definitions
36
+ # =======================================================================
37
+
38
+ # Methods mixed in to {Thor::Command}.
39
+ #
40
+ module CommandMixin
41
+
42
+ def bash_complete_cur_split request:
43
+ # The input was split at a `=`, so we're looking for a value for
44
+ # an option whose name will be at {Request#prev}.
45
+
46
+ logger.trace "Processing split Bash complete request...",
47
+ command: self.name,
48
+ request: request
49
+
50
+ matching_options = options.values.select { |option|
51
+ option.long_switch_tokens.
52
+ reject { |token| token.end_with? '=' }.
53
+ include? request.prev
54
+ }
55
+
56
+ case matching_options.length
57
+ when 0
58
+ return [].tap { |results|
59
+ logger.trace "No matching options found",
60
+ results: results,
61
+ options: options.transform_values { |option|
62
+ {
63
+ tokens: option.all_switch_tokens,
64
+ names: option.all_switch_names,
65
+ }
66
+ }
67
+ }
68
+
69
+ when 1
70
+ option = matching_options[0]
71
+
72
+ logger.trace "Unique option found for `request.prev`",
73
+ prev: request.prev,
74
+ option: option
75
+
76
+ if option.enum
77
+ return option.enum.
78
+ select { |value|
79
+ value.to_s.start_with? request.cur
80
+ }.
81
+ tap { |results|
82
+ logger.trace \
83
+ "Matched against enum option #{ option.name }",
84
+ prev: request.prev,
85
+ results: results,
86
+ option: option
87
+ }
88
+
89
+ elsif option.complete
90
+ return option.complete.call.
91
+ select { |value|
92
+ value.to_s.start_with? request.cur
93
+ }.
94
+ tap { |results|
95
+ logger.trace \
96
+ "Matched against complete option #{ option.name }",
97
+ prev: request.prev,
98
+ results: results,
99
+ option: option
100
+ }
101
+
102
+ else
103
+ return [].tap { |results|
104
+ logger.trace \
105
+ ( "Matched against non-enum option #{ option.name } " +
106
+ "but we don't have any completions to provide" ),
107
+ prev: request.prev,
108
+ results: results,
109
+ option: option
110
+ }
111
+ end
112
+
113
+ else
114
+ return [].tap { |results|
115
+ logger.trace "Multiple options found for prev",
116
+ results: results,
117
+ prev: request.prev,
118
+ options: matching_options
119
+ }
120
+ end
121
+ end
122
+
123
+
124
+ def bash_complete_cur request:, arg_count:, klass:
125
+ return bash_complete_cur_split( request: request ) if request.split
126
+
127
+ if request.cur == ''
128
+ results = [ '--help' ]
129
+
130
+ argument = klass.arguments( command: self )[arg_count]
131
+
132
+ if argument
133
+ results += argument.bash_complete( request: request, klass: klass )
134
+ end
135
+
136
+ results += options.values.flat_map { |option| option.long_switch_tokens }
137
+
138
+ return results.
139
+ tap { |results|
140
+ logger.trace "`request.cur` is ''; returning all long opts",
141
+ results: results
142
+ }
143
+ end
144
+
145
+ if request.cur.start_with? '-'
146
+ return options.values.flat_map { |option|
147
+ option.long_switch_names.flat_map { |name|
148
+ if option.boolean?
149
+ [ "--#{ name }", "--no-#{ name }" ]
150
+ else
151
+ "--#{ name }="
152
+ end
153
+ }
154
+ }.
155
+ +( [ '--help' ] ).
156
+ select { |token| token.start_with? request.cur }.
157
+ tap { |results|
158
+ logger.trace "Completing partial switch token",
159
+ results: results
160
+ }
161
+ end
162
+
163
+ argument = klass.arguments( command: self )[arg_count]
164
+ if argument
165
+ return argument.
166
+ bash_complete( request: request, klass: klass).
167
+ tap { |results|
168
+ logger.trace "We b here"
169
+ }
170
+ end
171
+
172
+ return []
173
+ end
174
+
175
+
176
+ def bash_complete request:, index:, klass:
177
+ # logger.level = :trace
178
+
179
+ logger.trace "ENTERING #{ self.class }##{ __method__ }",
180
+ request: request,
181
+ index: index,
182
+ index_word: request.words[index]
183
+
184
+ # Index we'll increment as we scan past options
185
+ scan_index = index
186
+ arg_count = 0
187
+
188
+ # Skip over args (for now)
189
+ while scan_index < request.cword &&
190
+ scan_index < request.words.length &&
191
+ !request.words[scan_index].start_with?( '-' )
192
+ scan_index += 1
193
+ arg_count += 1
194
+ end
195
+
196
+ if scan_index == request.cword ||
197
+ ( request.split &&
198
+ scan_index == request.cword - 1 )
199
+ return bash_complete_cur request: request, arg_count: arg_count, klass: klass
200
+ end
201
+
202
+ unless scan_index < request.words.length
203
+ # We ran out of words without hitting a command name or ending
204
+ # empty string (for which we would provide all commands as options)
205
+ #
206
+ # TODO In the future we can deal with half-written class options
207
+ # I guess? Maybe? But for now we just give up.
208
+ #
209
+ return [].tap { |results|
210
+ logger.trace "No option or empty string found",
211
+ results: []
212
+ }
213
+ end
214
+
215
+ # match_word = request.words[scan_index]
216
+
217
+ # matching_options = options.values.select { |option|
218
+ # option.all_switch_tokens.any? { |token| token.start_with? match_word }
219
+ # }
220
+
221
+ return [].tap { |results|
222
+ logger.trace "Not implemented",
223
+ results: results
224
+ }
225
+
226
+ end
227
+
228
+ end # module CommandMixin
229
+
230
+
231
+ # /Namespace
232
+ # =======================================================================
233
+
234
+ end # module Bash
235
+ end # module Completion
236
+ end # class Thor