diff --git a/examples/command-default-force/README.md b/examples/command-default-force/README.md index c65df87a..371ef9fe 100644 --- a/examples/command-default-force/README.md +++ b/examples/command-default-force/README.md @@ -93,7 +93,7 @@ args: none tester all - Run all tests Usage: - tester all + tester [all] tester all --help | -h Options: diff --git a/examples/command-default/README.md b/examples/command-default/README.md index f77f9480..1e4b3761 100644 --- a/examples/command-default/README.md +++ b/examples/command-default/README.md @@ -102,6 +102,38 @@ args: - ${args[source]} = something +```` + +### `$ ./ftp upload` + +````shell +missing required argument: SOURCE +usage: ftp [upload] SOURCE + + +```` + +### `$ ./ftp upload -h` + +````shell +ftp upload - Upload a file + +Alias: u + +Usage: + ftp [upload] SOURCE + ftp upload --help | -h + +Options: + --help, -h + Show this help + +Arguments: + SOURCE + File to upload + + + ```` ### `$ ./ftp upload something` diff --git a/examples/conflicts/README.md b/examples/conflicts/README.md index f18b4807..ef215eee 100644 --- a/examples/conflicts/README.md +++ b/examples/conflicts/README.md @@ -51,12 +51,15 @@ Usage: Options: --cache Enable cache + Conflicts: --no-cache --no-cache Disable cache + Conflicts: --cache, --fast --fast Run faster + Conflicts: --no-cache --help, -h Show this help diff --git a/examples/needs/.gitignore b/examples/needs/.gitignore new file mode 100644 index 00000000..573c0c4f --- /dev/null +++ b/examples/needs/.gitignore @@ -0,0 +1 @@ +cli diff --git a/examples/needs/README.md b/examples/needs/README.md new file mode 100644 index 00000000..a3ff0b50 --- /dev/null +++ b/examples/needs/README.md @@ -0,0 +1,113 @@ +# Needy Flags Example + +Demonstrates the use of needy flags that must be executed together. + +This example was generated with: + +```bash +$ bashly init --minimal +# ... now edit src/bashly.yml to match the example ... +$ bashly generate +``` + +----- + +## `bashly.yml` + +````yaml +name: cli +help: Sample application to demonstrate the use of needy flags +version: 0.1.0 + +flags: +- long: --add + short: -a + arg: alias + help: Alias to add + # When using --add, --command and --target must also be provided + needs: [--command, --target] + +- long: --command + short: -c + arg: command + help: Command for the alias + # Note that this relationship is marked on both sides + needs: [--add] + +- long: --target + short: -t + arg: target + help: Where to add the alias + needs: [--add] + allowed: [global, local] +```` + + + +## Output + +### `$ ./cli -h` + +````shell +cli - Sample application to demonstrate the use of needy flags + +Usage: + cli [OPTIONS] + cli --help | -h + cli --version | -v + +Options: + --add, -a ALIAS + Alias to add + Needs: --command, --target + + --command, -c COMMAND + Command for the alias + Needs: --add + + --target, -t TARGET + Where to add the alias + Allowed: global, local + Needs: --add + + --help, -h + Show this help + + --version, -v + Show version number + + + +```` + +### `$ ./cli --add deploy` + +````shell +--add requires --command + + +```` + +### `$ ./cli --add deploy --command 'git push'` + +````shell +--add requires --target + + +```` + +### `$ ./cli --add deploy --command 'git push' --target local` + +````shell +# this file is located in 'src/root_command.sh' +# you can edit it freely and regenerate (it will not be overwritten) +args: +- ${args[--add]} = deploy +- ${args[--command]} = git push +- ${args[--target]} = local + + +```` + + + diff --git a/examples/needs/src/bashly.yml b/examples/needs/src/bashly.yml new file mode 100644 index 00000000..c45f1ab6 --- /dev/null +++ b/examples/needs/src/bashly.yml @@ -0,0 +1,26 @@ +name: cli +help: Sample application to demonstrate the use of needy flags +version: 0.1.0 + +flags: +- long: --add + short: -a + arg: alias + help: Alias to add + # When using --add, --command and --target must also be provided + needs: [--command, --target] + +- long: --command + short: -c + arg: command + help: Command for the alias + # Note that this relationship is marked on both sides + needs: [--add] + +- long: --target + short: -t + arg: target + help: Where to add the alias + needs: [--add] + allowed: [global, local] + diff --git a/examples/needs/src/root_command.sh b/examples/needs/src/root_command.sh new file mode 100644 index 00000000..91e64307 --- /dev/null +++ b/examples/needs/src/root_command.sh @@ -0,0 +1,3 @@ +echo "# this file is located in 'src/root_command.sh'" +echo "# you can edit it freely and regenerate (it will not be overwritten)" +inspect_args diff --git a/examples/needs/test.sh b/examples/needs/test.sh new file mode 100644 index 00000000..8972d0a1 --- /dev/null +++ b/examples/needs/test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -x + +bashly generate + +### Try Me ### + +./cli -h +./cli --add deploy +./cli --add deploy --command 'git push' +./cli --add deploy --command 'git push' --target local diff --git a/examples/render-mandoc/README.md b/examples/render-mandoc/README.md index 5462306d..08109e37 100644 --- a/examples/render-mandoc/README.md +++ b/examples/render-mandoc/README.md @@ -102,7 +102,7 @@ ISSUE TRACKER AUTHORS Lana Lang. -Version 0.1.0 July 2024 download(1) +Version 0.1.0 August 2024 download(1) ```` diff --git a/examples/render-mandoc/docs/download.1 b/examples/render-mandoc/docs/download.1 index ff4e5a37..b26fb422 100644 --- a/examples/render-mandoc/docs/download.1 +++ b/examples/render-mandoc/docs/download.1 @@ -1,6 +1,6 @@ .\" Automatically generated by Pandoc 3.2 .\" -.TH "download" "1" "July 2024" "Version 0.1.0" "Sample application" +.TH "download" "1" "August 2024" "Version 0.1.0" "Sample application" .SH NAME \f[B]download\f[R] \- Sample application .SH SYNOPSIS diff --git a/examples/render-mandoc/docs/download.md b/examples/render-mandoc/docs/download.md index 986ae471..0e746e7e 100644 --- a/examples/render-mandoc/docs/download.md +++ b/examples/render-mandoc/docs/download.md @@ -1,6 +1,6 @@ % download(1) Version 0.1.0 | Sample application % Lana Lang -% July 2024 +% August 2024 NAME ================================================== diff --git a/lib/bashly/docs/flag.yml b/lib/bashly/docs/flag.yml index 9bd4c0f4..8cac689b 100644 --- a/lib/bashly/docs/flag.yml +++ b/lib/bashly/docs/flag.yml @@ -113,6 +113,29 @@ flag.long: short: -s help: Clone using SSH +flag.needs: + help: Specify that this flag needs one or more additional flags. Use the long name of these needed flags. + url: https://bashly.dannyb.co/configuration/flag/#needs + example: |- + flags: + - long: --add + arg: alias + + # When using --add, --command and --target must also be provided + needs: [--command, --target] + + - long: --command + arg: command + help: Command for the alias + + # Note that this relationship is marked on both sides + needs: [--add] + + - long: --target + arg: target + help: Where to add the alias + needs: [--add] + flag.private: help: Specify that this flag should not be displayed in the help text. url: https://bashly.dannyb.co/configuration/flag/#private diff --git a/lib/bashly/libraries/render/mandoc/mandoc.gtx b/lib/bashly/libraries/render/mandoc/mandoc.gtx index 24f298bd..b2819d43 100644 --- a/lib/bashly/libraries/render/mandoc/mandoc.gtx +++ b/lib/bashly/libraries/render/mandoc/mandoc.gtx @@ -127,6 +127,9 @@ if flags.any? if flag.conflicts > - Conflicts With: **{{ flag.conflicts.join(', ') }}** end + if flag.needs + > - Needs: **{{ flag.needs.join(', ') }}** + end > end end diff --git a/lib/bashly/libraries/render/markdown/markdown.gtx b/lib/bashly/libraries/render/markdown/markdown.gtx index e90df632..2663e4b6 100644 --- a/lib/bashly/libraries/render/markdown/markdown.gtx +++ b/lib/bashly/libraries/render/markdown/markdown.gtx @@ -152,7 +152,8 @@ if flags.any? > ## Options > flags.each do |flag| - attributes = flag.required || flag.repeatable || flag.default || flag.allowed || flag.conflicts + attributes = flag.required || flag.repeatable || flag.default || + flag.allowed || flag.conflicts || flag.needs > #### *{{ flag.usage_string }}* > @@ -177,6 +178,9 @@ if flags.any? if flag.conflicts > | Conflicts With: | *{{ flag.conflicts.join(', ') }}* end + if flag.needs + > | Needs: | *{{ flag.needs.join(', ') }}* + end > end end diff --git a/lib/bashly/libraries/strings/strings.yml b/lib/bashly/libraries/strings/strings.yml index 3a9cf066..02f597ad 100644 --- a/lib/bashly/libraries/strings/strings.yml +++ b/lib/bashly/libraries/strings/strings.yml @@ -18,6 +18,8 @@ required: "(required)" repeatable: "(repeatable)" default: "Default: %{value}" allowed: "Allowed: %{values}" +needs: "Needs: %{values}" +conflicts: "Conflicts: %{values}" # Fixed flags help text help_flag_text: Show this help @@ -25,6 +27,7 @@ version_flag_text: Show version number # Error messages flag_requires_an_argument: "%{name} requires an argument: %{usage}" +flag_needs_another: "%{name} needs %{need}" invalid_argument: "invalid argument: %s" invalid_flag: "invalid option: %s" invalid_command: "invalid command: %s" diff --git a/lib/bashly/script/command.rb b/lib/bashly/script/command.rb index 72761f06..93b4ed02 100644 --- a/lib/bashly/script/command.rb +++ b/lib/bashly/script/command.rb @@ -286,6 +286,10 @@ def required_flags flags.select(&:required) end + def needy_flags + flags.select(&:needs) + end + # Returns true if this is the root command (no parents) def root_command? parents.empty? diff --git a/lib/bashly/script/flag.rb b/lib/bashly/script/flag.rb index fbb6326d..b2410ba5 100644 --- a/lib/bashly/script/flag.rb +++ b/lib/bashly/script/flag.rb @@ -1,3 +1,5 @@ +require 'shellwords' + module Bashly module Script class Flag < Base @@ -6,8 +8,8 @@ class Flag < Base class << self def option_keys @option_keys ||= %i[ - allowed arg completions conflicts default help long repeatable - required short unique validate private + allowed arg completions conflicts default help long needs + repeatable required short unique validate private ] end end diff --git a/lib/bashly/views/command/needy_flags_filter.gtx b/lib/bashly/views/command/needy_flags_filter.gtx new file mode 100644 index 00000000..b20f5b3b --- /dev/null +++ b/lib/bashly/views/command/needy_flags_filter.gtx @@ -0,0 +1,9 @@ +if needy_flags.any? + = view_marker + + needy_flags.each do |flag| + = flag.render(:needs) + end + + > +end diff --git a/lib/bashly/views/command/parse_requirements.gtx b/lib/bashly/views/command/parse_requirements.gtx index 597218d5..d71fd160 100644 --- a/lib/bashly/views/command/parse_requirements.gtx +++ b/lib/bashly/views/command/parse_requirements.gtx @@ -13,6 +13,7 @@ end = render(:parse_requirements_while).indent 2 = render(:required_args_filter).indent 2 = render(:required_flags_filter).indent 2 += render(:needy_flags_filter).indent 2 = render(:catch_all_filter).indent 2 = render(:default_assignments).indent 2 = render(:validations).indent 2 diff --git a/lib/bashly/views/flag/needs.gtx b/lib/bashly/views/flag/needs.gtx new file mode 100644 index 00000000..ba0f75fc --- /dev/null +++ b/lib/bashly/views/flag/needs.gtx @@ -0,0 +1,22 @@ +if needs + + = view_marker + + if needs.count == 1 + > if [[ -n ${args['{{ name }}']+x} ]] && [[ -z "${args[{{ needs.first }}]:-}" ]]; then + > printf "%s\n" "{{ strings[:flag_needs_another] % { name: name, need: needs.first } }}" >&2 + > exit 1 + > fi + > + else + > if [[ -n ${args['{{ name }}']+x} ]]; then + > for need in {{ needs.join ' ' }}; do + > if [[ -z "${args[$need]:-}" ]]; then + > printf "%s\n" "{{ strings[:flag_needs_another] % { name: name, need: "$need" } }}" >&2 + > exit 1 + > fi + > done + > fi + > + end +end \ No newline at end of file diff --git a/lib/bashly/views/flag/usage.gtx b/lib/bashly/views/flag/usage.gtx index d4b4c6c5..0a9c7fa8 100644 --- a/lib/bashly/views/flag/usage.gtx +++ b/lib/bashly/views/flag/usage.gtx @@ -15,5 +15,13 @@ if default end end +if needs + > printf " {{ strings[:needs] % { values: needs.join(', ') } }}\n" +end + +if conflicts + > printf " {{ strings[:conflicts] % { values: conflicts.join(', ') } }}\n" +end + > echo > \ No newline at end of file diff --git a/schemas/bashly.json b/schemas/bashly.json index ff42a2d9..c1a3f26b 100644 --- a/schemas/bashly.json +++ b/schemas/bashly.json @@ -211,7 +211,7 @@ "minLength": 1, "uniqueItems": true, "items": { - "description": "A mutually exclusive flag of the current flag", + "description": "The long form of the required flag", "type": "string", "minLength": 1, "examples": [ @@ -219,6 +219,21 @@ ] } }, + "needs": { + "title": "needs", + "description": "Additional flags required by the current flag\nhttps://bashly.dannyb.co/configuration/flag/#needs", + "type": "array", + "minLength": 1, + "uniqueItems": true, + "items": { + "description": "The long form of the required flag", + "type": "string", + "minLength": 1, + "examples": [ + "--command" + ] + } + }, "completions": { "title": "completions", "description": "Completions of the current flag\nhttps://bashly.dannyb.co/configuration/flag/#completions", diff --git a/schemas/strings.json b/schemas/strings.json index bbe01117..b57df61a 100644 --- a/schemas/strings.json +++ b/schemas/strings.json @@ -102,12 +102,19 @@ "minLength": 1, "default": "Allowed: %{values}" }, - "examples_caption_on_error": { - "title": "examples caption on error", - "description": "The string to show before the examples when show_examples_on_error is enabled\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", + "needs": { + "title": "needs", + "description": "The string template for the list of needed flags\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", "type": "string", "minLength": 1, - "default": "examples:" + "default": "Needs: %{values}" + }, + "conflicts": { + "title": "conflicts", + "description": "The string template for the list of conflicting flags\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", + "type": "string", + "minLength": 1, + "default": "Conflicts: %{values}" }, "help_flag_text": { "title": "help flag text", @@ -130,6 +137,13 @@ "minLength": 1, "default": "%{name} requires an argument: %{usage}" }, + "flag_needs_another": { + "title": "flag needs another flag", + "description": "The error message template for missing flag needs\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", + "type": "string", + "minLength": 1, + "default": "%{name} needs %{need}" + }, "invalid_argument": { "title": "invalid argument", "description": "The error message template for invalid argument\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", @@ -186,6 +200,13 @@ "minLength": 1, "default": "missing dependency: %{dependency}" }, + "examples_caption_on_error": { + "title": "examples caption on error", + "description": "The string to show before the examples when show_examples_on_error is enabled\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", + "type": "string", + "minLength": 1, + "default": "examples:" + }, "disallowed_flag": { "title": "disallowed flag", "description": "The error message template for forbidden flag\nhttps://bashly.dannyb.co/advanced/strings/#custom-strings", diff --git a/spec/approvals/cli/doc/full b/spec/approvals/cli/doc/full index f00a8f58..18fe0697 100644 --- a/spec/approvals/cli/doc/full +++ b/spec/approvals/cli/doc/full @@ -680,6 +680,32 @@ flag.long See https://bashly.dannyb.co/configuration/flag/#long +flag.needs + + Specify that this flag needs one or more additional flags. Use the long name + of these needed flags. + + flags: + - long: --add + arg: alias + + # When using --add, --command and --target must also be provided + needs: [--command, --target] + + - long: --command + arg: command + help: Command for the alias + + # Note that this relationship is marked on both sides + needs: [--add] + + - long: --target + arg: target + help: Where to add the alias + needs: [--add] + + See https://bashly.dannyb.co/configuration/flag/#needs + flag.private Specify that this flag should not be displayed in the help text. diff --git a/spec/approvals/cli/doc/index b/spec/approvals/cli/doc/index index 0f10911c..d9d7d9d6 100644 --- a/spec/approvals/cli/doc/index +++ b/spec/approvals/cli/doc/index @@ -42,6 +42,7 @@ flag.conflicts flag.default flag.help flag.long +flag.needs flag.private flag.repeatable flag.required diff --git a/spec/approvals/examples/conflicts b/spec/approvals/examples/conflicts index 1b409343..824e26ac 100644 --- a/spec/approvals/examples/conflicts +++ b/spec/approvals/examples/conflicts @@ -14,12 +14,15 @@ Usage: Options: --cache Enable cache + Conflicts: --no-cache --no-cache Disable cache + Conflicts: --cache, --fast --fast Run faster + Conflicts: --no-cache --help, -h Show this help diff --git a/spec/approvals/examples/needs b/spec/approvals/examples/needs new file mode 100644 index 00000000..4f5c97ca --- /dev/null +++ b/spec/approvals/examples/needs @@ -0,0 +1,44 @@ ++ bashly generate +creating user files in src +skipped src/root_command.sh (exists) +created ./cli +run ./cli --help to test your bash script ++ ./cli -h +cli - Sample application to demonstrate the use of needy flags + +Usage: + cli [OPTIONS] + cli --help | -h + cli --version | -v + +Options: + --add, -a ALIAS + Alias to add + Needs: --command, --target + + --command, -c COMMAND + Command for the alias + Needs: --add + + --target, -t TARGET + Where to add the alias + Allowed: global, local + Needs: --add + + --help, -h + Show this help + + --version, -v + Show version number + ++ ./cli --add deploy +--add requires --command ++ ./cli --add deploy --command 'git push' +--add requires --target ++ ./cli --add deploy --command 'git push' --target local +# this file is located in 'src/root_command.sh' +# you can edit it freely and regenerate (it will not be overwritten) +args: +- ${args[--add]} = deploy +- ${args[--command]} = git push +- ${args[--target]} = local diff --git a/spec/approvals/examples/render-mandoc b/spec/approvals/examples/render-mandoc index 102665eb..17464c88 100644 --- a/spec/approvals/examples/render-mandoc +++ b/spec/approvals/examples/render-mandoc @@ -44,4 +44,4 @@ ISSUE TRACKER AUTHORS Lana Lang. -Version 0.1.0 July 2024 download(1) +Version 0.1.0 August 2024 download(1) diff --git a/spec/approvals/libraries/render/mandoc/render-1-download.1 b/spec/approvals/libraries/render/mandoc/render-1-download.1 index c3c44bc8..788988a0 100644 --- a/spec/approvals/libraries/render/mandoc/render-1-download.1 +++ b/spec/approvals/libraries/render/mandoc/render-1-download.1 @@ -27,4 +27,4 @@ EXAMPLES download example.com ./output -f -Version 0.1.0 MONTH YEAR ... APPNAME +Version 0.1.0 MONTH YEAR ... APPNAME diff --git a/spec/bashly/script/command_spec.rb b/spec/bashly/script/command_spec.rb index 87c92174..a096bbdb 100644 --- a/spec/bashly/script/command_spec.rb +++ b/spec/bashly/script/command_spec.rb @@ -455,6 +455,15 @@ end end + describe '#needy_flags' do + let(:fixture) { :needy_flags } + + it 'returns an array of only the needy Flag objects' do + expect(subject.needy_flags.size).to eq 2 + expect(subject.needy_flags.first.long).to eq '--add' + end + end + describe '#public_commands' do let(:fixture) { :private_commands } diff --git a/spec/fixtures/script/commands.yml b/spec/fixtures/script/commands.yml index 0fc3ff10..b4c49077 100644 --- a/spec/fixtures/script/commands.yml +++ b/spec/fixtures/script/commands.yml @@ -359,6 +359,19 @@ help: get something from somewhere catch_all: params +:needy_flags: + name: add + help: add a command alias + + flags: + - long: --add + arg: alias + needs: [--command] + - long: --command + arg: command + needs: [--add] + + :nested_aliases: name: cli commands: diff --git a/support/schema/bashly.yml b/support/schema/bashly.yml index f24e033e..70ac28a6 100644 --- a/support/schema/bashly.yml +++ b/support/schema/bashly.yml @@ -189,11 +189,25 @@ definitions: minLength: 1 uniqueItems: true items: - description: A mutually exclusive flag of the current flag + description: The long form of the required flag type: string minLength: 1 examples: - --no-cache + needs: + title: needs + description: |- + Additional flags required by the current flag + https://bashly.dannyb.co/configuration/flag/#needs + type: array + minLength: 1 + uniqueItems: true + items: + description: The long form of the required flag + type: string + minLength: 1 + examples: + - --command completions: title: completions description: |-