atli 0.1.9 → 0.1.10

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.
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