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,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "state"
4
+ require_relative "util"
5
+
6
+ # The core Tabry state machine / parser which reads tokens, figures out if it's
7
+ # a sub/flag/arg/etc. and advances the state
8
+ #
9
+ # The output is a State object -- the final state of the machine.
10
+ module Tabry
11
+ class Machine
12
+ attr_reader :state, :config
13
+
14
+ # Returns a state
15
+ def self.run(config, tokens)
16
+ machine = new(config)
17
+ machine.run(tokens)
18
+ machine.state
19
+ end
20
+
21
+ def initialize(config)
22
+ @config = config
23
+ @state = State.new(mode: :subcommand, subcommand_stack: [], args: [], flags: {})
24
+ end
25
+
26
+ def run(tokens)
27
+ tokens.each do |token|
28
+ step(token)
29
+ Tabry::Util.debug "AFTER token #{token.inspect}"
30
+ Tabry::Util.debug "STATE: #{state.inspect}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def step(token)
37
+ send :"step_#{state.mode}", token
38
+ end
39
+
40
+ def current_sub
41
+ # TODO: a bit of a waste to look this up every time.
42
+ config.dig_sub(state.subcommand_stack)
43
+ end
44
+
45
+ def step_subcommand(token)
46
+ step_subcommand_match_subcommand(token) ||
47
+ step_subcommand_match_dashdash(token) ||
48
+ step_subcommand_match_flag(token) ||
49
+ step_subcommand_match_help(token) ||
50
+ step_subcommand_match_arg(token)
51
+ end
52
+
53
+ def step_subcommand_match_help(token)
54
+ return false if state.help
55
+ return false unless state.mode == :subcommand
56
+
57
+ if (state.subcommand_stack.empty? && token == "help") ||
58
+ (!state.dashdash && %w[--help -?].include?(token))
59
+ state.help = true
60
+ Tabry::Util.debug "MATCHED help ON token #{token.inspect}"
61
+ return true
62
+ end
63
+ false
64
+ end
65
+
66
+ def step_subcommand_match_subcommand(token)
67
+ return false unless state.args.empty?
68
+
69
+ sub = current_sub.subs.match(token)
70
+ return false unless sub
71
+
72
+ state.subcommand_stack << sub.name
73
+ Tabry::Util.debug "MATCHED sub #{sub.name} ON token #{token.inspect}"
74
+ true
75
+ end
76
+
77
+ def step_subcommand_match_dashdash(token)
78
+ return false if state.dashdash
79
+ return false unless token == "--"
80
+
81
+ state.dashdash = true
82
+ true
83
+ end
84
+
85
+ def step_subcommand_match_flag(token)
86
+ return false if state.dashdash
87
+
88
+ flag, arg_value = nil
89
+ # Reverse: most specific sub's flag takes precedence in case of multiple matching
90
+ config.dig_sub_array(state.subcommand_stack).reverse.each do |sub|
91
+ flag, arg_value = sub.flags.match(token)
92
+ break if flag
93
+ end
94
+ return false unless flag
95
+
96
+ if arg_value
97
+ state.flags[flag.name] = arg_value
98
+ elsif flag.arg
99
+ state.mode = :flagarg
100
+ state.current_flag = flag.name
101
+ else
102
+ state.flags[flag.name] = true
103
+ end
104
+
105
+ Tabry::Util.debug "MATCHED flag #{flag.name} ON token #{token.inspect}"
106
+ true
107
+ end
108
+
109
+ # this accepts anything, for now...
110
+ def step_subcommand_match_arg(token)
111
+ state.args << token
112
+
113
+ Tabry::Util.debug "MATCHED arg ON token #{token.inspect}"
114
+ true
115
+ end
116
+
117
+ # Accepts anything, for now
118
+ def step_flagarg(token)
119
+ state.flags[state.current_flag] = token
120
+ state.mode = :subcommand
121
+ true
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "arg_base"
4
+ require_relative "options_list"
5
+ require_relative "include_arg"
6
+
7
+ module Tabry
8
+ module Models
9
+ class Arg < ArgBase
10
+ def self.new(**args)
11
+ if args[:raw]["include"]
12
+ IncludeArg.new(**args)
13
+ else
14
+ super(**args)
15
+ end
16
+ end
17
+
18
+ FIELDS = {
19
+ name: :string,
20
+ options: [:list_object, :OptionsList],
21
+ optional: :boolean,
22
+ description: :string,
23
+ varargs: :boolean,
24
+ title: :string,
25
+ }.freeze
26
+
27
+ attr_reader(*FIELDS.keys)
28
+
29
+ def flatten
30
+ self
31
+ end
32
+
33
+ # Alias (more clear intention)
34
+ def varargs?
35
+ varargs
36
+ end
37
+
38
+ def title
39
+ @title || name
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ArgBase < ConfigObject
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Models
5
+ # TODO: SubBase for both this and Sub, Sub has stuff we don't want
6
+ class ArgInclude < Sub
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_string_hash"
4
+ require_relative "arg_include"
5
+
6
+ module Tabry
7
+ module Models
8
+ class ArgIncludes < ConfigStringHash
9
+ def initialize(**args)
10
+ super(**args, klass: ArgInclude)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "arg"
4
+ require_relative "config_error"
5
+
6
+ module Tabry
7
+ module Models
8
+ class ArgsList < ConfigList
9
+ def initialize(**args)
10
+ super(**args, klass: Arg)
11
+ end
12
+
13
+ def n_passed_in_varargs(n_total_passed_in_args)
14
+ case to_a.count(&:varargs?)
15
+ when 0
16
+ 0
17
+ when 1
18
+ n_regular_passed_in_args = to_a.length - 1
19
+ [n_total_passed_in_args - n_regular_passed_in_args, 0].max
20
+ else
21
+ arg_names = to_a.map { |a| "#{a.name || "[unnamed]"}#{a.varargs? ? " (varargs)" : ""}" }
22
+ raise ConfigError, "More than one varargs args in one sub: #{arg_names.join(", ")}"
23
+ end
24
+ end
25
+
26
+ def varargs_arg
27
+ to_a.find(&:varargs?)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+ require_relative "sub"
5
+ require_relative "option_includes"
6
+ require_relative "arg_includes"
7
+
8
+ module Tabry
9
+ module Models
10
+ class Config < ConfigObject
11
+ FIELDS = {
12
+ cmd: :string,
13
+ main: [:object, :Sub],
14
+ option_includes: [:object, :OptionIncludes],
15
+ arg_includes: [:object, :ArgIncludes],
16
+ }.freeze
17
+
18
+ attr_reader(*FIELDS.keys)
19
+
20
+ def initialize(raw:)
21
+ super(raw: raw, root: self)
22
+ end
23
+
24
+ def dig_sub(sub_stack)
25
+ sub_stack.reduce(main) { |sub, sub_name| sub.subs.by_name[sub_name] }
26
+ end
27
+
28
+ def dig_sub_array(sub_stack)
29
+ subs = [main]
30
+ sub_stack.each do |sub_name|
31
+ subs << subs.last.subs.by_name[sub_name]
32
+ end
33
+ subs
34
+ end
35
+
36
+ def inspect
37
+ # TODO: remove hack, but make everything in models have a hack so _root is not shown
38
+ return "#<Tabry::Config>" if caller.any? { |c| c.include?("in `inspect'") }
39
+
40
+ super
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Models
5
+ class ConfigError < StandardError
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_error"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ConfigList
8
+ attr_reader :unflattened
9
+
10
+ include Enumerable
11
+
12
+ def initialize(raw:, root:, klass:)
13
+ raise "missing root" unless root
14
+
15
+ raw ||= []
16
+ unless raw.is_a?(Array)
17
+ raise ConfigError, "#{self.class.name} must be an array. Got #{raw.class}"
18
+ end
19
+
20
+ @unflattened = raw.map { |a| klass.new(raw: a, root: root) }
21
+ end
22
+
23
+ def to_a
24
+ flatten
25
+ end
26
+
27
+ def flatten
28
+ @flatten ||= unflattened.map(&:flatten).flatten
29
+ end
30
+
31
+ def [](*args)
32
+ to_a.[](*args)
33
+ end
34
+
35
+ def each(...)
36
+ to_a.each(...)
37
+ end
38
+
39
+ def length
40
+ to_a.length
41
+ end
42
+
43
+ def empty?
44
+ to_a.empty?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_error"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ConfigObject
8
+ def to_s
9
+ inspect
10
+ end
11
+
12
+ def inspect
13
+ field_strings = self.class::FIELDS.keys.map do |f|
14
+ val = instance_variable_get(:"@#{f}")
15
+ if val.nil?
16
+ nil
17
+ else
18
+ "#{f}=#{val.inspect}"
19
+ end
20
+ end
21
+ desc = [self.class.name, *field_strings.compact].join(" ")
22
+ "#<#{desc}>"
23
+ end
24
+
25
+ attr_reader :_root, :_raw
26
+
27
+ def initialize(raw:, root:)
28
+ @_raw = raw
29
+ @_root = root or raise "missing root"
30
+ unknown_fields = @_raw.keys - self.class::FIELDS.keys.map(&:to_s)
31
+ unless unknown_fields.empty?
32
+ raise ConfigError, "Unknown field(s) #{unknown_fields.inspect} for #{self.class}"
33
+ end
34
+
35
+ raw.each do |key, val|
36
+ type, *extra = Array(self.class::FIELDS[key.to_sym])
37
+ instance_variable_set :"@#{key}", send(:"init_field_#{type}", key, val, *extra)
38
+ end
39
+ end
40
+
41
+ # Gets fields, asserts is either nil or of given type
42
+ def assert_of_class(key, val, klasses)
43
+ unless Array(klasses).any? { |klass| val.is_a?(klass) }
44
+ raise ConfigError,
45
+ "Invalid type #{val.class} for #{self.class} field #{key.inspect}, expected #{klasses.inspect}"
46
+ end
47
+ end
48
+
49
+ def init_field_string(key, val)
50
+ assert_of_class(key, val, String)
51
+ val
52
+ end
53
+
54
+ def init_field_string_array(key, vals)
55
+ assert_of_class(key, vals, Array)
56
+ vals.each_with_index do |val, i|
57
+ assert_of_class("#{key}[#{i}]", val, String)
58
+ end
59
+ vals
60
+ end
61
+
62
+ def init_field_object(key, val, object_class)
63
+ assert_of_class(key, val, Hash)
64
+ Object.const_get("Tabry::Models::#{object_class}").new(raw: val, root: _root)
65
+ end
66
+
67
+ def init_field_list_object(key, val, object_class)
68
+ assert_of_class(key, val, Array)
69
+ Object.const_get("Tabry::Models::#{object_class}").new(raw: val, root: _root)
70
+ end
71
+
72
+ def init_field_boolean(key, val)
73
+ assert_of_class(key, val, [TrueClass, FalseClass])
74
+ val
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_error"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ConfigStringHash
8
+ attr_reader :to_h, :_raw, :_root
9
+
10
+ include Enumerable
11
+
12
+ def initialize(raw:, root:, klass:)
13
+ @_raw = raw
14
+ @_root = root
15
+
16
+ raw ||= {}
17
+ unless raw.is_a?(Hash)
18
+ raise ConfigError, "#{self.class.name} must be a Hash. Got #{raw.class}"
19
+ end
20
+ unless raw.keys.all? { |k| k.is_a?(String) }
21
+ raise ConfigError, "#{self.class.name} keys must all be string keys."
22
+ end
23
+
24
+ @to_h = raw.transform_values { |a| klass.new(raw: a, root: root) }
25
+ end
26
+
27
+ def [](*args)
28
+ to_h.[](*args)
29
+ end
30
+
31
+ def each(...)
32
+ to_h.each(...)
33
+ end
34
+
35
+ def empty?
36
+ to_h.empty?
37
+ end
38
+
39
+ def keys
40
+ to_h.keys
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "option_base"
4
+
5
+ module Tabry
6
+ module Models
7
+ class ConstOption < OptionBase
8
+ def options(token)
9
+ if value.start_with?(token)
10
+ [value]
11
+ else
12
+ []
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+
5
+ module Tabry
6
+ module Models
7
+ class DirOption < ConfigObject
8
+ FIELDS = {
9
+ type: :string,
10
+ }.freeze
11
+
12
+ attr_reader(*FIELDS.keys)
13
+
14
+ # Handled by tabru=bash/tabry-bash.sh/shell, we just return a symbol to
15
+ # communicate to tabry-bash
16
+ def options(_token)
17
+ [:directory]
18
+ end
19
+
20
+ def flatten
21
+ self
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+
5
+ module Tabry
6
+ module Models
7
+ class FileOption < ConfigObject
8
+ FIELDS = {
9
+ type: :string,
10
+ }.freeze
11
+
12
+ attr_reader(*FIELDS.keys)
13
+
14
+ # Handled by tabru=bash/tabry-bash.sh/shell, we just return a symbol to
15
+ # communicate to tabry-bash
16
+ def options(_token)
17
+ [:file]
18
+ end
19
+
20
+ def flatten
21
+ self
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_object"
4
+ require_relative "options_list"
5
+ require_relative "include_flag"
6
+
7
+ module Tabry
8
+ module Models
9
+ class Flag < ConfigObject
10
+ def self.new(**args)
11
+ if args[:raw]["include"]
12
+ IncludeFlag.new(**args)
13
+ else
14
+ super(**args)
15
+ end
16
+ end
17
+
18
+ FIELDS = {
19
+ aliases: :string_array,
20
+ description: :string,
21
+ name: :string, # TODO: required
22
+ required: :boolean,
23
+ arg: :boolean,
24
+ options: [:list_object, :OptionsList]
25
+ }.freeze
26
+
27
+ def flatten
28
+ self
29
+ end
30
+
31
+ def match_with_value(token)
32
+ [name, *aliases].each do |al|
33
+ if al.length > 1 && token.start_with?("--#{al}=")
34
+ return token.sub("--#{al}=", "")
35
+ end
36
+ end
37
+ nil
38
+ end
39
+
40
+ def match(token)
41
+ [name, *aliases].any? { |al| token == alias_with_dash(al) }
42
+ end
43
+
44
+ def name_with_dashes
45
+ @name_with_dashes ||= alias_with_dash(name)
46
+ end
47
+
48
+ def alias_with_dash(ali)
49
+ (ali.length == 1) ? "-#{ali}" : "--#{ali}"
50
+ end
51
+
52
+ attr_reader(*FIELDS.keys)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_list"
4
+ require_relative "flag"
5
+
6
+ module Tabry
7
+ module Models
8
+ class FlagsList < ConfigList
9
+ def initialize(**args)
10
+ super(**args, klass: Flag)
11
+ end
12
+
13
+ def first_required_flag(used:)
14
+ to_a.find do |flag|
15
+ flag.required && !used[flag.name]
16
+ end
17
+ end
18
+
19
+ def options(token, used:)
20
+ to_a.map do |flag|
21
+ if token&.start_with?("-") && flag.name_with_dashes.start_with?(token) && !used[flag.name]
22
+ flag.name_with_dashes
23
+ end
24
+ end.compact
25
+ end
26
+
27
+ def match(token)
28
+ to_a.each do |flag|
29
+ if (arg_value = flag.match_with_value(token))
30
+ return flag, arg_value
31
+ elsif flag.match(token)
32
+ return flag
33
+ end
34
+ end
35
+ false
36
+ end
37
+
38
+ def [](flag_name)
39
+ if flag_name.is_a?(Integer)
40
+ to_a[flag_name]
41
+ else
42
+ to_a.find { |f| f.name == flag_name }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Models
5
+ class IncludeArg
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].args.flatten
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tabry
4
+ module Models
5
+ class IncludeFlag
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].flags.flatten
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "option_base"
4
+
5
+ module Tabry
6
+ module Models
7
+ class IncludeOption < OptionBase
8
+ attr_reader :include_name, :_root
9
+
10
+ def options(token)
11
+ flatten.options(token)
12
+ end
13
+
14
+ def flatten
15
+ inc = _root.option_includes[value]
16
+ raise "Bad tabry config: include #{value.inspect} not found" unless inc
17
+
18
+ inc.flatten
19
+ end
20
+ end
21
+ end
22
+ end