samovar 2.3.0 → 2.4.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.
@@ -4,7 +4,16 @@
4
4
  # Copyright, 2016-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents nested sub-commands in a command.
8
+ #
9
+ # A `Nested` parser allows you to define multiple sub-commands that can be invoked from the parent command.
7
10
  class Nested
11
+ # Initialize a new nested command parser.
12
+ #
13
+ # @parameter key [Symbol] The name of the attribute to store the selected command in.
14
+ # @parameter commands [Hash] A mapping of command names to command classes.
15
+ # @parameter default [String | Nil] The default command name if none is provided.
16
+ # @parameter required [Boolean] Whether a command is required.
8
17
  def initialize(key, commands, default: nil, required: false)
9
18
  @key = key
10
19
  @commands = commands
@@ -15,15 +24,36 @@ module Samovar
15
24
  @required = required
16
25
  end
17
26
 
27
+ # The name of the attribute to store the selected command in.
28
+ #
29
+ # @attribute [Symbol]
18
30
  attr :key
31
+
32
+ # A mapping of command names to command classes.
33
+ #
34
+ # @attribute [Hash]
19
35
  attr :commands
36
+
37
+ # The default command name if none is provided.
38
+ #
39
+ # @attribute [String | Nil]
20
40
  attr :default
41
+
42
+ # Whether a command is required.
43
+ #
44
+ # @attribute [Boolean]
21
45
  attr :required
22
46
 
47
+ # Generate a string representation for usage output.
48
+ #
49
+ # @returns [String] The usage string.
23
50
  def to_s
24
51
  "<#{@key}>"
25
52
  end
26
53
 
54
+ # Generate an array representation for usage output.
55
+ #
56
+ # @returns [Array] The usage array.
27
57
  def to_a
28
58
  usage = [self.to_s]
29
59
 
@@ -44,7 +74,12 @@ module Samovar
44
74
  return usage
45
75
  end
46
76
 
47
- # @param default [Command] the default command if any.
77
+ # Parse a nested command from the input.
78
+ #
79
+ # @parameter input [Array(String)] The command-line arguments.
80
+ # @parameter parent [Command | Nil] The parent command.
81
+ # @parameter default [Command | Nil] The default command instance.
82
+ # @returns [Command | Object | Nil] The parsed command instance, or the default if no match.
48
83
  def parse(input, parent = nil, default = nil)
49
84
  if command = @commands[input.first]
50
85
  name = input.shift
@@ -56,10 +91,13 @@ module Samovar
56
91
  elsif @default
57
92
  @commands[@default].new(input, name: @default, parent: parent)
58
93
  elsif @required
59
- raise MissingValueError.new(parent, self)
94
+ raise MissingValueError.new(parent, @key)
60
95
  end
61
96
  end
62
97
 
98
+ # Generate usage information for this nested command.
99
+ #
100
+ # @parameter rows [Output::Rows] The rows to append usage information to.
63
101
  def usage(rows)
64
102
  rows << self
65
103
 
data/lib/samovar/one.rb CHANGED
@@ -4,7 +4,17 @@
4
4
  # Copyright, 2016-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents a single positional argument in a command.
8
+ #
9
+ # A `One` parser extracts exactly one argument from the command line that matches the specified pattern.
7
10
  class One
