run_kit 0.1.1 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1edf6020353f3bdcf0062314c7ff6d6fef8c9277ba77f8210b3bc89c0bf921f8
4
- data.tar.gz: a25aab76b2d20c109d2c3422744f7c7294a9efbe88590a0a59a1517767de6bed
3
+ metadata.gz: 1876301102a9221f5207f024bcd108a95d3547cf96d270f475700959d8209cb6
4
+ data.tar.gz: b21d96ce661cbe5e45f2de8b6bf9cb1718d2bb7305bce4e15773aa7a9c4882e2
5
5
  SHA512:
6
- metadata.gz: 16ad6826101160c4f469210857e4194f282a8ad79316a3a993f26d7b02ca00ff49eb067458896eb08aba242edeeab48ee3574660b9d871449b0d44715d61b008
7
- data.tar.gz: cdf3e9157ea53b4ba2734224bdaab4bcc15f707e07d76ffc50516b3c2f1f4d80d5f3891247eadfdf82d3056a39f850822952a41906f08ce01269ee99645db165
6
+ metadata.gz: a82974e7beaa687457ff4b8555e8d1b38a284c89d4ef756755bbccc92c16ff857cdfe9d5083620bb53265d6b04de43d7fdca168559c4bef25ef99c052e98fbae
7
+ data.tar.gz: 43946b37bb5f6a155b5eb2d27220afa96df7705827db9e0a7ddb4aa5b86f6234629908436cdd288ed20cc8b751c48c413f3441d265cf4c8925de373629f7e99f
data/.rubocop.yml CHANGED
@@ -35,7 +35,6 @@ Naming/MemoizedInstanceVariableName: { Enabled: true } # clean memo ivars
35
35
  Naming/MethodName: { Enabled: true } # keep method names conventional
36
36
  Performance/MapCompact: { Enabled: true } # filter_map-ish style
37
37
  Performance/RegexpMatch: { Enabled: false } # local style is fine
38
- Performance/SelectMap: { Enabled: true } # filter_map-ish style
39
38
  Style/BlockDelimiters: { Enabled: true } # do/end vs braces
40
39
  Style/ClassAndModuleChildren: { Enabled: true, EnforcedStyle: nested } # explicit nesting
41
40
  Style/ClassMethodsDefinitions: { Enabled: true } # avoid "class << self"
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source "https://rubygems.org"
1
+ source "https://rubygems.org", cooldown: 7
2
2
  gemspec
3
3
 
4
4
  group :development, :test do
data/README.md CHANGED
@@ -30,6 +30,15 @@ Automatic `--help` for the above. Uses color and wraps to terminal:
30
30
 
31
31
  <img width="379" height="108" alt="image" src="https://github.com/user-attachments/assets/3d11c8ca-6b57-4dec-9c11-28bf23162ded" />
32
32
 
33
+ Flags can also read from ENV:
34
+
35
+ ```ruby
36
+ o.bool "--force", env: true # ENV["FORCE"]
37
+ o.str "--token", env: "API_TOKEN" # ENV["API_TOKEN"]
38
+ ```
39
+
40
+ Configured variables appear in `--help`. Command-line values override ENV, which overrides defaults. Boolean ENV values accept `true/1/yes/on` and `false/0/no/off/empty`, ignoring case.
41
+
33
42
  ## RunKit::Shell
34
43
 
35
44
  `RunKit::Shell` is a mixin with many helpers for bin scripts:
@@ -102,6 +111,14 @@ Note: There has been some effort to get the Pathname helpers into Ruby itself, w
102
111
 
103
112
  ### Changelog
104
113
 
114
+ #### 0.1.3 (Sep 2026)
115
+
116
+ - move PROGBAR constant into RunKit::
117
+
118
+ #### 0.1.2 (Sep 2026)
119
+
120
+ - allow options to read from ENV
121
+
105
122
  #### 0.1.1 Sep 2026
106
123
 
107
124
  - add title to progressbar default
@@ -34,20 +34,15 @@ module RunKit
34
34
  # each.with_progresbar
35
35
  module Enumerator
36
36
  def with_progressbar(options = {}, &block)
37
- defaults = {
38
- format: "%t: %j%% %B #{RunKit::Term.paint_ansi("%c/%u %e", RunKit::Term.ansi256_fg(242))}",
39
- progress_mark: RunKit::Term.paint_ansi("━", RunKit::Term.ansi256_fg(46)),
40
- remainder_mark: RunKit::Term.paint_ansi("━", RunKit::Term.ansi256_fg(237)),
37
+ options = RunKit::PROGRESSBAR.merge(
41
38
  output: $stdout.isatty ? $stdout : $stderr,
42
- total: size,
43
- length: 72,
44
- }
45
- options = defaults.merge(options)
39
+ total: size
40
+ ).merge(options)
46
41
 
