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.
@@ -1,33 +1,45 @@
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 'mapping/model'
7
- require 'console/terminal'
6
+ require "mapping/model"
7
+ require "console/terminal"
8
8
 
9
- require_relative '../error'
9
+ require_relative "../error"
10
10
 
11
- require_relative 'header'
11
+ require_relative "header"
12
12
 
13
- require_relative 'row'
14
- require_relative 'rows'
13
+ require_relative "row"
14
+ require_relative "rows"
15
15
 
16
16
  module Samovar
17
17
  module Output
18
+ # Formats and prints usage information to a terminal.
19
+ #
20
+ # Uses the `mapping` gem to handle different output object types with custom formatting rules.
18
21
  class UsageFormatter < Mapping::Model
22
+ # Print usage information to the output.
23
+ #
24
+ # @parameter rows [Rows] The rows to format and print.
25
+ # @parameter output [IO] The output stream to print to.
26
+ # @yields {|formatter| ...} Optional block to customize the formatter.
19
27
  def self.print(rows, output)
20
- formatter = self.new(rows, output)
28
+ formatter = self.new(output)
21
29
 
22
30
  yield formatter if block_given?
23
31
 
24
- formatter.print
32
+ formatter.print(rows)
25
33
  end
26
34
 
27
- def initialize(rows, output)
28
- @rows = rows
35
+ # Initialize a new usage formatter.
36
+ #
37
+ # @parameter rows [Rows] The rows to format.
38
+ # @parameter output [IO] The output stream to print to.
39
+ def initialize(output)
29
40
  @output = output
30
41
  @width = 80
42
+ @first = true
31
43
 
32
44
  @terminal = Console::Terminal.for(@output)
33
45
  @terminal[:header] = @terminal.style(nil, nil, :bright)
@@ -45,7 +57,11 @@ module Samovar
45
57
  end
46
58
 
47
59
  map(Header) do |header, rows|
48
- @terminal.puts unless header == @rows.first
60
+ if @first
61
+ @first = false
62
+ else
63
+ @terminal.puts
64
+ end
49
65
 
50
66
  command_line = header.object.command_line(header.name)
51
67
  @terminal.puts "#{rows.indentation}#{command_line}", style: :header
@@ -64,8 +80,10 @@ module Samovar
64
80
  items.collect{|row, rows| map(row, rows)}
65
81
  end
66
82
 
67
- def print
68
- map(@rows)
83
+ # Print the formatted usage output.
84
+ def print(rows, first: @first)
85
+ @first = first
86
+ map(rows)
69
87
  end
70
88
  end
71
89
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'output/usage_formatter'
6
+ require_relative "output/usage_formatter"
data/lib/samovar/split.rb CHANGED
@@ -1,11 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
6
  module Samovar
7
+ # Represents a split point in the command-line arguments.
8
+ #
9
+ # A `Split` parser divides the argument list at a marker (typically `--`), allowing you to separate arguments meant for your command from those passed to another tool.
7
10
  class Split
8
- def initialize(key, description, marker: '--', default: nil, required: false)
11
+ # Initialize a new split parser.
12
+ #
13
+ # @parameter key [Symbol] The name of the attribute to store the values after the split.
14
+ # @parameter description [String] A description of the split for help output.
15
+ # @parameter marker [String] The marker that indicates the split point.
16
+ # @parameter default [Object] The default value if no split is present.
17
+ # @parameter required [Boolean] Whether the split is required.
18
+ def initialize(key, description, marker: "--", default: nil, required: false)
9
19
  @key = key
10
20
  @description = description
11
21
  @marker = marker
@@ -13,16 +23,41 @@ module Samovar
13
23
  @required = required
14
24
  end
15
25
 
26
+ # The name of the attribute to store the values after the split.
27
+ #
28
+ # @attribute [Symbol]
16
29
  attr :key
30
+
31
+ # A description of the split for help output.
32
+ #
33
+ # @attribute [String]
17
34
  attr :description
35
+
36
+ # The marker that indicates the split point.
37
+ #
38
+ # @attribute [String]
18
39
  attr :marker
40
+
41
+ # The default value if no split is present.
42
+ #
43
+ # @attribute [Object]
19
44
  attr :default
45
+
46
+ # Whether the split 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
  "#{@marker} <#{@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 arguments after the split marker.
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 [Array(String) | Object | Nil] The arguments after the split, or the default if no split.
38
79
  def parse(input, parent = nil, default = nil)
