completely 0.7.6 → 0.8.0.rc3

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: b709faa1af61168b7ead8e09a70dc68a2c38fe20e0ba976b0fb9243e0f69b210
4
- data.tar.gz: af78d6cac9356d1fa5fb54b73e73967b7026a392b682f2f3cb3dc915356323a8
3
+ metadata.gz: c08af2e45d441174aa7ba877f55ce3ab49ed37c066abba5b522f404226c4fb09
4
+ data.tar.gz: 4096cfb3dd365eece6ad5801cde77ca2127fe0500404839949d8e4b6a3c0ec85
5
5
  SHA512:
6
- metadata.gz: fddaadff6dd7131f0eea16e68f73a1d410c3dc9f1303b6298107a537f4a5c7daa854a7552cf07f0ab322d90661760cd36783f233bb6ed0d0614cf63fb28a551d
7
- data.tar.gz: 128153a3250521b993fe197d65cee5c5a0d260ccd073ff0d4539209d2ddc58f51b7e8b81ee5974365f1036943cacd244992cefadcef054d86026d3daff15cfd3
6
+ metadata.gz: b8d65f92118b3700b77803005d936a3b1ddee313936207387496d7685bd6361ffcb3fbe6c28bd8c3b5b7fb7d4d1bd1b259e1e898da1b9739df07b99e17e89c1f
7
+ data.tar.gz: 398311bc0f57a4722a10d37776aad1a4e0651986a35832fc54a087618fcc3995543a21e35880deaa8ada9b88bfbc91183201c0a8bdf174ebb69ccd34e6233006
data/README.md CHANGED
@@ -39,20 +39,15 @@ $ alias completely='docker run --rm -it --user $(id -u):$(id -g) --volume "$PWD:
39
39
 
40
40
  ## Configuration syntax
41
41
 
42
- Completely works with a simple YAML configuration file as input, and generates
43
- a bash completions script as output.
42
+ Completely works with a YAML configuration file as input, and generates a bash
43
+ completions script as output.
44
44
 
45
- The configuration file is built of blocks that look like this:
45
+ There are three configuration formats:
46
46
 
47
- ```yaml
48
- pattern:
49
- - --argument
50
- - --param
51
- - command
52
- ```
53
-
54
- Each pattern contains an array of words (or functions) that will be suggested
55
- for the auto complete process.
47
+ - **Pattern config**: Recommended for new projects. It describes command shapes,
48
+ option groups, and value sources explicitly.
49
+ - **Flat config**: The original simple pattern-to-suggestions format.
50
+ - **Nested config**: A nested spelling of the flat config format.
56
51
 
57
52
  You can save a sample YAML file by running:
58
53
 
@@ -60,165 +55,162 @@ You can save a sample YAML file by running:
60
55
  $ completely init
61
56
  ```
62
57
 
63
- This will generate a file named `completely.yaml` with this content:
64
-
65
- ```yaml
66
- mygit:
67
- - -h
68
- - -v
69
- - --help
70
- - --version
71
- - init
72
- - status
73
-
74
- mygit init:
75
- - --bare
76
- - <directory>
77
-
78
- mygit status:
79
- - --help
80
- - --verbose
81
- - --branch
82
- - -b
83
-
84
- mygit status*--branch: &branches
85
- - $(git branch --format='%(refname:short)' 2>/dev/null)
58
+ This creates a `completely.yaml` file using the recommended pattern config
59
+ format. You can also choose a format explicitly:
86
60
 
87
- mygit status*-b: *branches
61
+ ```bash
62
+ $ completely init --format pattern
63
+ $ completely init --format flat
64
+ $ completely init --format nested
88
65
  ```
89
66
 
90
- Each pattern in this configuration file will be checked against the user's
91
- input, and if the input matches the pattern, the list that follows it will be
92
- suggested as completions.
93
-
94
- Note that the suggested completions will not show flags (strings that start with
95
- a hyphen `-`) unless the input ends with a hyphen.
96
-
97
- To generate the bash script, simply run:
67
+ To generate the bash script, run:
98
68
 
99
69
  ```bash
100
70
  $ completely generate
101
71
 
102
- # or, to just preview it without saving:
72
+ # or, to preview it without saving:
103
73
  $ completely preview
104
74
  ```
105
75
 
106
- For more options (like setting input/output path), run:
76
+ For more options, run:
107
77
 
108
78
  ```bash
109
79
  $ completely --help
110
80
  ```
111
81
 
112
- ### Suggesting files, directories and other bash built-ins
82
+ ### Pattern config
113
83
 
114
- In addition to specifying a simple array of completion words, you may use
115
- the special syntax `<..>` to suggest more advanced functions.
84
+ Pattern config is the recommended format for new completion files.
116
85
 
117
86
  ```yaml
118
- pattern:
119
- - <file>
120
- - <directory>
87
+ patterns:
88
+ - mygit [root options]
89
+ - mygit init [init options] <directory>
90
+ - mygit status [status options]
91
+
92
+ options:
93
+ root:
94
+ - -h|--help
95
+ - -v|--version
96
+ init:
97
+ - --bare
98
+ status:
99
+ - --help
100
+ - --branch|-b <branch>
101
+ - --format <format>
102
+ - --verbose (repeatable)
103
+
104
+ tokens:
105
+ directory: +directory
106
+ branch: $(git branch --format='%(refname:short)' 2>/dev/null)
107
+ format: [short, long]
121
108
  ```
122
109
 
123
- These suggestions will add the list of files and directories
124
- (when `<file>` is used) or just directories (when `<directory>` is used) to
125
- the list of suggestions.
110
+ The `patterns` section describes valid command shapes:
126
111
 
