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.
- checksums.yaml +7 -0
- data/bin/tabry-bash +48 -0
- data/bin/tabry-help +17 -0
- data/bin/tabry-test-options +9 -0
- data/bin/tabry-test-parse +20 -0
- data/lib/tabry/cli/arg_proxy.rb +71 -0
- data/lib/tabry/cli/base.rb +34 -0
- data/lib/tabry/cli/builder.rb +104 -0
- data/lib/tabry/cli/flag_proxy.rb +45 -0
- data/lib/tabry/cli/internals.rb +10 -0
- data/lib/tabry/cli/util/config.rb +48 -0
- data/lib/tabry/cli/util.rb +51 -0
- data/lib/tabry/config_loader.rb +55 -0
- data/lib/tabry/core_ext/string/colors.rb +91 -0
- data/lib/tabry/machine.rb +124 -0
- data/lib/tabry/models/arg.rb +43 -0
- data/lib/tabry/models/arg_base.rb +10 -0
- data/lib/tabry/models/arg_include.rb +9 -0
- data/lib/tabry/models/arg_includes.rb +14 -0
- data/lib/tabry/models/args_list.rb +31 -0
- data/lib/tabry/models/config.rb +44 -0
- data/lib/tabry/models/config_error.rb +8 -0
- data/lib/tabry/models/config_list.rb +48 -0
- data/lib/tabry/models/config_object.rb +78 -0
- data/lib/tabry/models/config_string_hash.rb +44 -0
- data/lib/tabry/models/const_option.rb +17 -0
- data/lib/tabry/models/dir_option.rb +25 -0
- data/lib/tabry/models/file_option.rb +25 -0
- data/lib/tabry/models/flag.rb +55 -0
- data/lib/tabry/models/flags_list.rb +47 -0
- data/lib/tabry/models/include_arg.rb +18 -0
- data/lib/tabry/models/include_flag.rb +18 -0
- data/lib/tabry/models/include_option.rb +22 -0
- data/lib/tabry/models/include_sub.rb +18 -0
- data/lib/tabry/models/option.rb +33 -0
- data/lib/tabry/models/option_base.rb +20 -0
- data/lib/tabry/models/option_includes.rb +14 -0
- data/lib/tabry/models/options_list.rb +18 -0
- data/lib/tabry/models/shell_option.rb +13 -0
- data/lib/tabry/models/sub.rb +59 -0
- data/lib/tabry/models/subs_list.rb +28 -0
- data/lib/tabry/options_finder.rb +87 -0
- data/lib/tabry/result.rb +110 -0
- data/lib/tabry/runner.rb +27 -0
- data/lib/tabry/state.rb +14 -0
- data/lib/tabry/usage_generator.rb +137 -0
- data/lib/tabry/util.rb +15 -0
- data/sh/tabry_bash.sh +61 -0
- data/sh/tabry_bash_help.sh +7 -0
- data/spec/fixtures/basiccli.json +1 -0
- data/spec/fixtures/basiccli.tabry +5 -0
- data/spec/fixtures/basiccli2.tabry +5 -0
- data/spec/fixtures/basiccli2.yml +7 -0
- data/spec/fixtures/vehicles.tabry +60 -0
- data/spec/fixtures/vehicles.yaml +135 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/tabry/cli/arg_proxy_spec.rb +76 -0
- data/spec/tabry/cli/builder_spec.rb +226 -0
- data/spec/tabry/config_loader_spec.rb +69 -0
- data/spec/tabry/machine_spec.rb +109 -0
- data/spec/tabry/models/args_list_spec.rb +36 -0
- data/spec/tabry/models/config_spec.rb +62 -0
- data/spec/tabry/models/const_option_spec.rb +17 -0
- data/spec/tabry/models/dir_option_spec.rb +16 -0
- data/spec/tabry/models/file_option_spec.rb +16 -0
- data/spec/tabry/models/options_list_spec.rb +47 -0
- data/spec/tabry/models/shell_option_spec.rb +19 -0
- data/spec/tabry/models/subs_list_spec.rb +24 -0
- data/spec/tabry/options_finder_spec.rb +91 -0
- data/spec/tabry/result_spec.rb +236 -0
- data/spec/tabry/runner_spec.rb +35 -0
- data/spec/tabry/usage_generator_spec.rb +67 -0
- data/tabry.gemspec +27 -0
- 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,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
|
data/lib/tabry/result.rb
ADDED
@@ -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
|
data/lib/tabry/runner.rb
ADDED
@@ -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
|
data/lib/tabry/state.rb
ADDED
@@ -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
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 @@
|
|
1
|
+
{"cmd":"basiccli","main":{"subs":[{"name":"foo","args":[{"name":"bar"}]}]}}
|