run_kit 0.1.3 → 0.1.5
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 +37 -5
- data/demo.rb +15 -3
- data/lib/run_kit/options/config.rb +43 -16
- data/lib/run_kit/options/help.rb +63 -40
- data/lib/run_kit/options/main.rb +86 -28
- data/lib/run_kit/options/parser.rb +12 -14
- data/lib/run_kit/shell.rb +4 -4
- data/logo.png +0 -0
- data/run_kit.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d199e66b4f3d3fc9d3316baed91dc47ee182dde5d7cc229b4522069cc11de05a
|
|
4
|
+
data.tar.gz: f80d68b84e9d02d450d95a78282e1b7785defe8ebf50a951d2e95e606349bd56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66a535125d24e6a1b5c609c096e525d14a5c43c35a35a5011439a4edc8ed68b45860ed955ad242bbf4cb2642e060dc3d41d042cc7d9cb5528cf445c9ecb2ce71
|
|
7
|
+
data.tar.gz: a4d76419621604a84efbaf0d99cf84591b181a9a04398773ecc29905b8856b87e74032b7d6c03dc68ba47d657cdd5aa5e6c11e3dfaa4c3f8e6910874e0ac48d2
|
data/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
RunKit is a small toolkit for cli. It provides option parsing, shell and file helpers, term colors, and a handful of core extensions.
|
|
4
4
|
|
|
5
|
+
<img src="./logo.png" width="40%">
|
|
6
|
+
|
|
5
7
|
### Installation
|
|
6
8
|
|
|
7
9
|
```ruby
|
|
8
10
|
# install gem
|
|
9
|
-
$ gem install
|
|
11
|
+
$ gem install run_kit
|
|
10
12
|
|
|
11
13
|
# or add to your Gemfile
|
|
12
|
-
gem "
|
|
14
|
+
gem "run_kit"
|
|
13
15
|
```
|
|
14
16
|
|
|
15
17
|
## RunKit::Options
|
|
@@ -37,7 +39,28 @@ o.bool "--force", env: true # ENV["FORCE"]
|
|
|
37
39
|
o.str "--token", env: "API_TOKEN" # ENV["API_TOKEN"]
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
Custom validation:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
o.validate = lambda do |options|
|
|
46
|
+
raise "--count must be positive" if options.count <= 0
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Also supports subcommands, `git`-style:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
o.bool "-n", "--dry-run"
|
|
54
|
+
o.cmd "build", "Build the project" do |c|
|
|
55
|
+
c.str "--target <target>", default: "release"
|
|
56
|
+
end
|
|
57
|
+
o.cmd "test", "Run tests" do |c|
|
|
58
|
+
c.bool "--verbose"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# myapp build --target debug
|
|
62
|
+
# => #<data command="build", dry_run=false, target="debug", _args=[]>
|
|
63
|
+
```
|
|
41
64
|
|
|
42
65
|
## RunKit::Shell
|
|
43
66
|
|
|
@@ -48,7 +71,7 @@ Configured variables appear in `--help`. Command-line values override ENV, which
|
|
|
48
71
|
| `csv_read` / `csv_write` | Read/write CSV (add .gz for gzip) |
|
|
49
72
|
| `file_read` / `file_write` | Atomic read/write files (add .gz for gzip) |
|
|
50
73
|
| `json_read` / `json_write` | Atomic read/write json (add .gz for gzip) |
|
|
51
|
-
| `jsonl_read / `jsonl_write`
|
|
74
|
+
| `jsonl_read` / `jsonl_write` | Atomic read/write jsonl (add .gz for gzip) |
|
|
52
75
|
| |
|
|
53
76
|
| `csv_write_stdout` | Write CSV to stdout |
|
|
54
77
|
| `gunzip` / `gzip` | (De)compress a string |
|
|
@@ -66,7 +89,7 @@ Configured variables appear in `--help`. Command-line values override ENV, which
|
|
|
66
89
|
| |
|
|
67
90
|
| `banner` / `warning` / `fatal` | Pretty banner in green, orange or red (fatal exits) |
|
|
68
91
|
| `program_name` | Return executable name |
|
|
69
|
-
| `prompt?` | Ask
|
|
92
|
+
| `prompt?` | Ask user for confirmation |
|
|
70
93
|
| `md5` / `sha256` | Hash strings |
|
|
71
94
|
|
|
72
95
|
### RunKit CoreExt
|
|
@@ -111,6 +134,15 @@ Note: There has been some effort to get the Pathname helpers into Ruby itself, w
|
|
|
111
134
|
|
|
112
135
|
### Changelog
|
|
113
136
|
|
|
137
|
+
#### 0.1.5 (Sep 2026)
|
|
138
|
+
|
|
139
|
+
- after some investigation, decided that naked should default to false
|
|
140
|
+
|
|
141
|
+
#### 0.1.4 (Sep 2026)
|
|
142
|
+
|
|
143
|
+
- add subcommand support (`o.cmd`)
|
|
144
|
+
- add custom validation (`o.validate`)
|
|
145
|
+
|
|
114
146
|
#### 0.1.3 (Sep 2026)
|
|
115
147
|
|
|
116
148
|
- move PROGBAR constant into RunKit::
|
data/demo.rb
CHANGED
|
@@ -3,9 +3,21 @@
|
|
|
3
3
|
require_relative "lib/run_kit"
|
|
4
4
|
|
|
5
5
|
options = RunKit.parse do |o|
|
|
6
|
-
o.
|
|
7
|
-
o.
|
|
8
|
-
o.
|
|
6
|
+
o.version = "1.0"
|
|
7
|
+
o.bool "--dry-run", "Preview without making changes"
|
|
8
|
+
o.desc = "this is made up"
|
|
9
|
+
|
|
10
|
+
o.cmd "fetch", "Fetch a URL" do |c|
|
|
11
|
+
c.int "-n", "--count <n>", "How many times to run", default: 1
|
|
12
|
+
c.str "--mode <mode>", "Run quickly, or not", choices: %w[fast slow]
|
|
13
|
+
c.positional "<url>", "URL to fetch"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
o.cmd "build", "Build the project" do |c|
|
|
17
|
+
c.naked = false
|
|
18
|
+
c.str "--target <target>", "Build target", choices: %w[debug release], default: "release"
|
|
19
|
+
c.bool "--force", "Force a rebuild", env: "DEMO_FORCE"
|
|
20
|
+
end
|
|
9
21
|
end
|
|
10
22
|
|
|
11
23
|
p options
|
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
module RunKit
|
|
7
7
|
module Options
|
|
8
8
|
class Config
|
|
9
|
-
attr_accessor :
|
|
10
|
-
attr_reader :
|
|
9
|
+
attr_accessor :banner, :color, :desc, :exit, :help, :naked, :root, :validate, :version
|
|
10
|
+
attr_reader :help_flag, :name, :version_flag
|
|
11
11
|
alias_method :naked?, :naked
|
|
12
12
|
|
|
13
|
-
def initialize
|
|
14
|
-
@
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
def initialize(name: nil)
|
|
14
|
+
@naked = false
|
|
15
|
+
self.name = name || Shell.program_name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def name=(name)
|
|
19
|
+
@name = name.to_s
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Add a positional param declared as `<url>`.
|
|
@@ -26,6 +28,18 @@ module RunKit
|
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Add a subcommand with its own nested Config, eg `myapp build`.
|
|
32
|
+
def cmd(name, desc = nil)
|
|
33
|
+
name = name.to_s
|
|
34
|
+
raise ArgumentError, "duplicate command #{name}" if commands.key?(name)
|
|
35
|
+
Config.new(name:).tap do
|
|
36
|
+
_1.desc, _1.root = desc, self
|
|
37
|
+
yield _1 if block_given?
|
|
38
|
+
raise ArgumentError, "nested commands are not supported" if _1.commands.any?
|
|
39
|
+
commands[name] = _1
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
29
43
|
# Add separator text at the current point in generated help.
|
|
30
44
|
def sep(text = "")
|
|
31
45
|
[flags.length, text].tap do
|
|
@@ -62,7 +76,9 @@ module RunKit
|
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
# long-form aliases
|
|
79
|
+
alias_method :app_name=, :name=
|
|
65
80
|
alias_method :boolean, :bool
|
|
81
|
+
alias_method :command, :cmd
|
|
66
82
|
alias_method :integer, :int
|
|
67
83
|
alias_method :pathname, :path
|
|
68
84
|
alias_method :positional, :pos
|
|
@@ -81,27 +97,38 @@ module RunKit
|
|
|
81
97
|
# one-liners
|
|
82
98
|
def flag(switch) = lookup[switch]
|
|
83
99
|
def flag?(switch) = lookup.key?(switch)
|
|
100
|
+
def full_name = root ? "#{root.full_name} #{name}" : name
|
|
84
101
|
def key?(key) = lookup.key?(key)
|
|
85
102
|
def required = flags.select(&:required?)
|
|
86
103
|
|
|
104
|
+
# memoized accessors
|
|
105
|
+
def commands = @commands ||= {}
|
|
106
|
+
def flags = @flags ||= []
|
|
107
|
+
def lookup = @lookup ||= {}
|
|
108
|
+
def positionals = @positionals ||= []
|
|
109
|
+
def separators = @separators ||= []
|
|
110
|
+
|
|
87
111
|
# Complete one-time setup after the caller has declared overrides.
|
|
88
112
|
def prepare!
|
|
89
113
|
return if @prepared
|
|
90
114
|
@prepared = true
|
|
115
|
+
|
|
116
|
+
# Children inherit shared settings before adding builtins.
|
|
117
|
+
if root
|
|
118
|
+
self.color, self.exit, self.version = root.color, root.exit, root.version
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# now defaults
|
|
91
122
|
@exit ||= lambda { |status| Kernel.exit(status) }
|
|
92
|
-
@help_flag =
|
|
93
|
-
@version_flag =
|
|
123
|
+
@help_flag = bool("-h", "--help", "Show this message")
|
|
124
|
+
@version_flag = bool("-v", "--version", "Show version") if version
|
|
125
|
+
|
|
126
|
+
# setup subcommands
|
|
127
|
+
commands.each_value(&:prepare!)
|
|
94
128
|
end
|
|
95
129
|
|
|
96
130
|
protected
|
|
97
131
|
|
|
98
|
-
# Add help/version flags, but only for switches the user did not override.
|
|
99
|
-
def add_builtin(switches, help_text)
|
|
100
|
-
unused = switches.select { !flag?(_1) }
|
|
101
|
-
return if unused.empty?
|
|
102
|
-
add_flag(Flag.new(:bool, unused + [help_text]))
|
|
103
|
-
end
|
|
104
|
-
|
|
105
132
|
def add_flag(flag)
|
|
106
133
|
# dup check
|
|
107
134
|
raise ArgumentError, "reserved flag key: _args" if flag.key == :_args
|
data/lib/run_kit/options/help.rb
CHANGED
|
@@ -18,65 +18,88 @@ 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
|
+
help = [].tap do
|
|
22
|
+
_1 << desc if config.desc
|
|
23
|
+
_1 << banner
|
|
24
|
+
_1 << commands_text if config.commands.any?
|
|
25
|
+
_1 << flags_text(config, "Options:", builtins: !config.root)
|
|
26
|
+
_1 << flags_text(config.root, "Other options:") if config.root
|
|
27
|
+
end.compact.join("\n\n")
|
|
28
|
+
"#{help}\n"
|
|
29
|
+
end
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
# Render a heading and aligned flags, preserving explicit separator lines.
|
|
32
|
+
def flags_text(source, title, builtins: true)
|
|
33
|
+
# gather flags
|
|
34
|
+
flags = source.flags
|
|
35
|
+
flags -= [source.help_flag, source.version_flag] unless builtins
|
|
36
|
+
return if flags.empty? && source.separators.empty?
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
[].tap do |lines|
|
|
39
|
+
lines << color.blue(title)
|
|
40
|
+
label_width = flags.map { Term.width(flag_label(_1)) }.max
|
|
41
|
+
flags.each.with_index do |flag, idx|
|
|
42
|
+
lines.concat(separator_lines(source, idx))
|
|
43
|
+
buf = StringIO.new
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
# left
|
|
46
|
+
label = flag_label(flag)
|
|
47
|
+
buf << " " * INDENT
|
|
48
|
+
buf << label
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
# right
|
|
51
|
+
help = flag.help
|
|
52
|
+
if flag.env
|
|
53
|
+
env_help = "[env: #{flag.env}]"
|
|
54
|
+
help = help ? "#{help} #{env_help}" : env_help
|
|
55
|
+
end
|
|
56
|
+
if help
|
|
57
|
+
buf << " " * (label_width - Term.width(label) + 2)
|
|
58
|
+
indent = INDENT + label_width + 2
|
|
59
|
+
buf << Term.wrap(help, width - indent).gsub("\n", "\n#{" " * indent}")
|
|
60
|
+
end
|
|
61
|
+
lines << buf.string
|
|
48
62
|
end
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
buf << separator_text(config.flags.length)
|
|
52
|
-
|
|
53
|
-
buf.string
|
|
63
|
+
lines.concat(separator_lines(source, flags.length))
|
|
64
|
+
end.join("\n")
|
|
54
65
|
end
|
|
55
66
|
|
|
56
|
-
# Build the usage line from the
|
|
67
|
+
# Build the usage line from the command's full name and positionals.
|
|
57
68
|
def banner
|
|
58
69
|
text = config.banner
|
|
59
|
-
text ||= [color.blue("Usage:"), color.green(config.
|
|
70
|
+
text ||= [color.blue("Usage:"), color.green(config.full_name), "[options]"].tap do
|
|
60
71
|
_1.push(*config.positionals.map(&:meta))
|
|
72
|
+
_1.push(color.yellow("<command>")) if config.commands.any?
|
|
61
73
|
end.join(" ")
|
|
62
74
|
Term.wrap(text, width)
|
|
63
75
|
end
|
|
64
76
|
|
|
65
|
-
# Render
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
# Render the list of subcommands, aligned like the flag list above.
|
|
78
|
+
def commands_text
|
|
79
|
+
[].tap do |lines|
|
|
80
|
+
lines << color.blue("Commands:")
|
|
81
|
+
label_width = config.commands.keys.map { Term.width(_1) }.max
|
|
82
|
+
config.commands.each do |name, child|
|
|
83
|
+
lines << StringIO.new.tap do |buf|
|
|
84
|
+
buf << " " * INDENT << color.green(name)
|
|
85
|
+
if child.desc
|
|
86
|
+
buf << " " * (label_width - Term.width(name) + 2)
|
|
87
|
+
indent = INDENT + label_width + 2
|
|
88
|
+
buf << Term.wrap(child.desc, width - indent).gsub("\n", "\n#{" " * indent}")
|
|
89
|
+
end
|
|
90
|
+
end.string
|
|
73
91
|
end
|
|
74
|
-
end.
|
|
92
|
+
end.join("\n")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Keep blank and multiline separators exactly as supplied.
|
|
96
|
+
def separator_lines(source, position)
|
|
97
|
+
source.separators.filter_map { |pos, str| color.blue(str) if pos == position }
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
# one-liners
|
|
78
101
|
def color = @color ||= Color.new(config.color)
|
|
79
|
-
def
|
|
102
|
+
def desc = Term.wrap(config.desc, width)
|
|
80
103
|
|
|
81
104
|
protected
|
|
82
105
|
|
data/lib/run_kit/options/main.rb
CHANGED
|
@@ -16,67 +16,125 @@
|
|
|
16
16
|
# | meta | placeholder shown for a param | <xxx> from `--port <xxx>` |
|
|
17
17
|
# | positional | configured required param slot | o.positional "<url>" |
|
|
18
18
|
# | separator | help section heading | "Network:" |
|
|
19
|
+
# | command | subcommand with its own Config | o.cmd "build" { ... } |
|
|
19
20
|
#
|
|
20
21
|
|
|
21
22
|
module RunKit
|
|
22
23
|
module Options
|
|
23
24
|
class Main
|
|
24
|
-
|
|
25
|
+
# root is the top-level config; ctx is the active parser or validator's config.
|
|
26
|
+
attr_reader :ctx, :root
|
|
25
27
|
|
|
26
|
-
def initialize = @
|
|
27
|
-
def app_name = config.app_name
|
|
28
|
+
def initialize = @root = Config.new
|
|
28
29
|
|
|
29
30
|
# Parse argv and turn internal parser outcomes into CLI behavior.
|
|
30
31
|
def parse(argv)
|
|
31
|
-
|
|
32
|
+
@ctx = root
|
|
33
|
+
root.prepare!
|
|
34
|
+
|
|
35
|
+
# handle --help and --version
|
|
36
|
+
return exit_fn(0) if early_exit?(argv)
|
|
32
37
|
|
|
33
38
|
begin
|
|
34
|
-
options =
|
|
39
|
+
options = root.commands.empty? ? parse_with_ctx(root, argv) : subcommand(argv)
|
|
35
40
|
klass = Data.define(*options.keys)
|
|
36
|
-
klass.new(**options)
|
|
37
|
-
rescue Error => ex
|
|
38
|
-
|
|
39
|
-
warn "#{app_name}: try '#{app_name} --help' for more information"
|
|
40
|
-
exit_fn(1, error: ex.message)
|
|
41
|
-
rescue HelpRequested, NakedRequested, VersionRequested => ex
|
|
42
|
-
early_exit(ex)
|
|
43
|
-
exit_fn(0)
|
|
41
|
+
klass.new(**options).tap { validate(_1) }
|
|
42
|
+
rescue Error, NakedRequested => ex
|
|
43
|
+
handle_error(ex)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
protected
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
# Handle --help or --version
|
|
50
|
+
def early_exit?(argv)
|
|
51
|
+
if argv.include?("--help") || argv.include?("-h")
|
|
52
|
+
selected = root.commands[argv.first] || root
|
|
53
|
+
puts Help.new(selected)
|
|
54
|
+
return true
|
|
55
|
+
end
|
|
56
|
+
if root.version && (argv.include?("--version") || argv.include?("-v"))
|
|
57
|
+
puts "#{root.name} #{root.version}"
|
|
58
|
+
return true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Peek at the first bare argument to pick a subcommand, then parse the
|
|
63
|
+
# rest with its own Config and merge the two option hashes together.
|
|
64
|
+
def subcommand(argv)
|
|
65
|
+
# Parse root options before the subcommand.
|
|
66
|
+
root_options = parse_with_ctx(root, argv, passthru: true)
|
|
67
|
+
name, *rest = root_options[:_args]
|
|
68
|
+
raise NakedRequested if !name
|
|
69
|
+
|
|
70
|
+
# Find and parse the child.
|
|
71
|
+
child = root.commands[name]
|
|
72
|
+
raise Error, "unknown command '#{name}'" if !child
|
|
73
|
+
child_options = parse_with_ctx(child, rest)
|
|
74
|
+
|
|
75
|
+
# merge
|
|
76
|
+
root_options.merge(child_options).merge(command: name)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Validate the final options, root first.
|
|
80
|
+
def validate(options)
|
|
81
|
+
[ctx.root, ctx].compact.each do
|
|
82
|
+
validate_with_ctx(_1, options)
|
|
83
|
+
rescue RuntimeError => ex
|
|
84
|
+
raise Error, ex.message
|
|
57
85
|
end
|
|
58
86
|
end
|
|
59
87
|
|
|
88
|
+
# Render the outcome using the active context.
|
|
89
|
+
def handle_error(ex)
|
|
90
|
+
if ex.is_a?(Error)
|
|
91
|
+
warn "#{ctx.full_name}: #{ex.message}"
|
|
92
|
+
warn "#{ctx.full_name}: try '#{ctx.full_name} --help' for more information"
|
|
93
|
+
return exit_fn(1, error: ex.message)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts Help.new(ctx)
|
|
97
|
+
exit_fn(0)
|
|
98
|
+
end
|
|
99
|
+
|
|
60
100
|
def exit_fn(status, error: nil)
|
|
61
101
|
args = [].tap do
|
|
62
102
|
_1 << status
|
|
63
|
-
_1 << error if
|
|
103
|
+
_1 << error if root.exit.arity == 2
|
|
64
104
|
end
|
|
65
|
-
|
|
105
|
+
root.exit.call(*args)
|
|
66
106
|
nil
|
|
67
107
|
end
|
|
108
|
+
|
|
109
|
+
#
|
|
110
|
+
# Keep the active config after a failure so the outer rescue can use it.
|
|
111
|
+
# This is error context, not a push/pop command stack.
|
|
112
|
+
#
|
|
113
|
+
|
|
114
|
+
def with_ctx(ctx)
|
|
115
|
+
@ctx = ctx
|
|
116
|
+
yield
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def parse_with_ctx(ctx, argv, passthru: false)
|
|
120
|
+
with_ctx(ctx) do
|
|
121
|
+
Parser.new(ctx).parse(argv, passthru:)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_with_ctx(ctx, options)
|
|
126
|
+
with_ctx(ctx) do
|
|
127
|
+
ctx.validate&.call(options)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
68
130
|
end
|
|
69
131
|
|
|
70
132
|
class Error < StandardError; end
|
|
71
|
-
|
|
72
|
-
# early exits
|
|
73
|
-
class HelpRequested < Exception; end # rubocop:disable Lint/InheritException
|
|
74
133
|
class NakedRequested < Exception; end # rubocop:disable Lint/InheritException
|
|
75
|
-
class VersionRequested < Exception; end # rubocop:disable Lint/InheritException
|
|
76
134
|
|
|
77
135
|
# main entry point
|
|
78
136
|
def self.parse(argv = ARGV)
|
|
79
|
-
Main.new.tap { yield _1.
|
|
137
|
+
Main.new.tap { yield _1.root if block_given? }.parse(argv)
|
|
80
138
|
end
|
|
81
139
|
end
|
|
82
140
|
end
|
|
@@ -12,14 +12,14 @@ module RunKit
|
|
|
12
12
|
@config = config
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# Reset transient state, parse argv, and assemble the result.
|
|
16
|
-
|
|
15
|
+
# Reset transient state, parse argv, and assemble the result. When
|
|
16
|
+
# passthru is set, scanning halts at the first bare arg (for subcommands).
|
|
17
|
+
def parse(argv, passthru: false)
|
|
17
18
|
# 1. naked?
|
|
18
19
|
raise NakedRequested if config.naked? && argv.empty?
|
|
19
20
|
|
|
20
|
-
# 2. Parse argv.
|
|
21
|
-
|
|
22
|
-
argv_options = parse_argv(argv)
|
|
21
|
+
# 2. Parse argv.
|
|
22
|
+
argv_options = parse_argv(argv, passthru:)
|
|
23
23
|
|
|
24
24
|
# 3. defaults => ENV => ARGV
|
|
25
25
|
options = {}.merge(config.defaults, parse_env, argv_options)
|
|
@@ -41,7 +41,7 @@ module RunKit
|
|
|
41
41
|
# main parser
|
|
42
42
|
#
|
|
43
43
|
|
|
44
|
-
def parse_argv(argv)
|
|
44
|
+
def parse_argv(argv, passthru: false)
|
|
45
45
|
{}.tap do |result|
|
|
46
46
|
# any non-flags we find below
|
|
47
47
|
operands = []
|
|
@@ -52,7 +52,12 @@ module RunKit
|
|
|
52
52
|
case item
|
|
53
53
|
when Flag::SWITCH_RE, Flag::INLINE_RE then result.merge!(parse_switch(item, Regexp.last_match, queue))
|
|
54
54
|
when /\A-[^-]/ then result.merge!(parse_smashed(item, queue))
|
|
55
|
-
when "", /\A[^-]/
|
|
55
|
+
when "", /\A[^-]/
|
|
56
|
+
operands << item
|
|
57
|
+
if passthru
|
|
58
|
+
operands.concat(queue)
|
|
59
|
+
break
|
|
60
|
+
end
|
|
56
61
|
when "--" then break operands.concat(queue)
|
|
57
62
|
else; raise Error, "unexpected argument '#{item}' found"
|
|
58
63
|
end
|
|
@@ -77,7 +82,6 @@ module RunKit
|
|
|
77
82
|
|
|
78
83
|
# -x or --xyz?
|
|
79
84
|
if (flag = config.flag(switch))
|
|
80
|
-
builtin!(flag)
|
|
81
85
|
param = queue.shift if flag.takes_param? && separator.empty?
|
|
82
86
|
return {flag.key => flag.parse(switch, param)}
|
|
83
87
|
end
|
|
@@ -103,7 +107,6 @@ module RunKit
|
|
|
103
107
|
switch = "-#{group[idx]}"
|
|
104
108
|
flag = config.flag(switch)
|
|
105
109
|
raise Error, "unexpected argument '#{group}' found" unless flag
|
|
106
|
-
builtin!(flag)
|
|
107
110
|
|
|
108
111
|
# For `-qnLee`, `Lee` belongs to `-n`; for `-qn Lee`, shift the queue.
|
|
109
112
|
if flag.takes_param?
|
|
@@ -136,11 +139,6 @@ module RunKit
|
|
|
136
139
|
# helpers
|
|
137
140
|
#
|
|
138
141
|
|
|
139
|
-
def builtin!(flag)
|
|
140
|
-
raise HelpRequested if flag == config.help_flag
|
|
141
|
-
raise VersionRequested if flag == config.version_flag
|
|
142
|
-
end
|
|
143
|
-
|
|
144
142
|
def find_negated_flag(switch)
|
|
145
143
|
if (m = Flag::NEGATE_RE.match(switch))
|
|
146
144
|
flag = config.flag("--#{m[1]}")
|
data/lib/run_kit/shell.rb
CHANGED
|
@@ -29,9 +29,9 @@ module RunKit
|
|
|
29
29
|
# json file read/write, including gz
|
|
30
30
|
#
|
|
31
31
|
|
|
32
|
-
def json_read(path, symbolize_names: true) = JSON.parse(file_read(path), symbolize_names:)
|
|
32
|
+
def json_read(path, symbolize_names: true) = JSON.parse(file_read(path), allow_comments: true, symbolize_names:)
|
|
33
33
|
def json_write(path, json) = file_write(path, JSON.pretty_generate(json))
|
|
34
|
-
def jsonl_read(path, symbolize_names: true) = file_read(path).split("\n").map { JSON.parse(_1, symbolize_names:) }
|
|
34
|
+
def jsonl_read(path, symbolize_names: true) = file_read(path).split("\n").map { JSON.parse(_1, allow_comments: true, symbolize_names:) }
|
|
35
35
|
def jsonl_write(path, json) = file_write(path, json.map { JSON.generate(_1) }.join("\n"))
|
|
36
36
|
|
|
37
37
|
#
|
|
@@ -262,8 +262,8 @@ module RunKit
|
|
|
262
262
|
data = gunzip(data) if compress
|
|
263
263
|
case format
|
|
264
264
|
when :bin then data.force_encoding("ascii-8bit")
|
|
265
|
-
when :json then JSON.parse(data)
|
|
266
|
-
when :jsonl then data.split("\n").map { JSON.parse(_1) }
|
|
265
|
+
when :json then JSON.parse(data, allow_comments: true)
|
|
266
|
+
when :jsonl then data.split("\n").map { JSON.parse(_1, allow_comments: true) }
|
|
267
267
|
when :marshal then Marshal.load(data)
|
|
268
268
|
when :str, :string then data.force_encoding("utf-8")
|
|
269
269
|
else; raise "unknown format #{format.inspect}"
|
data/logo.png
ADDED
|
Binary file
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Doppelt
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- lib/run_kit/options/positional.rb
|
|
65
65
|
- lib/run_kit/shell.rb
|
|
66
66
|
- lib/run_kit/term.rb
|
|
67
|
+
- logo.png
|
|
67
68
|
- prek.toml
|
|
68
69
|
- run_kit.gemspec
|
|
69
70
|
homepage: https://github.com/gurgeous/run_kit
|
|
@@ -87,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
88
|
- !ruby/object:Gem::Version
|
|
88
89
|
version: '0'
|
|
89
90
|
requirements: []
|
|
90
|
-
rubygems_version:
|
|
91
|
+
rubygems_version: 4.0.6
|
|
91
92
|
specification_version: 4
|
|
92
93
|
summary: Run kit.
|
|
93
94
|
test_files: []
|