127
- You may use any of the below keywords to add additional suggestions:
112
+ - Plain words are command words, for example `mygit`, `init`, and `status`.
113
+ - Command aliases can be written with `|`, for example `status|st`.
114
+ - `[name options]` references `options.name`. `[name]` is also accepted.
115
+ - `<token>` references `tokens.token`.
116
+ - `<token>...` marks the final positional as repeatable.
128
117
 
129
- | Keyword | Meaning
130
- |---------------|---------------------
131
- | `<alias>` | Alias names
132
- | `<arrayvar>` | Array variable names
133
- | `<binding>` | Readline key binding names
134
- | `<builtin>` | Names of shell builtin commands
135
- | `<command>` | Command names
136
- | `<directory>` | Directory names
137
- | `<disabled>` | Names of disabled shell builtins
138
- | `<enabled>` | Names of enabled shell builtins
139
- | `<export>` | Names of exported shell variables
140
- | `<file>` | File names
141
- | `<function>` | Names of shell functions
142
- | `<group>` | Group names
143
- | `<helptopic>` | Help topics as accepted by the help builtin
144
- | `<hostname>` | Hostnames, as taken from the file specified by the HOSTFILE shell variable
145
- | `<job>` | Job names
146
- | `<keyword>` | Shell reserved words
147
- | `<running>` | Names of running jobs
148
- | `<service>` | Service names
149
- | `<signal>` | Signal names
150
- | `<stopped>` | Names of stopped jobs
151
- | `<user>` | User names
152
- | `<variable>` | Names of all shell variables
153
-
154
- For those interested in the technical details, any word between `<...>` will
155
- simply be added using the [`compgen -A action`][compgen] function, so you can
156
- in fact use any of its supported arguments.
157
-
158
- ### Suggesting custom dynamic suggestions
159
-
160
- You can also use any command that outputs a whitespace-delimited list as a
161
- suggestions list, by wrapping it in `$(..)`. For example, in order to add git
162
- branches to your suggestions, use the following:
118
+ The `options` section defines option groups:
163
119
 
164
120
  ```yaml
165
- mygit:
166
- - $(git branch --format='%(refname:short)' 2>/dev/null)
121
+ options:
122
+ status:
123
+ - --help
124
+ - --branch|-b <branch>
125
+ - --verbose (repeatable)
167
126
  ```
168
127
 
169
- The `2> /dev/null` is used so that if the command is executed in a directory
170
- without a git repository, it will still behave as expected.
128
+ An option can be a plain flag, aliases separated with `|`, or a flag that
129
+ expects a value token.
171
130
 
172
- ### Completion scope and limitations
131
+ Options are unique by default. If an option should be suggested again after it
132
+ was already used, add `(repeatable)`:
173
133
 
174
- - Completion words are treated as whitespace-delimited tokens.
175
- - Literal completion phrases that contain spaces are not supported as a single completion item.
176
- - Quotes and other special shell characters in literal completion words are not escaped automatically.
177
- - Dynamic `$(...)` completion commands should output plain whitespace-delimited words.
178
-
179
- ### Suggesting flag arguments
134
+ ```yaml
135
+ options:
136
+ status:
137
+ - --tag <tag> (repeatable)
138
+ ```
180
139
 
181
- Adding a `*` wildcard in the middle of a pattern can be useful for suggesting
182
- arguments for flags. For example:
140
+ The final positional in a pattern can be repeatable:
183
141
 
184
142
  ```yaml
185
- mygit checkout:
186
- - --branch
187
- - -b
143
+ patterns:
144
+ - mygit upload <file>...
145
+ ```
188
146
 
189
- mygit checkout*--branch:
190
- - $(git branch --format='%(refname:short)' 2>/dev/null)
147
+ Only the final positional may be repeatable.
191
148
 
192
- mygit checkout*-b:
193
- - $(git branch --format='%(refname:short)' 2>/dev/null)
149
+ The `tokens` section defines completion sources. Each token value can be one of
150
+ these forms:
151
+
152
+ ```yaml
153
+ tokens:
154
+ source: ~
155
+ directory: +directory
156
+ branch: $(git branch --format='%(refname:short)' 2>/dev/null)
157
+ format: [short, long]
158
+ target: [+file, +directory, README.md, $(git branch --format='%(refname:short)' 2>/dev/null)]
159
+ literal: ++file
194
160
  ```
195
161
 
196
- The above will suggest git branches for commands that end with `-b` or `--branch`.
197
- To avoid code duplication, you may use YAML aliases, so the above can also be
198
- written like this:
162
+ - A null value such as `~` defines a token without completion suggestions.
163
+ - A value starting with `+`, such as `+directory`, uses a bash built-in completion action.
164
+ - A value starting with `++`, such as `++file`, provides the literal completion word `+file`.
165
+ - Plain strings, including `$(...)` command substitutions, are added to the completion word list.
166
+ - An array combines multiple source items.
167
+
168
+ Every `[name]` option group and every `<token>` used by patterns or options must
169
+ be defined. This keeps typos from generating broken completion scripts.
170
+
171
+ ### Flat config
172
+
173
+ Flat config is the original Completely format. It is simpler, and remains
174
+ supported.
199
175
 
200
176
  ```yaml
201
- mygit checkout:
177
+ mygit:
178
+ - -h
179
+ - -v
180
+ - --help
181
+ - --version
182
+ - init
183
+ - status
184
+
185
+ mygit init:
186
+ - --bare
187
+ - <directory>
188
+
189
+ mygit status:
190
+ - --help
191
+ - --verbose
202
192
  - --branch
203
193
  - -b
204
194
 
205
- mygit checkout*--branch: &branches
195
+ mygit status*--branch: &branches
206
196
  - $(git branch --format='%(refname:short)' 2>/dev/null)
207
197
 
208
- mygit checkout*-b: *branches
198
+ mygit status*-b: *branches
209
199
  ```