47
42
  return enum_for(__method__) if !block
48
43
 
49
- if !options[:hide]
50
- bar = ProgressBar.create(options)
44
+ bar = if !options[:hide]
45
+ ProgressBar.create(options)
51
46
  end
52
47
  RunKit::Term.with_hidden_cursor(options[:output]) do
53
48
  each do
@@ -37,28 +37,28 @@ module RunKit
37
37
  # flags
38
38
  #
39
39
 
40
- def bool(*opts, default: nil, required: false)
41
- add_flag(Flag.new(:bool, opts, default:, required:))
40
+ def bool(*opts, default: nil, required: false, env: nil)
41
+ add_flag(Flag.new(:bool, opts, default:, required:, env:))
42
42
  end
43
43
 
44
- def float(*opts, default: nil, required: false, choices: nil)
45
- add_flag(Flag.new(:float, opts, default:, required:, choices:))
44
+ def float(*opts, default: nil, required: false, choices: nil, env: nil)
45
+ add_flag(Flag.new(:float, opts, default:, required:, choices:, env:))
46
46
  end
47
47
 
48
- def int(*opts, default: nil, required: false, choices: nil)
49
- add_flag(Flag.new(:int, opts, default:, required:, choices:))
48
+ def int(*opts, default: nil, required: false, choices: nil, env: nil)
49
+ add_flag(Flag.new(:int, opts, default:, required:, choices:, env:))
50
50
  end
51
51
 
52
- def path(*opts, default: nil, required: false, choices: nil)
53
- add_flag(Flag.new(:path, opts, default:, required:, choices:))
52
+ def path(*opts, default: nil, required: false, choices: nil, env: nil)
53
+ add_flag(Flag.new(:path, opts, default:, required:, choices:, env:))
54
54
  end
55
55
 
56
- def str(*opts, default: nil, required: false, choices: nil)
57
- add_flag(Flag.new(:str, opts, default:, required:, choices:))
56
+ def str(*opts, default: nil, required: false, choices: nil, env: nil)
57
+ add_flag(Flag.new(:str, opts, default:, required:, choices:, env:))
58
58
  end
59
59
 
60
- def sym(*opts, default: nil, required: false, choices: nil)
61
- add_flag(Flag.new(:sym, opts, default:, required:, choices:))
60
+ def sym(*opts, default: nil, required: false, choices: nil, env: nil)
61
+ add_flag(Flag.new(:sym, opts, default:, required:, choices:, env:))
62
62
  end
63
63
 
64
64
  # long-form aliases
@@ -12,12 +12,14 @@ module RunKit
12
12
  # --no-foo
13
13
  NEGATE_RE = /\A--no-(\w[\w-]*)\z/
14
14
 
15
+ TRUE_ENV = %w[1 true yes on]
16
+ FALSE_ENV = ["", "0", "false", "no", "off"]
15
17
  KINDS = %i[bool float int path str sym]
16
18
 
17
- attr_reader :choices, :default, :help, :kind, :meta, :required, :switches
19
+ attr_reader(*%i[choices default env help kind meta required switches])
18
20
 
19
21
  # ctor
20
- def initialize(kind, opts, default: nil, required: false, choices: nil)
22
+ def initialize(kind, opts, default: nil, required: false, choices: nil, env: nil)
21
23
  @choices, @kind, @required = choices, kind, required
22
24
 
23
25
  # extract @help from last string
@@ -28,6 +30,7 @@ module RunKit
28
30
 
29
31
  # Extract meta from the final switch, if written as `--port <int>`.
30
32
  @meta = build_meta
33
+ @env = (env == true) ? key.to_s.upcase : env
31
34
 
32
35
  # default, with some special handling for bool
33
36
  @default = default
@@ -50,22 +53,21 @@ module RunKit
50
53
  end
51
54
 
52
55
  raise Error, "option '#{switch}' requires a value" if !param
53
- parsed = begin
54
- case kind
55
- when :float then Float(param)
56
- when :int then Integer(param, 10)
57
- when :path then Pathname.new(param)
58
- when :str then param
59
- when :sym then param.to_sym
60
- end
61
- rescue ArgumentError
62
- raise Error, "invalid value '#{param}' for option '#{switch}'"
63
- end
56
+ parse_value(param, "option '#{switch}'")
57
+ end
64
58
 
65
- if choices && !choices.include?(parsed)
66
- raise Error, "invalid value '#{parsed}' for option '#{switch}', must be one of #{choices.join(", ")}"
59
+ # Parse a value supplied by ENV[$SOMETHING]
60
+ def parse_env(param)
61
+ source = "environment variable '#{env}'"
62
+
63
+ # special handling for bools
64
+ if bool?
65
+ return true if TRUE_ENV.include?(param.downcase)
66
+ return false if FALSE_ENV.include?(param.downcase)
67
+ raise Error, "invalid value '#{param}' for #{source}"
67
68
  end
