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,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