210
200
 
211
- ### Alternative nested syntax
201
+ Each pattern is checked against the user's input. If the input matches the
202
+ pattern, the list that follows it is suggested as completions.
212
203
 
213
- Completely also supports an alternative nested syntax. You can generate this
214
- example by running:
204
+ Suggested completions do not show flags (strings that start with a hyphen `-`)
205
+ unless the input ends with a hyphen.
215
206
 
216
- ```bash
217
- $ completely init --nested
218
- ```
207
+ Adding a `*` wildcard in the middle of a pattern can be used for suggesting flag
208
+ arguments. In the example above, branches are suggested after `--branch` or `-b`.
209
+
210
+ ### Nested config
219
211
 
220
- The example configuration below will generate the exact same script as the one
221
- shown at the beginning of this document.
212
+ Nested config is an alternate spelling of flat config. It generates the same
213
+ completion behavior as the flat example above.
222
214
 
223
215
  ```yaml
224
216
  mygit:
@@ -237,18 +229,74 @@ mygit:
237
229
  - +-b: *branches
238
230
  ```
239
231
 
240
- The rules here are as follows:
232
+ The rules are:
233
+
234
+ - Each pattern can have a mixed array of strings and hashes.
235
+ - Strings and hash keys are used as completion strings for that pattern.
236
+ - Hashes can contain a nested mixed array of the same structure.
237
+ - Hash keys are appended to the parent prefix. In the example above, the `init`
238
+ hash creates the pattern `mygit init`.
239
+ - To provide a wildcard such as `mygit status*--branch`, prefix the hash key with
240
+ `+` or `*`, for example `+--branch` or `"*--branch"`. When using `*`, quote the
241
+ key because asterisks have special meaning in YAML.
242
+
243
+ ### Completion sources
244
+
245
+ Pattern config and the original flat/nested formats use the same underlying bash
246
+ completion sources, but they spell built-ins differently.
247
+
248
+ Pattern config uses named tokens:
249
+
250
+ ```yaml
251
+ tokens:
252
+ file: +file
253
+ directory: +directory
254
+ branch: $(git branch --format='%(refname:short)' 2>/dev/null)
255
+ format: [short, long]
256
+ ```
257
+
258
+ Flat and nested configs use completion words directly:
241
259
 
242
- - Each pattern (e.g., `mygit`) can have a mixed array of strings and hashes.
243
- - Strings and hash keys (e.e., `--help` and `init` respectively) will be used
244
- as completion strings for that pattern.
245
- - Hashes may contain a nested mixed array of the same structure.
246
- - When a hash is provided, the hash key will be appended to the parent prefix.
247
- In the example above, the `init` hash will create the pattern `mygit init`.
248
- - In order to provide a wildcard (like `mygit status*--branch` in the standard
249
- configuration syntax), you can provide either a `*` or a `+` prefix to the
250
- hash key (e.g., `+--branch` or `"*--branch"`). Note that when using a `*`,
251
- the hash key must be quoted since asterisks have special meaning in YAML.
260
+ ```yaml
261
+ mygit init:
262
+ - <file>
263
+ - <directory>
264
+ - $(git branch --format='%(refname:short)' 2>/dev/null)
265
+ ```
266
+
267
+ The built-in names map to `compgen -A` actions:
268
+
269
+ | Built-in | Meaning
270
+ |---------------|---------------------
271
+ | `alias` | Alias names
272
+ | `arrayvar` | Array variable names
273
+ | `binding` | Readline key binding names
274
+ | `builtin` | Names of shell builtin commands
275
+ | `command` | Command names
276
+ | `directory` | Directory names
277
+ | `disabled` | Names of disabled shell builtins
278
+ | `enabled` | Names of enabled shell builtins
279
+ | `export` | Names of exported shell variables
280
+ | `file` | File names
281
+ | `function` | Names of shell functions
282
+ | `group` | Group names
283
+ | `helptopic` | Help topics as accepted by the help builtin
284
+ | `hostname` | Hostnames, as taken from the file specified by the HOSTFILE shell variable
285
+ | `job` | Job names
286
+ | `keyword` | Shell reserved words
287
+ | `running` | Names of running jobs
288
+ | `service` | Service names
289
+ | `signal` | Signal names
290
+ | `stopped` | Names of stopped jobs
291
+ | `user` | User names
292
+ | `variable` | Names of all shell variables
293
+
294
+ ### Completion scope and limitations
295
+
296
+ - Completion words are treated as whitespace-delimited tokens.
297
+ - Literal completion phrases that contain spaces are not supported as a single completion item.
298
+ - Quotes and other special shell characters in literal completion words are not escaped automatically.
299
+ - Dynamic `$(...)` completion commands should output plain whitespace-delimited words.
252
300
 
253
301
 
254
302
  ## Using the generated completion scripts
@@ -293,9 +341,26 @@ require 'completely'
293
341
  # Load from file
294
342
  completions = Completely::Completions.load "input.yaml"
295
343
 