39
80
  if offset = input.index(@marker)
40
81
  input.pop(input.size - offset).tap(&: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
data/lib/samovar/table.rb CHANGED
@@ -1,10 +1,18 @@
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
6
  module Samovar
7
+ # Represents a table of parsing rows for a command.
8
+ #
9
+ # A table manages the collection of options, arguments, and nested commands that define how to parse a command line.
7
10
  class Table
11
+ # Create a nested table that inherits from the parent class's table.
12
+ #
13
+ # @parameter klass [Class] The command class to create a table for.
14
+ # @parameter parent [Table | Nil] The parent table to inherit from.
15
+ # @returns [Table] The new table.
8
16
  def self.nested(klass, parent = nil)
9
17
  if klass.superclass.respond_to?(:table)
10
18
  parent = klass.superclass.table
@@ -13,12 +21,19 @@ module Samovar
13
21
  self.new(parent, name: klass.name)
14
22
  end
15
23
 
24
+ # Initialize a new table.
25
+ #
26
+ # @parameter parent [Table | Nil] The parent table to inherit from.
27
+ # @parameter name [String | Nil] The name of the command this table belongs to.
16
28
  def initialize(parent = nil, name: nil)
17
29
  @parent = parent
18
30
  @name = name
19
31
  @rows = {}
20
32
  end
21
33
 
34
+ # Freeze this table.
35
+ #
36
+ # @returns [Table] The frozen table.
22
37
  def freeze
23
38
  return self if frozen?
24
39
 
@@ -27,14 +42,24 @@ module Samovar
27
42
  super
28
43
  end
29
44
 
45
+ # Get a row by key.
46
+ #
47
+ # @parameter key [Symbol] The key to look up.
48
+ # @returns [Object | Nil] The row with the given key.
30
49
  def [] key
31
50
  @rows[key]
32
51
  end
33
52
 
53
+ # Iterate over each row.
54
+ #
55
+ # @yields {|row| ...} Each row in the table.
34
56
  def each(&block)
35
57
  @rows.each_value(&block)
36
58
  end
37
59
 
60
+ # Add a row to the table.
61
+ #
62
+ # @parameter row The row to add.
38
63
  def << row
39
64
  if existing_row = @rows[row.key] and existing_row.respond_to?(:merge!)
40
65
  existing_row.merge!(row)
@@ -44,10 +69,17 @@ module Samovar
44
69
  end
45
70
  end
46
71
 
72
+ # Check if this table is empty.
73
+ #
74
+ # @returns [Boolean] True if this table and its parent are empty.
47
75
  def empty?
48
76
  @rows.empty? && @parent&.empty?
49
77
  end
50
78
 
79
+ # Merge this table's rows into another table.
80
+ #
81
+ # @parameter table [Table] The table to merge into.
82
+ # @returns [Table] The merged table.
51
83
  def merge_into(table)
52
84
  @parent&.merge_into(table)
53
85
 
@@ -58,6 +90,9 @@ module Samovar
58
90
  return table
59
91
  end
60
92
 
93
+ # Get a merged table that includes parent rows.
94
+ #
95
+ # @returns [Table] The merged table.
61
96
  def merged
62
97
  if @parent.nil? or @parent.empty?
63
98
  return self
@@ -66,10 +101,17 @@ module Samovar
66
101
  end
67
102
  end
68
103
 
104
+ # Generate a usage string from all rows.
105
+ #
106
+ # @returns [String] The usage string.
69
107
  def usage
70
- @rows.each_value.collect(&:to_s).reject(&:empty?).join(' ')
108
+ @rows.each_value.collect(&:to_s).reject(&:empty?).join(" ")
71
109
  end
72
110
 
111
+ # Parse the input according to the rows in this table.
112
+ #
113
+ # @parameter input [Array(String)] The command-line arguments.
114
+ # @parameter parent [Command] The parent command to store results in.
73
115
  def parse(input, parent)
74
116
  @rows.each do |key, row|
75
117
  next unless row.respond_to?(:parse)
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
  # Copyright, 2018, by Gabriel Mazetto.
6
6
 
7
+ # @namespace
7
8
  module Samovar
8
- VERSION = "2.3.0"
9
+ VERSION = "2.4.0"
9
10
  end
data/lib/samovar.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2016-2023, by Samuel Williams.
4
+ # Copyright, 2016-2025, by Samuel Williams.
5
5
 
6
- require_relative 'samovar/version'
7
- require_relative 'samovar/command'
6
+ require_relative "samovar/version"
7
+ require_relative "samovar/command"
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2016-2024, by Samuel Williams.
3
+ Copyright, 2016-2025, by Samuel Williams.
4
4
  Copyright, 2018, by Gabriel Mazetto.
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/readme.md CHANGED
@@ -12,165 +12,17 @@ I've been using [Optimist](https://github.com/ManageIQ/optimist) and while it's
12
12
 
13
13
  One of the other issues I had with existing frameworks is testability. Most frameworks expect to have some pretty heavy logic directly in the binary executable, or at least don't structure your code in a way which makes testing easy. Samovar structures your command processing logic into classes which can be easily tested in isolation, which means that you can mock up and [spec your command-line executables easily](https://github.com/ioquatix/teapot/blob/master/spec/teapot/command_spec.rb).
14
14
 
15
- ## Examples
16
-
17
- - [Teapot](https://github.com/ioquatix/teapot/blob/master/lib/teapot/command.rb) is a build system and uses multiple top-level commands.
18
- - [Utopia](https://github.com/ioquatix/utopia/blob/master/lib/utopia/command.rb) is a web application platform and uses nested commands.
19
- - [Synco](https://github.com/ioquatix/synco/blob/master/lib/synco/command.rb) is a backup tool and sends commands across the network and has lots of options with default values.
20
-
21
- ## Installation
22
-
23
- Add this line to your application's Gemfile:
24
-
25
- gem 'samovar'
26
-
27
- And then execute:
28
-
29
- $ bundle
30
-
31
- Or install it yourself as:
32
-
33
- $ gem install samovar
34
-
35
15
  ## Usage
36
16
 
37
- Generally speaking, you should create `Command` classes that represent specific functions in your program. The top level command might look something like this:
17
+ Please see the [project documentation](https://ioquatix.github.io/samovar/) for more details.
38
18
 
39
- ``` ruby
40
- require 'samovar'
41
-
42
- class List < Samovar::Command
43
- self.description = "List the current directory"
44
-
45
- def call
46
- system("ls -lah")
47
- end
48
- end
49
-
50
- class Application < Samovar::Command
51
- options do
52
- option '--help', "Do you need help?"
53
- end
54
-
55
- nested :command, {
56
- 'list' => List
57
- }, default: 'list'
58
-
59
- def call
60
- if @options[:help]
61
- self.print_usage
62
- else
63
- @command.call
64
- end
65
- end
66
- end
67
-
68
- Application.call # Defaults to ARGV.
69
- ```
70
-
71
- ### Basic Options
72
-
73
- ``` ruby
74
- require 'samovar'
75
-
76
- class Application < Samovar::Command
77
- options do
78
- option '-f/--frobulate <text>', "Frobulate the text"
79
- option '-x | -y', "Specify either x or y axis.", key: :axis
80
- option '-F/--yeah/--flag', "A boolean flag with several forms."
81
- option '--things <a,b,c>', "A list of things" do |value|
82
- value.split(/\s*,\s*/)
83
- end
84
- end
85
- end
86
-
87
- application = Application.new(['-f', 'Algebraic!'])
88
- application.options[:frobulate] # 'Algebraic!'
89
-
90
- application = Application.new(['-x', '-y'])
91
- application.options[:axis] # :y
92
-
93
- application = Application.new(['-F'])
94
- application.options[:flag] # true
95
-
96
- application = Application.new(['--things', 'x,y,z'])
97
- application.options[:things] # ['x', 'y', 'z']
98
- ```
99
-
100
- ### Nested Commands
101
-
102
- ``` ruby
103
- require 'samovar'
104
-
105
- class Create < Samovar::Command
106
- def invoke(parent)
107
- puts "Creating"
108
- end
109
- end
110
-
111
- class Application < Samovar::Command
112
- nested '<command>',
113
- 'create' => Create
114
-
115
- def invoke(program_name: File.basename($0))
116
- if @command
117
- @command.invoke
118
- else
119
- print_usage(program_name)
120
- end
121
- end
122
- end
123
-
124
- Application.new(['create']).invoke
125
- ```
19
+ - [Getting Started](https://ioquatix.github.io/samovar/guides/getting-started/index) - This guide explains how to use `samovar` to build command-line tools and applications.
126
20
 
127
- ### ARGV Splits
21
+ ## See Also
128
22
 
129
- ``` ruby
130
- require 'samovar'
131
-
132
- class Application < Samovar::Command
133
- many :packages
134
- split :argv
135
- end
136
-
137
- application = Application.new(['foo', 'bar', 'baz', '--', 'apples', 'oranges', 'feijoas'])
138
- application.packages # ['foo', 'bar', 'baz']
139
- application.argv # ['apples', 'oranges', 'feijoas']
140
- ```
141
-
142
- ### Parsing Tokens
143
-
144
- ``` ruby
145
- require 'samovar'
146
-
147
- class Application < Samovar::Command
148
- self.description = "Mix together your favorite things."
149
-
150
- one :fruit, "Name one fruit"
151
- many :cakes, "Any cakes you like"
152
- end
153
-
154
- application = Application.new(['apple', 'chocolate cake', 'fruit cake'])
155
- application.fruit # 'apple'
156
- application.cakes # ['chocolate cake', 'fruit cake']
157
- ```
158
-
159
- ### Explicit Commands
160
-
161
- Given a custom `Samovar::Command` subclass, you can instantiate it with options:
162
-
163
- ``` ruby
164
- application = Application['--root', path]
165
- ```
166
-
167
- You can also duplicate an existing command instance with additions/changes:
168
-
169
- ``` ruby
170
- concurrent_application = application['--threads', 12]
171
- ```
172
-
173
- These forms can be useful when invoking one command from another, or in unit tests.
23
+ - [Teapot](https://github.com/ioquatix/teapot/blob/master/lib/teapot/command.rb) is a build system and uses multiple top-level commands.
24
+ - [Synco](https://github.com/ioquatix/synco/blob/master/lib/synco/command.rb) is a backup tool and sends commands across the network and has lots of options with default values.
25
+ - [Bake](https://github.com/ioquatix/bake) is an alternative task runner that makes it easy to expose Ruby methods as command-line tasks.
174
26
 
175
27
  ## Contributing
176
28
 
@@ -184,13 +36,13 @@ We welcome contributions to this project.
184
36
 
185
37
  ### Developer Certificate of Origin
186
38
 
187
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
39
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
188
40
 
189
- ### Contributor Covenant
41
+ ### Community Guidelines
190
42
 
191
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
43
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
192
44
 
193
- ## Future Work
45
+ ### Future Work
194
46
 
195
47
  ### Multi-value Options
196
48
 
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,12 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samovar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Gabriel Mazetto
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain:
12
11
  - |
@@ -38,7 +37,7 @@ cert_chain:
38
37
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
39
38
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
40
39
  -----END CERTIFICATE-----
41
- date: 2024-03-28 00:00:00.000000000 Z
40
+ date: 1980-01-02 00:00:00.000000000 Z
42
41
  dependencies:
43
42
  - !ruby/object:Gem::Dependency
44
43
  name: console
@@ -68,8 +67,6 @@ dependencies:
68
67
  - - "~>"
69
68
  - !ruby/object:Gem::Version
70
69
  version: '1.0'
71
- description:
72
- email:
73
70
  executables: []
74
71
  extensions: []
75
72
  extra_rdoc_files: []
@@ -99,8 +96,9 @@ homepage: https://github.com/ioquatix/samovar
99
96
  licenses:
100
97
  - MIT
101
98
  metadata:
99
+ documentation_uri: https://ioquatix.github.io/samovar/
102
100
  funding_uri: https://github.com/sponsors/ioquatix/
103
- post_install_message:
101
+ source_code_uri: https://github.com/ioquatix/samovar.git
104
102
  rdoc_options: []
105
103
  require_paths:
106
104
  - lib
@@ -108,15 +106,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
106
  requirements:
109
107
  - - ">="
110
108
  - !ruby/object:Gem::Version
111
- version: '3.0'
109
+ version: '3.2'
112
110
  required_rubygems_version: !ruby/object:Gem::Requirement
113
111
  requirements:
114
112
  - - ">="
115
113
  - !ruby/object:Gem::Version
116
114
  version: '0'
117
115
  requirements: []
118
- rubygems_version: 3.5.3
119
- signing_key:
116
+ rubygems_version: 3.6.9
120
117
  specification_version: 4
121
118
  summary: Samovar is a flexible option parser excellent support for sub-commands and
122
119
  help documentation.
metadata.gz.sig CHANGED
Binary file