diff --git a/README.md b/README.md index 6203e99..bdeca38 100644 --- a/README.md +++ b/README.md @@ -39,20 +39,15 @@ $ alias completely='docker run --rm -it --user $(id -u):$(id -g) --volume "$PWD: ## Configuration syntax -Completely works with a simple YAML configuration file as input, and generates -a bash completions script as output. +Completely works with a YAML configuration file as input, and generates a bash +completions script as output. -The configuration file is built of blocks that look like this: +There are three configuration formats: -```yaml -pattern: -- --argument -- --param -- command -``` - -Each pattern contains an array of words (or functions) that will be suggested -for the auto complete process. +- **Pattern config**: Recommended for new projects. It describes command shapes, + option groups, and value sources explicitly. +- **Flat config**: The original simple pattern-to-suggestions format. +- **Nested config**: A nested spelling of the flat config format. You can save a sample YAML file by running: @@ -60,165 +55,162 @@ You can save a sample YAML file by running: $ completely init ``` -This will generate a file named `completely.yaml` with this content: - -```yaml -mygit: -- -h -- -v -- --help -- --version -- init -- status - -mygit init: -- --bare -- - -mygit status: -- --help -- --verbose -- --branch -- -b - -mygit status*--branch: &branches -- $(git branch --format='%(refname:short)' 2>/dev/null) +This creates a `completely.yaml` file using the recommended pattern config +format. You can also choose a format explicitly: -mygit status*-b: *branches +```bash +$ completely init --format pattern +$ completely init --format flat +$ completely init --format nested ``` -Each pattern in this configuration file will be checked against the user's -input, and if the input matches the pattern, the list that follows it will be -suggested as completions. - -Note that the suggested completions will not show flags (strings that start with -a hyphen `-`) unless the input ends with a hyphen. - -To generate the bash script, simply run: +To generate the bash script, run: ```bash $ completely generate -# or, to just preview it without saving: +# or, to preview it without saving: $ completely preview ``` -For more options (like setting input/output path), run: +For more options, run: ```bash $ completely --help ``` -### Suggesting files, directories and other bash built-ins +### Pattern config -In addition to specifying a simple array of completion words, you may use -the special syntax `<..>` to suggest more advanced functions. +Pattern config is the recommended format for new completion files. ```yaml -pattern: -- -- +patterns: + - mygit [root options] + - mygit init [init options] + - mygit status [status options] + +options: + root: + - -h|--help + - -v|--version + init: + - --bare + status: + - --help + - --branch|-b + - --format + - --verbose (repeatable) + +tokens: + directory: +directory + branch: $(git branch --format='%(refname:short)' 2>/dev/null) + format: [short, long] ``` -These suggestions will add the list of files and directories -(when `` is used) or just directories (when `` is used) to -the list of suggestions. +The `patterns` section describes valid command shapes: -You may use any of the below keywords to add additional suggestions: +- Plain words are command words, for example `mygit`, `init`, and `status`. +- Command aliases can be written with `|`, for example `status|st`. +- `[name options]` references `options.name`. `[name]` is also accepted. +- `` references `tokens.token`. +- `...` marks the final positional as repeatable. -| Keyword | Meaning -|---------------|--------------------- -| `` | Alias names -| `` | Array variable names -| `` | Readline key binding names -| `` | Names of shell builtin commands -| `` | Command names -| `` | Directory names -| `` | Names of disabled shell builtins -| `` | Names of enabled shell builtins -| `` | Names of exported shell variables -| `` | File names -| `` | Names of shell functions -| `` | Group names -| `` | Help topics as accepted by the help builtin -| `` | Hostnames, as taken from the file specified by the HOSTFILE shell variable -| `` | Job names -| `` | Shell reserved words -| `` | Names of running jobs -| `` | Service names -| `` | Signal names -| `` | Names of stopped jobs -| `` | User names -| `` | Names of all shell variables - -For those interested in the technical details, any word between `<...>` will -simply be added using the [`compgen -A action`][compgen] function, so you can -in fact use any of its supported arguments. - -### Suggesting custom dynamic suggestions - -You can also use any command that outputs a whitespace-delimited list as a -suggestions list, by wrapping it in `$(..)`. For example, in order to add git -branches to your suggestions, use the following: +The `options` section defines option groups: ```yaml -mygit: -- $(git branch --format='%(refname:short)' 2>/dev/null) +options: + status: + - --help + - --branch|-b + - --verbose (repeatable) ``` -The `2> /dev/null` is used so that if the command is executed in a directory -without a git repository, it will still behave as expected. +An option can be a plain flag, aliases separated with `|`, or a flag that +expects a value token. -### Completion scope and limitations +Options are unique by default. If an option should be suggested again after it +was already used, add `(repeatable)`: -- Completion words are treated as whitespace-delimited tokens. -- Literal completion phrases that contain spaces are not supported as a single completion item. -- Quotes and other special shell characters in literal completion words are not escaped automatically. -- Dynamic `$(...)` completion commands should output plain whitespace-delimited words. - -### Suggesting flag arguments +```yaml +options: + status: + - --tag (repeatable) +``` -Adding a `*` wildcard in the middle of a pattern can be useful for suggesting -arguments for flags. For example: +The final positional in a pattern can be repeatable: ```yaml -mygit checkout: -- --branch -- -b +patterns: + - mygit upload ... +``` -mygit checkout*--branch: -- $(git branch --format='%(refname:short)' 2>/dev/null) +Only the final positional may be repeatable. -mygit checkout*-b: -- $(git branch --format='%(refname:short)' 2>/dev/null) +The `tokens` section defines completion sources. Each token value can be one of +these forms: + +```yaml +tokens: + source: ~ + directory: +directory + branch: $(git branch --format='%(refname:short)' 2>/dev/null) + format: [short, long] + target: [+file, +directory, README.md, $(git branch --format='%(refname:short)' 2>/dev/null)] + literal: ++file ``` -The above will suggest git branches for commands that end with `-b` or `--branch`. -To avoid code duplication, you may use YAML aliases, so the above can also be -written like this: +- A null value such as `~` defines a token without completion suggestions. +- A value starting with `+`, such as `+directory`, uses a bash built-in completion action. +- A value starting with `++`, such as `++file`, provides the literal completion word `+file`. +- Plain strings, including `$(...)` command substitutions, are added to the completion word list. +- An array combines multiple source items. + +Every `[name]` option group and every `` used by patterns or options must +be defined. This keeps typos from generating broken completion scripts. + +### Flat config + +Flat config is the original Completely format. It is simpler, and remains +supported. ```yaml -mygit checkout: +mygit: +- -h +- -v +- --help +- --version +- init +- status + +mygit init: +- --bare +- + +mygit status: +- --help +- --verbose - --branch - -b -mygit checkout*--branch: &branches +mygit status*--branch: &branches - $(git branch --format='%(refname:short)' 2>/dev/null) -mygit checkout*-b: *branches +mygit status*-b: *branches ``` -### Alternative nested syntax +Each pattern is checked against the user's input. If the input matches the +pattern, the list that follows it is suggested as completions. -Completely also supports an alternative nested syntax. You can generate this -example by running: +Suggested completions do not show flags (strings that start with a hyphen `-`) +unless the input ends with a hyphen. -```bash -$ completely init --nested -``` +Adding a `*` wildcard in the middle of a pattern can be used for suggesting flag +arguments. In the example above, branches are suggested after `--branch` or `-b`. + +### Nested config -The example configuration below will generate the exact same script as the one -shown at the beginning of this document. +Nested config is an alternate spelling of flat config. It generates the same +completion behavior as the flat example above. ```yaml mygit: @@ -237,18 +229,74 @@ mygit: - +-b: *branches ``` -The rules here are as follows: +The rules are: + +- Each pattern can have a mixed array of strings and hashes. +- Strings and hash keys are used as completion strings for that pattern. +- Hashes can contain a nested mixed array of the same structure. +- Hash keys are appended to the parent prefix. In the example above, the `init` + hash creates the pattern `mygit init`. +- To provide a wildcard such as `mygit status*--branch`, prefix the hash key with + `+` or `*`, for example `+--branch` or `"*--branch"`. When using `*`, quote the + key because asterisks have special meaning in YAML. + +### Completion sources + +Pattern config and the original flat/nested formats use the same underlying bash +completion sources, but they spell built-ins differently. + +Pattern config uses named tokens: + +```yaml +tokens: + file: +file + directory: +directory + branch: $(git branch --format='%(refname:short)' 2>/dev/null) + format: [short, long] +``` + +Flat and nested configs use completion words directly: -- Each pattern (e.g., `mygit`) can have a mixed array of strings and hashes. -- Strings and hash keys (e.e., `--help` and `init` respectively) will be used - as completion strings for that pattern. -- Hashes may contain a nested mixed array of the same structure. -- When a hash is provided, the hash key will be appended to the parent prefix. - In the example above, the `init` hash will create the pattern `mygit init`. -- In order to provide a wildcard (like `mygit status*--branch` in the standard - configuration syntax), you can provide either a `*` or a `+` prefix to the - hash key (e.g., `+--branch` or `"*--branch"`). Note that when using a `*`, - the hash key must be quoted since asterisks have special meaning in YAML. +```yaml +mygit init: +- +- +- $(git branch --format='%(refname:short)' 2>/dev/null) +``` + +The built-in names map to `compgen -A` actions: + +| Built-in | Meaning +|---------------|--------------------- +| `alias` | Alias names +| `arrayvar` | Array variable names +| `binding` | Readline key binding names +| `builtin` | Names of shell builtin commands +| `command` | Command names +| `directory` | Directory names +| `disabled` | Names of disabled shell builtins +| `enabled` | Names of enabled shell builtins +| `export` | Names of exported shell variables +| `file` | File names +| `function` | Names of shell functions +| `group` | Group names +| `helptopic` | Help topics as accepted by the help builtin +| `hostname` | Hostnames, as taken from the file specified by the HOSTFILE shell variable +| `job` | Job names +| `keyword` | Shell reserved words +| `running` | Names of running jobs +| `service` | Service names +| `signal` | Signal names +| `stopped` | Names of stopped jobs +| `user` | User names +| `variable` | Names of all shell variables + +### Completion scope and limitations + +- Completion words are treated as whitespace-delimited tokens. +- Literal completion phrases that contain spaces are not supported as a single completion item. +- Quotes and other special shell characters in literal completion words are not escaped automatically. +- Dynamic `$(...)` completion commands should output plain whitespace-delimited words. ## Using the generated completion scripts @@ -293,9 +341,26 @@ require 'completely' # Load from file completions = Completely::Completions.load "input.yaml" -# Or, from a hash +# Or, from a pattern config hash +input = { + "patterns" => [ + "mygit init [init options] ", + "mygit status|st [status options]" + ], + "options" => { + "init" => ["--bare"], + "status" => ["--verbose|-v", "--branch|-b "] + }, + "tokens" => { + "directory" => "directory", + "branch" => "$(git branch --format='%(refname:short)' 2>/dev/null)" + } +} +completions = Completely::Completions.new input + +# Flat and nested config hashes are also supported by the same API. input = { - "mygit" => %w[--help --version status init commit], + "mygit" => %w[--help --version status init], "mygit status" => %w[--help --verbose --branch] } completions = Completely::Completions.new input @@ -327,8 +392,10 @@ autoload -Uz +X bashcompinit && bashcompinit ## Customizing the `complete` command In case you wish to customize the `complete` command call in the generated -script, you can do so by adding any additional flags to the `completely.yaml` -configuration file using the special `completely_options` key. For example: +script, you can do so by adding any additional flags to the +`completely.yaml` configuration file using the special `completely_options` +key. Completely passes these options to Bash's `complete` command as is. For +example: ```yaml completely_options: diff --git a/Runfile b/Runfile index 70349b0..a5c8c1d 100644 --- a/Runfile +++ b/Runfile @@ -11,17 +11,30 @@ import 'debug' help 'Test the completely JSON schema' action :schema do files = %w[ - lib/completely/templates/sample.yaml - lib/completely/templates/sample-nested.yaml - spec/fixtures/complete_options.yaml + lib/completely/templates/pattern-config/sample.yaml + lib/completely/templates/flat-config/sample.yaml + lib/completely/templates/flat-config/sample-nested.yaml + spec/fixtures/flat-config/complete_options.yaml + spec/fixtures/pattern-config/basic.yaml + spec/fixtures/pattern-config/complete_options.yaml + spec/fixtures/pattern-config/mixed-source.yaml + spec/fixtures/pattern-config/nil-source.yaml + spec/fixtures/pattern-config/repeatable.yaml + spec/fixtures/pattern-config/repeatable-positionals.yaml + spec/fixtures/pattern-config/spaced-token.yaml ] files.each do |file| command = "check-jsonschema --schemafile schemas/completely.json #{file}" say "\n$ check-jsonschema bb`#{file}`" success = system command - exit 1 unless success + unless success + say 'r`FAILED`' + exit 1 + end end + + say 'g`ALL PASS`' end help 'Run preconfigured shfmt on any script' diff --git a/lib/completely.rb b/lib/completely.rb index 54a3d95..0cf4b02 100644 --- a/lib/completely.rb +++ b/lib/completely.rb @@ -1,4 +1,6 @@ require 'completely/exceptions' +require 'completely/flat_config' +require 'completely/pattern_config' require 'completely/config' require 'completely/pattern' require 'completely/completions' diff --git a/lib/completely/commands/base.rb b/lib/completely/commands/base.rb index 3107280..dd6d5f4 100644 --- a/lib/completely/commands/base.rb +++ b/lib/completely/commands/base.rb @@ -6,7 +6,7 @@ class Base < MisterBin::Command class << self def param_config_path param 'CONFIG_PATH', <<~USAGE - Path to the YAML configuration file [default: completely.yaml]. + Path to the Completely YAML configuration file (pattern, flat, or nested) [default: completely.yaml]. Can also be set by an environment variable. USAGE end @@ -18,7 +18,7 @@ def option_function def environment_config_path environment 'COMPLETELY_CONFIG_PATH', - 'Path to a completely configuration file [default: completely.yaml].' + 'Path to a Completely YAML configuration file [default: completely.yaml].' end def environment_debug @@ -62,7 +62,7 @@ def config_basename def syntax_warning say! "\nr`WARNING:`\nr`Your configuration is invalid.`" - say! 'r`All patterns must start with the same word.`' + say! 'r`All completion patterns must use the same command name.`' end end end diff --git a/lib/completely/commands/generate.rb b/lib/completely/commands/generate.rb index fb013ff..4329572 100644 --- a/lib/completely/commands/generate.rb +++ b/lib/completely/commands/generate.rb @@ -16,7 +16,7 @@ class Generate < Base option '-i --install PROGRAM', 'Install the generated script as completions for PROGRAM.' param 'CONFIG_PATH', <<~USAGE - Path to the YAML configuration file [default: completely.yaml]. + Path to the Completely YAML configuration file (pattern, flat, or nested) [default: completely.yaml]. Use '-' to read from stdin. Can also be set by an environment variable. diff --git a/lib/completely/commands/init.rb b/lib/completely/commands/init.rb index a9d9d1a..d891b9c 100644 --- a/lib/completely/commands/init.rb +++ b/lib/completely/commands/init.rb @@ -3,12 +3,12 @@ module Completely module Commands class Init < Base - help 'Create a new sample YAML configuration file' + help 'Create a new sample Completely YAML configuration file' - usage 'completely init [--nested] [CONFIG_PATH]' + usage 'completely init [--format FORMAT] [CONFIG_PATH]' usage 'completely init (-h|--help)' - option '-n --nested', 'Generate a nested configuration' + option '-f --format FORMAT', 'Sample format: pattern, flat, or nested [default: pattern]' param_config_path environment_config_path @@ -26,16 +26,29 @@ def sample @sample ||= File.read sample_path end - def nested? - args['--nested'] + def format + @format ||= args['--format'] || 'pattern' end def sample_path @sample_path ||= begin - sample_name = nested? ? 'sample-nested' : 'sample' - File.expand_path "../templates/#{sample_name}.yaml", __dir__ + raise Error, "Invalid format: #{format}" unless sample_filenames.key? format + + File.expand_path "../templates/#{sample_filename}", __dir__ end end + + def sample_filename + sample_filenames.fetch format + end + + def sample_filenames + @sample_filenames ||= { + 'flat' => 'flat-config/sample.yaml', + 'nested' => 'flat-config/sample-nested.yaml', + 'pattern' => 'pattern-config/sample.yaml', + } + end end end end diff --git a/lib/completely/commands/test.rb b/lib/completely/commands/test.rb index 359749f..07bacfb 100644 --- a/lib/completely/commands/test.rb +++ b/lib/completely/commands/test.rb @@ -6,8 +6,8 @@ class Test < Base summary 'Test completions' help 'This command can be used to test that your completions script responds with ' \ - 'the right completions. It works by reading your completely.yaml file, generating ' \ - 'a completions script, and generating a temporary testing script.' + 'the right completions. It works by reading a Completely YAML configuration file, ' \ + 'generating a completions script, and generating a temporary testing script.' usage 'completely test [--keep] COMPLINE...' usage 'completely test (-h|--help)' diff --git a/lib/completely/completions.rb b/lib/completely/completions.rb index 955a4f1..90b3c21 100644 --- a/lib/completely/completions.rb +++ b/lib/completely/completions.rb @@ -16,7 +16,7 @@ def read(io, function_name: nil) end def initialize(config, function_name: nil) - @config = config.is_a?(Config) ? config : Config.new(config) + @config = normalize_config config @function_name = function_name end @@ -29,6 +29,8 @@ def patterns end def valid? + return pattern_programs.uniq.one? if pattern_config? + pattern_prefixes.uniq.one? end @@ -62,7 +64,10 @@ def patterns! end def template_path - @template_path ||= File.expand_path('templates/template.erb', __dir__) + @template_path ||= begin + template = pattern_config? ? 'pattern-config/template.erb' : 'flat-config/template.erb' + File.expand_path("templates/#{template}", __dir__) + end end def template @@ -70,7 +75,7 @@ def template end def command - @command ||= flat_config.keys.first.split.first + @command ||= pattern_config? ? config.model[:program] : flat_config.keys.first.split.first end def function_name @@ -91,5 +96,86 @@ def complete_options_line "#{options} " end + + def pattern_config? + config.is_a? PatternConfig + end + + def pattern_routes + config.model[:routes] + end + + def pattern_programs + pattern_routes.map { |route| route.dig(:words, 0, :name) } + end + + def pattern_root_words + pattern_routes.flat_map do |route| + word = route[:words][1] + word ? [word[:name], *word[:aliases]] : [] + end.uniq + end + + def pattern_route_id(route) + pattern_routes.index route + end + + def pattern_route_conditions(route) + route[:words][1..].map.with_index do |word, index| + names = [word[:name], *word[:aliases]] + names.map { |name| %["${non_options[#{index}]}" == "#{bash_escape name}"] }.join(' || ') + end + end + + def pattern_route_word_count(route) + route[:words].size - 1 + end + + def pattern_route_options(route) + route[:option_groups].flat_map do |name| + config.model[:options][name] || [] + end + end + + def pattern_options_with_values + config.model[:options].values.flatten.select { |option| option[:value] } + end + + def pattern_source_empty?(source) + source[:items].empty? + end + + def pattern_source_compgen(source) + wordlist = source[:items] + .select { |item| item[:type] == :value } + .map { |item| item[:value] } + .join(' ') + + builtins = source[:items] + .select { |item| item[:type] == :builtin } + .map { |item| "-A #{bash_escape item[:value]}" } + + parts = [] + parts << %[-W "#{bash_double_quote_escape wordlist}"] unless wordlist.empty? + parts.concat builtins + parts.join(' ') + end + + def bash_escape(value) + value.to_s.gsub('\\', '\\\\\\').gsub('"', '\\"') + end + + def bash_double_quote_escape(value) + value.to_s.gsub('\\', '\\\\\\').gsub('"', '\\"') + end + + def normalize_config(config) + case config + when FlatConfig, PatternConfig + config + else + Config.build config + end + end end end diff --git a/lib/completely/config.rb b/lib/completely/config.rb index 643904c..1f94fb1 100644 --- a/lib/completely/config.rb +++ b/lib/completely/config.rb @@ -1,67 +1,26 @@ module Completely class Config - attr_reader :config, :options - class << self def parse(str) - new YAML.load(str, aliases: true) + build YAML.load(str, aliases: true) rescue Psych::Exception => e raise ParseError, "Invalid YAML: #{e.message}" end def load(path) = parse(File.read(path)) def read(io) = parse(io.read) - end - - def initialize(config) - @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {} - @config = config - end - - def flat_config - result = {} - - config.each do |root_key, root_list| - result.merge! process_key(root_key, root_list) - end - - result - end - private - - def process_key(prefix, list) - result = {} - result[prefix] = collect_immediate_children list - result.merge! process_nested_items(prefix, list) - result - end - - def collect_immediate_children(list) - list.map do |item| - x = item.is_a?(Hash) ? item.keys.first : item - x.gsub(/^[*+]/, '') + def build(config) + if pattern_config? config + PatternConfig.new config + else + FlatConfig.new config + end end - end - - def process_nested_items(prefix, list) - result = {} - list.each do |item| - next unless item.is_a? Hash - - nested_prefix = generate_nested_prefix(prefix, item) - nested_list = item.values.first - result.merge!(process_key(nested_prefix, nested_list)) + def pattern_config?(config) + config.is_a?(Hash) && config.has_key?('patterns') end - - result - end - - def generate_nested_prefix(prefix, item) - appended_prefix = item.keys.first.gsub(/^\+/, '*') - appended_prefix = " #{appended_prefix}" unless appended_prefix.start_with? '*' - "#{prefix}#{appended_prefix}" end end end diff --git a/lib/completely/flat_config.rb b/lib/completely/flat_config.rb new file mode 100644 index 0000000..49ac6ad --- /dev/null +++ b/lib/completely/flat_config.rb @@ -0,0 +1,56 @@ +module Completely + class FlatConfig + attr_reader :config, :options + + def initialize(config) + @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {} + @config = config + end + + def flat_config + result = {} + + config.each do |root_key, root_list| + result.merge! process_key(root_key, root_list) + end + + result + end + + private + + def process_key(prefix, list) + result = {} + result[prefix] = collect_immediate_children list + result.merge! process_nested_items(prefix, list) + result + end + + def collect_immediate_children(list) + list.map do |item| + x = item.is_a?(Hash) ? item.keys.first : item + x.gsub(/^[*+]/, '') + end + end + + def process_nested_items(prefix, list) + result = {} + + list.each do |item| + next unless item.is_a? Hash + + nested_prefix = generate_nested_prefix(prefix, item) + nested_list = item.values.first + result.merge!(process_key(nested_prefix, nested_list)) + end + + result + end + + def generate_nested_prefix(prefix, item) + appended_prefix = item.keys.first.gsub(/^\+/, '*') + appended_prefix = " #{appended_prefix}" unless appended_prefix.start_with? '*' + "#{prefix}#{appended_prefix}" + end + end +end diff --git a/lib/completely/pattern_config.rb b/lib/completely/pattern_config.rb new file mode 100644 index 0000000..d525c0e --- /dev/null +++ b/lib/completely/pattern_config.rb @@ -0,0 +1,201 @@ +module Completely + class PatternConfig + attr_reader :config, :options + + def initialize(config) + @options = config.delete('completely_options')&.transform_keys(&:to_sym) || {} + @config = config + end + + def model + validate! + + @model ||= { + program: program, + routes: routes, + options: parsed_options, + tokens: tokens, + } + end + + def flat_config + raise Error, 'Pattern config cannot be converted to flat config' + end + + private + + def patterns + @patterns ||= Array config['patterns'] + end + + def option_groups + @option_groups ||= config['options'] || {} + end + + def token_sources + @token_sources ||= config['tokens'] || {} + end + + def program + routes.first.dig(:words, 0, :name) + end + + def routes + @routes ||= patterns.map { |pattern| parse_pattern pattern } + end + + def parsed_options + @parsed_options ||= option_groups.to_h do |name, entries| + [name, Array(entries).map { |entry| parse_option entry }] + end + end + + def tokens + @tokens ||= token_sources.to_h do |name, source| + [name, parse_source(name, source)] + end + end + + def validate! + missing_options = referenced_options - option_groups.keys + missing_tokens = referenced_tokens - token_sources.keys + + errors = [] + errors << "Unknown option group: #{missing_options.join ', '}" if missing_options.any? + errors << "Unknown token: #{missing_tokens.join ', '}" if missing_tokens.any? + errors.concat repeatable_positional_errors + raise ParseError, errors.join("\n") if errors.any? + end + + def repeatable_positional_errors + patterns.filter_map do |pattern| + positionals = pattern_parts(pattern).select { |part| token? part } + next unless positionals[0...-1].any? { |part| repeatable_token? part } + + "Repeatable positional must be the last positional in pattern: #{pattern}" + end + end + + def referenced_options + patterns.flat_map do |pattern| + pattern_parts(pattern).filter_map { |part| option_group_name(part) if option_group?(part) } + end.uniq + end + + def referenced_tokens + pattern_tokens + option_tokens + end + + def pattern_tokens + patterns.flat_map do |pattern| + pattern_parts(pattern).filter_map { |part| token_name(part) if token?(part) } + end + end + + def option_tokens + option_groups.values.flatten.filter_map do |entry| + value_part = option_parts(entry).find { |part| token? part } + token_name(value_part) if value_part + end + end + + def parse_pattern(pattern) + result = { words: [], option_groups: [], positionals: [] } + + pattern_parts(pattern).each do |part| + if option_group?(part) + result[:option_groups] << option_group_name(part) + elsif token?(part) + result[:positionals] << parse_token(part) + else + result[:words] << parse_word(part) + end + end + + result + end + + def parse_word(part) + names = part.split('|') + { name: names.first, aliases: names[1..] || [] } + end + + def pattern_parts(pattern) + pattern.scan(/\[[^\]]+\]|<[^>]+>\.\.\.|<[^>]+>|\S+/) + end + + def parse_option(entry) + flag_part, *parts = option_parts entry + value_part = parts.find { |part| token? part } + metadata_parts = parts.select { |part| metadata? part } + unknown_parts = parts - [value_part] - metadata_parts + raise ParseError, "Invalid option syntax: #{entry}" if unknown_parts.any? + + names = flag_part.split('|') + + result = { names: names, repeatable: false } + result[:value] = parse_token(value_part) if value_part + metadata_parts.each { |part| apply_option_metadata result, part } + result + end + + def option_parts(entry) + entry.scan(/<[^>]+>|\([^)]+\)|\S+/) + end + + def apply_option_metadata(result, part) + case part + when '(repeatable)' + result[:repeatable] = true + else + raise ParseError, "Unknown option metadata: #{part}" + end + end + + def metadata?(part) + part.start_with?('(') && part.end_with?(')') + end + + def parse_token(part) + repeatable = repeatable_token? part + token_part = repeatable ? part.delete_suffix('...') : part + name = token_name token_part + result = { name: name, source: parse_source(name, token_sources[name]) } + result[:repeatable] = true if repeatable + result + end + + def parse_source(_name, source) + source_items = source.is_a?(Array) ? source : [source] + items = source_items.compact.map { |item| parse_source_item item } + { items: items } + end + + def parse_source_item(item) + return { type: :value, value: item.to_s[1..] } if item.to_s.start_with? '++' + return { type: :builtin, value: item.to_s[1..] } if item.to_s.start_with? '+' + + { type: :value, value: item.to_s } + end + + def option_group?(part) + part.start_with?('[') && part.end_with?(']') + end + + def option_group_name(part) + part[1..-2].sub(/\s+options\z/, '') + end + + def token?(part) + part.match?(/\A<[^>]+>(?:\.\.\.)?\z/) + end + + def repeatable_token?(part) + part.end_with? '...' + end + + def token_name(part) + part.delete_suffix('...')[/\A<(.+)>\z/, 1] + end + end +end diff --git a/lib/completely/templates/sample-nested.yaml b/lib/completely/templates/flat-config/sample-nested.yaml similarity index 100% rename from lib/completely/templates/sample-nested.yaml rename to lib/completely/templates/flat-config/sample-nested.yaml diff --git a/lib/completely/templates/sample.yaml b/lib/completely/templates/flat-config/sample.yaml similarity index 100% rename from lib/completely/templates/sample.yaml rename to lib/completely/templates/flat-config/sample.yaml diff --git a/lib/completely/templates/template.erb b/lib/completely/templates/flat-config/template.erb similarity index 100% rename from lib/completely/templates/template.erb rename to lib/completely/templates/flat-config/template.erb diff --git a/lib/completely/templates/pattern-config/sample.yaml b/lib/completely/templates/pattern-config/sample.yaml new file mode 100644 index 0000000..c307c2b --- /dev/null +++ b/lib/completely/templates/pattern-config/sample.yaml @@ -0,0 +1,21 @@ +patterns: + - mygit [root options] + - mygit init [init options] + - mygit status [status options] + +options: + root: + - -h|--help + - -v|--version + init: + - --bare + status: + - --help + - --branch|-b + - --format + - --verbose (repeatable) + +tokens: + directory: +directory + branch: $(git branch --format='%(refname:short)' 2>/dev/null) + format: [short, long] diff --git a/lib/completely/templates/pattern-config/template.erb b/lib/completely/templates/pattern-config/template.erb new file mode 100644 index 0000000..718d0b2 --- /dev/null +++ b/lib/completely/templates/pattern-config/template.erb @@ -0,0 +1,149 @@ +# <%= "#{command} completion".ljust 56 %> -*- shell-script -*- + +# This bash completions script was generated by +# completely (https://github.com/bashly-framework/completely) +# Modifying it manually is not recommended + +<%= function_name %>_flag_expects_value() { + case "$1" in +% pattern_options_with_values.each do |option| + <%= option[:names].map { |name| bash_escape name }.join('|') %>) return 0 ;; +% end + esac + + return 1 +} + +<%= function_name %>() { + local cur=${COMP_WORDS[COMP_CWORD]} + local prev= + if ((COMP_CWORD > 0)); then + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} + fi + + local completed=() + if ((COMP_CWORD > 1)); then + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + fi + + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if <%= function_name %>_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 +% pattern_routes.each do |route| +% conditions = pattern_route_conditions(route) + if (( ${#non_options[@]} >= <%= pattern_route_word_count route %> )) && + (( <%= pattern_route_word_count route %> > route_word_count ))<%= conditions.empty? ? '' : ' &&' %> +% conditions.each_with_index do |condition, index| + [[ <%= condition %> ]]<%= index == conditions.size - 1 ? '' : ' &&' %> +% end + then + route_id=<%= pattern_route_id route %> + route_word_count=<%= pattern_route_word_count route %> + route_has_positionals=<%= route[:positionals].empty? ? 0 : 1 %> + positional_index=$((${#non_options[@]} - <%= pattern_route_word_count route %>)) + fi + +% end + COMPREPLY=() + + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "<%= bash_double_quote_escape pattern_root_words.join(' ') %>" -- "$cur") + return + fi + + case "$route_id:$prev" in +% pattern_routes.each do |route| +% pattern_route_options(route).select { |option| option[:value] }.each do |option| + <%= pattern_route_id route %>:<%= option[:names].map { |name| bash_escape name }.join("|#{pattern_route_id route}:") %>) +% if pattern_source_empty? option[:value][:source] + return +% else + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen option[:value][:source] %> -- "$cur") + return +% end + ;; +% end +% end + esac + + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in +% pattern_routes.each do |route| + <%= pattern_route_id route %>) + local words=() +% pattern_route_options(route).each do |option| +% if option[:repeatable] + words+=(<%= option[:names].map { |name| %["#{bash_escape name}"] }.join(' ') %>) +% else + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + <%= option[:names].map { |name| bash_escape name }.join('|') %>) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=(<%= option[:names].map { |name| %["#{bash_escape name}"] }.join(' ') %>) + fi +% end +% end + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; +% end + esac + fi + +% pattern_routes.each do |route| +% route[:positionals].each_with_index do |positional, index| +% next unless positional[:repeatable] + if [[ "$route_id" == "<%= pattern_route_id route %>" ]] && (( positional_index >= <%= index %> )); then +% if pattern_source_empty? positional[:source] + return +% else + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen positional[:source] %> -- "$cur") + return +% end + fi + +% end +% end + case "$route_id:$positional_index" in +% pattern_routes.each do |route| +% route[:positionals].each_with_index do |positional, index| +% next if positional[:repeatable] + <%= pattern_route_id route %>:<%= index %>) +% if pattern_source_empty? positional[:source] + return +% else + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen <%= pattern_source_compgen positional[:source] %> -- "$cur") + return +% end + ;; +% end +% end + esac +} && + complete <%= complete_options_line %>-F <%= function_name %> <%= command %> + +# ex: filetype=sh diff --git a/lib/completely/templates/tester-template.erb b/lib/completely/templates/tester.bash.erb similarity index 100% rename from lib/completely/templates/tester-template.erb rename to lib/completely/templates/tester.bash.erb diff --git a/lib/completely/tester.rb b/lib/completely/tester.rb index 48d095f..2cbbe8e 100644 --- a/lib/completely/tester.rb +++ b/lib/completely/tester.rb @@ -37,7 +37,7 @@ def absolute_script_path end def template_path - @template_path ||= File.expand_path 'templates/tester-template.erb', __dir__ + @template_path ||= File.expand_path 'templates/tester.bash.erb', __dir__ end def template diff --git a/schemas/completely.json b/schemas/completely.json index 507639e..1c63049 100644 --- a/schemas/completely.json +++ b/schemas/completely.json @@ -1,6 +1,110 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Completely configuration", + "description": "Configuration for generating bash completions with Completely.", + "oneOf": [ + { "$ref": "#/definitions/patternConfig" }, + { "$ref": "#/definitions/flatOrNestedConfig" } + ], "definitions": { + "completelyOptions": { + "type": "object", + "properties": { + "complete_options": { + "type": "string", + "examples": ["-o nosort"] + } + }, + "required": ["complete_options"], + "additionalProperties": false + }, + "nonEmptyString": { + "type": "string", + "minLength": 1 + }, + "pattern": { + "allOf": [ + { "$ref": "#/definitions/nonEmptyString" } + ], + "examples": [ + "mygit [root options]", + "mygit init [init options] ", + "mygit upload ...", + "mygit status|st [status options]" + ] + }, + "option": { + "type": "string", + "pattern": "^\\S+(?:\\s+<[^>]+>)?(?:\\s+\\(repeatable\\))?$", + "examples": [ + "--help", + "--version", + "-h|--help", + "--branch|-b ", + "--tag (repeatable)" + ] + }, + "tokenSource": { + "oneOf": [ + { + "allOf": [ + { "$ref": "#/definitions/nonEmptyString" } + ], + "examples": [ + "+directory", + "+file", + "README.md", + "$(git branch --format='%(refname:short)' 2>/dev/null)" + ] + }, + { + "type": "array", + "items": { "$ref": "#/definitions/tokenValue" }, + "examples": [ + ["short", "long"], + [0, 10, 100], + ["+file", "+directory", "README.md"], + [null, "+file"] + ] + }, + { + "type": "null", + "examples": [null] + } + ] + }, + "tokenValue": { + "oneOf": [ + { "$ref": "#/definitions/nonEmptyString" }, + { "type": "null" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + "patternConfig": { + "type": "object", + "required": ["patterns"], + "properties": { + "completely_options": { "$ref": "#/definitions/completelyOptions" }, + "patterns": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/definitions/pattern" } + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "$ref": "#/definitions/option" } + } + }, + "tokens": { + "type": "object", + "additionalProperties": { "$ref": "#/definitions/tokenSource" } + } + }, + "additionalProperties": false + }, "word": { "type": "string", "minLength": 1, @@ -38,34 +142,25 @@ { "$ref": "#/definitions/word" }, { "type": "object", - "additionalProperties": { - "type": "array", - "items": { "$ref": "#/definitions/wordOrMapping" } - } + "additionalProperties": { "$ref": "#/definitions/entryArray" } } ] }, "entryArray": { "type": "array", "items": { "$ref": "#/definitions/wordOrMapping" } - } - }, - "type": "object", - "properties": { - "completely_options": { + }, + "flatOrNestedConfig": { "type": "object", "properties": { - "complete_options": { - "type": "string", - "examples": ["-o nosort"] + "completely_options": { "$ref": "#/definitions/completelyOptions" } + }, + "patternProperties": { + "^(?!(completely_options|patterns|options|tokens)$).*": { + "$ref": "#/definitions/entryArray" } }, - "required": ["complete_options"], "additionalProperties": false } - }, - "patternProperties": { - "^(?!completely_options$).*": { "$ref": "#/definitions/entryArray" } - }, - "additionalProperties": false + } } diff --git a/spec/approvals/cli/commands b/spec/approvals/cli/commands index 639d394..77fad2b 100644 --- a/spec/approvals/cli/commands +++ b/spec/approvals/cli/commands @@ -1,7 +1,7 @@ Completely - Bash Completions Generator Commands: - init Create a new sample YAML configuration file + init Create a new sample Completely YAML configuration file preview Generate the bash completion script to stdout generate Generate the bash completion script to file or stdout test Test completions diff --git a/spec/approvals/cli/generate/help b/spec/approvals/cli/generate/help index 0cf96a1..d757eda 100644 --- a/spec/approvals/cli/generate/help +++ b/spec/approvals/cli/generate/help @@ -21,7 +21,8 @@ Options: Parameters: CONFIG_PATH - Path to the YAML configuration file [default: completely.yaml]. + Path to the Completely YAML configuration file (pattern, flat, or nested) + [default: completely.yaml]. Use '-' to read from stdin. Can also be set by an environment variable. @@ -38,7 +39,7 @@ Parameters: Environment Variables: COMPLETELY_CONFIG_PATH - Path to a completely configuration file [default: completely.yaml]. + Path to a Completely YAML configuration file [default: completely.yaml]. COMPLETELY_OUTPUT_PATH Path to the output bash script. diff --git a/spec/approvals/cli/init/flat b/spec/approvals/cli/init/flat new file mode 100644 index 0000000..665ba20 --- /dev/null +++ b/spec/approvals/cli/init/flat @@ -0,0 +1 @@ +Saved completely.yaml diff --git a/spec/approvals/cli/init/help b/spec/approvals/cli/init/help index 69f3d38..66984df 100644 --- a/spec/approvals/cli/init/help +++ b/spec/approvals/cli/init/help @@ -1,21 +1,22 @@ -Create a new sample YAML configuration file +Create a new sample Completely YAML configuration file Usage: - completely init [--nested] [CONFIG_PATH] + completely init [--format FORMAT] [CONFIG_PATH] completely init (-h|--help) Options: - -n --nested - Generate a nested configuration + -f --format FORMAT + Sample format: pattern, flat, or nested [default: pattern] -h --help Show this help Parameters: CONFIG_PATH - Path to the YAML configuration file [default: completely.yaml]. + Path to the Completely YAML configuration file (pattern, flat, or nested) + [default: completely.yaml]. Can also be set by an environment variable. Environment Variables: COMPLETELY_CONFIG_PATH - Path to a completely configuration file [default: completely.yaml]. + Path to a Completely YAML configuration file [default: completely.yaml]. diff --git a/spec/approvals/cli/init/invalid-format b/spec/approvals/cli/init/invalid-format new file mode 100644 index 0000000..cb0130e --- /dev/null +++ b/spec/approvals/cli/init/invalid-format @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/approvals/cli/init/pattern b/spec/approvals/cli/init/pattern new file mode 100644 index 0000000..665ba20 --- /dev/null +++ b/spec/approvals/cli/init/pattern @@ -0,0 +1 @@ +Saved completely.yaml diff --git a/spec/approvals/cli/preview/help b/spec/approvals/cli/preview/help index fb122af..162be93 100644 --- a/spec/approvals/cli/preview/help +++ b/spec/approvals/cli/preview/help @@ -13,12 +13,13 @@ Options: Parameters: CONFIG_PATH - Path to the YAML configuration file [default: completely.yaml]. + Path to the Completely YAML configuration file (pattern, flat, or nested) + [default: completely.yaml]. Can also be set by an environment variable. Environment Variables: COMPLETELY_CONFIG_PATH - Path to a completely configuration file [default: completely.yaml]. + Path to a Completely YAML configuration file [default: completely.yaml]. COMPLETELY_DEBUG If not empty, the generated script will include an additional debugging diff --git a/spec/approvals/cli/test/help b/spec/approvals/cli/test/help index 8918a91..1e55834 100644 --- a/spec/approvals/cli/test/help +++ b/spec/approvals/cli/test/help @@ -1,8 +1,8 @@ Test completions This command can be used to test that your completions script responds with the -right completions. It works by reading your completely.yaml file, generating a -completions script, and generating a temporary testing script. +right completions. It works by reading a Completely YAML configuration file, +generating a completions script, and generating a temporary testing script. Usage: completely test [--keep] COMPLINE... @@ -24,7 +24,7 @@ Parameters: Environment Variables: COMPLETELY_CONFIG_PATH - Path to a completely configuration file [default: completely.yaml]. + Path to a Completely YAML configuration file [default: completely.yaml]. COMPLETELY_DEBUG If not empty, the generated script will include an additional debugging diff --git a/spec/approvals/cli/warning b/spec/approvals/cli/warning index 533ce47..56d30e6 100644 --- a/spec/approvals/cli/warning +++ b/spec/approvals/cli/warning @@ -1,4 +1,4 @@ WARNING: Your configuration is invalid. -All patterns must start with the same word. +All completion patterns must use the same command name. diff --git a/spec/approvals/completions/script-pattern b/spec/approvals/completions/script-pattern new file mode 100644 index 0000000..4b8788d --- /dev/null +++ b/spec/approvals/completions/script-pattern @@ -0,0 +1,136 @@ +# mygit completion -*- shell-script -*- + +# This bash completions script was generated by +# completely (https://github.com/bashly-framework/completely) +# Modifying it manually is not recommended + +_mygit_completions_flag_expects_value() { + case "$1" in + --branch|-b) return 0 ;; + esac + + return 1 +} + +_mygit_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + local prev= + if ((COMP_CWORD > 0)); then + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} + fi + + local completed=() + if ((COMP_CWORD > 1)); then + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + fi + + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _mygit_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "init" ]] + then + route_id=0 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) + fi + + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "status" || "${non_options[0]}" == "st" ]] + then + route_id=1 + route_word_count=1 + route_has_positionals=0 + positional_index=$((${#non_options[@]} - 1)) + fi + + COMPREPLY=() + + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "init status st" -- "$cur") + return + fi + + case "$route_id:$prev" in + 1:--branch|1:-b) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "$(echo main dev)" -- "$cur") + return + ;; + esac + + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --bare) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--bare") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + 1) + local words=() + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --verbose|-v) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--verbose" "-v") + fi + local option_seen=0 + for completed_option in "${completed_options[@]}"; do + case "$completed_option" in + --branch|-b) option_seen=1 ;; + esac + done + if ((!option_seen)); then + words+=("--branch" "-b") + fi + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi + + case "$route_id:$positional_index" in + 0:0) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -A directory -- "$cur") + return + ;; + esac +} && + complete -F _mygit_completions mygit + +# ex: filetype=sh diff --git a/spec/approvals/completions/script-pattern-complete-options b/spec/approvals/completions/script-pattern-complete-options new file mode 100644 index 0000000..a1977db --- /dev/null +++ b/spec/approvals/completions/script-pattern-complete-options @@ -0,0 +1,89 @@ +# cli completion -*- shell-script -*- + +# This bash completions script was generated by +# completely (https://github.com/bashly-framework/completely) +# Modifying it manually is not recommended + +_cli_completions_flag_expects_value() { + case "$1" in + esac + + return 1 +} + +_cli_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + local prev= + if ((COMP_CWORD > 0)); then + prev=${COMP_WORDS[$((COMP_CWORD - 1))]} + fi + + local completed=() + if ((COMP_CWORD > 1)); then + completed=("${COMP_WORDS[@]:1:$((COMP_CWORD - 1))}") + fi + + local non_options=() + local completed_options=() + local skip_next=0 + for word in "${completed[@]}"; do + if ((skip_next)); then + skip_next=0 + continue + fi + + if [[ "${word:0:1}" == "-" ]]; then + completed_options+=("$word") + if _cli_completions_flag_expects_value "$word"; then + skip_next=1 + fi + continue + fi + + non_options+=("$word") + done + + local route_id= + local route_word_count=-1 + local route_has_positionals=0 + local positional_index=0 + if (( ${#non_options[@]} >= 1 )) && + (( 1 > route_word_count )) && + [[ "${non_options[0]}" == "set" ]] + then + route_id=0 + route_word_count=1 + route_has_positionals=1 + positional_index=$((${#non_options[@]} - 1)) + fi + + COMPREPLY=() + + if [[ -z "$route_id" ]] || { (( route_word_count == 0 )) && (( !route_has_positionals )) && [[ "${cur:0:1}" != "-" ]]; }; then + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "set" -- "$cur") + return + fi + + case "$route_id:$prev" in + esac + + if [[ "${cur:0:1}" == "-" ]]; then + case "$route_id" in + 0) + local words=() + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "${words[*]}" -- "$cur") + return + ;; + esac + fi + + case "$route_id:$positional_index" in + 0:0) + while read -r; do COMPREPLY+=("$REPLY"); done < <(compgen -W "0 10 20 30 40 50 60 70 80 90 100" -- "$cur") + return + ;; + esac +} && + complete -o nosort -F _cli_completions cli + +# ex: filetype=sh diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index 9d152ea..a7f44c8 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -3,7 +3,7 @@ before do reset_tmp_dir - system 'cp lib/completely/templates/sample.yaml completely.yaml' + system 'cp lib/completely/templates/flat-config/sample.yaml completely.yaml' end after do @@ -43,7 +43,7 @@ context 'with COMPLETELY_CONFIG_PATH env var' do before do reset_tmp_dir - system 'cp lib/completely/templates/sample.yaml spec/tmp/hello.yml' + system 'cp lib/completely/templates/flat-config/sample.yaml spec/tmp/hello.yml' system 'rm -f completely.yaml' ENV['COMPLETELY_CONFIG_PATH'] = 'spec/tmp/hello.yml' end @@ -148,7 +148,7 @@ context 'with an invalid configuration' do it 'outputs a warning to STDERR' do - expect { subject.execute %w[generate spec/fixtures/broken.yaml spec/tmp/out.bash] } + expect { subject.execute %w[generate spec/fixtures/flat-config/broken.yaml spec/tmp/out.bash] } .to output_approval('cli/warning').to_stderr end end diff --git a/spec/completely/commands/init_spec.rb b/spec/completely/commands/init_spec.rb index 65a5cc4..01d5a30 100644 --- a/spec/completely/commands/init_spec.rb +++ b/spec/completely/commands/init_spec.rb @@ -4,8 +4,9 @@ before { system 'rm -f completely.yaml' } after { system 'rm -f completely.yaml' } - let(:sample) { File.read 'lib/completely/templates/sample.yaml' } - let(:sample_nested) { File.read 'lib/completely/templates/sample-nested.yaml' } + let(:sample) { File.read 'lib/completely/templates/flat-config/sample.yaml' } + let(:sample_nested) { File.read 'lib/completely/templates/flat-config/sample-nested.yaml' } + let(:sample_pattern) { File.read 'lib/completely/templates/pattern-config/sample.yaml' } context 'with --help' do it 'shows long usage' do @@ -16,24 +17,44 @@ context 'without arguments' do it 'creates a new sample file named completely.yaml' do expect { subject.execute %w[init] }.to output_approval('cli/init/no-args') + expect(File.read 'completely.yaml').to eq sample_pattern + end + end + + context 'with --format flat' do + it 'creates a sample using the flat configuration' do + expect { subject.execute %w[init --format flat] }.to output_approval('cli/init/flat') expect(File.read 'completely.yaml').to eq sample end end - context 'with --nested' do + context 'with --format nested' do it 'creates a sample using the nested configuration' do - expect { subject.execute %w[init --nested] }.to output_approval('cli/init/nested') + expect { subject.execute %w[init --format nested] }.to output_approval('cli/init/nested') expect(File.read 'completely.yaml').to eq sample_nested end end + context 'with --format pattern' do + it 'creates a sample using the pattern configuration' do + expect { subject.execute %w[init --format pattern] }.to output_approval('cli/init/pattern') + expect(File.read 'completely.yaml').to eq sample_pattern + end + end + + context 'with an invalid format' do + it 'raises an error' do + expect { subject.execute %w[init --format invalid] }.to raise_approval('cli/init/invalid-format') + end + end + context 'with CONFIG_PATH' do before { reset_tmp_dir } it 'creates a new sample file with the requested name' do expect { subject.execute %w[init spec/tmp/in.yaml] } .to output_approval('cli/init/custom-path') - expect(File.read 'spec/tmp/in.yaml').to eq sample + expect(File.read 'spec/tmp/in.yaml').to eq sample_pattern end end @@ -48,12 +69,12 @@ it 'creates a new sample file with the requested name' do expect { subject.execute %w[init] } .to output_approval('cli/init/custom-path-env') - expect(File.read 'spec/tmp/hello.yml').to eq sample + expect(File.read 'spec/tmp/hello.yml').to eq sample_pattern end end context 'when the config file already exists' do - before { system 'cp lib/completely/templates/sample.yaml completely.yaml' } + before { system 'cp lib/completely/templates/flat-config/sample.yaml completely.yaml' } after { system 'rm -f completely.yaml' } it 'raises an error' do diff --git a/spec/completely/commands/preview_spec.rb b/spec/completely/commands/preview_spec.rb index d531a6d..6b8685f 100644 --- a/spec/completely/commands/preview_spec.rb +++ b/spec/completely/commands/preview_spec.rb @@ -1,7 +1,7 @@ describe Commands::Preview do subject { described_class.new } - before { system 'cp lib/completely/templates/sample.yaml completely.yaml' } + before { system 'cp lib/completely/templates/flat-config/sample.yaml completely.yaml' } after { system 'rm -f completely.yaml' } context 'with --help' do @@ -27,7 +27,7 @@ context 'with COMPLETELY_CONFIG_PATH env var' do before do reset_tmp_dir - system 'cp lib/completely/templates/sample.yaml spec/tmp/hello.yml' + system 'cp lib/completely/templates/flat-config/sample.yaml spec/tmp/hello.yml' system 'rm -f completely.yaml' ENV['COMPLETELY_CONFIG_PATH'] = 'spec/tmp/hello.yml' end @@ -42,7 +42,7 @@ context 'with an invalid configuration' do it 'outputs a warning to STDERR' do - expect { subject.execute %w[preview spec/fixtures/broken.yaml] } + expect { subject.execute %w[preview spec/fixtures/flat-config/broken.yaml] } .to output_approval('cli/warning').to_stderr end end diff --git a/spec/completely/commands/test_spec.rb b/spec/completely/commands/test_spec.rb index 04a7787..3f554d2 100644 --- a/spec/completely/commands/test_spec.rb +++ b/spec/completely/commands/test_spec.rb @@ -2,11 +2,14 @@ subject { described_class.new } before do - system 'cp lib/completely/templates/sample.yaml completely.yaml' + system 'cp lib/completely/templates/flat-config/sample.yaml completely.yaml' ENV['COMPLETELY_CONFIG_PATH'] = nil end - after { system 'rm -f completely.yaml' } + after do + system 'rm -f completely.yaml' + ENV['COMPLETELY_CONFIG_PATH'] = nil + end context 'with --help' do it 'shows long usage' do diff --git a/spec/completely/completions_spec.rb b/spec/completely/completions_spec.rb index 7e9cf4f..04c39e2 100644 --- a/spec/completely/completions_spec.rb +++ b/spec/completely/completions_spec.rb @@ -1,7 +1,7 @@ describe Completions do subject { described_class.load path } - let(:path) { "spec/fixtures/#{file}.yaml" } + let(:path) { "spec/fixtures/flat-config/#{file}.yaml" } let(:file) { 'basic' } describe '::read' do @@ -11,6 +11,22 @@ end end + describe '#initialize' do + it 'builds a config from a hash' do + completions = described_class.new({ 'cli' => %w[--help --version] }) + + expect(completions.config).to be_a FlatConfig + expect(completions.config.config).to eq({ 'cli' => %w[--help --version] }) + end + + it 'accepts a built config object' do + config = FlatConfig.new({ 'cli' => %w[--help --version] }) + completions = described_class.new config + + expect(completions.config).to be config + end + end + describe '#valid?' do context 'when all patterns start with the same word' do it 'returns true' do @@ -25,6 +41,22 @@ expect(subject).not_to be_valid end end + + context 'with pattern config' do + let(:path) { 'spec/fixtures/pattern-config/basic.yaml' } + + it 'returns true when all patterns use the same program' do + expect(subject).to be_valid + end + end + + context 'with pattern config using different programs' do + let(:path) { 'spec/fixtures/pattern-config/invalid-programs.yaml' } + + it 'returns false' do + expect(subject).not_to be_valid + end + end end describe '#patterns' do @@ -39,6 +71,14 @@ expect(subject.script).to match_approval 'completions/script' end + context 'with a pattern configuration file' do + let(:path) { 'spec/fixtures/pattern-config/basic.yaml' } + + it 'returns a bash completions script' do + expect(subject.script).to match_approval 'completions/script-pattern' + end + end + context 'with a configuration file that only includes patterns with spaces' do let(:file) { 'only-spaces' } @@ -64,6 +104,14 @@ expect(subject.script).to match_approval 'completions/script-complete-options' end end + + context 'with a pattern configuration file that includes complete_options' do + let(:path) { 'spec/fixtures/pattern-config/complete_options.yaml' } + + it 'adds the complete_options to the complete command' do + expect(subject.script).to match_approval 'completions/script-pattern-complete-options' + end + end end describe '#wrapper_function' do diff --git a/spec/completely/config_spec.rb b/spec/completely/config_spec.rb index 6d16e59..46d6a09 100644 --- a/spec/completely/config_spec.rb +++ b/spec/completely/config_spec.rb @@ -1,7 +1,7 @@ describe Config do subject { described_class.load path } - let(:path) { "spec/fixtures/#{file}.yaml" } + let(:path) { "spec/fixtures/flat-config/#{file}.yaml" } let(:file) { 'nested' } let(:config_string) { 'cli: [--help, --version]' } let(:config_hash) { { 'cli' => %w[--help --version] } } @@ -11,6 +11,19 @@ expect(described_class.parse(config_string).config).to eq config_hash end + it 'returns a flat config for the existing configuration format' do + expect(described_class.parse(config_string)).to be_a FlatConfig + end + + it 'returns a pattern config for the pattern configuration format' do + config = described_class.parse <<~YAML + patterns: + - cli [options] + YAML + + expect(config).to be_a PatternConfig + end + context 'when the string is not a valid YAML' do it 'raises ParseError' do expect { described_class.parse('not: a: yaml') }.to raise_error(Completely::ParseError) diff --git a/spec/completely/pattern_config_integration.yml b/spec/completely/pattern_config_integration.yml new file mode 100644 index 0000000..f21054a --- /dev/null +++ b/spec/completely/pattern_config_integration.yml @@ -0,0 +1,157 @@ +basic: +- compline: "mygit " + expected: [init, st, status] + +- compline: "mygit stat" + expected: [status] + +- compline: "mygit status -" + expected: [--branch, --verbose, -b, -v] + +- compline: "mygit init " + expected: [another-dir, dir with spaces, dummy-dir] + +- compline: "mygit st -" + expected: [--branch, --verbose, -b, -v] + +- compline: "mygit status --branch " + expected: [dev, main] + +- compline: "mygit status -b m" + expected: [main] + +- compline: "mygit init d" + expected: [dir with spaces, dummy-dir] + +- compline: "mygit init dummy-dir -" + expected: [--bare] + +static-values: +- compline: "mygit deploy st" + expected: [staging] + +spaced-token: +- compline: "mygit status --format l" + expected: [long] + +complete_options: +- compline: "cli set " + expected: ['0', '10', '100', '20', '30', '40', '50', '60', '70', '80', '90'] + +route-specificity: +- compline: "cli " + expected: [r, repo] + +- compline: "cli -" + expected: [--root, --user] + +- compline: "cli repo -" + expected: [--repo, --user] + +- compline: "cli repo remote -" + expected: [--remote] + +- compline: "cli r rm -" + expected: [--remote] + +- compline: "cli repo --root remote -" + expected: [--remote] + +- compline: "cli repo --user admin remote -" + expected: [--remote] + +route-specificity-reversed: +- compline: "cli repo remote -" + expected: [--remote] + +- compline: "cli repo -" + expected: [--repo] + +- compline: "cli -" + expected: [--root] + +repeatable: +- compline: "cli download -" + expected: [--help, --protocol, --user, --verbose, --version, -h, -p, -u, -v] + +- compline: "cli download --version -" + expected: [--help, --protocol, --user, --verbose, -h, -p, -u, -v] + +- compline: "cli download -h -" + expected: [--protocol, --user, --verbose, --version, -p, -u, -v] + +- compline: "cli download --help -" + expected: [--protocol, --user, --verbose, --version, -p, -u, -v] + +- compline: "cli download -v -" + expected: [--help, --protocol, --user, --verbose, --version, -h, -p, -u, -v] + +- compline: "cli download -u " + expected: [alice, bob] + +- compline: "cli download -u alice -" + expected: [--help, --protocol, --user, --verbose, --version, -h, -p, -u, -v] + +- compline: "cli download -p ssh -" + expected: [--help, --user, --verbose, --version, -h, -u, -v] + +interleaved: +- compline: "cli " + expected: [clone] + +- compline: "cli clone " + expected: [source1, source2] + +- compline: "cli clone -v " + expected: [source1, source2] + +- compline: "cli clone -v source1 " + expected: [dest1, dest2] + +- compline: "cli clone -v source1 -p " + expected: [https, ssh] + +- compline: "cli clone -v source1 -p ssh " + expected: [dest1, dest2] + +nil-source: +- compline: "cli copy " + expected: [] + +- compline: "cli copy source1 " + expected: [target1, target2] + +- compline: "cli copy source1 t" + expected: [target1, target2] + +mixed-source: +- compline: "cli copy " + expected: [another-dir, dir with spaces, dummy-dir, target1, target2] + +- compline: "cli copy t" + expected: [target1, target2] + +- compline: "cli literal " + expected: ["+file"] + +repeatable-positionals: +- compline: "cli " + expected: [copy, upload] + +- compline: "cli upload " + expected: [file1, file2] + +- compline: "cli upload file1 " + expected: [file1, file2] + +- compline: "cli upload file1 file2 " + expected: [file1, file2] + +- compline: "cli copy " + expected: [source1, source2] + +- compline: "cli copy source1 " + expected: [target1, target2] + +- compline: "cli copy source1 target1 " + expected: [target1, target2] diff --git a/spec/completely/pattern_config_integration_spec.rb b/spec/completely/pattern_config_integration_spec.rb new file mode 100644 index 0000000..4e76631 --- /dev/null +++ b/spec/completely/pattern_config_integration_spec.rb @@ -0,0 +1,27 @@ +describe 'generated pattern config script' do + subject { Completions.load config_path } + + let(:config_path) { File.expand_path("../fixtures/pattern-config/#{fixture}.yaml", __dir__) } + + let(:response) do + Dir.chdir 'spec/fixtures/integration' do + subject.tester.test(compline).sort + end + end + + config = YAML.load_file 'spec/completely/pattern_config_integration.yml' + + config.each do |fixture, use_cases| + use_cases.each do |use_case| + describe "#{fixture} ▶ '#{use_case['compline']}'" do + let(:fixture) { fixture } + let(:compline) { use_case['compline'] } + let(:expected) { use_case['expected'] } + + it "returns #{use_case['expected'].join ' '}" do + expect(response).to eq expected + end + end + end + end +end diff --git a/spec/completely/pattern_config_spec.rb b/spec/completely/pattern_config_spec.rb new file mode 100644 index 0000000..ca7d353 --- /dev/null +++ b/spec/completely/pattern_config_spec.rb @@ -0,0 +1,202 @@ +describe PatternConfig do + subject(:config) { Config.load 'spec/fixtures/pattern-config/basic.yaml' } + + describe '#model' do + it 'returns the command name' do + expect(config.model[:program]).to eq 'mygit' + end + + it 'returns route words' do + words = config.model[:routes].map { |route| route[:words] } + + expect(words).to eq [ + [{ name: 'mygit', aliases: [] }, { name: 'init', aliases: [] }], + [{ name: 'mygit', aliases: [] }, { name: 'status', aliases: ['st'] }], + ] + end + + it 'returns route option groups' do + option_groups = config.model[:routes].map { |route| route[:option_groups] } + + expect(option_groups).to eq [['init'], ['status']] + end + + it 'returns route positionals' do + positionals = config.model[:routes].map { |route| route[:positionals] } + + expect(positionals).to eq [ + [{ name: 'directory', source: { items: [{ type: :builtin, value: 'directory' }] } }], + [], + ] + end + + it 'returns init options' do + expect(config.model[:options]['init']).to eq [{ names: ['--bare'], repeatable: false }] + end + + it 'returns status flag options' do + expect(config.model[:options]['status'].first).to eq( + names: ['--verbose', '-v'], + repeatable: false + ) + end + + it 'returns status options with values' do + expect(config.model[:options]['status'].last).to eq( + names: ['--branch', '-b'], + repeatable: false, + value: { + name: 'branch', + source: { items: [{ type: :value, value: '$(echo main dev)' }] }, + } + ) + end + + it 'returns token sources' do + expect(config.model[:tokens]).to eq( + 'directory' => { items: [{ type: :builtin, value: 'directory' }] }, + 'branch' => { items: [{ type: :value, value: '$(echo main dev)' }] } + ) + end + end + + describe '#flat_config' do + it 'does not convert pattern config to flat config' do + expect { config.flat_config } + .to raise_error Completely::Error, 'Pattern config cannot be converted to flat config' + end + end + + context 'when complete_options is defined' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/complete_options.yaml' } + + describe 'config' do + it 'ignores the completely_options YAML key' do + expect(config.config.keys).to eq %w[patterns tokens] + end + end + + describe 'options' do + it 'returns the completely_options hash from the YAML file' do + expect(config.options[:complete_options]).to eq '-o nosort' + end + end + end + + context 'with a missing option group' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/missing-option.yaml' } + + it 'raises ParseError' do + expect { config.model }.to raise_error Completely::ParseError, 'Unknown option group: missing' + end + end + + context 'with a missing token' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/missing-token.yaml' } + + it 'raises ParseError' do + expect { config.model }.to raise_error Completely::ParseError, 'Unknown token: directory' + end + end + + context 'with a nil token source' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/nil-source.yaml' } + + it 'returns an empty source' do + expect(config.model[:tokens]['source']).to eq(items: []) + end + + it 'uses the empty source for positionals' do + expect(config.model[:routes].first[:positionals].first).to eq( + name: 'source', + source: { items: [] } + ) + end + end + + context 'with a mixed token source' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/mixed-source.yaml' } + + it 'returns builtin and value items' do + expect(config.model[:tokens]['target']).to eq( + items: [ + { type: :builtin, value: 'directory' }, + { type: :value, value: 'target1' }, + { type: :value, value: '$(echo target2)' }, + ] + ) + end + + it 'escapes literal values that start with +' do + expect(config.model[:tokens]['value']).to eq( + items: [{ type: :value, value: '+file' }] + ) + end + end + + context 'with a repeatable option' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/repeatable.yaml' } + + let(:name_source) do + { + items: [ + { type: :value, value: 'alice' }, + { type: :value, value: 'bob' }, + ], + } + end + + it 'marks repeatable options' do + expect(config.model[:options]['download'].last).to eq( + names: ['-u', '--user'], + repeatable: true, + value: { + name: 'name', + source: name_source, + } + ) + end + end + + context 'with repeatable positionals' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/repeatable-positionals.yaml' } + + let(:file_source) do + { + items: [ + { type: :value, value: 'file1' }, + { type: :value, value: 'file2' }, + ], + } + end + + it 'marks repeatable positionals' do + expect(config.model[:routes].first[:positionals]).to eq [ + { + name: 'file', + repeatable: true, + source: file_source, + }, + ] + end + end + + context 'with a non-final repeatable positional' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/repeatable-positionals-invalid.yaml' } + + it 'raises ParseError' do + expect { config.model }.to raise_error( + Completely::ParseError, + 'Repeatable positional must be the last positional in pattern: cli copy ... ' + ) + end + end + + context 'with unknown option metadata' do + subject(:config) { Config.load 'spec/fixtures/pattern-config/unknown-metadata.yaml' } + + it 'raises ParseError' do + expect { config.model }.to raise_error Completely::ParseError, 'Unknown option metadata: (hidden)' + end + end +end diff --git a/spec/completely/zsh_spec.rb b/spec/completely/zsh_spec.rb index ef90968..8971a65 100644 --- a/spec/completely/zsh_spec.rb +++ b/spec/completely/zsh_spec.rb @@ -5,7 +5,7 @@ end end - let(:completions) { Completely::Completions.load 'spec/fixtures/basic.yaml' } + let(:completions) { Completely::Completions.load 'spec/fixtures/flat-config/basic.yaml' } let(:words) { 'completely generate ' } let(:tester_script) { completions.tester.tester_script words } let(:shell) { 'zsh' } diff --git a/spec/fixtures/basic.yaml b/spec/fixtures/flat-config/basic.yaml similarity index 100% rename from spec/fixtures/basic.yaml rename to spec/fixtures/flat-config/basic.yaml diff --git a/spec/fixtures/broken.yaml b/spec/fixtures/flat-config/broken.yaml similarity index 100% rename from spec/fixtures/broken.yaml rename to spec/fixtures/flat-config/broken.yaml diff --git a/spec/fixtures/complete_options.yaml b/spec/fixtures/flat-config/complete_options.yaml similarity index 100% rename from spec/fixtures/complete_options.yaml rename to spec/fixtures/flat-config/complete_options.yaml diff --git a/spec/fixtures/nested.yaml b/spec/fixtures/flat-config/nested.yaml similarity index 100% rename from spec/fixtures/nested.yaml rename to spec/fixtures/flat-config/nested.yaml diff --git a/spec/fixtures/only-spaces.yaml b/spec/fixtures/flat-config/only-spaces.yaml similarity index 100% rename from spec/fixtures/only-spaces.yaml rename to spec/fixtures/flat-config/only-spaces.yaml diff --git a/spec/fixtures/pattern-config/basic.yaml b/spec/fixtures/pattern-config/basic.yaml new file mode 100644 index 0000000..009c367 --- /dev/null +++ b/spec/fixtures/pattern-config/basic.yaml @@ -0,0 +1,14 @@ +patterns: + - mygit init [init options] + - mygit status|st [status options] + +options: + init: + - --bare + status: + - --verbose|-v + - --branch|-b + +tokens: + directory: +directory + branch: $(echo main dev) diff --git a/spec/fixtures/pattern-config/complete_options.yaml b/spec/fixtures/pattern-config/complete_options.yaml new file mode 100644 index 0000000..025cf7e --- /dev/null +++ b/spec/fixtures/pattern-config/complete_options.yaml @@ -0,0 +1,8 @@ +completely_options: + complete_options: -o nosort + +patterns: + - cli set + +tokens: + level: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100] diff --git a/spec/fixtures/pattern-config/interleaved.yaml b/spec/fixtures/pattern-config/interleaved.yaml new file mode 100644 index 0000000..3ae45f7 --- /dev/null +++ b/spec/fixtures/pattern-config/interleaved.yaml @@ -0,0 +1,12 @@ +patterns: + - cli clone [download] + +options: + download: + - -p|--protocol + - -v|--verbose + +tokens: + source: [source1, source2] + dest: [dest1, dest2] + protocol: [ssh, https] diff --git a/spec/fixtures/pattern-config/invalid-programs.yaml b/spec/fixtures/pattern-config/invalid-programs.yaml new file mode 100644 index 0000000..675cf76 --- /dev/null +++ b/spec/fixtures/pattern-config/invalid-programs.yaml @@ -0,0 +1,3 @@ +patterns: + - mygit init + - other status diff --git a/spec/fixtures/pattern-config/missing-option.yaml b/spec/fixtures/pattern-config/missing-option.yaml new file mode 100644 index 0000000..b72dc28 --- /dev/null +++ b/spec/fixtures/pattern-config/missing-option.yaml @@ -0,0 +1,2 @@ +patterns: + - mygit init [missing] diff --git a/spec/fixtures/pattern-config/missing-token.yaml b/spec/fixtures/pattern-config/missing-token.yaml new file mode 100644 index 0000000..9ef7472 --- /dev/null +++ b/spec/fixtures/pattern-config/missing-token.yaml @@ -0,0 +1,2 @@ +patterns: + - mygit init diff --git a/spec/fixtures/pattern-config/mixed-source.yaml b/spec/fixtures/pattern-config/mixed-source.yaml new file mode 100644 index 0000000..6f058d5 --- /dev/null +++ b/spec/fixtures/pattern-config/mixed-source.yaml @@ -0,0 +1,7 @@ +patterns: + - cli copy + - cli literal + +tokens: + target: [~, +directory, target1, $(echo target2)] + value: [++file] diff --git a/spec/fixtures/pattern-config/nil-source.yaml b/spec/fixtures/pattern-config/nil-source.yaml new file mode 100644 index 0000000..a2e12e5 --- /dev/null +++ b/spec/fixtures/pattern-config/nil-source.yaml @@ -0,0 +1,6 @@ +patterns: + - cli copy + +tokens: + source: ~ + target: [target1, target2] diff --git a/spec/fixtures/pattern-config/repeatable-positionals-invalid.yaml b/spec/fixtures/pattern-config/repeatable-positionals-invalid.yaml new file mode 100644 index 0000000..6419c3d --- /dev/null +++ b/spec/fixtures/pattern-config/repeatable-positionals-invalid.yaml @@ -0,0 +1,6 @@ +patterns: + - cli copy ... + +tokens: + source: [source1, source2] + target: [target1, target2] diff --git a/spec/fixtures/pattern-config/repeatable-positionals.yaml b/spec/fixtures/pattern-config/repeatable-positionals.yaml new file mode 100644 index 0000000..8ffc6a6 --- /dev/null +++ b/spec/fixtures/pattern-config/repeatable-positionals.yaml @@ -0,0 +1,8 @@ +patterns: + - cli upload ... + - cli copy ... + +tokens: + file: [file1, file2] + source: [source1, source2] + target: [target1, target2] diff --git a/spec/fixtures/pattern-config/repeatable.yaml b/spec/fixtures/pattern-config/repeatable.yaml new file mode 100644 index 0000000..f6e71c9 --- /dev/null +++ b/spec/fixtures/pattern-config/repeatable.yaml @@ -0,0 +1,14 @@ +patterns: + - cli download [download options] + +options: + download: + - -p|--protocol + - -h|--help + - --version + - -v|--verbose (repeatable) + - -u|--user (repeatable) + +tokens: + protocol: [http, ssh] + name: [alice, bob] diff --git a/spec/fixtures/pattern-config/route-specificity-reversed.yaml b/spec/fixtures/pattern-config/route-specificity-reversed.yaml new file mode 100644 index 0000000..dd39aac --- /dev/null +++ b/spec/fixtures/pattern-config/route-specificity-reversed.yaml @@ -0,0 +1,13 @@ +patterns: + - cli repo|r remote|rm [remote options] + - cli repo|r [repo options] + - cli [root options] + +options: + root: + - --root + repo: + - --repo + remote: + - --remote + diff --git a/spec/fixtures/pattern-config/route-specificity.yaml b/spec/fixtures/pattern-config/route-specificity.yaml new file mode 100644 index 0000000..525c85a --- /dev/null +++ b/spec/fixtures/pattern-config/route-specificity.yaml @@ -0,0 +1,17 @@ +patterns: + - cli [root options] + - cli repo|r [repo options] + - cli repo|r remote|rm [remote options] + +options: + root: + - --root + - --user + repo: + - --repo + - --user + remote: + - --remote + +tokens: + user: [admin, guest] diff --git a/spec/fixtures/pattern-config/spaced-token.yaml b/spec/fixtures/pattern-config/spaced-token.yaml new file mode 100644 index 0000000..b1ea106 --- /dev/null +++ b/spec/fixtures/pattern-config/spaced-token.yaml @@ -0,0 +1,9 @@ +patterns: + - mygit status [status options] + +options: + status: + - --format + +tokens: + status format: [short, long] diff --git a/spec/fixtures/pattern-config/static-values.yaml b/spec/fixtures/pattern-config/static-values.yaml new file mode 100644 index 0000000..53589b7 --- /dev/null +++ b/spec/fixtures/pattern-config/static-values.yaml @@ -0,0 +1,8 @@ +patterns: + - mygit deploy + +tokens: + environment: + - dev + - staging + - production diff --git a/spec/fixtures/pattern-config/unknown-metadata.yaml b/spec/fixtures/pattern-config/unknown-metadata.yaml new file mode 100644 index 0000000..59b8e5f --- /dev/null +++ b/spec/fixtures/pattern-config/unknown-metadata.yaml @@ -0,0 +1,6 @@ +patterns: + - cli download [download options] + +options: + download: + - --debug (hidden)