296
- # Or, from a hash
344
+ # Or, from a pattern config hash
345
+ input = {
346
+ "patterns" => [
347
+ "mygit init [init options] <directory>",
348
+ "mygit status|st [status options]"
349
+ ],
350
+ "options" => {
351
+ "init" => ["--bare"],
352
+ "status" => ["--verbose|-v", "--branch|-b <branch>"]
353
+ },
354
+ "tokens" => {
355
+ "directory" => "directory",
356
+ "branch" => "$(git branch --format='%(refname:short)' 2>/dev/null)"
357
+ }
358
+ }
359
+ completions = Completely::Completions.new input
360
+
361
+ # Flat and nested config hashes are also supported by the same API.
297
362
  input = {
298
- "mygit" => %w[--help --version status init commit],
363
+ "mygit" => %w[--help --version status init],
299
364
  "mygit status" => %w[--help --verbose --branch]
300
365
  }
301
366
  completions = Completely::Completions.new input
@@ -327,8 +392,10 @@ autoload -Uz +X bashcompinit && bashcompinit
327
392
  ## Customizing the `complete` command
328
393
 
329
394
  In case you wish to customize the `complete` command call in the generated
330
- script, you can do so by adding any additional flags to the `completely.yaml`
331
- configuration file using the special `completely_options` key. For example:
395
+ script, you can do so by adding any additional flags to the
396
+ `completely.yaml` configuration file using the special `completely_options`
397
+ key. Completely passes these options to Bash's `complete` command as is. For
398
+ example:
332
399
 