11
+ # Initialize a new positional argument parser.
12
+ #
13
+ # @parameter key [Symbol] The name of the attribute to store the value in.
14
+ # @parameter description [String] A description of the argument for help output.
15
+ # @parameter pattern [Regexp] A pattern to match valid values.
16
+ # @parameter default [Object] The default value if no argument is provided.
17
+ # @parameter required [Boolean] Whether the argument is required.
8
18
  def initialize(key, description, pattern: //, default: nil, required: false)
9
19
  @key = key
10
20
  @description = description
@@ -13,16 +23,41 @@ module Samovar
13
23
  @required = required
14
24
  end
15
25
 
26
+ # The name of the attribute to store the value in.
27
+ #
28
+ # @attribute [Symbol]
16
29
  attr :key
30
+
31
+ # A description of the argument for help output.
32
+ #
33
+ # @attribute [String]
17
34
  attr :description
35
+
36
+ # A pattern to match valid values.
37
+ #
38
+ # @attribute [Regexp]
18
39
  attr :pattern
40
+
41
+ # The default value if no argument is provided.
42
+ #
43
+ # @attribute [Object]
19
44
  attr :default
45
+
46
+ # Whether the argument is required.
47
+ #
48
+ # @attribute [Boolean]
20
49
  attr :required
21
50
 
51
+ # Generate a string representation for usage output.
52
+ #
53
+ # @returns [String] The usage string.
22
54
  def to_s
23
55
  "<#{@key}>"
24
56
  end
25
57
 
58
+ # Generate an array representation for usage output.
59
+ #
60
+ # @returns [Array] The usage array.
26
61
  def to_a
27
62
  usage = [to_s, @description]
28
63
 
@@ -35,13 +70,19 @@ module Samovar
35
70
  return usage
36
71
  end
37
72
 
73
+ # Parse a single argument from the input.
74
+ #
75
+ # @parameter input [Array(String)] The command-line arguments.
76
+ # @parameter parent [Command | Nil] The parent command.
77
+ # @parameter default [Object | Nil] An override for the default value.
78
+ # @returns [String | Object | Nil] The parsed value, or the default if no match.
38
79
  def parse(input, parent = nil, default = nil)
39
80
  if input.first =~ @pattern
40
81
  input.shift
41
82
  elsif default ||= @default
42
83
  return default
43
84
  elsif @required
44
- raise MissingValueError.new(parent, self)
85
+ raise MissingValueError.new(parent, @key)
45
86
  end
46
87
  end
47
88
  end
@@ -1,12 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
- require_relative 'flags'
6
+ require_relative "flags"
7
+ require_relative "error"
7
8
 
8
9
  module Samovar
10
+ # Represents a single command-line option.
11
+ #
12
+ # An option is a flag-based argument that can have various forms (short, long, with or without values).
9
13
  class Option
14
+ # Initialize a new option.
15
+ #
16
+ # @parameter flags [String] The flags specification (e.g., `-f/--flag <value>`).
17
+ # @parameter description [String] A description of the option for help output.
18
+ # @parameter key [Symbol | Nil] The key to use for storing the value (defaults to derived from flag).
19
+ # @parameter default [Object] The default value if the option is not provided.
20
+ # @parameter value [Object | Nil] A fixed value to use regardless of user input.
21
+ # @parameter type [Class | Proc | Nil] The type to coerce the value to.
22
+ # @parameter required [Boolean] Whether the option is required.
23
+ # @yields {|value| ...} An optional block to transform the parsed value.
10
24
  def initialize(flags, description, key: nil, default: nil, value: nil, type: nil, required: false, &block)
11
25
  @flags = Flags.new(flags)
12
26
  @description = description
@@ -28,17 +42,50 @@ module Samovar
28
42
  @block = block
29
43
  end
30
44
 
45
+ # The flags for this option.
46
+ #
47
+ # @attribute [Flags]
31
48
  attr :flags
49
+
50
+ # A description of the option for help output.
51
+ #
52
+ # @attribute [String]
32
53
  attr :description
54
+
55
+ # The key to use for storing the value.
56
+ #
57
+ # @attribute [Symbol]
33
58
  attr :key
59
+
60
+ # The default value if the option is not provided.
61
+ #
62
+ # @attribute [Object]
34
63
  attr :default
35
64
 
65
+ # A fixed value to use regardless of user input.
66
+ #
67
+ # @attribute [Object | Nil]
36
68
  attr :value
37
69
 
70
+ # The type to coerce the value to.
71
+ #
72
+ # @attribute [Class | Proc | Nil]
38
73
  attr :type
74
+
75
+ # Whether the option is required.
76
+ #
77
+ # @attribute [Boolean]
39
78
  attr :required
79
+
80
+ # An optional block to transform the parsed value.
81
+ #
82
+ # @attribute [Proc | Nil]
40
83
  attr :block
41
84
 
85
+ # Coerce the result to the specified type.
86
+ #
87
+ # @parameter result [Object] The value to coerce.
88
+ # @returns [Object] The coerced value.
42
89
  def coerce_type(result)
43
90
  if @type == Integer
44
91
  Integer(result)
@@ -53,6 +100,10 @@ module Samovar
53
100
  end
54
101
  end
55
102
 
103
+ # Coerce and transform the result.
104
+ #
105
+ # @parameter result [Object] The value to coerce and transform.
106
+ # @returns [Object] The coerced and transformed value.
56
107
  def coerce(result)
57
108
  if @type
58
109
  result = coerce_type(result)
@@ -65,21 +116,30 @@ module Samovar
65
116
  return result
66
117
  end
67
118
 
119
+ # Parse this option from the input.
120
+ #
121
+ # @parameter input [Array(String)] The command-line arguments.
122
+ # @parameter parent [Command | Nil] The parent command (unused, kept for compatibility).
123
+ # @parameter default [Object | Nil] An override for the default value (unused, kept for compatibility).
124
+ # @returns [Object | Nil] The parsed value.
68
125
  def parse(input, parent = nil, default = nil)
69
126
  result = @flags.parse(input)
127
+
70
128
  if result != nil
71
129
  @value.nil? ? coerce(result) : @value
72
- elsif default ||= @default
73
- return default
74
- elsif @required
75
- raise MissingValueError.new(parent, self)
76
130
  end
77
131
  end
78
132
 
133
+ # Generate a string representation for usage output.
134
+ #
135
+ # @returns [String] The usage string.
79
136
  def to_s
80
137
  @flags
81
138
  end
82
139
 
140
+ # Generate an array representation for usage output.
141
+ #
142
+ # @returns [Array] The usage array.
83
143
  def to_a
84
144
  if @default
85
145
  [@flags, @description, "(default: #{@default})"]
@@ -1,12 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2024, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'option'
6
+ require_relative "option"
7
7
 
8
8
  module Samovar
9
+ # Represents a collection of command-line options.
10
+ #
11
+ # Options provide a DSL for defining multiple option flags in a single block.
9
12
  class Options
13
+ # Parse and create an options collection from a block.
14
+ #
15
+ # @parameter arguments [Array] The arguments for the options collection.
16
+ # @parameter options [Hash] Additional options.
17
+ # @yields {|...| ...} A block that defines options using {#option}.
18
+ # @returns [Options] The frozen options collection.
10
19
  def self.parse(*arguments, **options, &block)
11
20
  options = self.new(*arguments, **options)
12
21
 
@@ -15,6 +24,10 @@ module Samovar
15
24
  return options.freeze
16
25
  end
17
26
 
27
+ # Initialize a new options collection.
28
+ #
29
+ # @parameter title [String] The title for this options group in usage output.
30
+ # @parameter key [Symbol] The key to use for storing parsed options.
18
31
  def initialize(title = "Options", key: :options)
19
32
  @title = title
20
33
  @ordered = []
@@ -27,6 +40,9 @@ module Samovar
27
40
  @defaults = {}
28
41
  end
29
42
 
43
+ # Initialize a duplicate of this options collection.
44
+ #
45
+ # @parameter source [Options] The source options to duplicate.
30
46
  def initialize_dup(source)
31
47
  super
32
48
 
@@ -35,12 +51,29 @@ module Samovar
35
51
  @defaults = @defaults.dup
36
52
  end
37
53
 
54
+ # The title for this options group in usage output.
55
+ #
56
+ # @attribute [String]
38
57
  attr :title
58
+
59
+ # The ordered list of options.
60
+ #
61
+ # @attribute [Array(Option)]
39
62
  attr :ordered
40
63
 
64
+ # The key to use for storing parsed options.
65
+ #
66
+ # @attribute [Symbol]
41
67
  attr :key
68
+
69
+ # The default values for options.
70
+ #
71
+ # @attribute [Hash]
42
72
  attr :defaults
43
73
 
74
+ # Freeze this options collection.
75
+ #
76
+ # @returns [Options] The frozen options collection.
44
77
  def freeze
45
78
  return self if frozen?
46
79
 
@@ -53,24 +86,41 @@ module Samovar
53
86
  super
54
87
  end
55
88
 
89
+ # Iterate over each option.
90
+ #
91
+ # @yields {|option| ...} Each option in the collection.
56
92
  def each(&block)
57
93
  @ordered.each(&block)
58
94
  end
59
95
 
96
+ # Check if this options collection is empty.
97
+ #
98
+ # @returns [Boolean] True if there are no options.
60
99
  def empty?
61
100
  @ordered.empty?
62
101
  end
63
102
 
103
+ # Define a new option in this collection.
104
+ #
105
+ # @parameter arguments [Array] The arguments for the option.
106
+ # @parameter options [Hash] Additional options.
107
+ # @yields {|value| ...} An optional block to transform the parsed value.
64
108
  def option(*arguments, **options, &block)
65
109
  self << Option.new(*arguments, **options, &block)
66
110
  end
67
111
 
112
+ # Merge another options collection into this one.
113
+ #
114
+ # @parameter options [Options] The options to merge.
68
115
  def merge!(options)
69
116
  options.each do |option|
70
117
  self << option
71
118
  end
72
119
  end
73
120
 
121
+ # Add an option to this collection.
122
+ #
123
+ # @parameter option [Option] The option to add.
74
124
  def << option
75
125
  @ordered << option
76
126
  option.flags.each do |flag|
@@ -86,24 +136,41 @@ module Samovar
86
136
  end
87
137
  end
88
138
 
139
+ # Parse options from the input.
140
+ #
141
+ # @parameter input [Array(String)] The command-line arguments.
142
+ # @parameter parent [Command | Nil] The parent command.
143
+ # @parameter default [Hash | Nil] Default values to use.
144
+ # @returns [Hash] The parsed option values.
89
145
  def parse(input, parent = nil, default = nil)
90
146
  values = (default || @defaults).dup
91
147
 
92
148
  while option = @keyed[input.first]
93
- prefix = input.first
149
+ # prefix = input.first
94
150
  result = option.parse(input)
95
151
  if result != nil
96
152
  values[option.key] = result
97
153
  end
98
154
  end
99
155
 
156
+ # Validate required options
157
+ @ordered.each do |option|
158
+ if option.required && !values.key?(option.key)
159
+ raise MissingValueError.new(parent, option.key)
160
+ end
161
+ end
162
+
100
163
  return values
101
- end
102
-
164
+ end # Generate a string representation for usage output.
165
+ #
166
+ # @returns [String] The usage string.
103
167
  def to_s
104
- @ordered.collect(&:to_s).join(' ')
168
+ @ordered.collect(&:to_s).join(" ")
105
169
  end
106
170
 
171
+ # Generate usage information for this options collection.
172
+ #
173
+ # @parameter rows [Output::Rows] The rows to append usage information to.
107
174
  def usage(rows)
108
175
  @ordered.each do |option|
109
176
  rows << option
@@ -4,15 +4,34 @@
4
4
  # Copyright, 2016-2023, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Namespace for output formatting classes.
7
8
  module Output
9
+ end
10
+ end
11
+
12
+ module Samovar
13
+ module Output
14
+ # Represents column widths for aligned output formatting.
15
+ #
16
+ # Calculates the maximum width of each column across all rows for proper text alignment.
8
17
  class Columns
18
+ # Initialize column width calculator.
19
+ #
20
+ # @parameter rows [Array(Array)] The rows to calculate column widths from.
9
21
  def initialize(rows)
10
22
  @rows = rows
11
23
  @widths = calculate_widths(rows)
12
24
  end
13
25
 
26
+ # The calculated column widths.
27
+ #
28
+ # @attribute [Array(Integer)]
14
29
  attr :widths
15
30
 
31
+ # Calculate the maximum width for each column.
32
+ #
33
+ # @parameter rows [Array(Array)] The rows to analyze.
34
+ # @returns [Array(Integer)] The maximum width of each column.
16
35
  def calculate_widths(rows)
17
36
  widths = []
18
37
 
@@ -5,15 +5,33 @@
5
5
 
6
6
  module Samovar
7
7
  module Output
8
+ # Represents a header row in usage output.
9
+ #
10
+ # Headers display command names and their descriptions.
8
11
  class Header
12
+ # Initialize a new header.
13
+ #
14
+ # @parameter name [String] The command name.
15
+ # @parameter object [Command] The command class.
9
16
  def initialize(name, object)
10
17
  @name = name
11
18
  @object = object
12
19
  end
13
20
 
21
+ # The command name.
22
+ #
23
+ # @attribute [String]
14
24
  attr :name
25
+
26
+ # The command class.
27
+ #
28
+ # @attribute [Command]
15
29
  attr :object
16
30
 
31
+ # Generate an aligned header string.
32
+ #
33
+ # @parameter columns [Columns] The columns for alignment (unused for headers).
34
+ # @returns [String] The command line usage string.
17
35
  def align(columns)
18
36
  @object.command_line(@name)
19
37
  end
@@ -1,22 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
7
  module Output
8
+ # Represents a row in usage output.
9
+ #
10
+ # Rows display formatted option or argument information with proper column alignment.
8
11
  class Row < Array
12
+ # Initialize a new row.
13
+ #
14
+ # @parameter object [Object] The object to convert to a row (must respond to `to_a`).
9
15
  def initialize(object)
10
16
  @object = object
11
17
  super object.to_a.collect(&:to_s)
12
18
  end
13
19
 
20
+ # The source object for this row.
21
+ #
22
+ # @attribute [Object]
14
23
  attr :object
15
24
 
25
+ # Generate an aligned row string.
26
+ #
27
+ # @parameter columns [Columns] The columns for alignment.
28
+ # @returns [String] The aligned row string.
16
29
  def align(columns)
17
30
  self.collect.with_index do |value, index|
18
31
  value.ljust(columns.widths[index])
19
- end.join(' ')
32
+ end.join(" ")
20
33
  end
21
34
  end
22
35
  end
@@ -1,40 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
- require_relative 'header'
7
- require_relative 'columns'
8
- require_relative 'row'
6
+ require_relative "header"
7
+ require_relative "columns"
8
+ require_relative "row"
9
9
 
10
10
  module Samovar
11
11
  module Output
12
+ # Represents a collection of rows for usage output.
13
+ #
14
+ # Manages hierarchical usage information with support for nesting and formatting.
12
15
  class Rows
13
16
  include Enumerable
14
17
 
18
+ # Initialize a new rows collection.
19
+ #
20
+ # @parameter level [Integer] The indentation level for this collection.
15
21
  def initialize(level = 0)
16
22
  @level = level
17
23
  @rows = []
18
24
  end
19
25
 
26
+ # The indentation level.
27
+ #
28
+ # @attribute [Integer]
20
29
  attr :level
21
30
 
31
+ # Check if this collection is empty.
32
+ #
33
+ # @returns [Boolean] True if there are no rows.
22
34
  def empty?
23
35
  @rows.empty?
24
36
  end
25
37
 
38
+ # Get the first row.
39
+ #
40
+ # @returns [Object | Nil] The first row.
26
41
  def first
27
42
  @rows.first
28
43
  end
29
44
 
45
+ # Get the last row.
46
+ #
47
+ # @returns [Object | Nil] The last row.
30
48
  def last
31
49
  @rows.last
32
50
  end
33
51
 
52
+ # Get the indentation string for this level.
53
+ #
54
+ # @returns [String] The indentation string.
34
55
  def indentation
35
56
  @indentation ||= "\t" * @level
36
57
  end
37
58
 
59
+ # Iterate over each row.
60
+ #
61
+ # @parameter ignore_nested [Boolean] Whether to skip nested rows.
62
+ # @yields {|row, rows| ...} Each row with its parent collection.
38
63
  def each(ignore_nested: false, &block)
39
64
  return to_enum(:each, ignore_nested: ignore_nested) unless block_given?
40
65
 
@@ -47,16 +72,27 @@ module Samovar
47
72
  end
48
73
  end
49
74
 
75
+ # Add a row to this collection.
76
+ #
77
+ # @parameter object [Object] The object to add as a row.
78
+ # @returns [Rows] Self.
50
79
  def << object
51
80
  @rows << Row.new(object)
52
81
 
53
82
  return self
54
83
  end
55
84
 
85
+ # Get the columns for alignment.
86
+ #
87
+ # @returns [Columns] The columns calculator.
56
88
  def columns
57
89
  @columns ||= Columns.new(@rows.select{|row| row.is_a? Array})
58
90
  end
59
91
 
92
+ # Create a nested section in the output.
93
+ #
94
+ # @parameter arguments [Array] Arguments for the header.
95
+ # @yields {|rows| ...} A block that populates the nested rows.
60
96
  def nested(*arguments)
61
97
  @rows << Header.new(*arguments)
62
98