Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
345 changes: 206 additions & 139 deletions README.md

Large diffs are not rendered by default.

21 changes: 17 additions & 4 deletions Runfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions lib/completely.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'completely/exceptions'
require 'completely/flat_config'
require 'completely/pattern_config'
require 'completely/config'
require 'completely/pattern'
require 'completely/completions'
Expand Down
6 changes: 3 additions & 3 deletions lib/completely/commands/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/completely/commands/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 20 additions & 7 deletions lib/completely/commands/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
4 changes: 2 additions & 2 deletions lib/completely/commands/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
Expand Down
92 changes: 89 additions & 3 deletions lib/completely/completions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -29,6 +29,8 @@ def patterns
end

def valid?
return pattern_programs.uniq.one? if pattern_config?

pattern_prefixes.uniq.one?
end

Expand Down Expand Up @@ -62,15 +64,18 @@ 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
@template ||= File.read(template_path)
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
Expand All @@ -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
59 changes: 9 additions & 50 deletions lib/completely/config.rb
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions lib/completely/flat_config.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading