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