tabry 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/bin/tabry-bash +48 -0
  3. data/bin/tabry-help +17 -0
  4. data/bin/tabry-test-options +9 -0
  5. data/bin/tabry-test-parse +20 -0
  6. data/lib/tabry/cli/arg_proxy.rb +71 -0
  7. data/lib/tabry/cli/base.rb +34 -0
  8. data/lib/tabry/cli/builder.rb +104 -0
  9. data/lib/tabry/cli/flag_proxy.rb +45 -0
  10. data/lib/tabry/cli/internals.rb +10 -0
  11. data/lib/tabry/cli/util/config.rb +48 -0
  12. data/lib/tabry/cli/util.rb +51 -0
  13. data/lib/tabry/config_loader.rb +55 -0
  14. data/lib/tabry/core_ext/string/colors.rb +91 -0
  15. data/lib/tabry/machine.rb +124 -0
  16. data/lib/tabry/models/arg.rb +43 -0
  17. data/lib/tabry/models/arg_base.rb +10 -0
  18. data/lib/tabry/models/arg_include.rb +9 -0
  19. data/lib/tabry/models/arg_includes.rb +14 -0
  20. data/lib/tabry/models/args_list.rb +31 -0
  21. data/lib/tabry/models/config.rb +44 -0
  22. data/lib/tabry/models/config_error.rb +8 -0
  23. data/lib/tabry/models/config_list.rb +48 -0
  24. data/lib/tabry/models/config_object.rb +78 -0
  25. data/lib/tabry/models/config_string_hash.rb +44 -0
  26. data/lib/tabry/models/const_option.rb +17 -0
  27. data/lib/tabry/models/dir_option.rb +25 -0
  28. data/lib/tabry/models/file_option.rb +25 -0
  29. data/lib/tabry/models/flag.rb +55 -0
  30. data/lib/tabry/models/flags_list.rb +47 -0
  31. data/lib/tabry/models/include_arg.rb +18 -0
  32. data/lib/tabry/models/include_flag.rb +18 -0
  33. data/lib/tabry/models/include_option.rb +22 -0
  34. data/lib/tabry/models/include_sub.rb +18 -0
  35. data/lib/tabry/models/option.rb +33 -0
  36. data/lib/tabry/models/option_base.rb +20 -0
  37. data/lib/tabry/models/option_includes.rb +14 -0
  38. data/lib/tabry/models/options_list.rb +18 -0
  39. data/lib/tabry/models/shell_option.rb +13 -0
  40. data/lib/tabry/models/sub.rb +59 -0
  41. data/lib/tabry/models/subs_list.rb +28 -0
  42. data/lib/tabry/options_finder.rb +87 -0
  43. data/lib/tabry/result.rb +110 -0
  44. data/lib/tabry/runner.rb +27 -0
  45. data/lib/tabry/state.rb +14 -0
  46. data/lib/tabry/usage_generator.rb +137 -0
  47. data/lib/tabry/util.rb +15 -0
  48. data/sh/tabry_bash.sh +61 -0
  49. data/sh/tabry_bash_help.sh +7 -0
  50. data/spec/fixtures/basiccli.json +1 -0
  51. data/spec/fixtures/basiccli.tabry +5 -0
  52. data/spec/fixtures/basiccli2.tabry +5 -0
  53. data/spec/fixtures/basiccli2.yml +7 -0
  54. data/spec/fixtures/vehicles.tabry +60 -0
  55. data/spec/fixtures/vehicles.yaml +135 -0
  56. data/spec/spec_helper.rb +10 -0
  57. data/spec/tabry/cli/arg_proxy_spec.rb +76 -0
  58. data/spec/tabry/cli/builder_spec.rb +226 -0
  59. data/spec/tabry/config_loader_spec.rb +69 -0
  60. data/spec/tabry/machine_spec.rb +109 -0
  61. data/spec/tabry/models/args_list_spec.rb +36 -0
  62. data/spec/tabry/models/config_spec.rb +62 -0
  63. data/spec/tabry/models/const_option_spec.rb +17 -0
  64. data/spec/tabry/models/dir_option_spec.rb +16 -0
  65. data/spec/tabry/models/file_option_spec.rb +16 -0
  66. data/spec/tabry/models/options_list_spec.rb +47 -0
  67. data/spec/tabry/models/shell_option_spec.rb +19 -0
  68. data/spec/tabry/models/subs_list_spec.rb +24 -0
  69. data/spec/tabry/options_finder_spec.rb +91 -0
  70. data/spec/tabry/result_spec.rb +236 -0
  71. data/spec/tabry/runner_spec.rb +35 -0
  72. data/spec/tabry/usage_generator_spec.rb +67 -0
  73. data/tabry.gemspec +27 -0
  74. metadata +189 -0
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Models
5
+ class IncludeSub
6
+ attr_reader :include_name, :_root
7
+
8
+ def initialize(raw:, root:)
9
+ @include_name = raw["include"]
10
+ @_root = root
11
+ end
12
+
13
+ def flatten
14
+ _root.arg_includes[include_name].subs.flatten
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_error"
4
+ require_relative "const_option"
5
+ require_relative "shell_option"
6
+ require_relative "include_option"
7
+ require_relative "file_option"
8
+ require_relative "dir_option"
9
+
10
+ module Tabry
11
+ module Models
12
+ module Option
13
+ def self.new(**args)
14
+ # TODO: assert type
15
+ hash = args[:raw]
16
+ case hash["type"]
17
+ when "const"
18
+ ConstOption.new(**args)
19
+ when "shell"
20
+ ShellOption.new(**args)
21
+ when "include"
22
+ IncludeOption.new(**args)
23
+ when "file"
24
+ FileOption.new(**args)
25
+ when "dir"
26
+ DirOption.new(**args)
27
+ else
28
+ raise ConfigError, "unknown option type #{hash["type"]}"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+
5
+ module Tabry
6
+ module Models
7
+ class OptionBase < ConfigObject
8
+ FIELDS = {
9
+ type: :string,
10
+ value: :string,
11
+ }.freeze
12
+
13
+ attr_reader(*FIELDS.keys)
14
+
15
+ def flatten
16
+ self
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_string_hash"
4
+ require_relative "options_list"
5
+
6
+ module Tabry
7
+ module Models
8
+ class OptionIncludes < ConfigStringHash
9
+ def initialize(**args)
10
+ super(**args, klass: OptionsList)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_list"
4
+ require_relative "option"
5
+
6
+ module Tabry
7
+ module Models
8
+ class OptionsList < ConfigList
9
+ def initialize(**args)
10
+ super(**args, klass: Option)
11
+ end
12
+
13
+ def options(token)
14
+ to_a.map { |option| option.options(token) }.inject(&:|)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "option_base"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ShellOption < OptionBase
8
+ def options(token)
9
+ `#{value}`.chomp.split("\n").select { |opt| opt.start_with?(token) }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "args_list"
4
+ require_relative "flags_list"
5
+ require_relative "subs_list"
6
+ require_relative "include_sub"
7
+
8
+ module Tabry
9
+ module Models
10
+ class Sub < ConfigObject
11
+ def self.new(**args)
12
+ hash = args[:raw]
13
+ if hash["include"]
14
+ IncludeSub.new(**args)
15
+ else
16
+ super(**args)
17
+ end
18
+ end
19
+
20
+ FIELDS = {
21
+ aliases: :string_array,
22
+ args: [:list_object, :ArgsList],
23
+ description: :string,
24
+ flags: [:list_object, :FlagsList],
25
+ name: :string,
26
+ subs: [:list_object, :SubsList],
27
+ }.freeze
28
+
29
+ attr_reader(*FIELDS.keys)
30
+
31
+ # TODO: put this default stuff into ConfigObject
32
+ def subs
33
+ @subs ||= SubsList.new(raw: [], root: _root)
34
+ end
35
+
36
+ def flags
37
+ @flags ||= FlagsList.new(raw: [], root: _root)
38
+ end
39
+
40
+ def args
41
+ @args ||= ArgsList.new(raw: [], root: _root)
42
+ end
43
+
44
+ def flatten
45
+ self
46
+ end
47
+
48
+ def min_args
49
+ args.reject(&:optional).count
50
+ end
51
+
52
+ def max_args
53
+ return Float::INFINITY if args.any?(&:varargs?)
54
+
55
+ args.count
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_list"
4
+ require_relative "sub"
5
+
6
+ module Tabry
7
+ module Models
8
+ class SubsList < ConfigList
9
+ def initialize(**args)
10
+ super(**args, klass: Sub)
11
+ end
12
+
13
+ def by_name
14
+ @by_name ||= to_a.to_h { |sub| [sub.name, sub] }
15
+ end
16
+
17
+ def match(token)
18
+ to_a.find do |sub|
19
+ [sub.name, *sub.aliases].include?(token)
20
+ end
21
+ end
22
+
23
+ def options(token)
24
+ to_a.map(&:name).select { |name| name.start_with?(token) }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Tabry
6
+ class OptionsFinder
7
+ attr_reader :result
8
+
9
+ # Returns an array of options
10
+ def self.options(result, token)
11
+ new(result).options(token)
12
+ end
13
+
14
+ def initialize(result)
15
+ @result = result
16
+ end
17
+
18
+ def options(token)
19
+ # Bit of a hack: send this down to autocomplete shell commands
20
+ # TODO: set this only in ShellOption -- would require passing state down on thru
21
+ ENV["TABRY_AUTOCOMPLETE_STATE"] = {
22
+ cmd: result.config.cmd,
23
+ flags: result.state.flags,
24
+ args: result.state.args,
25
+ current_flag: result.state.current_flag
26
+ }.to_json
27
+
28
+ send(:"options_#{state.mode}", token || "")
29
+ end
30
+
31
+ private
32
+
33
+ def state
34
+ result.state
35
+ end
36
+
37
+ def current_sub
38
+ @current_sub ||= result.current_sub
39
+ end
40
+
41
+ def options_subcommand(token)
42
+ if token.to_s == ""
43
+ result.sub_stack.each do |sub|
44
+ required_flag = sub.flags.first_required_flag(used: state.flags)
45
+ if required_flag
46
+ return [required_flag.name_with_dashes]
47
+ end
48
+ end
49
+ end
50
+
51
+ options_subcommand_subs(token) |
52
+ options_subcommand_flags(token) |
53
+ options_subcommand_args(token)
54
+ end
55
+
56
+ def options_flagarg(token)
57
+ result.sub_stack.map do |sub|
58
+ sub.flags[state.current_flag]&.options&.options(token)
59
+ end.compact.flatten.uniq
60
+ end
61
+
62
+ def options_subcommand_subs(token)
63
+ if state.args.any?
64
+ # once an arg has been given, can no longer use a subcommand
65
+ []
66
+ else
67
+ current_sub.subs.options(token)
68
+ end
69
+ end
70
+
71
+ def options_subcommand_flags(token)
72
+ return [] if state.dashdash
73
+
74
+ result.sub_stack.map { |sub| sub.flags.options(token, used: state.flags) }.flatten.uniq
75
+ end
76
+
77
+ def options_subcommand_args(token)
78
+ arg = if current_sub.args.n_passed_in_varargs(state.args.length) > 0
79
+ current_sub.args.varargs_arg
80
+ else
81
+ current_sub.args[state.args.length]
82
+ end
83
+
84
+ arg&.options&.options(token) || []
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "usage_generator"
4
+
5
+ # Encapsulates a config and state, often representing the end state (except
6
+ # when you call options()), and adds functionality regarding this state.
7
+ module Tabry
8
+ class Result
9
+ attr_reader :config, :state
10
+
11
+ def initialize(config, state)
12
+ @config = config
13
+ @state = state
14
+ end
15
+
16
+ def current_sub
17
+ @current_sub ||= sub_stack.last
18
+ end
19
+
20
+ def sub_stack
21
+ @sub_stack ||= config.dig_sub_array(state.subcommand_stack)
22
+ end
23
+
24
+ def invalid_usage_reason
25
+ waiting_on_flag_arg? ||
26
+ wrong_number_of_args? ||
27
+ missing_required_flags?
28
+ end
29
+
30
+ def waiting_on_flag_arg?
31
+ if state.mode != :subcommand
32
+ "arg required for flag #{state.current_flag}"
33
+ end
34
+ end
35
+
36
+ def wrong_number_of_args?
37
+ n_args = state.args.length
38
+ if n_args == 0 && current_sub.subs.any?
39
+ if current_sub.args.any?
40
+ "missing subcommand or arg(s)"
41
+ else
42
+ "missing subcommand"
43
+ end
44
+ elsif !(current_sub.min_args..current_sub.max_args).include?(n_args)
45
+ if n_args == 0
46
+ if current_sub.max_args == 1
47
+ msg = "missing argument"
48
+ msg += " #{current_sub.args[0].name.inspect}" if current_sub.args[0].name
49
+ msg
50
+ elsif current_sub.min_args == 1
51
+ "missing one or more args"
52
+ else
53
+ "missing args"
54
+ end
55
+ else
56
+ "got #{state.args.count} args, " \
57
+ "must be between #{current_sub.min_args} and #{current_sub.max_args}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def non_varargs
63
+ @non_varargs ||= state.args.first(state.args.length - n_passed_in_varargs)
64
+ end
65
+
66
+ def varargs
67
+ @varargs ||= state.args.last(n_passed_in_varargs)
68
+ end
69
+
70
+ def n_passed_in_varargs
71
+ current_sub.args.n_passed_in_varargs(state.args.count)
72
+ end
73
+
74
+ def missing_required_flags?
75
+ current_sub.flags.each do |flag|
76
+ if flag.required && !state.flags[flag.name]
77
+ return "missing required flag #{flag.name}"
78
+ end
79
+ end
80
+ nil
81
+ end
82
+
83
+ def usage(cmd_name = nil)
84
+ cmd_name ||= config.cmd
85
+ Tabry::UsageGenerator.new(sub_stack, cmd_name).usage
86
+ end
87
+
88
+ def top_level?
89
+ state.subcommand_stack.empty?
90
+ end
91
+
92
+ def help?
93
+ state.help
94
+ end
95
+
96
+ def named_args
97
+ @named_args ||= {}.tap do |res|
98
+ if (varargs_name = current_sub.args.varargs_arg&.name)
99
+ res[varargs_name] = varargs
100
+ end
101
+
102
+ non_varargs.each_with_index do |arg_val, i|
103
+ if (arg_name = current_sub.args[i]&.name)
104
+ res[arg_name] = arg_val
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_loader"
4
+ require_relative "machine"
5
+ require_relative "options_finder"
6
+ require_relative "result"
7
+
8
+ # Loads a config, runs the machine, creates a Result object to encapsulate results.
9
+ # Also options() convenience function
10
+ module Tabry
11
+ class Runner
12
+ attr_reader :config
13
+
14
+ def initialize(config_name:)
15
+ @config = ConfigLoader.load(name: config_name)
16
+ end
17
+
18
+ def options(args, last = nil)
19
+ Tabry::OptionsFinder.options(parse(args), last)
20
+ end
21
+
22
+ def parse(args)
23
+ state = Tabry::Machine.run(config, args)
24
+ Tabry::Result.new(config, state)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ State = Struct.new(
5
+ :mode,
6
+ :subcommand_stack,
7
+ :args,
8
+ :flags,
9
+ :current_flag,
10
+ :dashdash,
11
+ :help,
12
+ keyword_init: true
13
+ )
14
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "core_ext/string/colors"
4
+
5
+ module Tabry
6
+ class UsageGenerator
7
+ attr_reader :sub, :cmdline_string, :top_level, :sub_stack
8
+
9
+ def initialize(sub_stack, cmd_name)
10
+ @sub_stack = sub_stack
11
+ @sub = sub_stack.last
12
+ non_main_subs = sub_stack[1..]
13
+ @cmdline_string = [cmd_name, *non_main_subs.map(&:name)].join(" ")
14
+ @top_level = sub_stack.length == 1
15
+ end
16
+
17
+ USAGE = "Usage:".bold
18
+
19
+ def usage
20
+ lines = []
21
+ if description
22
+ lines << description
23
+ lines << ""
24
+ end
25
+ if subs.any?
26
+ lines << "#{USAGE} #{cmdline_string} <subcommand> ..."
27
+ end
28
+
29
+ if args.any?
30
+ arg_strings = args.map do |a|
31
+ name = a.title || "arg"
32
+ str = "<#{name}>"
33
+ str += " [<#{name}>...]" if a.varargs?
34
+ str = "[#{str}]" if a.optional
35
+ str
36
+ end
37
+ lines << ["#{USAGE} #{cmdline_string}", *arg_strings].join(" ")
38
+ end
39
+
40
+ if subs.empty? && args.empty?
41
+ lines << "#{USAGE} #{cmdline_string}"
42
+ end
43
+
44
+ usage_add_subcommands(lines, top_level)
45
+ usage_add_args(lines)
46
+ usage_add_flags(lines)
47
+
48
+ lines.join("\n")
49
+ end
50
+
51
+ def add_text_with_indent(lines, text, indent_string)
52
+ return unless text
53
+
54
+ text.split("\n").each do |line|
55
+ lines << (indent_string + line)
56
+ end
57
+ end
58
+
59
+ def usage_add_subcommands(lines, top_level)
60
+ if subs.any? || top_level
61
+ lines << ""
62
+ lines << "SUBCOMMANDS".green.bold
63
+ end
64
+
65
+ subs_by_name = subs.by_name
66
+ sub_names = subs_by_name.keys
67
+ sub_names |= ["help"] if top_level
68
+
69
+ sub_names.sort.each do |name|
70
+ if (s = subs_by_name[name])
71
+ name_line = name.bold
72
+ if s.aliases&.length == 1
73
+ name_line += " (alias: #{s.aliases.first})"
74
+ elsif s.aliases
75
+ name_line += " (aliases: #{s.aliases.join(", ")})"
76
+ end
77
+
78
+ lines << name_line
79
+ add_text_with_indent(lines, s.description, " ")
80
+ elsif name == "help"
81
+ lines << "help".bold
82
+ lines << " Show help on command or subcommand"
83
+ end
84
+ end
85
+ end
86
+
87
+ def usage_add_flags(lines)
88
+ partial_sub_stack = []
89
+ sub_stack_with_names = sub_stack.map do |sub|
90
+ partial_sub_stack << sub.name
91
+ [sub, partial_sub_stack.compact.join(" ")]
92
+ end
93
+
94
+ sub_stack_with_names.reverse.each do |sub, full_sub_name|
95
+ next unless sub.flags.any?
96
+
97
+ lines << ""
98
+ lines << "FLAGS".green.bold
99
+ if sub != sub_stack.last
100
+ lines.last << if full_sub_name == ""
101
+ " (global)".green.bold
102
+ else
103
+ " (#{full_sub_name})".green.bold
104
+ end
105
+ end
106
+
107
+ sub.flags.map(&:name).sort.each do |name|
108
+ flag = sub.flags[name]
109
+ flag_name = [flag.name, *flag.aliases].map { |al| flag.alias_with_dash(al).bold }.join(", ")
110
+ flag_name << " <arg>" if flag.arg
111
+ flag_name << " (required)" if flag.required
112
+ lines << flag_name
113
+ add_text_with_indent(lines, flag.description, " ")
114
+ end
115
+ end
116
+ end
117
+
118
+ def usage_add_args(lines)
119
+ if args.any?(&:description)
120
+ lines << ""
121
+ lines << "ARGS".green.bold
122
+ args.each do |arg|
123
+ arg_name = arg.name.bold
124
+ arg_name += " (optional)" if arg.optional
125
+ lines << arg_name
126
+ add_text_with_indent(lines, arg.description, " ")
127
+ end
128
+ end
129
+ end
130
+
131
+ %i[subs args description].each do |met|
132
+ define_method met do
133
+ sub.send met
134
+ end
135
+ end
136
+ end
137
+ end
data/lib/tabry/util.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Util
5
+ module_function
6
+
7
+ def debug(msg)
8
+ warn msg if debug?
9
+ end
10
+
11
+ def debug?
12
+ !ENV["TABRY_DEBUG"].to_s.empty?
13
+ end
14
+ end
15
+ end
data/sh/tabry_bash.sh ADDED
@@ -0,0 +1,61 @@
1
+ #/usr/bin/env bash
2
+
3
+ # Bash completion
4
+ # _tabry_completions function is same for any command you use with tabry, you just need
5
+ # to do `complete -F _tabry_completions mycmd` for your command. This calls the tabry-bash
6
+ # ruby script. For more info see LANGUAGE_REFERENCE.md
7
+
8
+ _tabry_bash="$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )"/../bin/tabry-bash
9
+
10
+ _tabry_completions()
11
+ {
12
+ [[ -n "$TABRY_DEBUG" ]] && echo && echo -n tabry start bash: && date +%s.%N >&2
13
+ local saveifs="$IFS"
14
+ IFS=$'\n'
15
+
16
+ local result=`"$_tabry_bash" "$COMP_LINE" "$COMP_POINT"`
17
+ local specials
18
+
19
+ if [[ $result == *$'\n'$'\n'* ]]; then
20
+ # double newline signals use of specials (file, directory)
21
+ # Warning: fragile code ahead.
22
+ # Split on double-newline to get regular options and specials.
23
+ specials="$(echo "$result"|sed '1,/^$/d')"
24
+ result="$(echo "$result)"|sed '/^$/q')"
25
+
26
+ # First, add anything before the double newline in (regular options)
27
+ COMPREPLY=($result)
28
+
29
+ # File special
30
+ if [[ $'\n'$specials$'\n' == *$'\n'file$'\n'* ]]; then
31
+ # doesn't seem to be a "plusfiles" like there is for "plusdirs"
32
+ COMPREPLY+=($(compgen -A file "${COMP_WORDS[$COMP_CWORD]}"))
33
+ fi
34
+
35
+ # Directory special
36
+ if [[ $'\n'$specials$'\n' == *$'\n'directory$'\n'* ]]; then
37
+ # If there are only directory results, use nospace to not add a space after it,
38
+ # like "cd" tab completion does.
39
+ [[ ${#COMPREPLY[@]} -eq 0 ]] && compopt -o nospace
40
+ compopt -o plusdirs
41
+ fi
42
+
43
+ # "description_if_optionless" special: Options are are meant to be description or examples
44
+ # and not actual options. Add an empty option so we won't tab complete.
45
+ if [[ $'\n'$specials$'\n' == *$'\n'description_if_optionless$'\n'* ]]; then
46
+ compopt -o nosort
47
+ COMPREPLY+=('')
48
+ fi
49
+ else
50
+ COMPREPLY=($result)
51
+ fi
52
+
53
+ IFS="$saveifs"
54
+ [[ -n "$TABRY_DEBUG" ]] && echo -n tabry end bash: && date +%s.%N >&2
55
+ }
56
+
57
+ # To use: put a .json/.yml tabry config file in ~/.tabry (e.g. ~/.aws.json)
58
+ # Then, run this script in your shell startup scripts, plus (e.g.)
59
+ #
60
+ # complete -F _tabry_completions aws
61
+
@@ -0,0 +1,7 @@
1
+ # 'help mycommand' -> show tabry help for a tabry-CLI or command you have a
2
+ # tabry tab completion config for
3
+
4
+ _tabry_help="$( dirname "${BASH_SOURCE[0]}" )"/../bin/tabry-help
5
+ help() {
6
+ tabry-help "$@" 2>/dev/null || command help "$@"
7
+ }
@@ -0,0 +1 @@
1
+ {"cmd":"basiccli","main":{"subs":[{"name":"foo","args":[{"name":"bar"}]}]}}
@@ -0,0 +1,5 @@
1
+ cmd basiccli
2
+
3
+ sub foo {
4
+ arg bar
5
+ }
@@ -0,0 +1,5 @@
1
+ cmd basiccli2
2
+
3
+ sub waz {
4
+ arg bar
5
+ }