333
400
  ```yaml
334
401
  completely_options:
@@ -6,7 +6,7 @@ module Completely
6
6
  class << self
7
7
  def param_config_path
8
8
  param 'CONFIG_PATH', <<~USAGE
9
- Path to the YAML configuration file [default: completely.yaml].
9
+ Path to the Completely YAML configuration file (pattern, flat, or nested) [default: completely.yaml].
10
10
  Can also be set by an environment variable.
11
11
  USAGE
12
12
  end
@@ -18,7 +18,7 @@ module Completely
18
18
 
19
19
  def environment_config_path
20
20
  environment 'COMPLETELY_CONFIG_PATH',
21
- 'Path to a completely configuration file [default: completely.yaml].'
21
+ 'Path to a Completely YAML configuration file [default: completely.yaml].'
22
22
  end
23
23
 
24
24
  def environment_debug
@@ -62,7 +62,7 @@ module Completely
62
62
 
63
63
  def syntax_warning
64
64
  say! "\nr`WARNING:`\nr`Your configuration is invalid.`"
65
- say! 'r`All patterns must start with the same word.`'
65
+ say! 'r`All completion patterns must use the same command name.`'
66
66
  end
67
67
  end
68
68
  end
@@ -16,7 +16,7 @@ module Completely
16
16
  option '-i --install PROGRAM', 'Install the generated script as completions for PROGRAM.'
17
17
 
18
18
  param 'CONFIG_PATH', <<~USAGE
19
- Path to the YAML configuration file [default: completely.yaml].
19
+ Path to the Completely YAML configuration file (pattern, flat, or nested) [default: completely.yaml].
20
20
  Use '-' to read from stdin.
21
21
 
22
22
  Can also be set by an environment variable.
@@ -3,12 +3,12 @@ require 'completely/commands/base'
3
3
  module Completely
4
4
  module Commands
5
5
  class Init < Base
6
- help 'Create a new sample YAML configuration file'
6
+ help 'Create a new sample Completely YAML configuration file'
7
7
 
8
- usage 'completely init [--nested] [CONFIG_PATH]'
8
+ usage 'completely init [--format FORMAT] [CONFIG_PATH]'
9
9
  usage 'completely init (-h|--help)'
10
10
 
11
- option '-n --nested', 'Generate a nested configuration'
11
+ option '-f --format FORMAT', 'Sample format: pattern, flat, or nested [default: pattern]'
12
12
 
13
13
  param_config_path
14
14
  environment_config_path
@@ -26,16 +26,29 @@ module Completely
26
26
  @sample ||= File.read sample_path
27
27
  end
28
28
 
29
- def nested?
30
- args['--nested']
29
+ def format
30
+ @format ||= args['--format'] || 'pattern'
31
31
  end
32
32
 
33
33
  def sample_path
34
34
  @sample_path ||= begin
35
- sample_name = nested? ? 'sample-nested' : 'sample'
36
- File.expand_path "../templates/#{sample_name}.yaml", __dir__
35
+ raise Error, "Invalid format: #{format}" unless sample_filenames.key? format
36
+
37
+ File.expand_path "../templates/#{sample_filename}", __dir__
37
38
  end
38
39
  end
40
+
41
+ def sample_filename
42
+ sample_filenames.fetch format
43
+ end
44
+
45
+ def sample_filenames
46
+ @sample_filenames ||= {
47
+ 'flat' => 'flat-config/sample.yaml',
48
+ 'nested' => 'flat-config/sample-nested.yaml',
49
+ 'pattern' => 'pattern-config/sample.yaml',
50
+ }
51
+ end
39
52
  end
40
53
  end
41
54
  end
@@ -6,8 +6,8 @@ module Completely
6
6
  summary 'Test completions'
7
7
 
8
8
  help 'This command can be used to test that your completions script responds with ' \
9
- 'the right completions. It works by reading your completely.yaml file, generating ' \
10
- 'a completions script, and generating a temporary testing script.'
9
+ 'the right completions. It works by reading a Completely YAML configuration file, ' \
10
+ 'generating a completions script, and generating a temporary testing script.'
11
11
 
12
12
  usage 'completely test [--keep] COMPLINE...'
13
13
  usage 'completely test (-h|--help)'
@@ -16,7 +16,7 @@ module Completely
16
16
  end
17
17
 
18
18
  def initialize(config, function_name: nil)
19
- @config = config.is_a?(Config) ? config : Config.new(config)
19
+ @config = normalize_config config
20
20
  @function_name = function_name
21
21
  end
22
22
 
@@ -29,6 +29,8 @@ module Completely
29
29
  end
30
30
 
31
31
  def valid?
32
+ return pattern_programs.uniq.one? if pattern_config?
33
+
32
34
  pattern_prefixes.uniq.one?
33
35
  end
34
36
 
@@ -62,7 +64,10 @@ module Completely
62
64
  end
63
65
 
64
66
  def template_path
65
- @template_path ||= File.expand_path('templates/template.erb', __dir__)
67
+ @template_path ||= begin
68
+ template = pattern_config? ? 'pattern-config/template.erb' : 'flat-config/template.erb'
69
+ File.expand_path("templates/#{template}", __dir__)
70
+ end
66
71
  end
67
72
 
68
73
  def template
@@ -70,7 +75,7 @@ module Completely
70
75
  end
71
76
 
72
77
  def command
73
- @command ||= flat_config.keys.first.split.first
78
+ @command ||= pattern_config? ? config.model[:program] : flat_config.keys.first.split.first
74
79
  end
75
80
 
76
81
  def function_name
@@ -91,5 +96,86 @@ module Completely
91
96
 
92
97
  "#{options} "
93
98
  end
99
+
100
+ def pattern_config?
101
+ config.is_a? PatternConfig
102
+ end
103
+
104
+ def pattern_routes
105
+ config.model[:routes]
106
+ end
107
+
108
+ def pattern_programs
109
+ pattern_routes.map { |route| route.dig(:words, 0, :name) }
110
+ end
111
+
112
+ def pattern_root_words
113
+ pattern_routes.flat_map do |route|
114
+ word = route[:words][1]
115
+ word ? [word[:name], *word[:aliases]] : []
116
+ end.uniq
117
+ end
118
+
119
+ def pattern_route_id(route)
120
+ pattern_routes.index route
121
+ end
122
+
123
+ def pattern_route_conditions(route)
124
+ route[:words][1..].map.with_index do |word, index|
125
+ names = [word[:name], *word[:aliases]]
126
+ names.map { |name| %["${non_options[#{index}]}" == "#{bash_escape name}"] }.join(' || ')
127
+ end
128
+ end
129
+
130
+ def pattern_route_word_count(route)
131
+ route[:words].size - 1
132
+ end
133
+
134
+ def pattern_route_options(route)
135
+ route[:option_groups].flat_map do |name|
136
+ config.model[:options][name] || []
137
+ end
138
+ end
139
+
140
+ def pattern_options_with_values
141
+ config.model[:options].values.flatten.select { |option| option[:value] }
142
+ end
143
+
144
+ def pattern_source_empty?(source)
145
+ source[:items].empty?
146
+ end
147
+
148
+ def pattern_source_compgen(source)
149
+ wordlist = source[:items]
150
+ .select { |item| item[:type] == :value }
151
+ .map { |item| item[:value] }
152
+ .join(' ')
153
+
154
+ builtins = source[:items]
155
+ .select { |item| item[:type] == :builtin }
156
+ .map { |item| "-A #{bash_escape item[:value]}" }
157
+
158
+ parts = []
159
+ parts << %[-W "#{bash_double_quote_escape wordlist}"] unless wordlist.empty?
160
+ parts.concat builtins
161
+ parts.join(' ')
162
+ end
163
+
164
+ def bash_escape(value)
165
+ value.to_s.gsub('\\', '\\\\\\').gsub('"', '\\"')
166
+ end
167
+
168
+ def bash_double_quote_escape(value)
169
+ value.to_s.gsub('\\', '\\\\\\').gsub('"', '\\"')
170
+ end
171
+
172
+ def normalize_config(config)
173
+ case config
174
+ when FlatConfig, PatternConfig
175
+ config
176
+ else
177
+ Config.build config
178
+ end
179
+ end
94
180
  end
95
181
  end
@@ -1,67 +1,26 @@
1
1
  module Completely
2
2
  class Config
3
- attr_reader :config, :options
4
-
5
3
  class << self
6
4
  def parse(str)
7
- new YAML.load(str, aliases: true)
5
+ build YAML.load(str, aliases: true)
8
6
  rescue Psych::Exception => e
9
7
  raise ParseError, "Invalid YAML: #{e.message}"
10
8
  end
11
9
 
12
10
  def load(path) = parse(File.read(path))
13
11
  def read(io) = parse(io.read)
14
- end
15
-
16
- def initialize(config)
17
- @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {}
18
- @config = config
19
- end
20
-
21
- def flat_config
22
- result = {}
23
-
24
- config.each do |root_key, root_list|
25
- result.merge! process_key(root_key, root_list)
26
- end
27
-
28
- result
29
- end
30
12
 
31
- private
32
-
33
- def process_key(prefix, list)
34
- result = {}
35
- result[prefix] = collect_immediate_children list
36
- result.merge! process_nested_items(prefix, list)
37
- result
38
- end
39
-
40
- def collect_immediate_children(list)
41
- list.map do |item|
42
- x = item.is_a?(Hash) ? item.keys.first : item
43
- x.gsub(/^[*+]/, '')
13
+ def build(config)
14
+ if pattern_config? config
15
+ PatternConfig.new config
16
+ else
17
+ FlatConfig.new config
18
+ end
44
19
  end
45
- end
46
-
47
- def process_nested_items(prefix, list)
48
- result = {}
49
20
 
50
- list.each do |item|
51
- next unless item.is_a? Hash
52
-
53
- nested_prefix = generate_nested_prefix(prefix, item)
54
- nested_list = item.values.first
55
- result.merge!(process_key(nested_prefix, nested_list))
21
+ def pattern_config?(config)
22
+ config.is_a?(Hash) && config.has_key?('patterns')
56
23
  end
57
-
58
- result
59
- end
60
-
61
- def generate_nested_prefix(prefix, item)
62
- appended_prefix = item.keys.first.gsub(/^\+/, '*')
63
- appended_prefix = " #{appended_prefix}" unless appended_prefix.start_with? '*'
64
- "#{prefix}#{appended_prefix}"
65
24
  end
66
25
  end
67
26
  end
@@ -0,0 +1,56 @@
1
+ module Completely
2
+ class FlatConfig
3
+ attr_reader :config, :options
4
+
5
+ def initialize(config)
6
+ @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {}
7
+ @config = config
8
+ end
9
+
10
+ def flat_config
11
+ result = {}
12
+
13
+ config.each do |root_key, root_list|
14
+ result.merge! process_key(root_key, root_list)
15
+ end
16
+
17
+ result
18
+ end
19
+
20
+ private
21
+
22
+ def process_key(prefix, list)
23
+ result = {}
24
+ result[prefix] = collect_immediate_children list
25
+ result.merge! process_nested_items(prefix, list)
26
+ result
27
+ end
28
+
29
+ def collect_immediate_children(list)
30
+ list.map do |item|
31
+ x = item.is_a?(Hash) ? item.keys.first : item
32
+ x.gsub(/^[*+]/, '')
33
+ end
34
+ end
35
+
36
+ def process_nested_items(prefix, list)
37
+ result = {}
38
+
39
+ list.each do |item|
40
+ next unless item.is_a? Hash
41
+
42
+ nested_prefix = generate_nested_prefix(prefix, item)
43
+ nested_list = item.values.first
44
+ result.merge!(process_key(nested_prefix, nested_list))
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ def generate_nested_prefix(prefix, item)
51
+ appended_prefix = item.keys.first.gsub(/^\+/, '*')
52
+ appended_prefix = " #{appended_prefix}" unless appended_prefix.start_with? '*'
53
+ "#{prefix}#{appended_prefix}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,201 @@
1
+ module Completely
2
+ class PatternConfig
3
+ attr_reader :config, :options
4
+
5
+ def initialize(config)
6
+ @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {}
7
+ @config = config
8
+ end
9
+
10
+ def model
11
+ validate!
12
+
13
+ @model ||= {
14
+ program: program,
15
+ routes: routes,
16
+ options: parsed_options,
17
+ tokens: tokens,
18
+ }
19
+ end
20
+
21
+ def flat_config
22
+ raise Error, 'Pattern config cannot be converted to flat config'
23
+ end
24
+
25
+ private
26
+
27
+ def patterns
28
+ @patterns ||= Array config['patterns']
29
+ end
30
+
31
+ def option_groups
32
+ @option_groups ||= config['options'] || {}
33
+ end
34
+
35
+ def token_sources
36
+ @token_sources ||= config['tokens'] || {}
37
+ end
38
+
39
+ def program
40
+ routes.first.dig(:words, 0, :name)
41
+ end
42
+
43
+ def routes
44
+ @routes ||= patterns.map { |pattern| parse_pattern pattern }
45
+ end
46
+
47
+ def parsed_options
48
+ @parsed_options ||= option_groups.to_h do |name, entries|
49
+ [name, Array(entries).map { |entry| parse_option entry }]
50
+ end
51
+ end
52
+
53
+ def tokens
54
+ @tokens ||= token_sources.to_h do |name, source|
55
+ [name, parse_source(name, source)]
56
+ end
57
+ end
58
+
59
+ def validate!
60
+ missing_options = referenced_options - option_groups.keys
61
+ missing_tokens = referenced_tokens - token_sources.keys
62
+
63
+ errors = []
64
+ errors << "Unknown option group: #{missing_options.join ', '}" if missing_options.any?
65
+ errors << "Unknown token: #{missing_tokens.join ', '}" if missing_tokens.any?
66
+ errors.concat repeatable_positional_errors
67
+ raise ParseError, errors.join("\n") if errors.any?
68
+ end
69
+
70
+ def repeatable_positional_errors
71
+ patterns.filter_map do |pattern|
72
+ positionals = pattern_parts(pattern).select { |part| token? part }
73
+ next unless positionals[0...-1].any? { |part| repeatable_token? part }
74
+
75
+ "Repeatable positional must be the last positional in pattern: #{pattern}"
76
+ end
77
+ end
78
+
79
+ def referenced_options
80
+ patterns.flat_map do |pattern|
81
+ pattern_parts(pattern).filter_map { |part| option_group_name(part) if option_group?(part) }
82
+ end.uniq
83
+ end
84
+
85
+ def referenced_tokens
86
+ pattern_tokens + option_tokens
87
+ end
88
+
89
+ def pattern_tokens
90
+ patterns.flat_map do |pattern|
91
+ pattern_parts(pattern).filter_map { |part| token_name(part) if token?(part) }
92
+ end
93
+ end
94
+
95
+ def option_tokens
96
+ option_groups.values.flatten.filter_map do |entry|
97
+ value_part = option_parts(entry).find { |part| token? part }
98
+ token_name(value_part) if value_part
99
+ end
100
+ end
101
+
102
+ def parse_pattern(pattern)
103
+ result = { words: [], option_groups: [], positionals: [] }
104
+
105
+ pattern_parts(pattern).each do |part|
106
+ if option_group?(part)
107
+ result[:option_groups] << option_group_name(part)
108
+ elsif token?(part)
109
+ result[:positionals] << parse_token(part)
110
+ else
111
+ result[:words] << parse_word(part)
112
+ end
113
+ end
114
+
115
+ result
116
+ end
117
+
118
+ def parse_word(part)
119
+ names = part.split('|')
120
+ { name: names.first, aliases: names[1..] || [] }
121
+ end
122
+
123
+ def pattern_parts(pattern)
124
+ pattern.scan(/\[[^\]]+\]|<[^>]+>\.\.\.|<[^>]+>|\S+/)
125
+ end
126
+
127
+ def parse_option(entry)
128
+ flag_part, *parts = option_parts entry
129
+ value_part = parts.find { |part| token? part }
130
+ metadata_parts = parts.select { |part| metadata? part }
131
+ unknown_parts = parts - [value_part] - metadata_parts
132
+ raise ParseError, "Invalid option syntax: #{entry}" if unknown_parts.any?
133
+
134
+ names = flag_part.split('|')
135
+
136
+ result = { names: names, repeatable: false }
137
+ result[:value] = parse_token(value_part) if value_part
138
+ metadata_parts.each { |part| apply_option_metadata result, part }
139
+ result
140
+ end
141
+
142
+ def option_parts(entry)
143
+ entry.scan(/<[^>]+>|\([^)]+\)|\S+/)
144
+ end
145
+
146
+ def apply_option_metadata(result, part)
147
+ case part
148
+ when '(repeatable)'
149
+ result[:repeatable] = true
150
+ else
151
+ raise ParseError, "Unknown option metadata: #{part}"
152
+ end
153
+ end
154
+
155
+ def metadata?(part)
156
+ part.start_with?('(') && part.end_with?(')')
157
+ end
158
+
159
+ def parse_token(part)
160
+ repeatable = repeatable_token? part
161
+ token_part = repeatable ? part.delete_suffix('...') : part
162
+ name = token_name token_part
163
+ result = { name: name, source: parse_source(name, token_sources[name]) }
164
+ result[:repeatable] = true if repeatable
165
+ result
166
+ end
167
+
168
+ def parse_source(_name, source)
169
+ source_items = source.is_a?(Array) ? source : [source]
170
+ items = source_items.compact.map { |item| parse_source_item item }
171
+ { items: items }
172
+ end
173
+
174
+ def parse_source_item(item)
175
+ return { type: :value, value: item.to_s[1..] } if item.to_s.start_with? '++'
176
+ return { type: :builtin, value: item.to_s[1..] } if item.to_s.start_with? '+'
177
+
178
+ { type: :value, value: item.to_s }
179
+ end
180
+
181
+ def option_group?(part)
182
+ part.start_with?('[') && part.end_with?(']')
183
+ end
184
+
185
+ def option_group_name(part)
186
+ part[1..-2].sub(/\s+options\z/, '')
187
+ end
188
+
189
+ def token?(part)
190
+ part.match?(/\A<[^>]+>(?:\.\.\.)?\z/)
191
+ end
192
+
193
+ def repeatable_token?(part)
194
+ part.end_with? '...'
195
+ end
196
+
197
+ def token_name(part)
198
+ part.delete_suffix('...')[/\A<(.+)>\z/, 1]
199
+ end
200
+ end
201
+ end
@@ -0,0 +1,21 @@
1
+ patterns:
2
+ - mygit [root options]
3
+ - mygit init [init options] <directory>
4
+ - mygit status [status options]
5
+
6
+ options:
7
+ root:
8
+ - -h|--help
9
+ - -v|--version
10
+ init:
11
+ - --bare
12
+ status:
13
+ - --help
14
+ - --branch|-b <branch>
15
+ - --format <format>
16
+ - --verbose (repeatable)
17
+
18
+ tokens:
19
+ directory: +directory
20
+ branch: $(git branch --format='%(refname:short)' 2>/dev/null)
21
+ format: [short, long]
@@ -0,0 +1,149 @@
1
+ # <%= "#{command} completion".ljust 56 %> -*- shell-script -*-
2
+
3
+ # This bash completions script was generated by
4
+ # completely (https://github.com/bashly-framework/completely)
5
+ # Modifying it manually is not recommended
6
+
7
+ <%= function_name %>_flag_expects_value() {
8
+ case "$1" in
9
+ % pattern_options_with_values.each do |option|
10
+ <%= option[:names].map { |name| bash_escape name }.join('|') %>) return 0 ;;
11
+ % end
12
+ esac
13
+
14
+ return 1
15
+ }
16
+
17
+ <%= function_name %>() {
18
+ local cur=${COMP_WORDS[COMP_CWORD]}
19
+ local prev=
20
+ if ((COMP_CWORD > 0)); then
21
+ prev=${COMP_WORDS[$((COMP_CWORD - 1))]}
22
+ fi
23
+
24
+ local completed=()
25
+ if ((COMP_CWORD > 1)); then
26
+ completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}")
27
+ fi
28
+
29
+ local non_options=()
30
+ local completed_options=()
31
+ local skip_next=0
32
+ for word in "${completed[@]}"; do
33
+ if ((skip_next)); then
34
+ skip_next=0
35
+ continue
36
+ fi
37
+
38
+ if [[ "${word:0:1}" == "-" ]]; then
39
+ completed_options+=("$word")
40
+ if <%= function_name %>_flag_expects_value "$word"; then
41
+ skip_next=1
42
+ fi
43
+ continue
44
+ fi
45
+
46
+ non_options+=("$word")
47
+ done
48
+
49
+ local route_id=
50
+ local route_word_count=-1
51
+ local route_has_positionals=0
52
+ local positional_index=0
53
+ % pattern_routes.each do |route|
54
+ % conditions = pattern_route_conditions(route)
55
+ if (( ${#non_options[@]} >= <%= pattern_route_word_count route %> )) &&
56
+ (( <%= pattern_route_word_count route %> > route_word_count ))<%= conditions.empty? ? '' : ' &&' %>
57
+ % conditions.each_with_index do |condition, index|
58
+ [[ <%= condition %> ]]<%= index == conditions.size - 1 ? '' : ' &&' %>
59
+ % end
60
+ then
61
+ route_id=<%= pattern_route_id route %>
62
+ route_word_count=<%= pattern_route_word_count route %>
63
+ route_has_positionals=<%= route[:positionals].empty? ? 0 : 1 %>
64
+ positional_index=$((${#non_options[@]} - <%= pattern_route_word_count route %>))
65
+ fi
66
+
67
+ % end
68
+ COMPREPLY=()
69
+
70
+ if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then
71
+ while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "<%= bash_double_quote_escape pattern_root_words.join(' ') %>" -- "$cur")
72
+ return
73
+ fi
74
+
75
+ case "$route_id:$prev" in
76
+ % pattern_routes.each do |route|
77
+ % pattern_route_options(route).select { |option| option[:value] }.each do |option|
78
+ <%= pattern_route_id route %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_route_id route}:") %>)
79
+ % if pattern_source_empty? option[:value][:source]
80
+ return
81
+ % else
82
+ while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen option[:value][:source] %> -- "$cur")
83
+ return
84
+ % end
85
+ ;;
86
+ % end
87
+ % end
88
+ esac
89
+
90
+ if [[ "${cur:0:1}" == "-" ]]; then
91
+ case "$route_id" in
92
+ % pattern_routes.each do |route|
93
+ <%= pattern_route_id route %>)
94
+ local words=()
95
+ % pattern_route_options(route).each do |option|
96
+ % if option[:repeatable]
97
+ words+=(<%= option[:names].map { |name| %["#{bash_escape name}"] }.join(' ') %>)
98
+ % else
99
+ local option_seen=0
100
+ for completed_option in "${completed_options[@]}"; do
101
+ case "$completed_option" in
102
+ <%= option[:names].map { |name| bash_escape name }.join('|') %>) option_seen=1 ;;
103
+ esac
104
+ done
105
+ if ((!option_seen)); then
106
+ words+=(<%= option[:names].map { |name| %["#{bash_escape name}"] }.join(' ') %>)
107
+ fi
108
+ % end
109
+ % end
110
+ while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur")
111
+ return
112
+ ;;
113
+ % end
114
+ esac
115
+ fi
116
+
117
+ % pattern_routes.each do |route|
118
+ % route[:positionals].each_with_index do |positional, index|
119
+ % next unless positional[:repeatable]
120
+ if [[ "$route_id" == "<%= pattern_route_id route %>" ]] && (( positional_index >= <%= index %> )); then
121
+ % if pattern_source_empty? positional[:source]
122
+ return
123
+ % else
124
+ while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen positional[:source] %> -- "$cur")
125
+ return
126
+ % end
127
+ fi
128
+
129
+ % end
130
+ % end
131
+ case "$route_id:$positional_index" in
132
+ % pattern_routes.each do |route|
133
+ % route[:positionals].each_with_index do |positional, index|
134
+ % next if positional[:repeatable]
135
+ <%= pattern_route_id route %>:<%= index %>)
136
+ % if pattern_source_empty? positional[:source]
137
+ return
138
+ % else
139
+ while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen positional[:source] %> -- "$cur")
140
+ return
141
+ % end
142
+ ;;
143
+ % end
144
+ % end
145
+ esac
146
+ } &&
147
+ complete <%= complete_options_line %>-F <%= function_name %> <%= command %>
148
+
149
+ # ex: filetype=sh
@@ -37,7 +37,7 @@ module Completely
37
37
  end
38
38
 
39
39
  def template_path
40
- @template_path ||= File.expand_path 'templates/tester-template.erb', __dir__
40
+ @template_path ||= File.expand_path 'templates/tester.bash.erb', __dir__
41
41
  end
42
42
 
43
43
  def template
@@ -1,3 +1,3 @@
1
1
  module Completely
2
- VERSION = '0.7.6'
2
+ VERSION = '0.8.0.rc3'
3
3
  end
data/lib/completely.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'completely/exceptions'
2
+ require 'completely/flat_config'
3
+ require 'completely/pattern_config'
2
4
  require 'completely/config'
3
5
  require 'completely/pattern'
4
6
  require 'completely/completions'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completely
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.6
4
+ version: 0.8.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit
@@ -72,12 +72,16 @@ files:
72
72
  - lib/completely/completions.rb
73
73
  - lib/completely/config.rb
74
74
  - lib/completely/exceptions.rb
75
+ - lib/completely/flat_config.rb
75
76
  - lib/completely/installer.rb
76
77
  - lib/completely/pattern.rb
77
- - lib/completely/templates/sample-nested.yaml
78
- - lib/completely/templates/sample.yaml
79
- - lib/completely/templates/template.erb
80
- - lib/completely/templates/tester-template.erb
78
+ - lib/completely/pattern_config.rb
79
+ - lib/completely/templates/flat-config/sample-nested.yaml
80
+ - lib/completely/templates/flat-config/sample.yaml
81
+ - lib/completely/templates/flat-config/template.erb
82
+ - lib/completely/templates/pattern-config/sample.yaml
83
+ - lib/completely/templates/pattern-config/template.erb
84
+ - lib/completely/templates/tester.bash.erb
81
85
  - lib/completely/tester.rb
82
86
  - lib/completely/version.rb
83
87
  homepage: https://github.com/bashly-framework/completely