tabry 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }