From be45d2d2084a98e38dd6d454beb87703a4325da8 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 20 Jul 2021 20:03:20 +0000 Subject: [PATCH 1/7] initial completions implementation --- .github/workflows/test.yml | 2 +- bashly.gemspec | 3 ++- lib/bashly/commands/add.rb | 1 + lib/bashly/concerns/completions.rb | 36 ++++++++++++++++++++++++++++++ lib/bashly/models/base.rb | 1 + lib/bashly/models/command.rb | 2 ++ 6 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 lib/bashly/concerns/completions.rb diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fc4f1d40..be468396 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: LC_ALL: en_US.UTF-8 # consistent sort order strategy: - matrix: { ruby: ['2.4', '2.5', '2.6', '2.7', '3.0'] } + matrix: { ruby: ['2.7', '3.0'] } steps: - name: Checkout code diff --git a/bashly.gemspec b/bashly.gemspec index f372bf93..7b154d32 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -15,9 +15,10 @@ Gem::Specification.new do |s| s.executables = ['bashly'] s.homepage = 'https://github.com/dannyben/bashly' s.license = 'MIT' - s.required_ruby_version = ">= 2.3.0" + s.required_ruby_version = ">= 2.7.0" s.add_runtime_dependency 'colsole', '~> 0.6' + s.add_runtime_dependency 'completely', '~> 0.1' s.add_runtime_dependency 'mister_bin', '~> 0.7' s.add_runtime_dependency 'requires', '~> 0.1' end diff --git a/lib/bashly/commands/add.rb b/lib/bashly/commands/add.rb index 409e8e2f..b6566560 100644 --- a/lib/bashly/commands/add.rb +++ b/lib/bashly/commands/add.rb @@ -41,6 +41,7 @@ def yaml_command end private + def safe_copy_lib(libfile) safe_copy asset("templates/lib/#{libfile}"), "#{Settings.source_dir}/lib/#{libfile}" end diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb new file mode 100644 index 00000000..e7a14108 --- /dev/null +++ b/lib/bashly/concerns/completions.rb @@ -0,0 +1,36 @@ +module Bashly + # This is a `Command` concern responsible for providing bash completion data + module Completions + def completion_data(with_version: true) + result = { full_name => completion_words(with_version: with_version) } + + commands.each do |command| + result.merge! command.completion_data(with_version: false) + end + + result + end + + private + + def completion_flag_names + flags.map(&:name) + flags.map(&:short) + end + + def completion_actions + completions ? completions.map { |c| "<#{c}>" } : [] + end + + def completion_words(with_version: false) + trivial_flags = %w[--help -h] + trivial_flags += %w[--version -v] if with_version + all = ( + command_names + trivial_flags + + completion_flag_names + completion_actions + ) + + all.uniq.sort + end + + end +end diff --git a/lib/bashly/models/base.rb b/lib/bashly/models/base.rb index 75714f3e..97b95222 100644 --- a/lib/bashly/models/base.rb +++ b/lib/bashly/models/base.rb @@ -9,6 +9,7 @@ class Base allowed arg catch_all + completions default dependencies description diff --git a/lib/bashly/models/command.rb b/lib/bashly/models/command.rb index e49dd427..f0795b7b 100644 --- a/lib/bashly/models/command.rb +++ b/lib/bashly/models/command.rb @@ -1,6 +1,8 @@ module Bashly module Models class Command < Base + include Completions + # Returns the name to be used as an action. # - If it is the root command, the action is "root" # - Else, it is all the parents, except the first tone (root) joined From 2efca29c59319a8df3695fbb72c688bd0411e5e2 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Tue, 20 Jul 2021 21:30:31 +0000 Subject: [PATCH 2/7] implement 'bashly add comp' --- bashly.gemspec | 2 +- lib/bashly/commands/add.rb | 66 ++++++++++++++++++++++++++++++ lib/bashly/concerns/completions.rb | 14 +++++++ spec/approvals/cli/add/help | 16 ++++++++ spec/approvals/cli/generate/usage | 1 + 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/bashly.gemspec b/bashly.gemspec index 7b154d32..418c092a 100644 --- a/bashly.gemspec +++ b/bashly.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 2.7.0" s.add_runtime_dependency 'colsole', '~> 0.6' - s.add_runtime_dependency 'completely', '~> 0.1' + s.add_runtime_dependency 'completely', '~> 0.1', '>= 0.1.2' s.add_runtime_dependency 'mister_bin', '~> 0.7' s.add_runtime_dependency 'requires', '~> 0.1' end diff --git a/lib/bashly/commands/add.rb b/lib/bashly/commands/add.rb index b6566560..3f527254 100644 --- a/lib/bashly/commands/add.rb +++ b/lib/bashly/commands/add.rb @@ -8,15 +8,20 @@ class Add < Base usage "bashly add config [--force]" usage "bashly add colors [--force]" usage "bashly add yaml [--force]" + usage "bashly add comp FORMAT [OUTPUT]" usage "bashly add (-h|--help)" option "-f --force", "Overwrite existing files" + param "FORMAT", "Output format, can be one of:\n function : generate a function file to be included in your script.\n script : generate a standalone bash completions script\n yaml : generate a yaml compatible with 'completely'" + param "OUTPUT", "For the 'comp function' command: Name of the generated function.\nFor the 'comp script' or 'comp yaml' commands: path to output file.\nIn all cases, this is optional and will have sensible defaults." + command "strings", "Copy an additional configuration file to your project, allowing you to customize all the tips and error strings." command "lib", "Create the additional lib directory for additional user scripts. All *.sh scripts in this folder will be included in the final bash script." command "config", "Add standard functions for handling INI files to the lib directory." command "colors", "Add standard functions for printing colorful and formatted text to the lib directory." command "yaml", "Add standard functions for reading YAML files to the lib directory." + command "comp", "Generate a bash completions script or function." environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]" @@ -40,6 +45,23 @@ def yaml_command safe_copy_lib "yaml.sh" end + def comp_command + format = args['FORMAT'] + output = args['OUTPUT'] + + case format + when "function" + save_comp_function output + when "yaml" + save_comp_yaml output + when "script" + save_comp_script output + else + raise Error, "Unrecognized format: #{format}" + end + + end + private def safe_copy_lib(libfile) @@ -64,6 +86,50 @@ def deep_copy(source, target) FileUtils.mkdir_p target_dir unless Dir.exist? target_dir FileUtils.cp source, target end + + def config + @config ||= Config.new "#{Settings.source_dir}/bashly.yml" + end + + def command + @command ||= Models::Command.new config + end + + def completions + @completions ||= command.completion_data + end + + def completions_script + @completions_script ||= command.completion_script + end + + def completions_function + @completions_function ||= command.completion_function + end + + def save_comp_yaml(filename = nil) + filename ||= "completions.yaml" + File.write filename, completions.to_yaml + say "created !txtgrn!#{filename}" + end + + def save_comp_script(filename = nil) + filename ||= "completions.bash" + File.write filename, completions_script + say "created !txtgrn!#{filename}" + end + + def save_comp_function(name = nil) + name ||= "send_completions" + target_dir = "#{Settings.source_dir}/lib" + filename = "#{target_dir}/#{name}.sh" + + FileUtils.mkdir_p target_dir unless Dir.exist? target_dir + File.write filename, completions_function + + say "created !txtgrn!#{filename}" + end + end end end diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb index e7a14108..d802bd00 100644 --- a/lib/bashly/concerns/completions.rb +++ b/lib/bashly/concerns/completions.rb @@ -1,3 +1,5 @@ +require 'completely' + module Bashly # This is a `Command` concern responsible for providing bash completion data module Completions @@ -11,8 +13,20 @@ def completion_data(with_version: true) result end + def completion_script + completion_generator.script + end + + def completion_function(name = nil) + completion_generator.wrapper_function(name) + end + private + def completion_generator + Completely::Completions.new(completion_data) + end + def completion_flag_names flags.map(&:name) + flags.map(&:short) end diff --git a/spec/approvals/cli/add/help b/spec/approvals/cli/add/help index 559bcf58..438a4fb2 100644 --- a/spec/approvals/cli/add/help +++ b/spec/approvals/cli/add/help @@ -6,6 +6,7 @@ Usage: bashly add config [--force] bashly add colors [--force] bashly add yaml [--force] + bashly add comp FORMAT [OUTPUT] bashly add (-h|--help) Commands: @@ -27,6 +28,9 @@ Commands: yaml Add standard functions for reading YAML files to the lib directory. + comp + Generate a bash completions script or function. + Options: -f --force Overwrite existing files @@ -34,6 +38,18 @@ Options: -h --help Show this help +Parameters: + FORMAT + Output format, can be one of: + function : generate a function file to be included in your script. + script : generate a standalone bash completions script + yaml : generate a yaml compatible with 'completely' + + OUTPUT + For the 'comp function' command: Name of the generated function. + For the 'comp script' or 'comp yaml' commands: path to output file. + In all cases, this is optional and will have sensible defaults. + Environment Variables: BASHLY_SOURCE_DIR The path containing the bashly configuration and source files [default: src] diff --git a/spec/approvals/cli/generate/usage b/spec/approvals/cli/generate/usage index b80fc35d..f759837f 100644 --- a/spec/approvals/cli/generate/usage +++ b/spec/approvals/cli/generate/usage @@ -4,4 +4,5 @@ Usage: bashly add config [--force] bashly add colors [--force] bashly add yaml [--force] + bashly add comp FORMAT [OUTPUT] bashly add (-h|--help) From 548199ebaa5163637f80c9dcf627a28be06adaf1 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 21 Jul 2021 06:29:45 +0000 Subject: [PATCH 3/7] add specs for completions concern --- lib/bashly/commands/add.rb | 17 +++++++++- lib/bashly/concerns/completions.rb | 2 +- lib/bashly/concerns/renderable.rb | 5 +-- spec/approvals/cli/add/help | 5 +++ spec/approvals/completions/advanced | 28 ++++++++++++++++ spec/approvals/completions/function | 16 +++++++++ spec/approvals/completions/script | 18 +++++++++++ spec/approvals/completions/simple | 8 +++++ spec/bashly/concerns/completions_spec.rb | 41 ++++++++++++++++++++++++ spec/fixtures/models/commands.yml | 25 +++++++++++++++ 10 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 spec/approvals/completions/advanced create mode 100644 spec/approvals/completions/function create mode 100644 spec/approvals/completions/script create mode 100644 spec/approvals/completions/simple create mode 100644 spec/bashly/concerns/completions_spec.rb diff --git a/lib/bashly/commands/add.rb b/lib/bashly/commands/add.rb index 3f527254..56e4e2da 100644 --- a/lib/bashly/commands/add.rb +++ b/lib/bashly/commands/add.rb @@ -23,6 +23,10 @@ class Add < Base command "yaml", "Add standard functions for reading YAML files to the lib directory." command "comp", "Generate a bash completions script or function." + example "bashly add strings --force" + example "bashly add comp function" + example "bashly add comp script completions.bash" + environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]" def strings_command @@ -111,12 +115,18 @@ def save_comp_yaml(filename = nil) filename ||= "completions.yaml" File.write filename, completions.to_yaml say "created !txtgrn!#{filename}" + say "" + say "This file can be converted to a completions script using the !txtgrn!completely!txtrst! gem." end def save_comp_script(filename = nil) filename ||= "completions.bash" File.write filename, completions_script say "created !txtgrn!#{filename}" + say "" + say "To enable completions, run:" + say "" + say " !txtpur!$ source #{filename}" end def save_comp_function(name = nil) @@ -126,8 +136,13 @@ def save_comp_function(name = nil) FileUtils.mkdir_p target_dir unless Dir.exist? target_dir File.write filename, completions_function - + say "created !txtgrn!#{filename}" + say "" + say "In order to use it in your script, create a command or a flag (for example: !txtgrn!#{command.name} completions!txtrst! or !txtgrn!#{command.name} --completions!txtrst!) that calls the !txtgrn!#{name}!txtrst! function." + say "Your users can then run something like this to enable completions:" + say "" + say " !txtpur!$ eval \"$(#{command.name} completions)\"" end end diff --git a/lib/bashly/concerns/completions.rb b/lib/bashly/concerns/completions.rb index d802bd00..5a783449 100644 --- a/lib/bashly/concerns/completions.rb +++ b/lib/bashly/concerns/completions.rb @@ -43,7 +43,7 @@ def completion_words(with_version: false) completion_flag_names + completion_actions ) - all.uniq.sort + all.compact.uniq.sort end end diff --git a/lib/bashly/concerns/renderable.rb b/lib/bashly/concerns/renderable.rb index 11cb67ce..d6ade503 100644 --- a/lib/bashly/concerns/renderable.rb +++ b/lib/bashly/concerns/renderable.rb @@ -4,10 +4,7 @@ module Bashly module Renderable def render(view) template = File.read view_path(view) - # TODO: This new format is only supported in Ruby >= 2.6 - # So for now, we keep the old deprecated syntax - # ERB.new(template, trim_mode: '%-').result(binding) - ERB.new(template, nil, '%-').result(binding) + ERB.new(template, trim_mode: '%-').result(binding) end def strings diff --git a/spec/approvals/cli/add/help b/spec/approvals/cli/add/help index 438a4fb2..a4c9ab40 100644 --- a/spec/approvals/cli/add/help +++ b/spec/approvals/cli/add/help @@ -53,3 +53,8 @@ Parameters: Environment Variables: BASHLY_SOURCE_DIR The path containing the bashly configuration and source files [default: src] + +Examples: + bashly add strings --force + bashly add comp function + bashly add comp script completions.bash diff --git a/spec/approvals/completions/advanced b/spec/approvals/completions/advanced new file mode 100644 index 00000000..d759d907 --- /dev/null +++ b/spec/approvals/completions/advanced @@ -0,0 +1,28 @@ +--- +say: +- "--help" +- "--version" +- "-h" +- "-v" +- goodbye +- hello +say hello: +- "--help" +- "-h" +- world +say hello world: +- "--force" +- "--help" +- "--verbose" +- "-h" +say goodbye: +- "--help" +- "-h" +- universe +say goodbye universe: +- "--color" +- "--help" +- "--verbose" +- "-c" +- "-h" +- "-v" diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function new file mode 100644 index 00000000..486f929f --- /dev/null +++ b/spec/approvals/completions/function @@ -0,0 +1,16 @@ +custom_name() { + echo $'#!/usr/bin/env bash' + echo $'' + echo $'# This bash completions script was generated by' + echo $'# completely (https://github.com/dannyben/completely)' + echo $'# Modifying it manually is not recommended' + echo $'_get_completions() {' + echo $' local cur=${COMP_WORDS[COMP_CWORD]}' + echo $'' + echo $' case "$COMP_LINE" in' + echo $' \'get\'*) COMPREPLY=($(compgen -W "--force --help --verbose --version -h -v" -- "$cur")) ;;' + echo $' esac' + echo $'}' + echo $'' + echo $'complete -F _get_completions get' +} \ No newline at end of file diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script new file mode 100644 index 00000000..6a69488f --- /dev/null +++ b/spec/approvals/completions/script @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# This bash completions script was generated by +# completely (https://github.com/dannyben/completely) +# Modifying it manually is not recommended +_say_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + + case "$COMP_LINE" in + 'say goodbye universe'*) COMPREPLY=($(compgen -W "--color --help --verbose -c -h -v" -- "$cur")) ;; + 'say hello world'*) COMPREPLY=($(compgen -W "--force --help --verbose -h" -- "$cur")) ;; + 'say goodbye'*) COMPREPLY=($(compgen -W "--help -h universe" -- "$cur")) ;; + 'say hello'*) COMPREPLY=($(compgen -W "--help -h world" -- "$cur")) ;; + 'say'*) COMPREPLY=($(compgen -W "--help --version -h -v goodbye hello" -- "$cur")) ;; + esac +} + +complete -F _say_completions say diff --git a/spec/approvals/completions/simple b/spec/approvals/completions/simple new file mode 100644 index 00000000..682e50fc --- /dev/null +++ b/spec/approvals/completions/simple @@ -0,0 +1,8 @@ +--- +get: +- "--force" +- "--help" +- "--verbose" +- "--version" +- "-h" +- "-v" diff --git a/spec/bashly/concerns/completions_spec.rb b/spec/bashly/concerns/completions_spec.rb new file mode 100644 index 00000000..2f9b576d --- /dev/null +++ b/spec/bashly/concerns/completions_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Models::Command do + let(:fixture) { :completions_simple } + + subject do + options = load_fixture('models/commands')[fixture] + described_class.new options + end + + describe '#completion_data' do + it "returns a data structure for completely" do + expect(subject.completion_data.to_yaml).to match_approval("completions/simple") + end + end + + describe '#completion_function' do + it "returns a bash completion script wrapped in a function" do + expect(subject.completion_function "custom_name") + .to match_approval("completions/function") + end + end + + context "with a more complex command" do + let(:fixture) { :completions_advanced } + + describe '#completion_data' do + it "returns a data structure for completely" do + expect(subject.completion_data.to_yaml) + .to match_approval("completions/advanced") + end + end + + describe '#completion_script' do + it "returns a bash completion script" do + expect(subject.completion_script) + .to match_approval("completions/script") + end + end + end +end diff --git a/spec/fixtures/models/commands.yml b/spec/fixtures/models/commands.yml index 49283544..3f3cce72 100644 --- a/spec/fixtures/models/commands.yml +++ b/spec/fixtures/models/commands.yml @@ -127,3 +127,28 @@ catch_all: label: additional params help: Any additional argument or flag + +:completions_simple: + name: get + + flags: + - long: --force + - long: --verbose + +:completions_advanced: + name: say + commands: + - name: hello + commands: + - name: world + flags: + - long: --force + - long: --verbose + - name: goodbye + commands: + - name: universe + flags: + - long: --color + short: -c + - long: --verbose + short: -v From 294044c135b9fab4a722cf500d97454634ccfcb5 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 21 Jul 2021 06:53:21 +0000 Subject: [PATCH 4/7] add specs for the add command --- lib/bashly/commands/add.rb | 6 ++--- spec/approvals/cli/add/comp-function | 6 +++++ spec/approvals/cli/add/comp-function-file | 18 +++++++++++++++ spec/approvals/cli/add/comp-script | 5 ++++ spec/approvals/cli/add/comp-script-file | 16 +++++++++++++ spec/approvals/cli/add/comp-yaml | 3 +++ spec/approvals/cli/add/comp-yaml-file | 20 ++++++++++++++++ spec/approvals/cli/add/init | 2 ++ spec/bashly/commands/add_spec.rb | 28 +++++++++++++++++++++++ 9 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 spec/approvals/cli/add/comp-function create mode 100644 spec/approvals/cli/add/comp-function-file create mode 100644 spec/approvals/cli/add/comp-script create mode 100644 spec/approvals/cli/add/comp-script-file create mode 100644 spec/approvals/cli/add/comp-yaml create mode 100644 spec/approvals/cli/add/comp-yaml-file create mode 100644 spec/approvals/cli/add/init diff --git a/lib/bashly/commands/add.rb b/lib/bashly/commands/add.rb index 56e4e2da..4e824854 100644 --- a/lib/bashly/commands/add.rb +++ b/lib/bashly/commands/add.rb @@ -112,7 +112,7 @@ def completions_function end def save_comp_yaml(filename = nil) - filename ||= "completions.yaml" + filename ||= "#{Settings.target_dir}/completions.yaml" File.write filename, completions.to_yaml say "created !txtgrn!#{filename}" say "" @@ -120,11 +120,11 @@ def save_comp_yaml(filename = nil) end def save_comp_script(filename = nil) - filename ||= "completions.bash" + filename ||= "#{Settings.target_dir}/completions.bash" File.write filename, completions_script say "created !txtgrn!#{filename}" say "" - say "To enable completions, run:" + say "In order to enable completions, run:" say "" say " !txtpur!$ source #{filename}" end diff --git a/spec/approvals/cli/add/comp-function b/spec/approvals/cli/add/comp-function new file mode 100644 index 00000000..7e034b8d --- /dev/null +++ b/spec/approvals/cli/add/comp-function @@ -0,0 +1,6 @@ +created spec/tmp/src/lib/send_completions.sh + +In order to use it in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. +Your users can then run something like this to enable completions: + + $ eval "$(cli completions)" diff --git a/spec/approvals/cli/add/comp-function-file b/spec/approvals/cli/add/comp-function-file new file mode 100644 index 00000000..4323157e --- /dev/null +++ b/spec/approvals/cli/add/comp-function-file @@ -0,0 +1,18 @@ +send_completions() { + echo $'#!/usr/bin/env bash' + echo $'' + echo $'# This bash completions script was generated by' + echo $'# completely (https://github.com/dannyben/completely)' + echo $'# Modifying it manually is not recommended' + echo $'_cli_completions() {' + echo $' local cur=${COMP_WORDS[COMP_CWORD]}' + echo $'' + echo $' case "$COMP_LINE" in' + echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;' + echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;' + echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;' + echo $' esac' + echo $'}' + echo $'' + echo $'complete -F _cli_completions cli' +} \ No newline at end of file diff --git a/spec/approvals/cli/add/comp-script b/spec/approvals/cli/add/comp-script new file mode 100644 index 00000000..f539fd6e --- /dev/null +++ b/spec/approvals/cli/add/comp-script @@ -0,0 +1,5 @@ +created spec/tmp/completions.bash + +In order to enable completions, run: + + $ source spec/tmp/completions.bash diff --git a/spec/approvals/cli/add/comp-script-file b/spec/approvals/cli/add/comp-script-file new file mode 100644 index 00000000..92f29aaa --- /dev/null +++ b/spec/approvals/cli/add/comp-script-file @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# This bash completions script was generated by +# completely (https://github.com/dannyben/completely) +# Modifying it manually is not recommended +_cli_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + + case "$COMP_LINE" in + 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;; + 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;; + 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;; + esac +} + +complete -F _cli_completions cli diff --git a/spec/approvals/cli/add/comp-yaml b/spec/approvals/cli/add/comp-yaml new file mode 100644 index 00000000..7e8e1ef2 --- /dev/null +++ b/spec/approvals/cli/add/comp-yaml @@ -0,0 +1,3 @@ +created spec/tmp/completions.yaml + +This file can be converted to a completions script using the completely gem. diff --git a/spec/approvals/cli/add/comp-yaml-file b/spec/approvals/cli/add/comp-yaml-file new file mode 100644 index 00000000..bda8fbf8 --- /dev/null +++ b/spec/approvals/cli/add/comp-yaml-file @@ -0,0 +1,20 @@ +--- +cli: +- "--help" +- "--version" +- "-h" +- "-v" +- download +- upload +cli download: +- "--force" +- "--help" +- "-f" +- "-h" +cli upload: +- "--help" +- "--password" +- "--user" +- "-h" +- "-p" +- "-u" diff --git a/spec/approvals/cli/add/init b/spec/approvals/cli/add/init new file mode 100644 index 00000000..82a02c6a --- /dev/null +++ b/spec/approvals/cli/add/init @@ -0,0 +1,2 @@ +created spec/tmp/src/bashly.yml +run bashly generate to create the bash script diff --git a/spec/bashly/commands/add_spec.rb b/spec/bashly/commands/add_spec.rb index 159ccc0f..5f23452e 100644 --- a/spec/bashly/commands/add_spec.rb +++ b/spec/bashly/commands/add_spec.rb @@ -99,4 +99,32 @@ end end + context "with comp command" do + before do + reset_tmp_dir create_src: true + expect { subject.run %w[init] }.to output_approval('cli/add/init') + end + + context "with yaml subcommand" do + it "creates completions.yaml" do + expect { subject.run %w[add comp yaml] }.to output_approval('cli/add/comp-yaml') + expect(File.read "#{target_dir}/completions.yaml").to match_approval('cli/add/comp-yaml-file') + end + end + + context "with script subcommand" do + it "creates completions.bash" do + expect { subject.run %w[add comp script] }.to output_approval('cli/add/comp-script') + expect(File.read "#{target_dir}/completions.bash").to match_approval('cli/add/comp-script-file') + end + end + + context "with function subcommand" do + it "creates lib/send_completions.sh" do + expect { subject.run %w[add comp function] }.to output_approval('cli/add/comp-function') + expect(File.read "#{source_dir}/lib/send_completions.sh").to match_approval('cli/add/comp-function-file') + end + end + end + end From 226aaa70afb377ddcfb5770eb4f4c94adfed4529 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 21 Jul 2021 06:55:10 +0000 Subject: [PATCH 5/7] 100% coverage --- spec/approvals/cli/add/comp-error | 1 + spec/bashly/commands/add_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 spec/approvals/cli/add/comp-error diff --git a/spec/approvals/cli/add/comp-error b/spec/approvals/cli/add/comp-error new file mode 100644 index 00000000..a9ab2be3 --- /dev/null +++ b/spec/approvals/cli/add/comp-error @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/spec/bashly/commands/add_spec.rb b/spec/bashly/commands/add_spec.rb index 5f23452e..10471fe8 100644 --- a/spec/bashly/commands/add_spec.rb +++ b/spec/bashly/commands/add_spec.rb @@ -125,6 +125,12 @@ expect(File.read "#{source_dir}/lib/send_completions.sh").to match_approval('cli/add/comp-function-file') end end + + context "with an unrecognized subcommand" do + it "raises an error" do + expect { subject.run %w[add comp no-such-format] }.to raise_approval('cli/add/comp-error') + end + end end end From 6708dd28a64e3454f437be08c7b1ec77ba0fa4d1 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 21 Jul 2021 07:36:54 +0000 Subject: [PATCH 6/7] - Add bash completion generation --- examples/README.md | 1 + examples/completions/README.md | 161 +++++ examples/completions/cli | 571 ++++++++++++++++++ examples/completions/src/bashly.yml | 52 ++ .../completions/src/completions_command.sh | 9 + examples/completions/src/download_command.sh | 4 + examples/completions/src/initialize.sh | 6 + .../completions/src/lib/send_completions.sh | 18 + examples/completions/src/upload_command.sh | 4 + examples/completions/test.sh | 10 + spec/approvals/completions/advanced | 2 + spec/approvals/completions/function | 2 +- spec/approvals/completions/script | 2 +- spec/approvals/completions/simple | 1 + spec/approvals/examples/completions | 72 +++ spec/fixtures/models/commands.yml | 2 + 16 files changed, 915 insertions(+), 2 deletions(-) create mode 100644 examples/completions/README.md create mode 100644 examples/completions/cli create mode 100644 examples/completions/src/bashly.yml create mode 100644 examples/completions/src/completions_command.sh create mode 100644 examples/completions/src/download_command.sh create mode 100644 examples/completions/src/initialize.sh create mode 100644 examples/completions/src/lib/send_completions.sh create mode 100644 examples/completions/src/upload_command.sh create mode 100644 examples/completions/test.sh create mode 100644 spec/approvals/examples/completions diff --git a/examples/README.md b/examples/README.md index 5bff5cbc..ad50688b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -42,3 +42,4 @@ Each of these examples demonstrates one aspect or feature of bashly. - [config-ini](config-ini#readme) - using the config (INI) functions - [colors](colors#readme) - using the color print feature - [yaml](yaml#readme) - using the YAML reading functions +- [completions](completions#readme) - adding bash completion functionality diff --git a/examples/completions/README.md b/examples/completions/README.md new file mode 100644 index 00000000..359e1531 --- /dev/null +++ b/examples/completions/README.md @@ -0,0 +1,161 @@ +# Bash Completions Example + +Demonstrates how to build a script that supports bash completions. + +This example was generated with: + + $ bashly init + $ bashly add comp function + $ bashly generate + +----- + +## `bashly.yml` + +```yaml +name: cli +help: Sample application with bash completions +version: 0.1.0 + +commands: +- name: completions + help: |- + Generate bash completions + Usage: eval "\$(cli completions)" + +- name: download + short: d + help: Download a file + + args: + - name: source + required: true + help: URL to download from + - name: target + help: "Target filename (default: same as source)" + + flags: + - long: --force + short: -f + help: Overwrite existing files + + examples: + - cli download example.com + - cli download example.com ./output -f + + environment_variables: + - name: default_target_location + help: Set the default location to download to + +- name: upload + short: u + help: Upload a file + args: + - name: source + required: true + help: File to upload + + flags: + - long: --user + short: -u + arg: user + help: Username to use for logging in + required: true + - long: --password + short: -p + arg: password + help: Password to use for logging in +``` + +## Generated script output + +### `$ ./cli` + +```shell +cli - Sample application with bash completions + +Usage: + cli [command] + cli [command] --help | -h + cli --version | -v + +Commands: + completions Generate bash completions + download Download a file + upload Upload a file + + + +``` + +### `$ ./cli -h` + +```shell +cli - Sample application with bash completions + +Usage: + cli [command] + cli [command] --help | -h + cli --version | -v + +Commands: + completions Generate bash completions + download Download a file + upload Upload a file + +Options: + --help, -h + Show this help + + --version, -v + Show version number + + + +``` + +### `$ ./cli completions -h` + +```shell +cli completions + + Generate bash completions + Usage: eval "$(cli completions)" + +Usage: + cli completions + cli completions --help | -h + +Options: + --help, -h + Show this help + + + +``` + +### `$ ./cli completions` + +```shell +#!/usr/bin/env bash + +# This bash completions script was generated by +# completely (https://github.com/dannyben/completely) +# Modifying it manually is not recommended +_cli_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + + case "$COMP_LINE" in + 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;; + 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;; + 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;; + esac +} + +complete -F _cli_completions cli + + +``` + + + diff --git a/examples/completions/cli b/examples/completions/cli new file mode 100644 index 00000000..2440cc7a --- /dev/null +++ b/examples/completions/cli @@ -0,0 +1,571 @@ +#!/usr/bin/env bash +# This script was generated by bashly (https://github.com/DannyBen/bashly) +# Modifying it manually is not recommended + +# :command.version_command +version_command() { + echo "$version" +} + +# :command.usage +cli_usage() { + if [[ -n $long_usage ]]; then + printf "cli - Sample application with bash completions\n" + echo + else + printf "cli - Sample application with bash completions\n" + echo + fi + + printf "Usage:\n" + printf " cli [command]\n" + printf " cli [command] --help | -h\n" + printf " cli --version | -v\n" + echo + # :command.usage_commands + printf "Commands:\n" + echo " completions Generate bash completions" + echo " download Download a file" + echo " upload Upload a file" + echo + + if [[ -n $long_usage ]]; then + printf "Options:\n" + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + echo " --version, -v" + printf " Show version number\n" + echo + + fi +} + +# :command.usage +cli_completions_usage() { + if [[ -n $long_usage ]]; then + printf "cli completions\n" + echo + printf " Generate bash completions\n Usage: eval \"\$(cli completions)\"\n" + echo + else + printf "cli completions - Generate bash completions\n" + echo + fi + + printf "Usage:\n" + printf " cli completions\n" + printf " cli completions --help | -h\n" + echo + + if [[ -n $long_usage ]]; then + printf "Options:\n" + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + + fi +} + +# :command.usage +cli_download_usage() { + if [[ -n $long_usage ]]; then + printf "cli download - Download a file\n" + echo + else + printf "cli download - Download a file\n" + echo + fi + + printf "Shortcut: d\n" + echo + + printf "Usage:\n" + printf " cli download SOURCE [TARGET] [options]\n" + printf " cli download --help | -h\n" + echo + + if [[ -n $long_usage ]]; then + printf "Options:\n" + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + # :command.usage_flags + # :flag.usage + echo " --force, -f" + printf " Overwrite existing files\n" + echo + # :command.usage_args + printf "Arguments:\n" + + # :argument.usage + echo " SOURCE" + printf " URL to download from\n" + echo + + # :argument.usage + echo " TARGET" + printf " Target filename (default: same as source)\n" + echo + # :command.usage_environment_variables + printf "Environment Variables:\n" + + # :environment_variable.usage + echo " DEFAULT_TARGET_LOCATION" + printf " Set the default location to download to\n" + echo + # :command.usage_examples + printf "Examples:\n" + + printf " cli download example.com\n" + printf " cli download example.com ./output -f\n" + echo + + fi +} + +# :command.usage +cli_upload_usage() { + if [[ -n $long_usage ]]; then + printf "cli upload - Upload a file\n" + echo + else + printf "cli upload - Upload a file\n" + echo + fi + + printf "Shortcut: u\n" + echo + + printf "Usage:\n" + printf " cli upload SOURCE [options]\n" + printf " cli upload --help | -h\n" + echo + + if [[ -n $long_usage ]]; then + printf "Options:\n" + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + # :command.usage_flags + # :flag.usage + echo " --user, -u USER (required)" + printf " Username to use for logging in\n" + echo + + # :flag.usage + echo " --password, -p PASSWORD" + printf " Password to use for logging in\n" + echo + # :command.usage_args + printf "Arguments:\n" + + # :argument.usage + echo " SOURCE" + printf " File to upload\n" + echo + + fi +} + +# :command.inspect_args +inspect_args() { + readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) + if (( ${#args[@]} )); then + echo args: + for k in "${sorted_keys[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done + else + echo args: none + fi + + if (( ${#other_args[@]} )); then + echo + echo other_args: + echo "- \${other_args[*]} = ${other_args[*]}" + for i in "${!other_args[@]}"; do + echo "- \${other_args[$i]} = ${other_args[$i]}" + done + fi +} + +# :command.user_lib +# :src/lib/send_completions.sh +send_completions() { + echo $'#!/usr/bin/env bash' + echo $'' + echo $'# This bash completions script was generated by' + echo $'# completely (https://github.com/dannyben/completely)' + echo $'# Modifying it manually is not recommended' + echo $'_cli_completions() {' + echo $' local cur=${COMP_WORDS[COMP_CWORD]}' + echo $'' + echo $' case "$COMP_LINE" in' + echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;' + echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;' + echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;' + echo $' esac' + echo $'}' + echo $'' + echo $'complete -F _cli_completions cli' +} + +# :command.command_functions +# :command.function +cli_completions_command() { + # :src/completions_command.sh + # Call the `send_completions` function which was added by running: + # + # $ bashly add comp function + # + # Users can now enable bash completion for this script by running: + # + # $ eval "$(cli completions)" + # + send_completions +} + +# :command.function +cli_download_command() { + # :src/download_command.sh + echo "# this file is located in 'src/download_command.sh'" + echo "# code for 'cli download' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.function +cli_upload_command() { + # :src/upload_command.sh + echo "# this file is located in 'src/upload_command.sh'" + echo "# code for 'cli upload' goes here" + echo "# you can edit it freely and regenerate (it will not be overwritten)" + inspect_args +} + +# :command.parse_requirements +parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + cli_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action=$1 + + case $action in + -* ) + ;; + + completions ) + action="completions" + shift + cli_completions_parse_requirements "$@" + shift $# + ;; + + download | d ) + action="download" + shift + cli_download_parse_requirements "$@" + shift $# + ;; + + upload | u ) + action="upload" + shift + cli_upload_parse_requirements "$@" + shift $# + ;; + + # :command.command_fallback + * ) + cli_usage + exit 1 + ;; + + esac + # :command.required_args_filter + # :command.required_flags_filter + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + + -* ) + printf "invalid option: %s\n" "$key" + exit 1 + ;; + + * ) + # :command.parse_requirements_case + printf "invalid argument: %s\n" "$key" + exit 1 + ;; + + esac + done + # :command.default_assignments + # :command.whitelist_filter +} + +# :command.parse_requirements +cli_completions_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + cli_completions_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="completions" + # :command.required_args_filter + # :command.required_flags_filter + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + + -* ) + printf "invalid option: %s\n" "$key" + exit 1 + ;; + + * ) + # :command.parse_requirements_case + printf "invalid argument: %s\n" "$key" + exit 1 + ;; + + esac + done + # :command.default_assignments + # :command.whitelist_filter +} + +# :command.parse_requirements +cli_download_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + cli_download_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="download" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[source]=$1 + shift + else + printf "missing required argument: SOURCE\nusage: cli download SOURCE [TARGET] [options]\n" + exit 1 + fi + # :command.required_flags_filter + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + # :flag.case + --force | -f ) + args[--force]=1 + shift + ;; + + + -* ) + printf "invalid option: %s\n" "$key" + exit 1 + ;; + + * ) + # :command.parse_requirements_case + if [[ ! ${args[source]} ]]; then + args[source]=$1 + shift + elif [[ ! ${args[target]} ]]; then + args[target]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done + # :command.default_assignments + # :command.whitelist_filter +} + +# :command.parse_requirements +cli_upload_parse_requirements() { + # :command.fixed_flag_filter + case "$1" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + cli_upload_usage + exit 1 + ;; + + esac + # :command.environment_variables_filter + # :command.dependencies_filter + # :command.command_filter + action="upload" + # :command.required_args_filter + if [[ $1 && $1 != -* ]]; then + args[source]=$1 + shift + else + printf "missing required argument: SOURCE\nusage: cli upload SOURCE [options]\n" + exit 1 + fi + # :command.required_flags_filter + argstring="$*" + if [[ "$argstring" != *--user* && "$argstring" != *-u* ]]; then + printf "missing required flag: --user, -u USER\n" + exit 1 + fi + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + # :flag.case + --user | -u ) + if [[ $2 ]]; then + args[--user]="$2" + shift + shift + else + printf "%s\n" "--user requires an argument: --user, -u USER" + exit 1 + fi + ;; + + # :flag.case + --password | -p ) + if [[ $2 ]]; then + args[--password]="$2" + shift + shift + else + printf "%s\n" "--password requires an argument: --password, -p PASSWORD" + exit 1 + fi + ;; + + + -* ) + printf "invalid option: %s\n" "$key" + exit 1 + ;; + + * ) + # :command.parse_requirements_case + if [[ ! ${args[source]} ]]; then + args[source]=$1 + shift + else + printf "invalid argument: %s\n" "$key" + exit 1 + fi + ;; + + esac + done + # :command.default_assignments + # :command.whitelist_filter +} + +# :command.initialize +initialize() { + version="0.1.0" + long_usage='' + set -e + + # :src/initialize.sh + # Code here runs inside the initialize() function + # Use it for anything that you need to run before any other function, like + # setting environment vairables: + # CONFIG_FILE=settings.ini + # + # Feel free to empty (but not delete) this file. +} + +# :command.run +run() { + declare -A args + declare -a other_args + parse_requirements "$@" + + if [[ $action == "completions" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_completions_usage + else + cli_completions_command + fi + + elif [[ $action == "download" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_download_usage + else + cli_download_command + fi + + elif [[ $action == "upload" ]]; then + if [[ ${args[--help]} ]]; then + long_usage=yes + cli_upload_usage + else + cli_upload_command + fi + + elif [[ $action == "root" ]]; then + root_command + fi +} + +initialize +run "$@" diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml new file mode 100644 index 00000000..3fcd44f6 --- /dev/null +++ b/examples/completions/src/bashly.yml @@ -0,0 +1,52 @@ +name: cli +help: Sample application with bash completions +version: 0.1.0 + +commands: +- name: completions + help: |- + Generate bash completions + Usage: eval "\$(cli completions)" + +- name: download + short: d + help: Download a file + + args: + - name: source + required: true + help: URL to download from + - name: target + help: "Target filename (default: same as source)" + + flags: + - long: --force + short: -f + help: Overwrite existing files + + examples: + - cli download example.com + - cli download example.com ./output -f + + environment_variables: + - name: default_target_location + help: Set the default location to download to + +- name: upload + short: u + help: Upload a file + args: + - name: source + required: true + help: File to upload + + flags: + - long: --user + short: -u + arg: user + help: Username to use for logging in + required: true + - long: --password + short: -p + arg: password + help: Password to use for logging in diff --git a/examples/completions/src/completions_command.sh b/examples/completions/src/completions_command.sh new file mode 100644 index 00000000..79f951b4 --- /dev/null +++ b/examples/completions/src/completions_command.sh @@ -0,0 +1,9 @@ +# Call the `send_completions` function which was added by running: +# +# $ bashly add comp function +# +# Users can now enable bash completion for this script by running: +# +# $ eval "$(cli completions)" +# +send_completions diff --git a/examples/completions/src/download_command.sh b/examples/completions/src/download_command.sh new file mode 100644 index 00000000..950ed370 --- /dev/null +++ b/examples/completions/src/download_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/download_command.sh'" +echo "# code for 'cli download' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/completions/src/initialize.sh b/examples/completions/src/initialize.sh new file mode 100644 index 00000000..f2dbc52c --- /dev/null +++ b/examples/completions/src/initialize.sh @@ -0,0 +1,6 @@ +# Code here runs inside the initialize() function +# Use it for anything that you need to run before any other function, like +# setting environment vairables: +# CONFIG_FILE=settings.ini +# +# Feel free to empty (but not delete) this file. diff --git a/examples/completions/src/lib/send_completions.sh b/examples/completions/src/lib/send_completions.sh new file mode 100644 index 00000000..4323157e --- /dev/null +++ b/examples/completions/src/lib/send_completions.sh @@ -0,0 +1,18 @@ +send_completions() { + echo $'#!/usr/bin/env bash' + echo $'' + echo $'# This bash completions script was generated by' + echo $'# completely (https://github.com/dannyben/completely)' + echo $'# Modifying it manually is not recommended' + echo $'_cli_completions() {' + echo $' local cur=${COMP_WORDS[COMP_CWORD]}' + echo $'' + echo $' case "$COMP_LINE" in' + echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;' + echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;' + echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;' + echo $' esac' + echo $'}' + echo $'' + echo $'complete -F _cli_completions cli' +} \ No newline at end of file diff --git a/examples/completions/src/upload_command.sh b/examples/completions/src/upload_command.sh new file mode 100644 index 00000000..755e02ff --- /dev/null +++ b/examples/completions/src/upload_command.sh @@ -0,0 +1,4 @@ +echo "# this file is located in 'src/upload_command.sh'" +echo "# code for 'cli upload' goes here" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/completions/test.sh b/examples/completions/test.sh new file mode 100644 index 00000000..2304b7aa --- /dev/null +++ b/examples/completions/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -x + +bashly generate + +./cli +./cli -h +./cli completions -h +./cli completions \ No newline at end of file diff --git a/spec/approvals/completions/advanced b/spec/approvals/completions/advanced index d759d907..8cfae073 100644 --- a/spec/approvals/completions/advanced +++ b/spec/approvals/completions/advanced @@ -15,6 +15,8 @@ say hello world: - "--help" - "--verbose" - "-h" +- "" +- "" say goodbye: - "--help" - "-h" diff --git a/spec/approvals/completions/function b/spec/approvals/completions/function index 486f929f..85ac0375 100644 --- a/spec/approvals/completions/function +++ b/spec/approvals/completions/function @@ -8,7 +8,7 @@ custom_name() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $'' echo $' case "$COMP_LINE" in' - echo $' \'get\'*) COMPREPLY=($(compgen -W "--force --help --verbose --version -h -v" -- "$cur")) ;;' + echo $' \'get\'*) COMPREPLY=($(compgen -A file -W "--force --help --verbose --version -h -v" -- "$cur")) ;;' echo $' esac' echo $'}' echo $'' diff --git a/spec/approvals/completions/script b/spec/approvals/completions/script index 6a69488f..696fa91b 100644 --- a/spec/approvals/completions/script +++ b/spec/approvals/completions/script @@ -8,7 +8,7 @@ _say_completions() { case "$COMP_LINE" in 'say goodbye universe'*) COMPREPLY=($(compgen -W "--color --help --verbose -c -h -v" -- "$cur")) ;; - 'say hello world'*) COMPREPLY=($(compgen -W "--force --help --verbose -h" -- "$cur")) ;; + 'say hello world'*) COMPREPLY=($(compgen -A directory -A user -W "--force --help --verbose -h" -- "$cur")) ;; 'say goodbye'*) COMPREPLY=($(compgen -W "--help -h universe" -- "$cur")) ;; 'say hello'*) COMPREPLY=($(compgen -W "--help -h world" -- "$cur")) ;; 'say'*) COMPREPLY=($(compgen -W "--help --version -h -v goodbye hello" -- "$cur")) ;; diff --git a/spec/approvals/completions/simple b/spec/approvals/completions/simple index 682e50fc..d677f376 100644 --- a/spec/approvals/completions/simple +++ b/spec/approvals/completions/simple @@ -6,3 +6,4 @@ get: - "--version" - "-h" - "-v" +- "" diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions new file mode 100644 index 00000000..1fb324fb --- /dev/null +++ b/spec/approvals/examples/completions @@ -0,0 +1,72 @@ ++ bashly generate +creating user files in src +skipped src/initialize.sh (exists) +skipped src/completions_command.sh (exists) +skipped src/download_command.sh (exists) +skipped src/upload_command.sh (exists) +created ./cli +run ./cli --help to test your bash script ++ ./cli +cli - Sample application with bash completions + +Usage: + cli [command] + cli [command] --help | -h + cli --version | -v + +Commands: + completions Generate bash completions + download Download a file + upload Upload a file + ++ ./cli -h +cli - Sample application with bash completions + +Usage: + cli [command] + cli [command] --help | -h + cli --version | -v + +Commands: + completions Generate bash completions + download Download a file + upload Upload a file + +Options: + --help, -h + Show this help + + --version, -v + Show version number + ++ ./cli completions -h +cli completions + + Generate bash completions + Usage: eval "$(cli completions)" + +Usage: + cli completions + cli completions --help | -h + +Options: + --help, -h + Show this help + ++ ./cli completions +#!/usr/bin/env bash + +# This bash completions script was generated by +# completely (https://github.com/dannyben/completely) +# Modifying it manually is not recommended +_cli_completions() { + local cur=${COMP_WORDS[COMP_CWORD]} + + case "$COMP_LINE" in + 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;; + 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;; + 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;; + esac +} + +complete -F _cli_completions cli diff --git a/spec/fixtures/models/commands.yml b/spec/fixtures/models/commands.yml index 3f3cce72..31786e4e 100644 --- a/spec/fixtures/models/commands.yml +++ b/spec/fixtures/models/commands.yml @@ -130,6 +130,7 @@ :completions_simple: name: get + completions: [file] flags: - long: --force @@ -141,6 +142,7 @@ - name: hello commands: - name: world + completions: [directory, user] flags: - long: --force - long: --verbose From 7ec67482189fc63d04b1548b66f43f7762f76390 Mon Sep 17 00:00:00 2001 From: Danny Ben Shitrit Date: Wed, 21 Jul 2021 08:36:17 +0000 Subject: [PATCH 7/7] readme update for completions --- README.md | 71 +++++++++++++++++++ examples/completions/README.md | 9 ++- examples/completions/cli | 7 +- examples/completions/src/bashly.yml | 2 + .../completions/src/lib/send_completions.sh | 7 +- examples/completions/test.sh | 3 +- spec/approvals/examples/completions | 14 +++- 7 files changed, 100 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index ba6cd3cf..862822ef 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Create beautiful bash scripts from simple YAML configuration - [Flag options](#flag-options) - [Environment Variable options](#environment-variable-options) - [Extensible Scripts](#extensible-scripts) +- [Bash Completions](#bash-completions) - [Real World Examples](#real-world-examples) - [Contributing / Support](#contributing--support) @@ -75,6 +76,7 @@ Bahsly is responsible for: - Optional or required **option flags** (with or without flag arguments). - **Commands** (and subcommands). - Standard flags (like **--help** and **--version**). +- Preventing your script from running unless the command line is valid. - Providing you with a place to input your code for each of the functions your tool performs, and merging it back to the final script. - Providing you with additional (optional) framework-style, standard @@ -82,6 +84,7 @@ Bahsly is responsible for: - **Color output**. - **Config file management** (INI format). - **YAML parsing**. + - **Bash completions**. - and more. @@ -198,6 +201,7 @@ command and subcommands (under the `commands` definition). `commands` | Specify the array of [commands](#command-options). Each command will have its own args and flags. Note: if `commands` is provided, you cannot specify flags or args at the same level. `args` | Specify the array of [positional arguments](#argument-options) this script needs. `flags` | Specify the array of option [flags](#flag-options) this script needs. +`completions` | Specify an array of additional completion suggestions when used in conjunction with `bashly add comp`. See [Bash Completions](#bash-completions). `catch_all` | Specify that this command should allow for additional arbitrary arguments or flags. It can be set in one of three ways:
- Set to `true` to just enable it.
- Set to a string, to use this string in the usage help text.
- Set to a hash containing `label` and `help` keys, to show a detailed help for it when running with `--help`. `dependencies` | Specify an array of any required external dependencies (commands). The script execution will be halted with a friendly error unless all dependency commands exist. `group` | In case you have many commands, use this option to specify a caption to display before this command. This option is purely for display purposes, and needs to be specified only for the first command in each group. @@ -326,6 +330,71 @@ The generated script will execute `git status`. See the [extensible-delegate example](examples/extensible-delegate). +## Bash Completions + +Bashly comes with built-in bash completions generator, provided by the +[completely][completely] gem. + +By running any of the `bashly add comp` commands, you can add this +functionality to your script in one of three ways: + +- `bashly add comp function` - creates a function in your `./src/lib` directory + that echoes a completion script. You can then call this function from any + command (for example `yourcli completions`) and your users will be able to + install the completions by running `eval "$(yourcli completions)"`. +- `bashly add comp script` - creates a standalone completion script that can be + sourced or copies to the system's bash completions directory. +- `bashly add comp yaml` - creates the "raw data" YAML file. This is intended + mainly for development purposes. + +The bash completions generation is completely automatic, and you will have to +rerun the `bashly add comp *` command whenever you change your `bashly.yml` +script. + +In addition to suggesting subcommands and flags, you can instruct bashly to +also suggest files, directories, users and more. To do this, add another option +in your `bashly.yml` on the command you wish to alter: + +```yaml +# bashly.yml +commands: +- name: upload + help: Upload a file + completions: [directory, user] + +``` + +Valid completion additions are: + +| Keyword | 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 + +Note that these are taken from the [Programmable Completion Builtin][compgen], +and will simply be added using the `compgen -A action` command. + + ## Real World Examples - [Rush][rush] - a Personal Package Manager @@ -344,3 +413,5 @@ to contribute, feel free to [open an issue][issues]. [rush]: https://github.com/DannyBen/rush-cli [alf]: https://github.com/DannyBen/alf [git-changelog]: https://github.com/DannyBen/git-changelog +[completely]: https://github.com/DannyBen/completely +[compgen]: https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html diff --git a/examples/completions/README.md b/examples/completions/README.md index 359e1531..c5df165d 100644 --- a/examples/completions/README.md +++ b/examples/completions/README.md @@ -26,6 +26,7 @@ commands: - name: download short: d help: Download a file + completions: [file] args: - name: source @@ -50,6 +51,7 @@ commands: - name: upload short: u help: Upload a file + completions: [directory, user] args: - name: source required: true @@ -146,9 +148,10 @@ _cli_completions() { local cur=${COMP_WORDS[COMP_CWORD]} case "$COMP_LINE" in - 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;; - 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;; - 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;; + 'cli completions'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;; + 'cli download'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;; + 'cli upload'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;; + 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;; esac } diff --git a/examples/completions/cli b/examples/completions/cli index 2440cc7a..78d7838a 100644 --- a/examples/completions/cli +++ b/examples/completions/cli @@ -204,9 +204,10 @@ send_completions() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $'' echo $' case "$COMP_LINE" in' - echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;' - echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;' - echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;' + echo $' \'cli completions\'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;' + echo $' \'cli download\'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;' + echo $' \'cli upload\'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;' + echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;' echo $' esac' echo $'}' echo $'' diff --git a/examples/completions/src/bashly.yml b/examples/completions/src/bashly.yml index 3fcd44f6..02c1f553 100644 --- a/examples/completions/src/bashly.yml +++ b/examples/completions/src/bashly.yml @@ -11,6 +11,7 @@ commands: - name: download short: d help: Download a file + completions: [file] args: - name: source @@ -35,6 +36,7 @@ commands: - name: upload short: u help: Upload a file + completions: [directory, user] args: - name: source required: true diff --git a/examples/completions/src/lib/send_completions.sh b/examples/completions/src/lib/send_completions.sh index 4323157e..a1c60b2f 100644 --- a/examples/completions/src/lib/send_completions.sh +++ b/examples/completions/src/lib/send_completions.sh @@ -8,9 +8,10 @@ send_completions() { echo $' local cur=${COMP_WORDS[COMP_CWORD]}' echo $'' echo $' case "$COMP_LINE" in' - echo $' \'cli download\'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;;' - echo $' \'cli upload\'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;;' - echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;;' + echo $' \'cli completions\'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;;' + echo $' \'cli download\'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;;' + echo $' \'cli upload\'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;;' + echo $' \'cli\'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;;' echo $' esac' echo $'}' echo $'' diff --git a/examples/completions/test.sh b/examples/completions/test.sh index 2304b7aa..917ec9ec 100644 --- a/examples/completions/test.sh +++ b/examples/completions/test.sh @@ -2,9 +2,10 @@ set -x +bashly add comp function bashly generate ./cli ./cli -h ./cli completions -h -./cli completions \ No newline at end of file +./cli completions diff --git a/spec/approvals/examples/completions b/spec/approvals/examples/completions index 1fb324fb..0b1dd43c 100644 --- a/spec/approvals/examples/completions +++ b/spec/approvals/examples/completions @@ -1,3 +1,10 @@ ++ bashly add comp function +created src/lib/send_completions.sh + +In order to use it in your script, create a command or a flag (for example: cli completions or cli --completions) that calls the send_completions function. +Your users can then run something like this to enable completions: + + $ eval "$(cli completions)" + bashly generate creating user files in src skipped src/initialize.sh (exists) @@ -63,9 +70,10 @@ _cli_completions() { local cur=${COMP_WORDS[COMP_CWORD]} case "$COMP_LINE" in - 'cli download'*) COMPREPLY=($(compgen -W "--force --help -f -h" -- "$cur")) ;; - 'cli upload'*) COMPREPLY=($(compgen -W "--help --password --user -h -p -u" -- "$cur")) ;; - 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v download upload" -- "$cur")) ;; + 'cli completions'*) COMPREPLY=($(compgen -W "--help -h" -- "$cur")) ;; + 'cli download'*) COMPREPLY=($(compgen -A file -W "--force --help -f -h" -- "$cur")) ;; + 'cli upload'*) COMPREPLY=($(compgen -A directory -A user -W "--help --password --user -h -p -u" -- "$cur")) ;; + 'cli'*) COMPREPLY=($(compgen -W "--help --version -h -v completions download upload" -- "$cur")) ;; esac }