68
- parsed
69
+
70
+ parse_value(param, source)
69
71
  end
70
72
 
71
73
  # one-liners
@@ -96,6 +98,7 @@ module RunKit
96
98
  raise ArgumentError, "required must be true or false" unless required == true || required == false
97
99
  raise ArgumentError, "required flags cannot have defaults" if required && default != nil
98
100
  raise ArgumentError, "invalid default #{default.inspect} for #{kind}" if default != nil && !allowed?(default)
101
+ raise ArgumentError, "env must be true or a string" unless env.nil? || env.is_a?(String)
99
102
 
100
103
  # choices
101
104
  if choices
@@ -130,6 +133,25 @@ module RunKit
130
133
  when :sym then candidate.is_a?(Symbol)
131
134
  end
132
135
  end
136
+
137
+ def parse_value(param, source)
138
+ parsed = begin
139
+ case kind
140
+ when :float then Float(param)
141
+ when :int then Integer(param, 10)
142
+ when :path then Pathname.new(param)
143
+ when :str then param
144
+ when :sym then param.to_sym
145
+ end
146
+ rescue ArgumentError
147
+ raise Error, "invalid value '#{param}' for #{source}"
148
+ end
149
+
150
+ if choices && !choices.include?(parsed)
151
+ raise Error, "invalid value '#{parsed}' for #{source}, must be one of #{choices.join(", ")}"
152
+ end
153
+ parsed
154
+ end
133
155
  end
134
156
  end
135
157
  end
@@ -36,10 +36,15 @@ module RunKit
36
36
  buf << label
37
37
 
38
38
  # right
39
- if flag.help
39
+ help = flag.help
40
+ if flag.env
41
+ env_help = "[env: #{flag.env}]"
42
+ help = help ? "#{help} #{env_help}" : env_help
43
+ end
44
+ if help
40
45
  buf << " " * (label_width - Term.width(label) + 2)
41
46
  indent = INDENT + label_width + 2
42
- buf << Term.wrap(flag.help, width - indent).gsub("\n", "\n#{" " * indent}")
47
+ buf << Term.wrap(help, width - indent).gsub("\n", "\n#{" " * indent}")
43
48
  end
44
49
  buf << "\n"
45
50
  end
@@ -6,7 +6,7 @@
6
6
  module RunKit
7
7
  module Options
8
8
  class Parser
9
- attr_reader :config, :options, :queue
9
+ attr_reader :config
10
10
 
11
11
  def initialize(config)
12
12
  @config = config
@@ -14,9 +14,24 @@ module RunKit
14
14
 
15
15
  # Reset transient state, parse argv, and assemble the result.
16
16
  def parse(argv)
17
- @options, @queue = config.defaults, argv.dup
18
- parse_queue
19
- validate!
17
+ # 1. naked?
18
+ raise NakedRequested if config.naked? && argv.empty?
19
+
20
+ # 2. Parse argv. We do this first, because other things can raise and
21
+ # --help should trump other issues.
22
+ argv_options = parse_argv(argv)
23
+
24
+ # 3. defaults => ENV => ARGV
25
+ options = {}.merge(config.defaults, parse_env, argv_options)
26
+
27
+ # 4. validate final options
28
+ validate!(options)
29
+
30
+ # success! add predicate? keys
31
+ config.flags.select(&:bool?).map(&:key).each do
32
+ options[:"#{_1}?"] = options[_1] if options.key?(_1)
33
+ end
34
+
20
35
  options
21
36
  end
22
37
 
@@ -26,42 +41,36 @@ module RunKit
26
41
  # main parser
27
42
  #
28
43
 
29
- def parse_queue
30
- raise NakedRequested if config.naked? && queue.empty?
31
-
32
- # any non-flags we find below
33
- operands = []
34
-
35
- # process argv as queue
36
- while (item = queue.shift)
37
- case item
38
- when Flag::SWITCH_RE, Flag::INLINE_RE then parse_switch(item, Regexp.last_match)
39
- when /\A-[^-]/ then parse_smashed(item)
40
- when "", /\A[^-]/ then operands << item
41
- when "--" then break operands.concat(queue)
42
- else; raise Error, "unexpected argument '#{item}' found"
44
+ def parse_argv(argv)
45
+ {}.tap do |result|
46
+ # any non-flags we find below
47
+ operands = []
48
+
49
+ # process argv as queue
50
+ queue = argv.dup
51
+ while (item = queue.shift)
52
+ case item
53
+ when Flag::SWITCH_RE, Flag::INLINE_RE then result.merge!(parse_switch(item, Regexp.last_match, queue))
54
+ when /\A-[^-]/ then result.merge!(parse_smashed(item, queue))
55
+ when "", /\A[^-]/ then operands << item
56
+ when "--" then break operands.concat(queue)
57
+ else; raise Error, "unexpected argument '#{item}' found"
58
+ end
43
59
  end
