rubycli 0.1.7 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -2
- data/LICENSE +1 -1
- data/README.ja.md +357 -252
- data/README.md +386 -250
- data/examples/documentation_style_showcase.rb +110 -0
- data/examples/fallback_example.rb +14 -0
- data/examples/fallback_example_with_extra_docs.rb +13 -0
- data/examples/hello_app.rb +10 -0
- data/examples/hello_app_with_docs.rb +17 -0
- data/examples/hello_app_with_require.rb +19 -0
- data/examples/mismatched_constant_runner.rb +16 -0
- data/examples/multi_constant_runner.rb +20 -0
- data/examples/new_mode_runner.rb +46 -0
- data/examples/strict_choices_demo.rb +22 -0
- data/examples/typed_arguments_demo.rb +42 -0
- data/lib/rubycli/argument_mode_controller.rb +7 -11
- data/lib/rubycli/argument_parser.rb +295 -96
- data/lib/rubycli/cli.rb +26 -30
- data/lib/rubycli/command_line.rb +151 -86
- data/lib/rubycli/constant_capture.rb +563 -6
- data/lib/rubycli/documentation/metadata_parser.rb +108 -74
- data/lib/rubycli/environment.rb +17 -9
- data/lib/rubycli/eval_coercer.rb +27 -10
- data/lib/rubycli/help_renderer.rb +67 -62
- data/lib/rubycli/json_coercer.rb +2 -0
- data/lib/rubycli/result_emitter.rb +18 -8
- data/lib/rubycli/runner.rb +513 -0
- data/lib/rubycli/type_utils.rb +8 -6
- data/lib/rubycli/types.rb +2 -0
- data/lib/rubycli/version.rb +1 -1
- data/lib/rubycli.rb +7 -456
- metadata +59 -4
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Compact examples that highlight how Rubycli parses different documentation styles.
|
|
4
|
+
module DocumentationStyleShowcase
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Canonical uppercase placeholders covering required, optional, and repeated forms.
|
|
8
|
+
#
|
|
9
|
+
# NAME [String] Name to display
|
|
10
|
+
# COUNT [Integer] Repetition count
|
|
11
|
+
# --prefix PREFIX [String] Descriptor printed before the subject
|
|
12
|
+
# --tags TAG... [String[]] Comma-separated or JSON list of tags (repeatable)
|
|
13
|
+
# --quiet [Boolean] Suppress the trailing newline marker
|
|
14
|
+
def canonical(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
15
|
+
build_payload(:canonical, subject, count, prefix, tags, quiet)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Minimal uppercase form – same signature, but placeholders omit type hints entirely.
|
|
19
|
+
#
|
|
20
|
+
# NAME Name to display
|
|
21
|
+
# COUNT Repetition count
|
|
22
|
+
# --prefix PREFIX Descriptor printed before the subject
|
|
23
|
+
# --tags TAG... JSON/YAML array or comma-separated list
|
|
24
|
+
# --quiet
|
|
25
|
+
def canonical_min(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
26
|
+
build_payload(:canonical_min, subject, count, prefix, tags, quiet)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Angle-bracket placeholders with parenthesized type hints.
|
|
30
|
+
# <subject> [String] Subject text to render
|
|
31
|
+
# <count> [Integer] Repetition count
|
|
32
|
+
# --prefix=<prefix> [String] Prefix that appears before the subject
|
|
33
|
+
# --tags <tag>... [String[]] JSON/YAML array or comma-separated tags (repeatable)
|
|
34
|
+
# --quiet [Boolean] Suppress the trailing newline marker
|
|
35
|
+
def angled(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
36
|
+
build_payload(:angled, subject, count, prefix, tags, quiet)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Minimal angle-bracket form without explicit types.
|
|
40
|
+
#
|
|
41
|
+
# <subject> Subject text to render
|
|
42
|
+
# <count> Repetition count
|
|
43
|
+
# --prefix=<prefix> Prefix shown before the subject
|
|
44
|
+
# --tags <tag>... JSON/YAML array or comma-separated list
|
|
45
|
+
# --quiet
|
|
46
|
+
def angled_min(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
47
|
+
build_payload(:angled_min, subject, count, prefix, tags, quiet)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Bracketed type hints combined with ellipsis for repeated values.
|
|
51
|
+
#
|
|
52
|
+
# <subject> [String] Subject identifier to capture
|
|
53
|
+
# <count> [Integer, nil] Repetition count (accepts nil)
|
|
54
|
+
# --prefix PREFIX [String, nil] Heading for the entry
|
|
55
|
+
# --tags <tag>... [String[]] JSON/YAML array or comma-separated tags (repeatable)
|
|
56
|
+
# --quiet [Boolean] Suppress the trailing newline marker
|
|
57
|
+
# => [Hash] Summary of captured attributes
|
|
58
|
+
def typed(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
59
|
+
build_payload(:typed, subject, count, prefix, tags, quiet)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Minimal typed variant using compact notation.
|
|
63
|
+
#
|
|
64
|
+
# <subject> [String] Subject identifier
|
|
65
|
+
# <count> [Integer] Repetition count
|
|
66
|
+
# --prefix PREFIX [String] Title prefix
|
|
67
|
+
# --tags <tag>... [String[]] JSON/YAML array or comma-separated list
|
|
68
|
+
# --quiet [Boolean]
|
|
69
|
+
def typed_min(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
70
|
+
build_payload(:typed_min, subject, count, prefix, tags, quiet)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# YARD-style annotations without inline option metadata.
|
|
74
|
+
#
|
|
75
|
+
# @param subject [String] Subject to process
|
|
76
|
+
# @param count [Integer] Number of repetitions when composing the label
|
|
77
|
+
# @param prefix [String, nil] Descriptor printed before the subject
|
|
78
|
+
# @param tags [Array<String>, nil] Additional tags (JSON/YAML array or comma-separated, repeatable)
|
|
79
|
+
# @param quiet [Boolean] Suppress the trailing newline marker
|
|
80
|
+
# @return [Hash] Normalized configuration summary
|
|
81
|
+
def yard(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
82
|
+
build_payload(:yard, subject, count, prefix, tags, quiet)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Minimal YARD-style form with the shortest acceptable annotations.
|
|
86
|
+
#
|
|
87
|
+
# @param subject Subject to process
|
|
88
|
+
# @param count Repetition count
|
|
89
|
+
# @param prefix Descriptor printed before the subject
|
|
90
|
+
# @param tags Tags list (JSON/YAML array or comma-separated)
|
|
91
|
+
# @param quiet Quiet flag
|
|
92
|
+
def yard_min(subject, count = 1, prefix: nil, tags: nil, quiet: false)
|
|
93
|
+
build_payload(:yard_min, subject, count, prefix, tags, quiet)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
class << self
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def build_payload(style, subject, count, prefix, tags, quiet)
|
|
100
|
+
{
|
|
101
|
+
style: style,
|
|
102
|
+
subject: subject,
|
|
103
|
+
count: count,
|
|
104
|
+
prefix: prefix,
|
|
105
|
+
tags: tags,
|
|
106
|
+
quiet: quiet
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example showcasing inference when documentation is incomplete.
|
|
4
|
+
module FallbackExample
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# AMOUNT [Integer] Base amount to process
|
|
8
|
+
def scale(amount, factor = 2, clamp: nil, notify: false)
|
|
9
|
+
result = amount * factor
|
|
10
|
+
result = [result, clamp].min if clamp
|
|
11
|
+
puts "Scaled to #{result}" if notify
|
|
12
|
+
result
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example showing how comments beyond the live signature are treated.
|
|
4
|
+
module FallbackExampleWithExtraDocs
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# AMOUNT [Integer] Base amount to process
|
|
8
|
+
# CLAMP [Integer] (unused placeholder)
|
|
9
|
+
# --ghost [Boolean] Imaginary toggle that is not implemented
|
|
10
|
+
def scale(amount)
|
|
11
|
+
amount * 2
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example CLI target with documentation-driven options; see README section 2.
|
|
4
|
+
module HelloApp
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# NAME [String] Name to greet
|
|
8
|
+
# --shout [Boolean] Print in uppercase
|
|
9
|
+
def greet(name, shout: false)
|
|
10
|
+
message = "Hello, #{name}!"
|
|
11
|
+
message = message.upcase if shout
|
|
12
|
+
puts message
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Provide a constant that matches the file name so Rubycli infers it automatically.
|
|
17
|
+
HelloAppWithDocs = HelloApp
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rubycli'
|
|
4
|
+
|
|
5
|
+
module HelloApp
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# NAME [String] Name to greet
|
|
9
|
+
# --shout [Boolean] Print in uppercase
|
|
10
|
+
# => [String] Printed message
|
|
11
|
+
def greet(name, shout: false)
|
|
12
|
+
message = "Hello, #{name}!"
|
|
13
|
+
message = message.upcase if shout
|
|
14
|
+
puts message
|
|
15
|
+
message
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Rubycli.run(HelloApp)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Demonstrates a file whose constant does not match the file name. By default
|
|
4
|
+
# Rubycli will ask you to call out the constant explicitly:
|
|
5
|
+
# rubycli examples/mismatched_constant_runner.rb FriendlyGreeter greet --message "Hello"
|
|
6
|
+
# To auto-select it, pass --auto-target (or set RUBYCLI_AUTO_TARGET=auto).
|
|
7
|
+
class FriendlyGreeter
|
|
8
|
+
# NAME [String] Text to display
|
|
9
|
+
# --message MESSAGE [String] Greeting to print (defaults to "Hello")
|
|
10
|
+
# --quiet [Boolean] Skip the stdout line (the return value is still printed)
|
|
11
|
+
def self.greet(name = 'friend', message: 'Hello', quiet: false)
|
|
12
|
+
output = "#{message}, #{name}!"
|
|
13
|
+
puts(output) unless quiet
|
|
14
|
+
output
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Demonstrates how Rubycli behaves when a file defines multiple constants.
|
|
4
|
+
# By default Rubycli will prefer the constant whose name matches the file
|
|
5
|
+
# (MultiConstantRunner). To invoke HelperRunner instead, specify it explicitly:
|
|
6
|
+
# rubycli examples/multi_constant_runner.rb HelperRunner inspect
|
|
7
|
+
class HelperRunner
|
|
8
|
+
def self.inspect
|
|
9
|
+
puts('Helper invoked')
|
|
10
|
+
:helper
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class MultiConstantRunner
|
|
15
|
+
# TEXT [String] Message to display
|
|
16
|
+
def self.echo(text = 'hello')
|
|
17
|
+
puts(text)
|
|
18
|
+
text
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Demonstrates instance-only CLI with constructor arguments and eval/json modes.
|
|
4
|
+
class NewModeRunner
|
|
5
|
+
attr_reader :items, :options
|
|
6
|
+
|
|
7
|
+
# ITEMS [String[]] List input (comma-separated or a JSON array)
|
|
8
|
+
# --options [Hash] Extra settings (JSON or eval literals)
|
|
9
|
+
def initialize(items, options: {})
|
|
10
|
+
@items = items
|
|
11
|
+
@options = options
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# --mode MODE [Symbol] Execution mode (summary or reverse)
|
|
15
|
+
def run(mode: :summary)
|
|
16
|
+
case mode.to_sym
|
|
17
|
+
when :summary
|
|
18
|
+
{
|
|
19
|
+
count: items.size,
|
|
20
|
+
uppercased: items.map(&:upcase),
|
|
21
|
+
options: options
|
|
22
|
+
}
|
|
23
|
+
when :reverse
|
|
24
|
+
items.reverse
|
|
25
|
+
else
|
|
26
|
+
warn "unknown mode: #{mode}"
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Usage examples (from the repository root):
|
|
33
|
+
# # Instance methods only, so --new is required; the array can be JSON or comma-separated
|
|
34
|
+
# rubycli --new='["a","b","c"]' examples/new_mode_runner.rb run --mode reverse
|
|
35
|
+
# rubycli --new a,b,c examples/new_mode_runner.rb run --mode summary
|
|
36
|
+
#
|
|
37
|
+
# # JSON mode: every following argument is parsed strictly as JSON (bare words are rejected)
|
|
38
|
+
# rubycli --json-args --new='["a","b"]' examples/new_mode_runner.rb run
|
|
39
|
+
#
|
|
40
|
+
# # Eval mode: Ruby literals are accepted, but every argument must be valid Ruby
|
|
41
|
+
# rubycli --eval-args --new='%w[x y]' examples/new_mode_runner.rb run --mode ':reverse'
|
|
42
|
+
#
|
|
43
|
+
# # --new passes a single value (items), so keyword arguments go through a pre-script,
|
|
44
|
+
# # which builds the instance on its own and needs no --new
|
|
45
|
+
# rubycli --pre-script 'NewModeRunner.new(%w[a b c], options: {from: :pre})' \
|
|
46
|
+
# examples/new_mode_runner.rb run --mode summary
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Demonstrates literal enum choices and strict validation.
|
|
4
|
+
module StrictChoicesDemo
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# LEVEL %i[info warn error] Report severity
|
|
8
|
+
# --format TARGET ["text", "json", "yaml"] Output destination (strings)
|
|
9
|
+
# --notify [Boolean] Print a notification banner
|
|
10
|
+
def report(level, format: 'text', notify: false)
|
|
11
|
+
payload = { level: level, format: format, notify: notify }
|
|
12
|
+
message = "[#{level.to_s.upcase}] format=#{format}"
|
|
13
|
+
message = "#{message} (notify enabled)" if notify
|
|
14
|
+
puts message
|
|
15
|
+
payload
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if $PROGRAM_NAME == __FILE__
|
|
20
|
+
require 'rubycli'
|
|
21
|
+
Rubycli.run(StrictChoicesDemo)
|
|
22
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'bigdecimal'
|
|
6
|
+
require 'pathname'
|
|
7
|
+
|
|
8
|
+
# Demonstrates standard library type coercions.
|
|
9
|
+
module TypedArgumentsDemo
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# --date DATE [Date] Planned schedule date
|
|
13
|
+
# --moment TIME [Time] Execution timestamp
|
|
14
|
+
# --budget AMOUNT [BigDecimal] Budget amount
|
|
15
|
+
# --input FILE [Pathname] Path to source file
|
|
16
|
+
def ingest(
|
|
17
|
+
date: Date.today,
|
|
18
|
+
moment: Time.now,
|
|
19
|
+
budget: BigDecimal('0'),
|
|
20
|
+
input: Pathname.new('.')
|
|
21
|
+
)
|
|
22
|
+
summary = <<~TEXT
|
|
23
|
+
Date: #{date.iso8601}
|
|
24
|
+
Moment: #{moment.utc.iso8601}
|
|
25
|
+
Budget: #{budget.to_s('F')}
|
|
26
|
+
Input: #{input.expand_path}
|
|
27
|
+
TEXT
|
|
28
|
+
|
|
29
|
+
puts summary
|
|
30
|
+
{
|
|
31
|
+
date: date,
|
|
32
|
+
moment: moment,
|
|
33
|
+
budget: budget,
|
|
34
|
+
input: input
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
if $PROGRAM_NAME == __FILE__
|
|
40
|
+
require 'rubycli'
|
|
41
|
+
Rubycli.run(TypedArgumentsDemo)
|
|
42
|
+
end
|
|
@@ -29,13 +29,9 @@ module Rubycli
|
|
|
29
29
|
def apply_argument_coercions(positional_args, keyword_args)
|
|
30
30
|
ensure_modes_compatible!
|
|
31
31
|
|
|
32
|
-
if json_mode?
|
|
33
|
-
coerce_values!(positional_args, keyword_args) { |value| @json_coercer.coerce_json_value(value) }
|
|
34
|
-
end
|
|
32
|
+
coerce_values!(positional_args, keyword_args) { |value| @json_coercer.coerce_json_value(value) } if json_mode?
|
|
35
33
|
|
|
36
|
-
if eval_mode?
|
|
37
|
-
coerce_values!(positional_args, keyword_args) { |value| @eval_coercer.coerce_eval_value(value) }
|
|
38
|
-
end
|
|
34
|
+
coerce_values!(positional_args, keyword_args) { |value| @eval_coercer.coerce_eval_value(value) } if eval_mode?
|
|
39
35
|
rescue ::ArgumentError => e
|
|
40
36
|
raise Rubycli::ArgumentError, e.message
|
|
41
37
|
end
|
|
@@ -54,13 +50,13 @@ module Rubycli
|
|
|
54
50
|
end
|
|
55
51
|
|
|
56
52
|
def ensure_modes_compatible!
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
return unless json_mode? && eval_mode?
|
|
54
|
+
|
|
55
|
+
raise Rubycli::ArgumentError, '--json-args cannot be combined with --eval-args or --eval-lax'
|
|
60
56
|
end
|
|
61
57
|
|
|
62
|
-
def coerce_values!(positional_args, keyword_args)
|
|
63
|
-
positional_args.map!
|
|
58
|
+
def coerce_values!(positional_args, keyword_args, &block)
|
|
59
|
+
positional_args.map!(&block)
|
|
64
60
|
keyword_args.keys.each do |key|
|
|
65
61
|
keyword_args[key] = yield(keyword_args[key])
|
|
66
62
|
end
|