run_kit 0.1.5 → 0.1.6
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 +4 -4
- data/README.md +20 -2
- data/demo.rb +14 -9
- data/lib/run_kit/options/config.rb +41 -14
- data/lib/run_kit/options/flag.rb +14 -5
- data/lib/run_kit/options/help.rb +10 -6
- data/lib/run_kit/options/main.rb +22 -19
- data/lib/run_kit/options/parser.rb +35 -47
- data/lib/run_kit/options/positional.rb +6 -3
- data/lib/run_kit/shell.rb +4 -0
- data/run_kit.gemspec +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 88bb49c39cde1b38476bad542e725026bf1709aecd58ed4ffd676a1367a87faf
|
|
4
|
+
data.tar.gz: 016eed022444cb2aa2fbfeaf213eceee7259fabdf920bde34a7500d60f79ff46
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82159e72865bd71a9d6d696e2c7276e3b5051b82217093e68381ebbd051ef8faeaff9f53debefd09c4b71f4c77c20f335e20ab489aef6e3785a756d7bbfe9b1e
|
|
7
|
+
data.tar.gz: 5a38d4bb77f1e2a7440c69ebdf435a55e58a772c401f0756c1fea0cdbc9a8aaf8d429e739a846213927b4d300825c8e0ee13347e7ed325883ea8355e955449ca
|
data/README.md
CHANGED
|
@@ -58,10 +58,15 @@ o.cmd "test", "Run tests" do |c|
|
|
|
58
58
|
c.bool "--verbose"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# myapp build --target debug
|
|
62
|
-
# => #<data command="build", dry_run=
|
|
61
|
+
# myapp build --dry-run --target debug
|
|
62
|
+
# => #<data command="build", dry_run=true, target="debug", _args=[]>
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Put the command first, then global or command flags in any order. If the first
|
|
66
|
+
argument isn't a command, a `default: true` command receives all arguments.
|
|
67
|
+
Root positionals aren't supported with subcommands; global and command flags
|
|
68
|
+
must have distinct switches and keys.
|
|
69
|
+
|
|
65
70
|
## RunKit::Shell
|
|
66
71
|
|
|
67
72
|
`RunKit::Shell` is a mixin with many helpers for bin scripts:
|
|
@@ -74,7 +79,9 @@ end
|
|
|
74
79
|
| `jsonl_read` / `jsonl_write` | Atomic read/write jsonl (add .gz for gzip) |
|
|
75
80
|
| |
|
|
76
81
|
| `csv_write_stdout` | Write CSV to stdout |
|
|
82
|
+
| `csv_write_to_s` | Generate CSV string |
|
|
77
83
|
| `gunzip` / `gzip` | (De)compress a string |
|
|
84
|
+
| |
|
|
78
85
|
| `atomic_write` | Atomically replace a file |
|
|
79
86
|
| `cache_fetch` | Populate/fetch from file cache w/ block |
|
|
80
87
|
| `cp_metadata` | Copy file metadata from src to dst |
|
|
@@ -132,8 +139,19 @@ RunKit also installs a small set of core extensions to assist with bin scripts.
|
|
|
132
139
|
|
|
133
140
|
Note: There has been some effort to get the Pathname helpers into Ruby itself, without much success.
|
|
134
141
|
|
|
142
|
+
### Future Work
|
|
143
|
+
|
|
144
|
+
- argv? ask nak for ideas
|
|
145
|
+
|
|
135
146
|
### Changelog
|
|
136
147
|
|
|
148
|
+
#### 0.1.6 (Sep 2026)
|
|
149
|
+
|
|
150
|
+
- default subcommands with `default: true`
|
|
151
|
+
- `<url...>` positionals for 1+ values
|
|
152
|
+
- remove `naked` and just do the right thing
|
|
153
|
+
- add `csv_write_to_s` for CSV string output
|
|
154
|
+
|
|
137
155
|
#### 0.1.5 (Sep 2026)
|
|
138
156
|
|
|
139
157
|
- after some investigation, decided that naked should default to false
|
data/demo.rb
CHANGED
|
@@ -6,18 +6,23 @@ options = RunKit.parse do |o|
|
|
|
6
6
|
o.version = "1.0"
|
|
7
7
|
o.bool "--dry-run", "Preview without making changes"
|
|
8
8
|
o.desc = "this is made up"
|
|
9
|
+
o.str "--gub"
|
|
9
10
|
|
|
10
|
-
o.cmd
|
|
11
|
-
|
|
12
|
-
c.str "--mode <mode>", "Run quickly, or not", choices: %w[fast slow]
|
|
13
|
-
c.positional "<url>", "URL to fetch"
|
|
11
|
+
o.cmd("sub", default: true) do
|
|
12
|
+
_1.pos "<pos...>"
|
|
14
13
|
end
|
|
15
14
|
|
|
16
|
-
o.cmd "
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
15
|
+
# o.cmd "fetch", "Fetch a URL" do |c|
|
|
16
|
+
# c.int "-n", "--count <n>", "How many times to run", default: 1
|
|
17
|
+
# c.str "--mode <mode>", "Run quickly, or not", choices: %w[fast slow]
|
|
18
|
+
# c.positional "<url>", "URL to fetch"
|
|
19
|
+
# end
|
|
20
|
+
|
|
21
|
+
# o.cmd "build", "Build the project" do |c|
|
|
22
|
+
# c.naked = false
|
|
23
|
+
# c.str "--target <target>", "Build target", choices: %w[debug release], default: "release"
|
|
24
|
+
# c.bool "--force", "Force a rebuild", env: "DEMO_FORCE"
|
|
25
|
+
# end
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
p options
|
|
@@ -6,12 +6,11 @@
|
|
|
6
6
|
module RunKit
|
|
7
7
|
module Options
|
|
8
8
|
class Config
|
|
9
|
-
attr_accessor :banner, :color, :desc, :exit, :help, :
|
|
10
|
-
attr_reader
|
|
11
|
-
alias_method :
|
|
9
|
+
attr_accessor :banner, :color, :default, :desc, :exit, :help, :root, :validate, :version
|
|
10
|
+
attr_reader(*%i[help_flag name version_flag])
|
|
11
|
+
alias_method :default?, :default
|
|
12
12
|
|
|
13
13
|
def initialize(name: nil)
|
|
14
|
-
@naked = false
|
|
15
14
|
self.name = name || Shell.program_name
|
|
16
15
|
end
|
|
17
16
|
|
|
@@ -21,6 +20,10 @@ module RunKit
|
|
|
21
20
|
|
|
22
21
|
# Add a positional param declared as `<url>`.
|
|
23
22
|
def pos(meta, help = "")
|
|
23
|
+
check_inside!
|
|
24
|
+
if positionals.last&.variadic?
|
|
25
|
+
raise ArgumentError, "no positional arguments are allowed after #{positionals.last.meta}"
|
|
26
|
+
end
|
|
24
27
|
Positional.new(meta:, help:).tap do
|
|
25
28
|
raise ArgumentError, "duplicate positional #{_1.key}" if key?(_1.key)
|
|
26
29
|
positionals << _1
|
|
@@ -29,19 +32,22 @@ module RunKit
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
# Add a subcommand with its own nested Config, eg `myapp build`.
|
|
32
|
-
def cmd(name, desc = nil)
|
|
35
|
+
def cmd(name, desc = nil, default: false)
|
|
36
|
+
check_inside!
|
|
33
37
|
name = name.to_s
|
|
34
38
|
raise ArgumentError, "duplicate command #{name}" if commands.key?(name)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
commands
|
|
39
|
+
raise ArgumentError, "default command already set" if default && default_command
|
|
40
|
+
Config.new(name:).tap do |child|
|
|
41
|
+
child.default, child.desc, child.root = default, desc, self
|
|
42
|
+
with_inside(name) { yield child } if block_given?
|
|
43
|
+
raise ArgumentError, "nested commands are not supported" if child.commands.any?
|
|
44
|
+
commands[name] = child
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
# Add separator text at the current point in generated help.
|
|
44
49
|
def sep(text = "")
|
|
50
|
+
check_inside!
|
|
45
51
|
[flags.length, text].tap do
|
|
46
52
|
separators << _1
|
|
47
53
|
end
|
|
@@ -95,11 +101,9 @@ module RunKit
|
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
# one-liners
|
|
98
|
-
def
|
|
99
|
-
def flag?(switch) = lookup.key?(switch)
|
|
104
|
+
def default_command = commands.values.find(&:default?)
|
|
100
105
|
def full_name = root ? "#{root.full_name} #{name}" : name
|
|
101
106
|
def key?(key) = lookup.key?(key)
|
|
102
|
-
def required = flags.select(&:required?)
|
|
103
107
|
|
|
104
108
|
# memoized accessors
|
|
105
109
|
def commands = @commands ||= {}
|
|
@@ -111,6 +115,9 @@ module RunKit
|
|
|
111
115
|
# Complete one-time setup after the caller has declared overrides.
|
|
112
116
|
def prepare!
|
|
113
117
|
return if @prepared
|
|
118
|
+
if commands.any? && positionals.any?
|
|
119
|
+
raise ArgumentError, "a command with subcommands cannot also take positional arguments; add them to a subcommand instead"
|
|
120
|
+
end
|
|
114
121
|
@prepared = true
|
|
115
122
|
|
|
116
123
|
# Children inherit shared settings before adding builtins.
|
|
@@ -123,18 +130,38 @@ module RunKit
|
|
|
123
130
|
@help_flag = bool("-h", "--help", "Show this message")
|
|
124
131
|
@version_flag = bool("-v", "--version", "Show version") if version
|
|
125
132
|
|
|
133
|
+
# Global and command flags share one parser; only builtins may overlap.
|
|
134
|
+
if root
|
|
135
|
+
keys = lookup.keys - [help_flag, version_flag].compact.flat_map { [_1.key, *_1.switches] }
|
|
136
|
+
collisions = keys & root.lookup.keys
|
|
137
|
+
raise ArgumentError, "command #{name} conflicts with global options: #{collisions.join(", ")}" if collisions.any?
|
|
138
|
+
end
|
|
139
|
+
|
|
126
140
|
# setup subcommands
|
|
127
141
|
commands.each_value(&:prepare!)
|
|
128
142
|
end
|
|
129
143
|
|
|
130
144
|
protected
|
|
131
145
|
|
|
146
|
+
# Catch accidental use of the outer config while defining a command.
|
|
147
|
+
def with_inside(name)
|
|
148
|
+
@inside = name
|
|
149
|
+
yield
|
|
150
|
+
ensure
|
|
151
|
+
@inside = nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_inside!
|
|
155
|
+
raise ArgumentError, "you're adding a root option inside #{@inside.inspect} command" if @inside
|
|
156
|
+
end
|
|
157
|
+
|
|
132
158
|
def add_flag(flag)
|
|
159
|
+
check_inside!
|
|
133
160
|
# dup check
|
|
134
161
|
raise ArgumentError, "reserved flag key: _args" if flag.key == :_args
|
|
135
162
|
raise ArgumentError, "dup flag key: #{flag.key}" if key?(flag.key)
|
|
136
163
|
flag.switches.each do
|
|
137
|
-
raise ArgumentError, "dup flag switch: #{_1}" if
|
|
164
|
+
raise ArgumentError, "dup flag switch: #{_1}" if key?(_1)
|
|
138
165
|
end
|
|
139
166
|
|
|
140
167
|
# append
|
data/lib/run_kit/options/flag.rb
CHANGED
|
@@ -9,8 +9,6 @@ module RunKit
|
|
|
9
9
|
SWITCH_RE = /\A-(\w|-\w[\w-]*)\z/
|
|
10
10
|
# --foo=bar
|
|
11
11
|
INLINE_RE = /\A-(\w|-\w[\w-]*)=(.*)\z/m
|
|
12
|
-
# --no-foo
|
|
13
|
-
NEGATE_RE = /\A--no-(\w[\w-]*)\z/
|
|
14
12
|
|
|
15
13
|
TRUE_ENV = %w[1 true yes on]
|
|
16
14
|
FALSE_ENV = ["", "0", "false", "no", "off"]
|
|
@@ -30,6 +28,7 @@ module RunKit
|
|
|
30
28
|
|
|
31
29
|
# Extract meta from the final switch, if written as `--port <int>`.
|
|
32
30
|
@meta = build_meta
|
|
31
|
+
validate_switches!
|
|
33
32
|
@env = (env == true) ? key.to_s.upcase : env
|
|
34
33
|
|
|
35
34
|
# default, with some special handling for bool
|
|
@@ -39,6 +38,9 @@ module RunKit
|
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
validate
|
|
41
|
+
rescue ArgumentError => ex
|
|
42
|
+
ctx = reconstruct(kind, opts, default:, required:, choices:, env:)
|
|
43
|
+
raise ArgumentError, "#{ctx} - #{ex.message}", cause: nil
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
#
|
|
@@ -83,15 +85,16 @@ module RunKit
|
|
|
83
85
|
# validation
|
|
84
86
|
#
|
|
85
87
|
|
|
86
|
-
def
|
|
87
|
-
# switches
|
|
88
|
-
raise ArgumentError, "at least one switch is required" if switches.empty?
|
|
88
|
+
def validate_switches!
|
|
89
89
|
switches.each do
|
|
90
90
|
raise ArgumentError, "invalid switch: #{_1}" unless _1.is_a?(String)
|
|
91
91
|
raise ArgumentError, "invalid switch: #{_1}" unless _1.match?(SWITCH_RE)
|
|
92
92
|
end
|
|
93
|
+
raise ArgumentError, 'flag must start with "-" or "--"' if switches.empty?
|
|
93
94
|
raise ArgumentError, "duplicate switch" unless switches.uniq.length == switches.length
|
|
95
|
+
end
|
|
94
96
|
|
|
97
|
+
def validate
|
|
95
98
|
# params
|
|
96
99
|
raise ArgumentError, "invalid flag kind: #{kind}" unless KINDS.include?(kind)
|
|
97
100
|
raise ArgumentError, "boolean flags do not accept meta" if bool? && meta
|
|
@@ -114,6 +117,12 @@ module RunKit
|
|
|
114
117
|
# helpers
|
|
115
118
|
#
|
|
116
119
|
|
|
120
|
+
# Use the original declaration, before metadata and ENV normalization.
|
|
121
|
+
def reconstruct(kind, opts, **settings)
|
|
122
|
+
args = opts.map(&:inspect) + settings.filter_map { "#{_1}: #{_2.inspect}" if _2 }
|
|
123
|
+
["opt.#{kind}", args.join(", ")].reject(&:empty?).join(" ")
|
|
124
|
+
end
|
|
125
|
+
|
|
117
126
|
def build_meta
|
|
118
127
|
# Only the final spelling may carry an inferred `<meta>`.
|
|
119
128
|
if (m = /\A(\S+) <([^>]+)>\z/.match(switch))
|
data/lib/run_kit/options/help.rb
CHANGED
|
@@ -18,12 +18,13 @@ module RunKit
|
|
|
18
18
|
# Render generated help, unless the caller supplied complete help text.
|
|
19
19
|
def to_s
|
|
20
20
|
return config.help if config.help
|
|
21
|
+
options = flags_text(config, "Options:", builtins: !config.root)
|
|
21
22
|
help = [].tap do
|
|
22
23
|
_1 << desc if config.desc
|
|
23
24
|
_1 << banner
|
|
24
25
|
_1 << commands_text if config.commands.any?
|
|
25
|
-
_1 <<
|
|
26
|
-
_1 << flags_text(config.root, "Other options:") if config.root
|
|
26
|
+
_1 << options
|
|
27
|
+
_1 << flags_text(config.root, options ? "Other options:" : "Options:") if config.root
|
|
27
28
|
end.compact.join("\n\n")
|
|
28
29
|
"#{help}\n"
|
|
29
30
|
end
|
|
@@ -67,19 +68,22 @@ module RunKit
|
|
|
67
68
|
# Build the usage line from the command's full name and positionals.
|
|
68
69
|
def banner
|
|
69
70
|
text = config.banner
|
|
70
|
-
text ||= [color.blue("Usage:"), color.green(config.full_name)
|
|
71
|
-
_1.push(*config.positionals.map(&:meta))
|
|
71
|
+
text ||= [color.blue("Usage:"), color.green(config.full_name)].tap do
|
|
72
72
|
_1.push(color.yellow("<command>")) if config.commands.any?
|
|
73
|
+
_1.push("[options]", *config.positionals.map { |pos| color.yellow(pos.meta) })
|
|
73
74
|
end.join(" ")
|
|
74
75
|
Term.wrap(text, width)
|
|
75
76
|
end
|
|
76
77
|
|
|
77
78
|
# Render the list of subcommands, aligned like the flag list above.
|
|
78
79
|
def commands_text
|
|
80
|
+
commands = config.commands.map do |name, child|
|
|
81
|
+
[child.default? ? "#{name} (default)" : name, child]
|
|
82
|
+
end
|
|
79
83
|
[].tap do |lines|
|
|
80
84
|
lines << color.blue("Commands:")
|
|
81
|
-
label_width =
|
|
82
|
-
|
|
85
|
+
label_width = commands.map { Term.width(_1.first) }.max
|
|
86
|
+
commands.each do |name, child|
|
|
83
87
|
lines << StringIO.new.tap do |buf|
|
|
84
88
|
buf << " " * INDENT << color.green(name)
|
|
85
89
|
if child.desc
|
data/lib/run_kit/options/main.rb
CHANGED
|
@@ -39,7 +39,7 @@ module RunKit
|
|
|
39
39
|
options = root.commands.empty? ? parse_with_ctx(root, argv) : subcommand(argv)
|
|
40
40
|
klass = Data.define(*options.keys)
|
|
41
41
|
klass.new(**options).tap { validate(_1) }
|
|
42
|
-
rescue Error,
|
|
42
|
+
rescue Error, HelpRequested => ex
|
|
43
43
|
handle_error(ex)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -59,21 +59,23 @@ module RunKit
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
#
|
|
63
|
-
# rest with its own Config and merge the two option hashes together.
|
|
62
|
+
# Select from the first argument, then parse global and command flags together.
|
|
64
63
|
def subcommand(argv)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
rest = argv
|
|
65
|
+
if (child = root.commands[rest.first])
|
|
66
|
+
rest = rest.drop(1)
|
|
67
|
+
infer_help = false
|
|
68
|
+
else
|
|
69
|
+
child = root.default_command
|
|
70
|
+
infer_help = true
|
|
71
|
+
end
|
|
72
|
+
raise HelpRequested if !child && rest.empty?
|
|
73
|
+
if !child
|
|
74
|
+
switch = rest.first.start_with?("--") ? rest.first.split("=", 2).first : rest.first[0, 2]
|
|
75
|
+
raise Error, "global options must follow the command" if root.key?(switch)
|
|
76
|
+
raise Error, "unknown command '#{rest.first}'"
|
|
77
|
+
end
|
|
78
|
+
parse_with_ctx(child, rest, infer_help:).merge(command: child.name)
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
# Validate the final options, root first.
|
|
@@ -93,7 +95,8 @@ module RunKit
|
|
|
93
95
|
return exit_fn(1, error: ex.message)
|
|
94
96
|
end
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
# Inferred help on a bare invocation introduces the whole app.
|
|
99
|
+
puts Help.new(root)
|
|
97
100
|
exit_fn(0)
|
|
98
101
|
end
|
|
99
102
|
|
|
@@ -116,9 +119,9 @@ module RunKit
|
|
|
116
119
|
yield
|
|
117
120
|
end
|
|
118
121
|
|
|
119
|
-
def parse_with_ctx(ctx, argv,
|
|
122
|
+
def parse_with_ctx(ctx, argv, infer_help: true)
|
|
120
123
|
with_ctx(ctx) do
|
|
121
|
-
Parser.new(ctx).parse(argv,
|
|
124
|
+
Parser.new(ctx).parse(argv, infer_help:)
|
|
122
125
|
end
|
|
123
126
|
end
|
|
124
127
|
|
|
@@ -130,7 +133,7 @@ module RunKit
|
|
|
130
133
|
end
|
|
131
134
|
|
|
132
135
|
class Error < StandardError; end
|
|
133
|
-
class
|
|
136
|
+
class HelpRequested < Exception; end # rubocop:disable Lint/InheritException
|
|
134
137
|
|
|
135
138
|
# main entry point
|
|
136
139
|
def self.parse(argv = ARGV)
|
|
@@ -6,29 +6,27 @@
|
|
|
6
6
|
module RunKit
|
|
7
7
|
module Options
|
|
8
8
|
class Parser
|
|
9
|
-
attr_reader
|
|
9
|
+
attr_reader(*%i[config flags lookup])
|
|
10
10
|
|
|
11
11
|
def initialize(config)
|
|
12
12
|
@config = config
|
|
13
|
+
@flags = config.root ? (config.root.flags - [config.root.help_flag, config.root.version_flag]) + config.flags : config.flags
|
|
14
|
+
@lookup = (config.root&.lookup || {}).merge(config.lookup)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
raise NakedRequested if config.naked? && argv.empty?
|
|
17
|
+
# Parse global and command flags together, then assemble the result.
|
|
18
|
+
def parse(argv, infer_help: true)
|
|
19
|
+
# 1. Parse argv.
|
|
20
|
+
argv_options = parse_argv(argv)
|
|
20
21
|
|
|
21
|
-
# 2.
|
|
22
|
-
|
|
22
|
+
# 2. defaults => ENV => ARGV
|
|
23
|
+
options = {}.merge(config.root&.defaults || {}, config.defaults, parse_env, argv_options)
|
|
23
24
|
|
|
24
|
-
# 3.
|
|
25
|
-
options
|
|
26
|
-
|
|
27
|
-
# 4. validate final options
|
|
28
|
-
validate!(options)
|
|
25
|
+
# 3. Validate requirements after ENV resolution; bare missing inputs show help.
|
|
26
|
+
validate!(options, infer_help: infer_help && argv.empty?)
|
|
29
27
|
|
|
30
28
|
# success! add predicate? keys
|
|
31
|
-
|
|
29
|
+
flags.select(&:bool?).map(&:key).each do
|
|
32
30
|
options[:"#{_1}?"] = options[_1] if options.key?(_1)
|
|
33
31
|
end
|
|
34
32
|
|
|
@@ -41,10 +39,10 @@ module RunKit
|
|
|
41
39
|
# main parser
|
|
42
40
|
#
|
|
43
41
|
|
|
44
|
-
def parse_argv(argv
|
|
42
|
+
def parse_argv(argv)
|
|
45
43
|
{}.tap do |result|
|
|
46
44
|
# any non-flags we find below
|
|
47
|
-
|
|
45
|
+
args = []
|
|
48
46
|
|
|
49
47
|
# process argv as queue
|
|
50
48
|
queue = argv.dup
|
|
@@ -53,45 +51,37 @@ module RunKit
|
|
|
53
51
|
when Flag::SWITCH_RE, Flag::INLINE_RE then result.merge!(parse_switch(item, Regexp.last_match, queue))
|
|
54
52
|
when /\A-[^-]/ then result.merge!(parse_smashed(item, queue))
|
|
55
53
|
when "", /\A[^-]/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
operands.concat(queue)
|
|
59
|
-
break
|
|
60
|
-
end
|
|
61
|
-
when "--" then break operands.concat(queue)
|
|
54
|
+
args << item
|
|
55
|
+
when "--" then break args.concat(queue)
|
|
62
56
|
else; raise Error, "unexpected argument '#{item}' found"
|
|
63
57
|
end
|
|
64
58
|
end
|
|
65
59
|
|
|
66
60
|
# positionals
|
|
67
|
-
config.positionals.each { result[_1.key] =
|
|
61
|
+
config.positionals.each { result[_1.key] = _1.variadic? ? args.shift(args.length) : args.shift }
|
|
62
|
+
if config.positionals.any? && args.any?
|
|
63
|
+
raise Error, "unexpected argument '#{args.first}' found"
|
|
64
|
+
end
|
|
68
65
|
|
|
69
66
|
# _args
|
|
70
|
-
result[:_args] =
|
|
67
|
+
result[:_args] = args
|
|
71
68
|
end
|
|
72
69
|
end
|
|
73
70
|
|
|
74
71
|
#
|
|
75
|
-
# -x or -x=123 or --xyz or --xyz=123
|
|
72
|
+
# -x or -x=123 or --xyz or --xyz=123
|
|
76
73
|
#
|
|
77
74
|
|
|
78
75
|
def parse_switch(item, match, queue)
|
|
79
76
|
switch = "-#{match[1]}"
|
|
80
77
|
param = match[2]
|
|
81
|
-
separator = param ? "=" : ""
|
|
82
78
|
|
|
83
79
|
# -x or --xyz?
|
|
84
|
-
if (flag =
|
|
85
|
-
param = queue.shift if flag.takes_param? &&
|
|
80
|
+
if (flag = lookup[switch])
|
|
81
|
+
param = queue.shift if flag.takes_param? && !param
|
|
86
82
|
return {flag.key => flag.parse(switch, param)}
|
|
87
83
|
end
|
|
88
84
|
|
|
89
|
-
# --no-xyz?
|
|
90
|
-
if (neg = find_negated_flag(switch))
|
|
91
|
-
raise Error, "option '#{item}' does not take a value" if separator == "="
|
|
92
|
-
return {neg.key => false}
|
|
93
|
-
end
|
|
94
|
-
|
|
95
85
|
raise Error, "unexpected argument '#{item}' found"
|
|
96
86
|
end
|
|
97
87
|
|
|
@@ -105,7 +95,7 @@ module RunKit
|
|
|
105
95
|
result = {}
|
|
106
96
|
(1...group.length).each do |idx|
|
|
107
97
|
switch = "-#{group[idx]}"
|
|
108
|
-
flag =
|
|
98
|
+
flag = lookup[switch]
|
|
109
99
|
raise Error, "unexpected argument '#{group}' found" unless flag
|
|
110
100
|
|
|
111
101
|
# For `-qnLee`, `Lee` belongs to `-n`; for `-qn Lee`, shift the queue.
|
|
@@ -130,7 +120,7 @@ module RunKit
|
|
|
130
120
|
#
|
|
131
121
|
|
|
132
122
|
def parse_env
|
|
133
|
-
|
|
123
|
+
flags.select { _1.env && ENV.key?(_1.env) }.map do |flag|
|
|
134
124
|
[flag.key, flag.parse_env(ENV[flag.env])]
|
|
135
125
|
end.to_h
|
|
136
126
|
end
|
|
@@ -139,19 +129,17 @@ module RunKit
|
|
|
139
129
|
# helpers
|
|
140
130
|
#
|
|
141
131
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def validate!(options)
|
|
150
|
-
config.required.each do
|
|
151
|
-
raise Error, "required option '#{_1.switch}' is missing" if !options.key?(_1.key)
|
|
132
|
+
def validate!(options, infer_help: false)
|
|
133
|
+
# Infer help only for missing inputs, after ENV has been resolved.
|
|
134
|
+
flags.select(&:required?).each do
|
|
135
|
+
next if options.key?(_1.key)
|
|
136
|
+
raise HelpRequested if infer_help
|
|
137
|
+
raise Error, "required option '#{_1.switch}' is missing"
|
|
152
138
|
end
|
|
153
139
|
config.positionals.each do
|
|
154
|
-
|
|
140
|
+
next if _1.variadic? ? options[_1.key].any? : options[_1.key]
|
|
141
|
+
raise HelpRequested if infer_help
|
|
142
|
+
raise Error, "required argument '#{_1.meta}' is missing"
|
|
155
143
|
end
|
|
156
144
|
end
|
|
157
145
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#
|
|
2
|
-
# A required positional param like `<url
|
|
2
|
+
# A required positional param like `<url>` or `<url...>`.
|
|
3
3
|
#
|
|
4
4
|
|
|
5
5
|
module RunKit
|
|
6
6
|
module Options
|
|
7
7
|
class Positional
|
|
8
|
-
POSITIONAL_RE = /\A<([A-Z]\w*)
|
|
8
|
+
POSITIONAL_RE = /\A<([A-Z]\w*)(\.\.\.)?>\z/i
|
|
9
9
|
|
|
10
10
|
attr_reader :help, :meta
|
|
11
11
|
|
|
@@ -13,11 +13,14 @@ module RunKit
|
|
|
13
13
|
@help, @meta = help, meta
|
|
14
14
|
raise ArgumentError, "positional help must be a string" unless help.is_a?(String)
|
|
15
15
|
raise ArgumentError, "positional meta must be a string" unless meta.is_a?(String)
|
|
16
|
-
|
|
16
|
+
unless POSITIONAL_RE.match?(meta)
|
|
17
|
+
raise ArgumentError, "invalid positional #{meta.inspect}; wrap the argument name in angle brackets, e.g. o.pos(\"<url>\", \"URL to fetch\")"
|
|
18
|
+
end
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# one-liners
|
|
20
22
|
def key = @key ||= POSITIONAL_RE.match(meta)[1].to_sym
|
|
23
|
+
def variadic? = meta.end_with?("...>")
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
end
|
data/lib/run_kit/shell.rb
CHANGED
data/run_kit.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: run_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Doppelt
|
|
@@ -88,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
89
|
version: '0'
|
|
90
90
|
requirements: []
|
|
91
|
-
rubygems_version:
|
|
91
|
+
rubygems_version: 3.6.9
|
|
92
92
|
specification_version: 4
|
|
93
93
|
summary: Run kit.
|
|
94
94
|
test_files: []
|