44
- end
45
60
 
46
- # add pred? for bools
47
- config.flags.filter_map { _1.key if _1.bool? }.each do
48
- options[:"#{_1}?"] = options[_1] if options.key?(_1)
49
- end
61
+ # positionals
62
+ config.positionals.each { result[_1.key] = operands.shift }
50
63
 
51
- # positionals
52
- config.positionals.each do
53
- options[_1.key] = operands.shift
64
+ # _args
65
+ result[:_args] = operands
54
66
  end
55
-
56
- # _args
57
- options[:_args] = operands
58
67
  end
59
68
 
60
69
  #
61
70
  # -x or -x=123 or --xyz or --xyz=123 or --no-xyz
62
71
  #
63
72
 
64
- def parse_switch(item, match)
73
+ def parse_switch(item, match, queue)
65
74
  switch = "-#{match[1]}"
66
75
  param = match[2]
67
76
  separator = param ? "=" : ""
@@ -70,15 +79,13 @@ module RunKit
70
79
  if (flag = config.flag(switch))
71
80
  builtin!(flag)
72
81
  param = queue.shift if flag.takes_param? && separator.empty?
73
- options[flag.key] = flag.parse(switch, param)
74
- return
82
+ return {flag.key => flag.parse(switch, param)}
75
83
  end
76
84
 
77
85
  # --no-xyz?
78
86
  if (neg = find_negated_flag(switch))
79
87
  raise Error, "option '#{item}' does not take a value" if separator == "="
80
- options[neg.key] = false
81
- return
88
+ return {neg.key => false}
82
89
  end
83
90
 
84
91
  raise Error, "unexpected argument '#{item}' found"
@@ -90,7 +97,8 @@ module RunKit
90
97
 
91
98
  # Expand short-switch groups such as `-qv`. A parameter-taking switch ends
92
99
  # the group and consumes either its attached suffix or the next queue item.
93
- def parse_smashed(group)
100
+ def parse_smashed(group, queue)
101
+ result = {}
94
102
  (1...group.length).each do |idx|
95
103
  switch = "-#{group[idx]}"
96
104
  flag = config.flag(switch)
@@ -104,13 +112,24 @@ module RunKit
104
112
  else
105
113
  queue.shift
106
114
  end
107
- options[flag.key] = flag.parse(switch, param)
108
- return
115
+ result[flag.key] = flag.parse(switch, param)
116
+ return result
109
117
  end
110
118
 
111
119
  # bool
112
- options[flag.key] = true
120
+ result[flag.key] = true
113
121
  end
122
+ result
123
+ end
124
+
125
+ #
126
+ # env
127
+ #
128
+
129
+ def parse_env
130
+ config.flags.select { _1.env && ENV.key?(_1.env) }.map do |flag|
131
+ [flag.key, flag.parse_env(ENV[flag.env])]
132
+ end.to_h
114
133
  end
115
134
 
116
135
  #
@@ -129,7 +148,7 @@ module RunKit
129
148
  end
130
149
  end
131
150
 
132
- def validate!
151
+ def validate!(options)
133
152
  config.required.each do
134
153
  raise Error, "required option '#{_1.switch}' is missing" if !options.key?(_1.key)
135
154
  end
data/lib/run_kit.rb CHANGED
@@ -16,7 +16,14 @@ require_relative "run_kit/term"
16
16
  require_relative "run_kit/options"
17
17
  require_relative "run_kit/shell"
18
18
 
19
- # handy entry point for RunKit::Options
20
19
  module RunKit
20
+ PROGRESSBAR = {
21
+ format: "%t: %j%% %B #{Term.paint_ansi("%c/%u %e", Term.ansi256_fg(242))}",
22
+ progress_mark: Term.paint_ansi("━", Term.ansi256_fg(46)),
23
+ remainder_mark: Term.paint_ansi("━", Term.ansi256_fg(237)),
24
+ length: 72,
25
+ }
26
+
27
+ # handy entry point for RunKit::Options
21
28
  def self.parse(...) = Options.parse(...)
22
29
  end
data/run_kit.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "run_kit"
3
- s.version = "0.1.1"
3
+ s.version = "0.1.3"
4
4
  s.authors = ["Adam Doppelt"]
5
5
  s.email = "amd@gurge.com"
6
6
  s.summary = "Run kit."
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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Doppelt