git 1.19.1 → 5.5.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/.yardopts +5 -1
- data/AI_POLICY.md +24 -0
- data/CHANGELOG.md +689 -0
- data/CODE_OF_CONDUCT.md +25 -0
- data/CONTRIBUTING.md +1175 -97
- data/GOVERNANCE.md +106 -0
- data/LICENSE +1 -1
- data/MAINTAINERS.md +17 -4
- data/README.md +476 -320
- data/UPGRADING.md +1138 -0
- data/git.gemspec +124 -36
- data/lib/git/author.rb +39 -7
- data/lib/git/author_info.rb +66 -0
- data/lib/git/branch.rb +615 -65
- data/lib/git/branch_delete_failure.rb +34 -0
- data/lib/git/branch_delete_result.rb +66 -0
- data/lib/git/branch_info.rb +237 -0
- data/lib/git/branches.rb +167 -44
- data/lib/git/command_line/base.rb +247 -0
- data/lib/git/command_line/capturing.rb +308 -0
- data/lib/git/command_line/result.rb +88 -0
- data/lib/git/command_line/streaming.rb +236 -0
- data/lib/git/command_line.rb +52 -0
- data/lib/git/commands/add.rb +139 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +263 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +49 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +242 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +145 -0
- data/lib/git/commands/arguments.rb +4521 -0
- data/lib/git/commands/base.rb +502 -0
- data/lib/git/commands/branch/copy.rb +102 -0
- data/lib/git/commands/branch/create.rb +177 -0
- data/lib/git/commands/branch/delete.rb +88 -0
- data/lib/git/commands/branch/list.rb +178 -0
- data/lib/git/commands/branch/move.rb +102 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +53 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +385 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +271 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +153 -0
- data/lib/git/commands/checkout/files.rb +116 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +106 -0
- data/lib/git/commands/clean.rb +102 -0
- data/lib/git/commands/clone.rb +241 -0
- data/lib/git/commands/commit.rb +273 -0
- data/lib/git/commands/commit_tree.rb +101 -0
- data/lib/git/commands/config_option_syntax/add.rb +86 -0
- data/lib/git/commands/config_option_syntax/get.rb +121 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +118 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +95 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +96 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +119 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +111 -0
- data/lib/git/commands/config_option_syntax/list.rb +111 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +79 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +83 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +109 -0
- data/lib/git/commands/config_option_syntax/set.rb +119 -0
- data/lib/git/commands/config_option_syntax/unset.rb +92 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +94 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +156 -0
- data/lib/git/commands/diff.rb +657 -0
- data/lib/git/commands/diff_files.rb +519 -0
- data/lib/git/commands/diff_index.rb +499 -0
- data/lib/git/commands/fetch.rb +354 -0
- data/lib/git/commands/fsck.rb +138 -0
- data/lib/git/commands/gc.rb +134 -0
- data/lib/git/commands/grep.rb +339 -0
- data/lib/git/commands/init.rb +101 -0
- data/lib/git/commands/log.rb +634 -0
- data/lib/git/commands/ls_files.rb +195 -0
- data/lib/git/commands/ls_remote.rb +161 -0
- data/lib/git/commands/ls_tree.rb +135 -0
- data/lib/git/commands/maintenance/register.rb +77 -0
- data/lib/git/commands/maintenance/run.rb +109 -0
- data/lib/git/commands/maintenance/start.rb +71 -0
- data/lib/git/commands/maintenance/stop.rb +60 -0
- data/lib/git/commands/maintenance/unregister.rb +84 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +250 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +91 -0
- data/lib/git/commands/mv.rb +82 -0
- data/lib/git/commands/name_rev.rb +119 -0
- data/lib/git/commands/pull.rb +382 -0
- data/lib/git/commands/push.rb +251 -0
- data/lib/git/commands/read_tree.rb +154 -0
- data/lib/git/commands/remote/add.rb +96 -0
- data/lib/git/commands/remote/get_url.rb +68 -0
- data/lib/git/commands/remote/list.rb +56 -0
- data/lib/git/commands/remote/prune.rb +63 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +76 -0
- data/lib/git/commands/remote/set_branches.rb +70 -0
- data/lib/git/commands/remote/set_head.rb +89 -0
- data/lib/git/commands/remote/set_url.rb +78 -0
- data/lib/git/commands/remote/set_url_add.rb +70 -0
- data/lib/git/commands/remote/set_url_delete.rb +71 -0
- data/lib/git/commands/remote/show.rb +77 -0
- data/lib/git/commands/remote/update.rb +79 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +281 -0
- data/lib/git/commands/reset.rb +154 -0
- data/lib/git/commands/rev_parse.rb +304 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +62 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +158 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +113 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +119 -0
- data/lib/git/commands/show_ref/exists.rb +80 -0
- data/lib/git/commands/show_ref/list.rb +149 -0
- data/lib/git/commands/show_ref/verify.rb +122 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +81 -0
- data/lib/git/commands/stash/branch.rb +67 -0
- data/lib/git/commands/stash/clear.rb +43 -0
- data/lib/git/commands/stash/create.rb +60 -0
- data/lib/git/commands/stash/drop.rb +73 -0
- data/lib/git/commands/stash/list.rb +43 -0
- data/lib/git/commands/stash/pop.rb +87 -0
- data/lib/git/commands/stash/push.rb +112 -0
- data/lib/git/commands/stash/show.rb +158 -0
- data/lib/git/commands/stash/store.rb +72 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +174 -0
- data/lib/git/commands/symbolic_ref/delete.rb +72 -0
- data/lib/git/commands/symbolic_ref/read.rb +99 -0
- data/lib/git/commands/symbolic_ref/update.rb +79 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +142 -0
- data/lib/git/commands/tag/delete.rb +57 -0
- data/lib/git/commands/tag/list.rb +146 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +145 -0
- data/lib/git/commands/update_ref/delete.rb +90 -0
- data/lib/git/commands/update_ref/update.rb +103 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +60 -0
- data/lib/git/commands/worktree/add.rb +139 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +88 -0
- data/lib/git/config.rb +72 -5
- data/lib/git/config_entry_info.rb +106 -0
- data/lib/git/configuring.rb +795 -0
- data/lib/git/detached_head_info.rb +57 -0
- data/lib/git/diff.rb +437 -86
- data/lib/git/diff_file_numstat_info.rb +31 -0
- data/lib/git/diff_file_patch_info.rb +136 -0
- data/lib/git/diff_file_raw_info.rb +129 -0
- data/lib/git/diff_info.rb +162 -0
- data/lib/git/diff_path_status.rb +107 -0
- data/lib/git/diff_result.rb +34 -0
- data/lib/git/diff_stats.rb +111 -0
- data/lib/git/dirstat_info.rb +102 -0
- data/lib/git/encoding_utils.rb +32 -1
- data/lib/git/errors.rb +285 -0
- data/lib/git/escaped_path.rb +57 -5
- data/lib/git/execution_context/global.rb +31 -0
- data/lib/git/execution_context/repository.rb +151 -0
- data/lib/git/execution_context.rb +559 -0
- data/lib/git/factories.rb +813 -0
- data/lib/git/file_ref.rb +77 -0
- data/lib/git/fsck_object.rb +56 -0
- data/lib/git/fsck_result.rb +132 -0
- data/lib/git/log.rb +306 -90
- data/lib/git/object.rb +563 -141
- data/lib/git/parsers/branch.rb +240 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/config_entry.rb +110 -0
- data/lib/git/parsers/diff.rb +792 -0
- data/lib/git/parsers/fsck.rb +144 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_remote.rb +79 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/remote.rb +162 -0
- data/lib/git/parsers/stash.rb +292 -0
- data/lib/git/parsers/status.rb +251 -0
- data/lib/git/parsers/tag.rb +341 -0
- data/lib/git/parsers/worktree.rb +185 -0
- data/lib/git/path_resolver.rb +206 -0
- data/lib/git/remote.rb +165 -12
- data/lib/git/remote_info.rb +203 -0
- data/lib/git/repository/branching.rb +964 -0
- data/lib/git/repository/committing.rb +246 -0
- data/lib/git/repository/context_helpers.rb +293 -0
- data/lib/git/repository/diffing.rb +785 -0
- data/lib/git/repository/inspecting.rb +252 -0
- data/lib/git/repository/logging.rb +410 -0
- data/lib/git/repository/maintenance.rb +65 -0
- data/lib/git/repository/merging.rb +451 -0
- data/lib/git/repository/object_operations.rb +1551 -0
- data/lib/git/repository/remote_operations.rb +984 -0
- data/lib/git/repository/shared_private.rb +120 -0
- data/lib/git/repository/staging.rb +587 -0
- data/lib/git/repository/stashing.rb +623 -0
- data/lib/git/repository/status_operations.rb +249 -0
- data/lib/git/repository/worktree_operations.rb +339 -0
- data/lib/git/repository.rb +484 -2
- data/lib/git/stash.rb +109 -12
- data/lib/git/stash_info.rb +102 -0
- data/lib/git/stashes.rb +169 -26
- data/lib/git/status.rb +308 -122
- data/lib/git/status_file_info.rb +258 -0
- data/lib/git/status_info.rb +189 -0
- data/lib/git/tag_delete_failure.rb +34 -0
- data/lib/git/tag_delete_result.rb +66 -0
- data/lib/git/tag_info.rb +99 -0
- data/lib/git/url.rb +15 -8
- data/lib/git/version.rb +113 -2
- data/lib/git/version_constraint.rb +85 -0
- data/lib/git/worktree.rb +150 -8
- data/lib/git/worktree_info.rb +128 -0
- data/lib/git/worktrees.rb +118 -13
- data/lib/git.rb +632 -234
- metadata +369 -54
- data/.github/stale.yml +0 -25
- data/.github/workflows/continuous_integration.yml +0 -49
- data/.gitignore +0 -10
- data/Dockerfile.changelog-rs +0 -12
- data/Gemfile +0 -5
- data/ISSUE_TEMPLATE.md +0 -15
- data/PULL_REQUEST_TEMPLATE.md +0 -9
- data/RELEASING.md +0 -70
- data/Rakefile +0 -60
- data/lib/git/base/factory.rb +0 -99
- data/lib/git/base.rb +0 -711
- data/lib/git/command_line_result.rb +0 -86
- data/lib/git/failed_error.rb +0 -53
- data/lib/git/git_execute_error.rb +0 -7
- data/lib/git/index.rb +0 -5
- data/lib/git/lib.rb +0 -1328
- data/lib/git/path.rb +0 -31
- data/lib/git/signaled_error.rb +0 -50
- data/lib/git/working_directory.rb +0 -4
|
@@ -0,0 +1,4521 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Git
|
|
4
|
+
module Commands
|
|
5
|
+
# rubocop:disable Metrics/ParameterLists
|
|
6
|
+
|
|
7
|
+
# This class provides a DSL for mapping Ruby method arguments to git command-line
|
|
8
|
+
# arguments.
|
|
9
|
+
#
|
|
10
|
+
# ## Overview
|
|
11
|
+
#
|
|
12
|
+
# This class provides a DSL for defining how arguments passed to {#bind} should
|
|
13
|
+
# be mapped to git CLI argument arrays. The process follows four phases:
|
|
14
|
+
#
|
|
15
|
+
# 1. **Definition** of expected CLI arguments and their constraints
|
|
16
|
+
# 2. **Binding** of method arguments to the definition
|
|
17
|
+
# 3. **Validation** of values against argument constraints
|
|
18
|
+
# 4. **Building** of the CLI argument array
|
|
19
|
+
#
|
|
20
|
+
# See {Git::Commands::Init} for a usage example.
|
|
21
|
+
#
|
|
22
|
+
# For example, defining arguments for a command:
|
|
23
|
+
#
|
|
24
|
+
# ```ruby
|
|
25
|
+
# # 1. Definition of expected CLI arguments and their constraints
|
|
26
|
+
# args_def = Arguments.define do
|
|
27
|
+
# flag_option :force
|
|
28
|
+
# value_option :branch
|
|
29
|
+
# operand :repository, required: true
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # 2. Binding of method arguments to the definition
|
|
33
|
+
# # 3. Validation of values against argument constraints
|
|
34
|
+
# args = args_def.bind('https://github.com/user/repo', force: true, branch: 'main')
|
|
35
|
+
#
|
|
36
|
+
# # 4. Building of the CLI argument array
|
|
37
|
+
# args.to_a # => ['--force', '--branch', 'main', 'https://github.com/user/repo']
|
|
38
|
+
#
|
|
39
|
+
# # Bonus: accessing bound values
|
|
40
|
+
# args.force? # => true
|
|
41
|
+
# args.branch # => 'main'
|
|
42
|
+
# args.repository # => 'https://github.com/user/repo'
|
|
43
|
+
# ```
|
|
44
|
+
#
|
|
45
|
+
# ## Terminology
|
|
46
|
+
#
|
|
47
|
+
# This class bridges CLI and Ruby interfaces. While both use the term "arguments"
|
|
48
|
+
# for values passed to commands/methods, they differ in terminology for specific
|
|
49
|
+
# argument types:
|
|
50
|
+
#
|
|
51
|
+
# | CLI (POSIX) | Ruby Interface | Description |
|
|
52
|
+
# | ---------------------- | ---------------------- | --------------------------------------------------- |
|
|
53
|
+
# | argument specification | DSL definition | Declared command inputs and constraints |
|
|
54
|
+
# | arguments | arguments | Values passed when calling a command/method |
|
|
55
|
+
# | operands | positional arguments | Arguments identified by position |
|
|
56
|
+
# | options | keyword arguments | Arguments identified by name (`--force` / `force:`) |
|
|
57
|
+
#
|
|
58
|
+
# The following sections explain each interface in detail.
|
|
59
|
+
#
|
|
60
|
+
# ### CLI Interface (POSIX)
|
|
61
|
+
#
|
|
62
|
+
# An **argument specification** declares what command inputs are accepted and
|
|
63
|
+
# their constraints.
|
|
64
|
+
#
|
|
65
|
+
# For example:
|
|
66
|
+
#
|
|
67
|
+
# ```text
|
|
68
|
+
# git branch (--set-upstream-to=<upstream>|-u <upstream>) [<branch-name>]
|
|
69
|
+
# ```
|
|
70
|
+
#
|
|
71
|
+
# When a command is invoked, **arguments** are the values passed to it:
|
|
72
|
+
# - **Arguments**: Values passed when calling the command (everything after the
|
|
73
|
+
# command name)
|
|
74
|
+
# - **Operands**: Arguments identified by position
|
|
75
|
+
# - **Options**: Arguments identified by name (prefixed with `-` or `--`)
|
|
76
|
+
#
|
|
77
|
+
# For example:
|
|
78
|
+
#
|
|
79
|
+
# ```shell
|
|
80
|
+
# git branch --set-upstream-to=origin/main main
|
|
81
|
+
# ```
|
|
82
|
+
#
|
|
83
|
+
# - Operands: `main`
|
|
84
|
+
# - Options: `--set-upstream-to=origin/main`
|
|
85
|
+
#
|
|
86
|
+
# ### Ruby Interface
|
|
87
|
+
#
|
|
88
|
+
# A **DSL definition** declares what arguments the {#bind} method accepts and how
|
|
89
|
+
# they map to CLI arguments.
|
|
90
|
+
#
|
|
91
|
+
# For example:
|
|
92
|
+
#
|
|
93
|
+
# ```ruby
|
|
94
|
+
# Arguments.define do
|
|
95
|
+
# literal 'branch'
|
|
96
|
+
# value_option %i[set_upstream_to u], inline: true # primary name with short alias :u
|
|
97
|
+
# operand :branch_name
|
|
98
|
+
# end
|
|
99
|
+
# ```
|
|
100
|
+
#
|
|
101
|
+
# When {#bind} is called, **arguments** are the values passed to it:
|
|
102
|
+
# - **Arguments**: Values passed to {#bind}
|
|
103
|
+
# - **Positional arguments**: Arguments identified by position
|
|
104
|
+
# - **Keyword arguments**: Arguments identified by name
|
|
105
|
+
#
|
|
106
|
+
# For example:
|
|
107
|
+
#
|
|
108
|
+
# ```ruby
|
|
109
|
+
# args_def.bind('main', set_upstream_to: 'origin/main')
|
|
110
|
+
# ```
|
|
111
|
+
#
|
|
112
|
+
# - Positional argument: `'main'`
|
|
113
|
+
# - Keyword argument: `set_upstream_to: 'origin/main'`
|
|
114
|
+
#
|
|
115
|
+
# Calling {Bound#to_a} on the bound result produces the CLI argument array:
|
|
116
|
+
#
|
|
117
|
+
# ```ruby
|
|
118
|
+
# args_def.bind('main', set_upstream_to: 'origin/main').to_a
|
|
119
|
+
# # => ['branch', '--set-upstream-to=origin/main', 'main']
|
|
120
|
+
# ```
|
|
121
|
+
#
|
|
122
|
+
# ## Design
|
|
123
|
+
#
|
|
124
|
+
# The class operates in two stages:
|
|
125
|
+
#
|
|
126
|
+
# 1. **Definition stage**: DSL methods ({#flag_option}, {#value_option}, {#operand}, etc.)
|
|
127
|
+
# record argument definitions in internal data structures.
|
|
128
|
+
#
|
|
129
|
+
# 2. **Bind stage**: {#bind} binds Ruby values and validates them against constraints,
|
|
130
|
+
# returning a {Bound} object.
|
|
131
|
+
#
|
|
132
|
+
# The returned {Bound} object provides accessor methods for the bound values and handles
|
|
133
|
+
# the building phase, converting bound values to CLI arguments via {Bound#to_a}.
|
|
134
|
+
#
|
|
135
|
+
# Key internal components:
|
|
136
|
+
#
|
|
137
|
+
# - `@ordered_definitions`: Array tracking all definitions in definition order
|
|
138
|
+
# - `@option_definitions`: Hash mapping option names to their definitions
|
|
139
|
+
# - `@operand_definitions`: Array of operand (positional argument) definitions
|
|
140
|
+
# - `@alias_map`: Maps option aliases to their primary names
|
|
141
|
+
# - `BUILDERS`: Hash of lambdas that convert values to CLI arguments by type
|
|
142
|
+
# - {OperandAllocator}: Handles Ruby-like operand allocation
|
|
143
|
+
#
|
|
144
|
+
# ## Argument Ordering
|
|
145
|
+
#
|
|
146
|
+
# Arguments are rendered in the exact order they are defined in the DSL block,
|
|
147
|
+
# regardless of type (options, operands, or static flags). This is important
|
|
148
|
+
# for git commands where argument order matters, such as when using `--` to
|
|
149
|
+
# separate options from pathspecs.
|
|
150
|
+
#
|
|
151
|
+
# Use {#end_of_options} to emit `--` only when at least one following operand
|
|
152
|
+
# produces output, or {#literal} with `'--'` when `--` must always be present.
|
|
153
|
+
#
|
|
154
|
+
# @example Ordering example (end_of_options emits '--' only when path is present)
|
|
155
|
+
# args_def = Arguments.define do
|
|
156
|
+
# operand :ref
|
|
157
|
+
# end_of_options
|
|
158
|
+
# operand :path
|
|
159
|
+
# end
|
|
160
|
+
# args_def.bind('HEAD', 'file.txt').to_a # => ['HEAD', '--', 'file.txt']
|
|
161
|
+
# args_def.bind('HEAD').to_a # => ['HEAD'] # (no trailing --)
|
|
162
|
+
#
|
|
163
|
+
# ## Short Option Detection
|
|
164
|
+
#
|
|
165
|
+
# Option names are automatically formatted using POSIX conventions:
|
|
166
|
+
#
|
|
167
|
+
# - **Single-character names** use single-dash prefix: `:f` → `-f`
|
|
168
|
+
# - **Multi-character names** use double-dash prefix: `:force` → `--force`
|
|
169
|
+
#
|
|
170
|
+
# For inline values (`inline: true`), the separator also follows POSIX
|
|
171
|
+
# conventions:
|
|
172
|
+
#
|
|
173
|
+
# - **Short options** use no separator: `-n3`
|
|
174
|
+
# - **Long options** use `=` separator: `--name=value`
|
|
175
|
+
#
|
|
176
|
+
# Negated flags always use double-dash format (e.g., `-f` → `--no-f` when false).
|
|
177
|
+
#
|
|
178
|
+
# The `as:` parameter can override this automatic detection when needed.
|
|
179
|
+
#
|
|
180
|
+
# @example Short option detection
|
|
181
|
+
# args_def = Arguments.define do
|
|
182
|
+
# flag_option :f # true → '-f'
|
|
183
|
+
# flag_option :force # true → '--force'
|
|
184
|
+
# value_option :n, inline: true # 3 → '-n3'
|
|
185
|
+
# value_option :name, inline: true # 'test' → '--name=test'
|
|
186
|
+
# end
|
|
187
|
+
#
|
|
188
|
+
# args_def.bind(f: true, force: true, n: 3, name: 'test').to_a
|
|
189
|
+
# # => ['-f', '--force', '-n3', '--name=test']
|
|
190
|
+
#
|
|
191
|
+
# @example Explicit override with `as:`
|
|
192
|
+
# args_def = Arguments.define do
|
|
193
|
+
# flag_option :f, as: '--force'
|
|
194
|
+
# end
|
|
195
|
+
# args_def.bind(f: true).to_a # => ['--force']
|
|
196
|
+
#
|
|
197
|
+
# ## Option Types
|
|
198
|
+
#
|
|
199
|
+
# The DSL supports several option types with modifiers:
|
|
200
|
+
#
|
|
201
|
+
# ### Primary Option Types
|
|
202
|
+
# - {#flag_option} - Boolean flag (--flag when true, with `negatable: true` for --no-flag)
|
|
203
|
+
# - {#value_option} - Valued option (--flag value, with `inline: true` for --flag=value,
|
|
204
|
+
# or `as_operand: true` for operands)
|
|
205
|
+
# - {#flag_or_value_option} - Flag or value (--flag when true, --flag value when string,
|
|
206
|
+
# with `inline: true` and/or `negatable: true` modifiers)
|
|
207
|
+
# - {#key_value_option} - Key-value option that can be repeated (--trailer key=value)
|
|
208
|
+
# - {#literal} - Literal string always included in output
|
|
209
|
+
# - {#custom_option} - Custom option with builder block
|
|
210
|
+
# - {#execution_option} - Execution option (not included in CLI output, forwarded to command execution)
|
|
211
|
+
#
|
|
212
|
+
# {#value_option} supports a `repeatable: true` parameter that allows the option to accept
|
|
213
|
+
# an array of values. This repeats the flag for each value (or outputs each as an
|
|
214
|
+
# operand when using `as_operand: true`):
|
|
215
|
+
#
|
|
216
|
+
# Repeatable options:
|
|
217
|
+
#
|
|
218
|
+
# ```ruby
|
|
219
|
+
# value_option :config, repeatable: true
|
|
220
|
+
# # config: ['a=b', 'c=d'] => ['--config', 'a=b', '--config', 'c=d']
|
|
221
|
+
#
|
|
222
|
+
# value_option :sort, inline: true, repeatable: true
|
|
223
|
+
# # sort: ['refname', '-committerdate'] => ['--sort=refname', '--sort=-committerdate']
|
|
224
|
+
#
|
|
225
|
+
# end_of_options
|
|
226
|
+
# value_option :pathspecs, as_operand: true, repeatable: true
|
|
227
|
+
# # pathspecs: ['file1.txt', 'file2.txt'] => ['--', 'file1.txt', 'file2.txt']
|
|
228
|
+
# ```
|
|
229
|
+
#
|
|
230
|
+
# ## Common Option Parameters
|
|
231
|
+
#
|
|
232
|
+
# Most option types support parameters that affect **input validation** (checked
|
|
233
|
+
# during {#bind}):
|
|
234
|
+
#
|
|
235
|
+
# - **required:** - When true, the option key must be present in the provided
|
|
236
|
+
# opts. Raises ArgumentError if the key is missing. Defaults to false.
|
|
237
|
+
#
|
|
238
|
+
# Supported by: {#flag_option}, {#value_option}, {#flag_or_value_option},
|
|
239
|
+
# {#key_value_option}, {#custom_option}, {#operand}.
|
|
240
|
+
#
|
|
241
|
+
# - **allow_nil:** - When false (with required: true), the value cannot be nil.
|
|
242
|
+
# Raises ArgumentError if a nil value is provided. Defaults to true for
|
|
243
|
+
# options, false for operands.
|
|
244
|
+
#
|
|
245
|
+
# Supported by: same as **required:**.
|
|
246
|
+
#
|
|
247
|
+
# - **type:** - Validates the value is an instance of the specified class(es).
|
|
248
|
+
# Accepts a single class or an array of classes. Raises ArgumentError if type
|
|
249
|
+
# doesn't match. This parameter only performs type checking during validation;
|
|
250
|
+
# the conversion of values to CLI argument strings is handled separately during
|
|
251
|
+
# the build phase — see the *String Conversion* section below. Defaults to nil (no
|
|
252
|
+
# validation).
|
|
253
|
+
#
|
|
254
|
+
# Supported by: {#flag_option}, {#value_option}, {#flag_or_value_option}.
|
|
255
|
+
#
|
|
256
|
+
# Note that {#literal} and {#execution_option} do not support these validation parameters.
|
|
257
|
+
#
|
|
258
|
+
# These parameters affect **output generation** (what CLI arguments are
|
|
259
|
+
# produced):
|
|
260
|
+
#
|
|
261
|
+
# - **as:** - Override the CLI argument(s) derived from the option name
|
|
262
|
+
# Can be a String or an Array. Default is nil (derives from name).
|
|
263
|
+
#
|
|
264
|
+
# - **allow_empty:** - ({#value_option} only) When true, output the option
|
|
265
|
+
# even if the value is an empty string. Default is false (empty strings skipped).
|
|
266
|
+
#
|
|
267
|
+
# - **repeatable:** - ({#value_option}, {#flag_or_value_option}, and {#operand}
|
|
268
|
+
# only) Output an option or operand for each array element. Default is false.
|
|
269
|
+
#
|
|
270
|
+
# - **skip_cli:** - ({#operand} only) Bind, validate, and expose an operand
|
|
271
|
+
# accessor without emitting that operand in {Bound#to_a}. Default is false.
|
|
272
|
+
#
|
|
273
|
+
# @example Required option with non-nil value
|
|
274
|
+
# args_def = Arguments.define do
|
|
275
|
+
# value_option :upstream, inline: true, required: true, allow_nil: false
|
|
276
|
+
# end
|
|
277
|
+
# args_def.bind() #=> raise ArgumentError, "Required options not provided: :upstream"
|
|
278
|
+
# args_def.bind(upstream: nil) #=> raise ArgumentError, "Required options cannot be nil: :upstream"
|
|
279
|
+
# args_def.bind(upstream: 'origin').to_a # => ['--upstream=origin']
|
|
280
|
+
#
|
|
281
|
+
# @example Required option allowing nil (default)
|
|
282
|
+
# args_def = Arguments.define do
|
|
283
|
+
# value_option :branch, inline: true, required: true
|
|
284
|
+
# end
|
|
285
|
+
# args_def.bind() #=> raise ArgumentError, "Required options not provided: :branch"
|
|
286
|
+
# args_def.bind(branch: nil).to_a # => []
|
|
287
|
+
# args_def.bind(branch: 'main').to_a # => ['--branch=main']
|
|
288
|
+
#
|
|
289
|
+
# ## Operands (Positional Arguments)
|
|
290
|
+
#
|
|
291
|
+
# Operands are mapped using Ruby-like semantics:
|
|
292
|
+
#
|
|
293
|
+
# 1. Post-repeatable required operands are reserved first (from the end)
|
|
294
|
+
# 2. Pre-repeatable operands are filled with remaining values (required first, then optional)
|
|
295
|
+
# 3. Optional operands (with defaults) get values only if extras are available
|
|
296
|
+
# 4. Repeatable operand gets whatever is left in the middle
|
|
297
|
+
#
|
|
298
|
+
# This matches Ruby's parameter binding behavior, including patterns like `def
|
|
299
|
+
# foo(a = default, *rest, b)` where the required `b` is filled before optional
|
|
300
|
+
# `a`.
|
|
301
|
+
#
|
|
302
|
+
# @example Simple operand (like `git clone <repository>`)
|
|
303
|
+
# args_def = Arguments.define do
|
|
304
|
+
# literal 'clone'
|
|
305
|
+
# operand :repository, required: true
|
|
306
|
+
# end
|
|
307
|
+
# args_def.bind('https://github.com/user/repo').to_a
|
|
308
|
+
# # => ['clone', 'https://github.com/user/repo']
|
|
309
|
+
#
|
|
310
|
+
# @example Repeatable operand (like `git add <paths>...`)
|
|
311
|
+
# args_def = Arguments.define do
|
|
312
|
+
# literal 'add'
|
|
313
|
+
# operand :paths, repeatable: true
|
|
314
|
+
# end
|
|
315
|
+
# args_def.bind('file1', 'file2', 'file3').to_a
|
|
316
|
+
# # => ['add', 'file1', 'file2', 'file3']
|
|
317
|
+
#
|
|
318
|
+
# @example git mv pattern (like `git mv <sources>... <destination>`)
|
|
319
|
+
# args_def = Arguments.define do
|
|
320
|
+
# literal 'mv'
|
|
321
|
+
# operand :sources, repeatable: true, required: true
|
|
322
|
+
# operand :destination, required: true
|
|
323
|
+
# end
|
|
324
|
+
# args_def.bind('src1', 'src2', 'dest').to_a # => ['mv', 'src1', 'src2', 'dest']
|
|
325
|
+
#
|
|
326
|
+
# ## Nil Handling for Operands
|
|
327
|
+
#
|
|
328
|
+
# When nil values are allowed (see `required:` and `allow_nil:` above), they have
|
|
329
|
+
# special output behavior:
|
|
330
|
+
#
|
|
331
|
+
# - For non-repeating operands: nil values consume an operand slot during
|
|
332
|
+
# binding but are omitted from the resulting command-line arguments array
|
|
333
|
+
# - For repeatable operands: nil values within the array raise an error
|
|
334
|
+
#
|
|
335
|
+
# @example Nil value omitted from output
|
|
336
|
+
# args = Arguments.define do
|
|
337
|
+
# operand :tree_ish, required: true, allow_nil: true
|
|
338
|
+
# operand :paths, repeatable: true
|
|
339
|
+
# end.bind(nil, 'file1', 'file2')
|
|
340
|
+
# args.to_a # => ['file1', 'file2']
|
|
341
|
+
# args.tree_ish # => nil
|
|
342
|
+
# args.paths # => ['file1', 'file2']
|
|
343
|
+
#
|
|
344
|
+
# ## Option-like Operand Rejection
|
|
345
|
+
#
|
|
346
|
+
# Operands that appear **before** a `--` separator boundary in the argument
|
|
347
|
+
# definition are automatically validated to ensure their values don't start
|
|
348
|
+
# with `-`. This prevents user-supplied strings like `'-s'` from being
|
|
349
|
+
# misinterpreted as git flags when passed as positional arguments.
|
|
350
|
+
#
|
|
351
|
+
# The `--` boundary can come from:
|
|
352
|
+
# - A `literal '--'` definition
|
|
353
|
+
# - An `end_of_options` declaration
|
|
354
|
+
#
|
|
355
|
+
# Operands **after** the `--` boundary are not validated (they represent
|
|
356
|
+
# paths/filenames which may legitimately start with `-`). If no `--`
|
|
357
|
+
# boundary exists in the definition, **all** operands are validated.
|
|
358
|
+
#
|
|
359
|
+
# @example Operands before and after '--' end_of_options boundary
|
|
360
|
+
# args_def = Arguments.define do
|
|
361
|
+
# operand :commit1
|
|
362
|
+
# operand :commit2
|
|
363
|
+
# end_of_options
|
|
364
|
+
# operand :paths, repeatable: true
|
|
365
|
+
# end
|
|
366
|
+
# args_def.bind('-s') #=> raise ArgumentError, "operand :commit1 value '-s' looks like a command-line option"
|
|
367
|
+
# args_def.bind('HEAD', 'HEAD~1', '-file.txt').to_a
|
|
368
|
+
# # => ['HEAD', 'HEAD~1', '--', '-file.txt']
|
|
369
|
+
#
|
|
370
|
+
# @example All operands validated when no '--' boundary exists
|
|
371
|
+
# args_def = Arguments.define do
|
|
372
|
+
# operand :path1, required: true
|
|
373
|
+
# operand :path2, required: true
|
|
374
|
+
# end
|
|
375
|
+
# args_def.bind('-s', 'file.txt')
|
|
376
|
+
# #=> raise ArgumentError, "operand :path1 value '-s' looks like a command-line option"
|
|
377
|
+
#
|
|
378
|
+
# ## Options After Separator
|
|
379
|
+
#
|
|
380
|
+
# Options that produce CLI flags (e.g. `flag_option`, `value_option`,
|
|
381
|
+
# `key_value_option`, `custom_option`) cannot be defined after a `--`
|
|
382
|
+
# separator boundary. Git treats everything after `--` as operands, so
|
|
383
|
+
# flags emitted there would be misinterpreted.
|
|
384
|
+
#
|
|
385
|
+
# Only `value_option` with `as_operand: true` and `execution_option` are allowed
|
|
386
|
+
# after the boundary because they do not produce flag-prefixed output.
|
|
387
|
+
#
|
|
388
|
+
# For example, this will raise `ArgumentError` during definition:
|
|
389
|
+
#
|
|
390
|
+
# Arguments.define do
|
|
391
|
+
# literal '--'
|
|
392
|
+
# flag_option :verbose
|
|
393
|
+
# end #=> raises ArgumentError
|
|
394
|
+
#
|
|
395
|
+
# @example Allowed: value_option as_operand after '--'
|
|
396
|
+
# Arguments.define do
|
|
397
|
+
# literal '--'
|
|
398
|
+
# value_option :paths, as_operand: true, repeatable: true
|
|
399
|
+
# end
|
|
400
|
+
#
|
|
401
|
+
# ## Type Validation
|
|
402
|
+
#
|
|
403
|
+
# The `type:` parameter provides declarative type validation for option values.
|
|
404
|
+
# When validation fails, an ArgumentError is raised with a descriptive message.
|
|
405
|
+
#
|
|
406
|
+
# @example Single type validation
|
|
407
|
+
# args_def = Arguments.define do
|
|
408
|
+
# value_option :date, type: String, inline: true
|
|
409
|
+
# end
|
|
410
|
+
# args_def.bind(date: "2024-01-01").to_a # => ['--date=2024-01-01']
|
|
411
|
+
# args_def.bind(date: 12345) #=> raise ArgumentError, "The :date option must be a String, but was a Integer"
|
|
412
|
+
#
|
|
413
|
+
# @example Multiple type validation (allows any of the specified types)
|
|
414
|
+
# args_def = Arguments.define do
|
|
415
|
+
# value_option :timeout, type: [Integer, Float], inline: true
|
|
416
|
+
# end
|
|
417
|
+
# args_def.bind(timeout: 30).to_a # => ['--timeout=30']
|
|
418
|
+
# args_def.bind(timeout: 30.5).to_a # => ['--timeout=30.5']
|
|
419
|
+
# args_def.bind(timeout: "30")
|
|
420
|
+
# #=> raise ArgumentError, "The :timeout option must be a Integer or Float, but was a String"
|
|
421
|
+
#
|
|
422
|
+
# ## String Conversion
|
|
423
|
+
#
|
|
424
|
+
# During the build phase, value-bearing option types (`value_option`,
|
|
425
|
+
# `flag_or_value_option`, `key_value_option`) and `operand` definitions convert
|
|
426
|
+
# their bound values to CLI argument strings by calling `#to_s`. This means any
|
|
427
|
+
# object with a meaningful `#to_s` implementation — `Integer`, `Float`,
|
|
428
|
+
# `Pathname`, etc. — can be passed as a value without the DSL needing to know
|
|
429
|
+
# about the type.
|
|
430
|
+
#
|
|
431
|
+
# Note that `flag_option` values control *presence or absence* of a flag and are not
|
|
432
|
+
# stringified. `custom_option` builders receive the raw value and are responsible
|
|
433
|
+
# for producing CLI strings themselves.
|
|
434
|
+
#
|
|
435
|
+
# The `type:` parameter does not affect this conversion; it only validates the
|
|
436
|
+
# Ruby class of the value *before* stringification.
|
|
437
|
+
#
|
|
438
|
+
# @example Numeric values are stringified automatically
|
|
439
|
+
# args_def = Arguments.define do
|
|
440
|
+
# value_option :depth, inline: true
|
|
441
|
+
# value_option :jobs, inline: true
|
|
442
|
+
# end
|
|
443
|
+
# args_def.bind(depth: 5, jobs: 4).to_a # => ['--depth=5', '--jobs=4']
|
|
444
|
+
#
|
|
445
|
+
# @example Pathname is also accepted (no type: needed)
|
|
446
|
+
# args_def = Arguments.define do
|
|
447
|
+
# operand :path, required: true
|
|
448
|
+
# end
|
|
449
|
+
# args_def.bind(Pathname.new('/tmp/foo')).to_a # => ['/tmp/foo']
|
|
450
|
+
#
|
|
451
|
+
# ## Conflict Detection
|
|
452
|
+
#
|
|
453
|
+
# Use {#conflicts} to declare mutually exclusive arguments. Names may refer to
|
|
454
|
+
# **options** (flag, value, flag-or-value, etc.) or **operands** (positional
|
|
455
|
+
# arguments) interchangeably. When {#bind} is called, if more than one argument
|
|
456
|
+
# in a conflict group is "present", an ArgumentError is raised.
|
|
457
|
+
#
|
|
458
|
+
# An argument is considered **present** when its value is not `nil`, `false`,
|
|
459
|
+
# `[]`, or `''`.
|
|
460
|
+
#
|
|
461
|
+
# @example Option vs option conflict
|
|
462
|
+
# args_def = Arguments.define do
|
|
463
|
+
# flag_option :force
|
|
464
|
+
# flag_option :force_force
|
|
465
|
+
# conflicts :force, :force_force
|
|
466
|
+
# end
|
|
467
|
+
# args_def.bind(force: true, force_force: true) #=> raise ArgumentError, "cannot specify :force and :force_force"
|
|
468
|
+
#
|
|
469
|
+
# @example Mixed option and operand conflict
|
|
470
|
+
# args_def = Arguments.define do
|
|
471
|
+
# flag_option %i[merge m], as: '--merge'
|
|
472
|
+
# operand :tree_ish, required: true, allow_nil: true
|
|
473
|
+
# conflicts :merge, :tree_ish
|
|
474
|
+
# end
|
|
475
|
+
# args_def.bind('main', merge: true) #=> raise ArgumentError, "cannot specify :merge and :tree_ish"
|
|
476
|
+
# args_def.bind(nil, merge: true).to_a # => ['--merge']
|
|
477
|
+
#
|
|
478
|
+
# ## Forbidden Value Combinations
|
|
479
|
+
#
|
|
480
|
+
# {#conflicts} is presence-based — it cannot distinguish between semantically
|
|
481
|
+
# equivalent and contradictory combinations of negatable flags. Use
|
|
482
|
+
# {#forbid_values} to declare specific **exact-value tuples** that are invalid.
|
|
483
|
+
#
|
|
484
|
+
# A `forbid_values` declaration matches only when **every** listed name has a
|
|
485
|
+
# bound value equal to the declared value (Ruby `==`). Only matching tuples raise
|
|
486
|
+
# ArgumentError; all other value combinations are permitted. Names may be options
|
|
487
|
+
# or operands; aliases are canonicalized before comparison.
|
|
488
|
+
#
|
|
489
|
+
# This is most useful for negatable flags where some value-pairings are
|
|
490
|
+
# contradictory but others are semantically equivalent and should remain valid.
|
|
491
|
+
#
|
|
492
|
+
# The error message has the form:
|
|
493
|
+
#
|
|
494
|
+
# "cannot specify :name1=value1 with :name2=value2"
|
|
495
|
+
#
|
|
496
|
+
# @example Reject contradictory pairs without blocking equivalent ones
|
|
497
|
+
# args_def = Arguments.define do
|
|
498
|
+
# flag_option :all, negatable: true
|
|
499
|
+
# flag_option :ignore_removal, negatable: true
|
|
500
|
+
# forbid_values all: true, ignore_removal: true # --all --ignore-removal: contradictory
|
|
501
|
+
# forbid_values no_all: true, no_ignore_removal: true # --no-all --no-ignore-removal: contradictory
|
|
502
|
+
# end
|
|
503
|
+
# args_def.bind(all: true, ignore_removal: true)
|
|
504
|
+
# #=> raise ArgumentError, 'cannot specify :all=true with :ignore_removal=true'
|
|
505
|
+
# args_def.bind(all: true, no_ignore_removal: true).to_a # => ['--all', '--no-ignore-removal']
|
|
506
|
+
# args_def.bind(no_all: true, ignore_removal: true).to_a # => ['--no-all', '--ignore-removal']
|
|
507
|
+
#
|
|
508
|
+
# ## At-Least-One Presence Validation
|
|
509
|
+
#
|
|
510
|
+
# Use {#requires_one_of} to declare groups of arguments where at least one must be
|
|
511
|
+
# present. Names may refer to **options** (flag, value, flag-or-value, etc.) or
|
|
512
|
+
# **operands** (positional arguments) interchangeably. When {#bind} is called, if
|
|
513
|
+
# none of the arguments in a group is present, an ArgumentError is raised.
|
|
514
|
+
#
|
|
515
|
+
# @example Requiring at least one path source (options only)
|
|
516
|
+
# args_def = Arguments.define do
|
|
517
|
+
# value_option :pathspec_from_file, inline: true
|
|
518
|
+
# end_of_options
|
|
519
|
+
# value_option :pathspec, as_operand: true, repeatable: true
|
|
520
|
+
# requires_one_of :pathspec, :pathspec_from_file
|
|
521
|
+
# end
|
|
522
|
+
# args_def.bind
|
|
523
|
+
# #=> raise ArgumentError, 'at least one of :pathspec, :pathspec_from_file must be provided'
|
|
524
|
+
# args_def.bind(pathspec: ['file.txt']).to_a # => ['--', 'file.txt']
|
|
525
|
+
#
|
|
526
|
+
# @example Mixed option and operand group
|
|
527
|
+
# args_def = Arguments.define do
|
|
528
|
+
# flag_option :all
|
|
529
|
+
# operand :paths, repeatable: true
|
|
530
|
+
# requires_one_of :all, :paths
|
|
531
|
+
# end
|
|
532
|
+
# args_def.bind
|
|
533
|
+
# #=> raise ArgumentError, 'at least one of :all, :paths must be provided'
|
|
534
|
+
# args_def.bind('file.txt').to_a # => ['file.txt']
|
|
535
|
+
#
|
|
536
|
+
# ## Conditional Argument Requirements
|
|
537
|
+
#
|
|
538
|
+
# Use {#requires} and the `when:` form of {#requires_one_of} to declare that an
|
|
539
|
+
# argument (or at least one of a group) must be present **only when** a specific
|
|
540
|
+
# trigger argument is present. These constraints are evaluated during {#bind}: if
|
|
541
|
+
# the trigger is absent the check is skipped entirely.
|
|
542
|
+
#
|
|
543
|
+
# An ArgumentError is raised at definition time if either the required name(s) or
|
|
544
|
+
# the trigger name are not known arguments, catching typos early.
|
|
545
|
+
#
|
|
546
|
+
# @example Single conditional requirement
|
|
547
|
+
# args_def = Arguments.define do
|
|
548
|
+
# flag_option :pathspec_file_nul
|
|
549
|
+
# value_option :pathspec_from_file, inline: true
|
|
550
|
+
# requires :pathspec_from_file, when: :pathspec_file_nul
|
|
551
|
+
# end
|
|
552
|
+
# args_def.bind(pathspec_file_nul: true, pathspec_from_file: 'paths.txt').to_a
|
|
553
|
+
# # => ['--pathspec-file-nul', '--pathspec-from-file=paths.txt']
|
|
554
|
+
# args_def.bind(pathspec_file_nul: true)
|
|
555
|
+
# #=> raise ArgumentError, ':pathspec_file_nul requires :pathspec_from_file'
|
|
556
|
+
# args_def.bind # trigger absent — no error
|
|
557
|
+
#
|
|
558
|
+
# @example Conditional at-least-one-of group
|
|
559
|
+
# args_def = Arguments.define do
|
|
560
|
+
# flag_option :annotate
|
|
561
|
+
# value_option :message, inline: true
|
|
562
|
+
# value_option :file, inline: true
|
|
563
|
+
# requires_one_of :message, :file, when: :annotate
|
|
564
|
+
# end
|
|
565
|
+
# args_def.bind(annotate: true, message: 'v1.0').to_a # => ['--annotate', '--message=v1.0']
|
|
566
|
+
# args_def.bind(annotate: true)
|
|
567
|
+
# #=> raise ArgumentError, ':annotate requires at least one of :message, :file'
|
|
568
|
+
# args_def.bind # trigger absent — no error
|
|
569
|
+
#
|
|
570
|
+
# ## Value Constraints
|
|
571
|
+
#
|
|
572
|
+
# In addition to presence-based validation ({#conflicts}, {#requires_one_of},
|
|
573
|
+
# and {#requires}) and value-combination constraints ({#forbid_values}), you can
|
|
574
|
+
# restrict the *set of acceptable values* for any value-type option using
|
|
575
|
+
# {#allowed_values}. If a bound value falls outside the configured set, {#bind}
|
|
576
|
+
# raises ArgumentError with a descriptive message.
|
|
577
|
+
#
|
|
578
|
+
# This is typically used to model git options that accept only a fixed list of
|
|
579
|
+
# modes or strategies.
|
|
580
|
+
#
|
|
581
|
+
# @example Restricting option values
|
|
582
|
+
# args_def = Arguments.define do
|
|
583
|
+
# value_option :strategy, inline: true
|
|
584
|
+
# allowed_values :strategy, in: %w[ours theirs]
|
|
585
|
+
# end
|
|
586
|
+
# args_def.bind(strategy: 'ours').to_a # => ['--strategy=ours']
|
|
587
|
+
# args_def.bind(strategy: 'theirs').to_a # => ['--strategy=theirs']
|
|
588
|
+
# args_def.bind(strategy: 'rebase')
|
|
589
|
+
# # => raise ArgumentError, 'Invalid value for :strategy: expected one of ["ours", "theirs"], got "rebase"'
|
|
590
|
+
#
|
|
591
|
+
# @api private
|
|
592
|
+
#
|
|
593
|
+
class Arguments
|
|
594
|
+
# Define a new Arguments instance using the DSL
|
|
595
|
+
#
|
|
596
|
+
# @example Basic flag
|
|
597
|
+
# args_def = Arguments.define do
|
|
598
|
+
# flag_option :verbose
|
|
599
|
+
# end
|
|
600
|
+
# args_def.bind(verbose: true).to_a # => ['--verbose']
|
|
601
|
+
#
|
|
602
|
+
# @return [Arguments] the configured Arguments instance
|
|
603
|
+
#
|
|
604
|
+
# @yield [] block evaluated in the context of the new Arguments instance via
|
|
605
|
+
# `instance_eval`, so DSL methods ({#flag_option}, {#operand}, etc.) are called
|
|
606
|
+
# directly without an explicit receiver
|
|
607
|
+
#
|
|
608
|
+
def self.define(&block)
|
|
609
|
+
args = new
|
|
610
|
+
args.instance_eval(&block) if block
|
|
611
|
+
args
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Initialize an empty Arguments definition
|
|
615
|
+
def initialize
|
|
616
|
+
@option_definitions = {}
|
|
617
|
+
@alias_map = {} # Maps alias keys to primary keys
|
|
618
|
+
@operand_definitions = []
|
|
619
|
+
@conflicts = [] # Array of conflicting option pairs/groups
|
|
620
|
+
@forbidden_values = [] # Array of forbidden exact-value tuples
|
|
621
|
+
@requires_one_of = [] # Array of "at least one must be present" groups
|
|
622
|
+
@ordered_definitions = [] # Tracks all definitions in definition order
|
|
623
|
+
@past_separator = false # Tracks whether a '--' boundary has been defined
|
|
624
|
+
@end_of_options_declared = false # Guards against duplicate end_of_options calls
|
|
625
|
+
@negatable_companions = Set.new # Synthesized :no_<name> companion entries
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
# Define a boolean flag option (--flag when true)
|
|
629
|
+
#
|
|
630
|
+
# @example Basic flag
|
|
631
|
+
# args_def = Arguments.define do
|
|
632
|
+
# flag_option :force
|
|
633
|
+
# end
|
|
634
|
+
# args_def.bind(force: true).to_a # => ['--force']
|
|
635
|
+
# args_def.bind(force: false).to_a # => []
|
|
636
|
+
#
|
|
637
|
+
# @example Negatable flag (companion-key model)
|
|
638
|
+
# args_def = Arguments.define do
|
|
639
|
+
# flag_option :full, negatable: true
|
|
640
|
+
# end
|
|
641
|
+
# args_def.bind(full: true).to_a # => ['--full']
|
|
642
|
+
# args_def.bind(no_full: true).to_a # => ['--no-full']
|
|
643
|
+
# args_def.bind(full: false).to_a # => []
|
|
644
|
+
#
|
|
645
|
+
# @example Negatable flag with required: true (either companion key satisfies the requirement)
|
|
646
|
+
# args_def = Arguments.define do
|
|
647
|
+
# flag_option :verify, negatable: true, required: true
|
|
648
|
+
# end
|
|
649
|
+
# args_def.bind(verify: true).to_a # => ['--verify']
|
|
650
|
+
# args_def.bind(no_verify: true).to_a # => ['--no-verify']
|
|
651
|
+
# args_def.bind(verify: false)
|
|
652
|
+
# #=> raise ArgumentError, "at least one of :verify, :no_verify must be provided"
|
|
653
|
+
# args_def.bind
|
|
654
|
+
# #=> raise ArgumentError, "at least one of :verify, :no_verify must be provided"
|
|
655
|
+
#
|
|
656
|
+
# @example Repeatable flag with max_times
|
|
657
|
+
# args_def = Arguments.define do
|
|
658
|
+
# flag_option :force, max_times: 2
|
|
659
|
+
# end
|
|
660
|
+
# args_def.bind(force: true).to_a # => ['--force']
|
|
661
|
+
# args_def.bind(force: 1).to_a # => ['--force']
|
|
662
|
+
# args_def.bind(force: 2).to_a # => ['--force', '--force']
|
|
663
|
+
#
|
|
664
|
+
# @example Negatable flag with max_times
|
|
665
|
+
# args_def = Arguments.define do
|
|
666
|
+
# flag_option :force, negatable: true, max_times: 2
|
|
667
|
+
# end
|
|
668
|
+
# args_def.bind(no_force: true).to_a # => ['--no-force']
|
|
669
|
+
# args_def.bind(force: 2).to_a # => ['--force', '--force']
|
|
670
|
+
#
|
|
671
|
+
# @example With required and allow_nil: false
|
|
672
|
+
# args_def = Arguments.define do
|
|
673
|
+
# flag_option :force, required: true, allow_nil: false
|
|
674
|
+
# end
|
|
675
|
+
# args_def.bind() #=> raise ArgumentError, "Required options not provided: :force"
|
|
676
|
+
# args_def.bind(force: nil) #=> raise ArgumentError, "Required options cannot be nil: :force"
|
|
677
|
+
#
|
|
678
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
679
|
+
#
|
|
680
|
+
# @param as [String, Array<String>, nil] custom argument(s) to output (e.g., '-r' or ['--amend', '--no-edit'])
|
|
681
|
+
#
|
|
682
|
+
# @param negatable [Boolean] when true, registers a companion `no_<name>` key that emits
|
|
683
|
+
# `--no-<flag>` when set to `true`. Both keys use standard boolean semantics: `true`
|
|
684
|
+
# emits the flag, `false` or absent emits nothing. A conflict is automatically registered
|
|
685
|
+
# between the two keys so that `name: true, no_name: true` raises at bind time.
|
|
686
|
+
# The primary key must be snake_case (e.g. `:verify`, `:three_way`). When `as:` is
|
|
687
|
+
# given, it must be a long-form (`--flag`) String; Arrays and short-form flags (e.g.
|
|
688
|
+
# `-S`) are not compatible with `negatable: true` because the synthesized companion is
|
|
689
|
+
# always `--no-<flag>`.
|
|
690
|
+
#
|
|
691
|
+
# @param required [Boolean] whether the option must be provided (the key must be present
|
|
692
|
+
# in opts). When combined with `negatable: true`, a `requires_one_of [name, no_name]`
|
|
693
|
+
# group is automatically registered so that either the primary or companion key satisfies
|
|
694
|
+
# the requirement (e.g. `bind(no_verify: true)` satisfies `required: true` for `:verify`).
|
|
695
|
+
# Note that under the companion-key model, `bind(verify: false)` does **not** satisfy
|
|
696
|
+
# the requirement because `false` is treated as absent.
|
|
697
|
+
#
|
|
698
|
+
# @param allow_nil [Boolean] whether nil is allowed when required is true (defaults to true)
|
|
699
|
+
#
|
|
700
|
+
# When false with required: true, raises ArgumentError if value is nil.
|
|
701
|
+
# Cannot be combined with `negatable: true` and `required: true` — raises ArgumentError
|
|
702
|
+
# at definition time (nil is already caught by the auto `requires_one_of` group).
|
|
703
|
+
#
|
|
704
|
+
# @param max_times [Integer, nil] maximum number of times the flag may be repeated (default: nil)
|
|
705
|
+
# When set, the caller may pass a positive Integer up to this limit to emit the flag
|
|
706
|
+
# multiple times (e.g. `force: 2` emits `--force --force`). Must be an Integer >= 2;
|
|
707
|
+
# 0 and 1 raise ArgumentError at definition time. When nil (the default), only boolean
|
|
708
|
+
# values are accepted.
|
|
709
|
+
#
|
|
710
|
+
# @return [void]
|
|
711
|
+
#
|
|
712
|
+
# @raise [ArgumentError] if defined after an `end_of_options` or `literal '--'` boundary
|
|
713
|
+
#
|
|
714
|
+
# @raise [ArgumentError] if max_times is not nil and not an Integer >= 2
|
|
715
|
+
#
|
|
716
|
+
# @raise [ArgumentError] if negatable: true and the primary key is not snake_case
|
|
717
|
+
#
|
|
718
|
+
# @raise [ArgumentError] if negatable: true and the generated `no_<name>` key collides
|
|
719
|
+
# with an already-registered key
|
|
720
|
+
#
|
|
721
|
+
# @raise [ArgumentError] if negatable: true and as: is an Array
|
|
722
|
+
#
|
|
723
|
+
# @raise [ArgumentError] if negatable: true and as: is not a long-form (`--flag`) String
|
|
724
|
+
#
|
|
725
|
+
# @raise [ArgumentError] if negatable: true and required: true and allow_nil: false
|
|
726
|
+
def flag_option(names, as: nil, negatable: false, required: false, allow_nil: true, max_times: nil)
|
|
727
|
+
primary = Array(names).first
|
|
728
|
+
validate_max_times!(primary, max_times)
|
|
729
|
+
|
|
730
|
+
if negatable
|
|
731
|
+
register_negatable_flag_pair(names, as: as, required: required,
|
|
732
|
+
allow_nil: allow_nil, max_times: max_times)
|
|
733
|
+
else
|
|
734
|
+
register_option(names, type: :flag, as: as, expected_type: nil, validator: nil,
|
|
735
|
+
required: required, allow_nil: allow_nil, max_times: max_times)
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Define a valued option (--flag value as separate arguments)
|
|
740
|
+
#
|
|
741
|
+
# This option type supports three output modes controlled by `inline:` and `as_operand:`:
|
|
742
|
+
#
|
|
743
|
+
# - **Default**: `--flag value` (flag and value as separate arguments)
|
|
744
|
+
# - **Inline**: `--flag=value` (single argument with `inline: true`)
|
|
745
|
+
# - **Operand**: `value` (no flag, just the value with `as_operand: true`)
|
|
746
|
+
#
|
|
747
|
+
# @example Basic value (default mode)
|
|
748
|
+
# args_def = Arguments.define do
|
|
749
|
+
# value_option :branch
|
|
750
|
+
# end
|
|
751
|
+
# args_def.bind(branch: 'main').to_a # => ['--branch', 'main']
|
|
752
|
+
#
|
|
753
|
+
# @example Inline value
|
|
754
|
+
# args_def = Arguments.define do
|
|
755
|
+
# value_option :format, inline: true
|
|
756
|
+
# end
|
|
757
|
+
# args_def.bind(format: 'short').to_a # => ['--format=short']
|
|
758
|
+
#
|
|
759
|
+
# @example Operand value (no flag output)
|
|
760
|
+
# args_def = Arguments.define do
|
|
761
|
+
# value_option :ref, as_operand: true
|
|
762
|
+
# end
|
|
763
|
+
# args_def.bind(ref: 'HEAD').to_a # => ['HEAD']
|
|
764
|
+
#
|
|
765
|
+
# @example Operand with end_of_options boundary
|
|
766
|
+
# args_def = Arguments.define do
|
|
767
|
+
# end_of_options
|
|
768
|
+
# value_option :paths, as_operand: true
|
|
769
|
+
# end
|
|
770
|
+
# args_def.bind(paths: 'file.txt').to_a # => ['--', 'file.txt']
|
|
771
|
+
#
|
|
772
|
+
# @example Multi-valued (default mode) - repeats option for each value
|
|
773
|
+
# args_def = Arguments.define do
|
|
774
|
+
# value_option :config, repeatable: true
|
|
775
|
+
# end
|
|
776
|
+
# args_def.bind(config: 'a=b').to_a # => ['--config', 'a=b']
|
|
777
|
+
# args_def.bind(config: ['a=b', 'c=d']).to_a # => ['--config', 'a=b', '--config', 'c=d']
|
|
778
|
+
# args_def.bind(config: nil).to_a # => []
|
|
779
|
+
#
|
|
780
|
+
# @example Multi-valued with inline - repeats inline option for each value
|
|
781
|
+
# args_def = Arguments.define do
|
|
782
|
+
# value_option :sort, inline: true, repeatable: true
|
|
783
|
+
# end
|
|
784
|
+
# args_def.bind(sort: ['refname', '-committerdate']).to_a
|
|
785
|
+
# # => ['--sort=refname', '--sort=-committerdate']
|
|
786
|
+
#
|
|
787
|
+
# @example Multi-valued with operand - outputs values without flags
|
|
788
|
+
# args_def = Arguments.define do
|
|
789
|
+
# end_of_options
|
|
790
|
+
# value_option :pathspecs, as_operand: true, repeatable: true
|
|
791
|
+
# end
|
|
792
|
+
# args_def.bind(pathspecs: ['file1.txt', 'file2.txt']).to_a
|
|
793
|
+
# # => ['--', 'file1.txt', 'file2.txt']
|
|
794
|
+
#
|
|
795
|
+
# @example With type validation
|
|
796
|
+
# args_def = Arguments.define do
|
|
797
|
+
# value_option :branch, type: String
|
|
798
|
+
# end
|
|
799
|
+
# args_def.bind(branch: 'main').to_a # => ['--branch', 'main']
|
|
800
|
+
#
|
|
801
|
+
# @example With allow_empty
|
|
802
|
+
# args_def = Arguments.define do
|
|
803
|
+
# value_option :message, allow_empty: true
|
|
804
|
+
# end
|
|
805
|
+
# args_def.bind(message: "").to_a # => ['--message', '']
|
|
806
|
+
# args_def.bind(message: "text").to_a # => ['--message', 'text']
|
|
807
|
+
#
|
|
808
|
+
# args_def2 = Arguments.define do
|
|
809
|
+
# value_option :message # allow_empty defaults to false
|
|
810
|
+
# end
|
|
811
|
+
# args_def2.bind(message: "").to_a # => []
|
|
812
|
+
# args_def2.bind(message: "text").to_a # => ['--message', 'text']
|
|
813
|
+
#
|
|
814
|
+
# @example With required
|
|
815
|
+
# args_def = Arguments.define do
|
|
816
|
+
# value_option :message, required: true
|
|
817
|
+
# end
|
|
818
|
+
# args_def.bind(message: 'text').to_a # => ['--message', 'text']
|
|
819
|
+
# args_def.bind(message: nil).to_a # => []
|
|
820
|
+
# args_def.bind() #=> raise ArgumentError, "Required options not provided: :message"
|
|
821
|
+
#
|
|
822
|
+
# @example With required and allow_nil: false
|
|
823
|
+
# args_def = Arguments.define do
|
|
824
|
+
# value_option :message, required: true, allow_nil: false
|
|
825
|
+
# end
|
|
826
|
+
# args_def.bind(message: 'text').to_a # => ['--message', 'text']
|
|
827
|
+
# args_def.bind(message: nil) #=> raise ArgumentError, "Required options cannot be nil: :message"
|
|
828
|
+
# args_def.bind() #=> raise ArgumentError, "Required options not provided: :message"
|
|
829
|
+
#
|
|
830
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
831
|
+
#
|
|
832
|
+
# @param as [String, nil] custom option string (arrays not supported for value types)
|
|
833
|
+
#
|
|
834
|
+
# @param type [Class, Array<Class>, nil] expected type(s) for validation. Raises ArgumentError with
|
|
835
|
+
# descriptive message if value doesn't match.
|
|
836
|
+
#
|
|
837
|
+
# @param inline [Boolean] when true, outputs --flag=value as single argument instead of
|
|
838
|
+
# --flag value as separate arguments (default: false). Cannot be combined with as_operand:.
|
|
839
|
+
#
|
|
840
|
+
# @param as_operand [Boolean] when true, outputs value as operand without flag
|
|
841
|
+
# (default: false). Cannot be combined with inline:.
|
|
842
|
+
#
|
|
843
|
+
# @param allow_empty [Boolean] whether to include the option even when value is an empty string.
|
|
844
|
+
# When false (default), empty strings are skipped entirely. When true, the option and empty
|
|
845
|
+
# value are included in the output.
|
|
846
|
+
#
|
|
847
|
+
# @param repeatable [Boolean] whether to allow multiple values. When true, accepts an array
|
|
848
|
+
# of values and repeats the option for each value. A single value or nil is also accepted.
|
|
849
|
+
# Behavior varies by output mode (see examples below).
|
|
850
|
+
#
|
|
851
|
+
# @param required [Boolean] when true, the option key must be present in the provided options hash.
|
|
852
|
+
# Raises ArgumentError if the key is missing. Defaults to false.
|
|
853
|
+
#
|
|
854
|
+
# @param allow_nil [Boolean] when false (with required: true), the value cannot be nil.
|
|
855
|
+
# Raises ArgumentError if a nil value is provided. Defaults to true.
|
|
856
|
+
#
|
|
857
|
+
# @return [void]
|
|
858
|
+
#
|
|
859
|
+
# @raise [ArgumentError] if inline: and as_operand: are both true
|
|
860
|
+
#
|
|
861
|
+
# @raise [ArgumentError] if defined after an `end_of_options` or `literal '--'` boundary
|
|
862
|
+
# (unless as_operand: true)
|
|
863
|
+
def value_option(names, as: nil, type: nil, inline: false, as_operand: false,
|
|
864
|
+
allow_empty: false, repeatable: false, required: false, allow_nil: true)
|
|
865
|
+
validate_value_modifiers!(names, inline, as_operand)
|
|
866
|
+
|
|
867
|
+
option_type = determine_value_option_type(inline, as_operand)
|
|
868
|
+
register_option(names, type: option_type, as: as, expected_type: type,
|
|
869
|
+
allow_empty: allow_empty, repeatable: repeatable, required: required,
|
|
870
|
+
allow_nil: allow_nil)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Define a flag or value option
|
|
874
|
+
#
|
|
875
|
+
# This is a flexible option type that outputs:
|
|
876
|
+
# - Just the flag (--flag) when value is true
|
|
877
|
+
# - Nothing when value is false
|
|
878
|
+
# - Flag with value when value is any non-boolean, non-nil object (stringified via #to_s;
|
|
879
|
+
# e.g., --flag value or --flag=value if inline: true)
|
|
880
|
+
# - Nothing when value is nil
|
|
881
|
+
#
|
|
882
|
+
# @example Basic flag or value (new capability - not possible with old DSL)
|
|
883
|
+
# args_def = Arguments.define do
|
|
884
|
+
# flag_or_value_option :contains
|
|
885
|
+
# end
|
|
886
|
+
# args_def.bind(contains: true).to_a # => ['--contains']
|
|
887
|
+
# args_def.bind(contains: false).to_a # => []
|
|
888
|
+
# args_def.bind(contains: "abc123").to_a # => ['--contains', 'abc123']
|
|
889
|
+
# args_def.bind(contains: nil).to_a # => []
|
|
890
|
+
#
|
|
891
|
+
# @example With inline: true
|
|
892
|
+
# args_def = Arguments.define do
|
|
893
|
+
# flag_or_value_option :gpg_sign, inline: true
|
|
894
|
+
# end
|
|
895
|
+
# args_def.bind(gpg_sign: true).to_a # => ['--gpg-sign']
|
|
896
|
+
# args_def.bind(gpg_sign: false).to_a # => []
|
|
897
|
+
# args_def.bind(gpg_sign: "KEY").to_a # => ['--gpg-sign=KEY']
|
|
898
|
+
# args_def.bind(gpg_sign: nil).to_a # => []
|
|
899
|
+
#
|
|
900
|
+
# @example With negatable: true (companion-key model)
|
|
901
|
+
# args_def = Arguments.define do
|
|
902
|
+
# flag_or_value_option :verify, negatable: true
|
|
903
|
+
# end
|
|
904
|
+
# args_def.bind(verify: true).to_a # => ['--verify']
|
|
905
|
+
# args_def.bind(verify: false).to_a # => []
|
|
906
|
+
# args_def.bind(no_verify: true).to_a # => ['--no-verify']
|
|
907
|
+
# args_def.bind(verify: "KEYID").to_a # => ['--verify', 'KEYID']
|
|
908
|
+
# args_def.bind(verify: nil).to_a # => []
|
|
909
|
+
#
|
|
910
|
+
# @example With negatable: true and inline: true
|
|
911
|
+
# args_def = Arguments.define do
|
|
912
|
+
# flag_or_value_option :sign, negatable: true, inline: true
|
|
913
|
+
# end
|
|
914
|
+
# args_def.bind(sign: true).to_a # => ['--sign']
|
|
915
|
+
# args_def.bind(sign: false).to_a # => []
|
|
916
|
+
# args_def.bind(no_sign: true).to_a # => ['--no-sign']
|
|
917
|
+
# args_def.bind(sign: "KEY").to_a # => ['--sign=KEY']
|
|
918
|
+
# args_def.bind(sign: nil).to_a # => []
|
|
919
|
+
#
|
|
920
|
+
# @example With inline: true and repeatable: true
|
|
921
|
+
# args_def = Arguments.define do
|
|
922
|
+
# flag_or_value_option :recurse_submodules, inline: true, repeatable: true
|
|
923
|
+
# end
|
|
924
|
+
# args_def.bind(recurse_submodules: true).to_a # => ['--recurse-submodules']
|
|
925
|
+
# args_def.bind(recurse_submodules: 'lib/').to_a # => ['--recurse-submodules=lib/']
|
|
926
|
+
# args_def.bind(recurse_submodules: ['lib/', 'ext/']).to_a
|
|
927
|
+
# # => ['--recurse-submodules=lib/', '--recurse-submodules=ext/']
|
|
928
|
+
# args_def.bind(recurse_submodules: [nil])
|
|
929
|
+
# # => raise_error ArgumentError, /Invalid value for flag_or_inline_value/
|
|
930
|
+
#
|
|
931
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
932
|
+
#
|
|
933
|
+
# @param as [String, nil] custom option string
|
|
934
|
+
#
|
|
935
|
+
# @param type [Class, Array<Class>, nil] expected type(s) for validation
|
|
936
|
+
#
|
|
937
|
+
# @param negatable [Boolean] when true, registers a companion `no_<name>` key that emits
|
|
938
|
+
# `--no-<flag>` when set to `true`. The positive key retains flag-or-value semantics;
|
|
939
|
+
# the negative key is boolean-only (accepts only `true`/`false`/`nil`). A conflict is
|
|
940
|
+
# automatically registered so that `name: true, no_name: true` raises at bind time.
|
|
941
|
+
# The primary key must be snake_case. When `as:` is given, it must be a long-form
|
|
942
|
+
# (`--flag`) String; Arrays and short-form flags (e.g. `-S`) are not compatible with
|
|
943
|
+
# `negatable: true` because the synthesized companion is always `--no-<flag>`.
|
|
944
|
+
#
|
|
945
|
+
# @param inline [Boolean] when true, outputs --flag=value instead of --flag value (default: false)
|
|
946
|
+
#
|
|
947
|
+
# @param repeatable [Boolean] when true, accepts an Array of values and repeats the option
|
|
948
|
+
# for each element. Each element must be `true`, `false`, or a non-nil object (which is
|
|
949
|
+
# stringified via `#to_s`); nil elements raise ArgumentError at bind time.
|
|
950
|
+
# A single (non-Array) value is also accepted. Default false.
|
|
951
|
+
#
|
|
952
|
+
# @param required [Boolean] whether the option must be provided (the key must be present
|
|
953
|
+
# in opts). When combined with `negatable: true`, a `requires_one_of [name, no_name]`
|
|
954
|
+
# group is automatically registered so that either side satisfies the requirement. Note
|
|
955
|
+
# that `bind(name: false)` does **not** satisfy the requirement because `false` is
|
|
956
|
+
# treated as absent under the companion-key model.
|
|
957
|
+
#
|
|
958
|
+
# @param allow_nil [Boolean] whether nil is allowed when required is true. Defaults to true.
|
|
959
|
+
# Cannot be combined with `negatable: true` and `required: true` — raises ArgumentError
|
|
960
|
+
# at definition time (nil is already caught by the auto `requires_one_of` group).
|
|
961
|
+
#
|
|
962
|
+
# @return [void]
|
|
963
|
+
#
|
|
964
|
+
# @raise [ArgumentError] at bind time if `repeatable: true` is used and any
|
|
965
|
+
# Array element is nil
|
|
966
|
+
#
|
|
967
|
+
# @raise [ArgumentError] if defined after an `end_of_options` or `literal '--'` boundary
|
|
968
|
+
#
|
|
969
|
+
# @raise [ArgumentError] if negatable: true and the primary key is not snake_case
|
|
970
|
+
#
|
|
971
|
+
# @raise [ArgumentError] if negatable: true and the generated `no_<name>` key collides
|
|
972
|
+
# with an already-registered key
|
|
973
|
+
#
|
|
974
|
+
# @raise [ArgumentError] if negatable: true and as: is an Array
|
|
975
|
+
#
|
|
976
|
+
# @raise [ArgumentError] if negatable: true and as: is not a long-form (`--flag`) String
|
|
977
|
+
#
|
|
978
|
+
# @raise [ArgumentError] if negatable: true and required: true and allow_nil: false
|
|
979
|
+
def flag_or_value_option(names, as: nil, type: nil, negatable: false, inline: false,
|
|
980
|
+
repeatable: false, required: false, allow_nil: true)
|
|
981
|
+
if negatable
|
|
982
|
+
register_negatable_flag_or_value_pair(names, as: as, type: type, inline: inline,
|
|
983
|
+
repeatable: repeatable, required: required,
|
|
984
|
+
allow_nil: allow_nil)
|
|
985
|
+
else
|
|
986
|
+
option_type = inline ? :flag_or_inline_value : :flag_or_value
|
|
987
|
+
register_option(names, type: option_type, as: as, expected_type: type,
|
|
988
|
+
repeatable: repeatable, required: required, allow_nil: allow_nil)
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
# Define a key-value option that can be specified multiple times
|
|
993
|
+
#
|
|
994
|
+
# This is useful for git options like --trailer that take key=value pairs
|
|
995
|
+
# and can be repeated. Accepts Hash or Array of arrays for flexible input.
|
|
996
|
+
#
|
|
997
|
+
# @example Basic key-value (like --trailer)
|
|
998
|
+
# args_def = Arguments.define do
|
|
999
|
+
# key_value_option :trailers, as: '--trailer'
|
|
1000
|
+
# end
|
|
1001
|
+
# args_def.bind(trailers: { 'Signed-off-by' => 'John' }).to_a
|
|
1002
|
+
# # => ['--trailer', 'Signed-off-by=John']
|
|
1003
|
+
#
|
|
1004
|
+
# @example Hash with array values (multiple values for same key)
|
|
1005
|
+
# args_def = Arguments.define do
|
|
1006
|
+
# key_value_option :trailers, as: '--trailer'
|
|
1007
|
+
# end
|
|
1008
|
+
# args_def.bind(trailers: { 'Signed-off-by' => ['John', 'Jane'] }).to_a
|
|
1009
|
+
# # => ['--trailer', 'Signed-off-by=John', '--trailer', 'Signed-off-by=Jane']
|
|
1010
|
+
#
|
|
1011
|
+
# @example Array of arrays (full ordering control)
|
|
1012
|
+
# args_def = Arguments.define do
|
|
1013
|
+
# key_value_option :trailers, as: '--trailer'
|
|
1014
|
+
# end
|
|
1015
|
+
# args_def.bind(trailers: [['Signed-off-by', 'John'], ['Acked-by', 'Bob']]).to_a
|
|
1016
|
+
# # => ['--trailer', 'Signed-off-by=John', '--trailer', 'Acked-by=Bob']
|
|
1017
|
+
#
|
|
1018
|
+
# @example Key without value (nil value omits separator)
|
|
1019
|
+
# args_def = Arguments.define do
|
|
1020
|
+
# key_value_option :trailers, as: '--trailer'
|
|
1021
|
+
# end
|
|
1022
|
+
# args_def.bind(trailers: [['Acked-by', nil]]).to_a
|
|
1023
|
+
# # => ['--trailer', 'Acked-by']
|
|
1024
|
+
#
|
|
1025
|
+
# @example Nil in array values produces key-only entries
|
|
1026
|
+
# args_def = Arguments.define do
|
|
1027
|
+
# key_value_option :trailers, as: '--trailer'
|
|
1028
|
+
# end
|
|
1029
|
+
# args_def.bind(trailers: { 'Key' => ['Value1', nil, 'Value2'] }).to_a
|
|
1030
|
+
# # => ['--trailer', 'Key=Value1', '--trailer', 'Key', '--trailer', 'Key=Value2']
|
|
1031
|
+
#
|
|
1032
|
+
# @example With custom separator
|
|
1033
|
+
# args_def = Arguments.define do
|
|
1034
|
+
# key_value_option :trailers, as: '--trailer', key_separator: ': '
|
|
1035
|
+
# end
|
|
1036
|
+
# args_def.bind(trailers: { 'Signed-off-by' => 'John' }).to_a
|
|
1037
|
+
# # => ['--trailer', 'Signed-off-by: John']
|
|
1038
|
+
#
|
|
1039
|
+
# @example Empty values produce no output
|
|
1040
|
+
# args_def = Arguments.define do
|
|
1041
|
+
# key_value_option :trailers, as: '--trailer', required: true
|
|
1042
|
+
# end
|
|
1043
|
+
# args_def.bind(trailers: {}).to_a # => []
|
|
1044
|
+
# args_def.bind(trailers: []).to_a # => []
|
|
1045
|
+
# args_def.bind(trailers: nil).to_a # => []
|
|
1046
|
+
#
|
|
1047
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
1048
|
+
#
|
|
1049
|
+
# @param as [String, nil] custom option string (e.g., '--trailer')
|
|
1050
|
+
#
|
|
1051
|
+
# @param key_separator [String] separator between key and value (default: '=')
|
|
1052
|
+
#
|
|
1053
|
+
# @param inline [Boolean] when true, outputs --flag=key=value instead of --flag key=value
|
|
1054
|
+
#
|
|
1055
|
+
# @param required [Boolean] whether the option must be provided (key must exist in opts).
|
|
1056
|
+
# Note: empty hash/array is considered "present" and produces no output without error.
|
|
1057
|
+
#
|
|
1058
|
+
# @param allow_nil [Boolean] whether nil is allowed when required is true
|
|
1059
|
+
#
|
|
1060
|
+
# @return [void]
|
|
1061
|
+
#
|
|
1062
|
+
# @raise [ArgumentError] at bind time if array input is not a [key, value] pair or array of pairs
|
|
1063
|
+
#
|
|
1064
|
+
# @raise [ArgumentError] at bind time if a sub-array has more than 2 elements
|
|
1065
|
+
#
|
|
1066
|
+
# @raise [ArgumentError] at bind time if a key is nil, empty, or contains the separator
|
|
1067
|
+
#
|
|
1068
|
+
# @raise [ArgumentError] at bind time if a value is a Hash or Array (non-scalar)
|
|
1069
|
+
#
|
|
1070
|
+
# @raise [ArgumentError] if defined after an `end_of_options` or `literal '--'` boundary
|
|
1071
|
+
def key_value_option(names, as: nil, key_separator: '=', inline: false, required: false, allow_nil: true)
|
|
1072
|
+
option_type = inline ? :inline_key_value : :key_value
|
|
1073
|
+
register_option(names, type: option_type, as: as, key_separator: key_separator,
|
|
1074
|
+
required: required, allow_nil: allow_nil)
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# Define a literal string that is always included in the output
|
|
1078
|
+
#
|
|
1079
|
+
# Literals are output at their definition position (not grouped at the start).
|
|
1080
|
+
# This allows precise control over argument ordering, which is important for
|
|
1081
|
+
# git commands where argument position matters.
|
|
1082
|
+
#
|
|
1083
|
+
# @example Static flag for subcommand mode
|
|
1084
|
+
# args_def = Arguments.define do
|
|
1085
|
+
# literal '--delete'
|
|
1086
|
+
# flag_option :force
|
|
1087
|
+
# operand :branches, repeatable: true
|
|
1088
|
+
# end
|
|
1089
|
+
# args_def.bind('feature', force: true).to_a # => ['--delete', '--force', 'feature']
|
|
1090
|
+
#
|
|
1091
|
+
# @example Static separator between options and pathspecs
|
|
1092
|
+
# args_def = Arguments.define do
|
|
1093
|
+
# flag_option :force
|
|
1094
|
+
# operand :tree_ish
|
|
1095
|
+
# literal '--'
|
|
1096
|
+
# operand :paths, repeatable: true
|
|
1097
|
+
# end
|
|
1098
|
+
# args_def.bind('HEAD', 'file.txt', force: true).to_a
|
|
1099
|
+
# # => ['--force', 'HEAD', '--', 'file.txt']
|
|
1100
|
+
#
|
|
1101
|
+
# @param flag_string [String] the static flag string (e.g., '--', '--no-progress')
|
|
1102
|
+
#
|
|
1103
|
+
# @return [void]
|
|
1104
|
+
def literal(flag_string)
|
|
1105
|
+
@ordered_definitions << { kind: :static, flag: flag_string }
|
|
1106
|
+
@past_separator = true if flag_string == '--'
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
# Conditionally emit an options terminator only when at least one following
|
|
1110
|
+
# argument produces output
|
|
1111
|
+
#
|
|
1112
|
+
# This is the canonical form for declaring the options/operands boundary in a
|
|
1113
|
+
# command definition. Unlike {#literal} with `'--'` which always emits the
|
|
1114
|
+
# separator, `end_of_options` emits its terminator string only when at least one
|
|
1115
|
+
# argument defined after it will be emitted as part of the CLI (for example
|
|
1116
|
+
# operands or `value_option ... as_operand: true`). This avoids a trailing bare
|
|
1117
|
+
# terminator when no pathspecs or other post-separator arguments are provided.
|
|
1118
|
+
#
|
|
1119
|
+
# `end_of_options` also acts as an always-active validation boundary: operands
|
|
1120
|
+
# defined before it are always validated for option-like values (starting with
|
|
1121
|
+
# `-`), regardless of whether the terminator will ultimately be emitted.
|
|
1122
|
+
#
|
|
1123
|
+
# @example Basic usage (git checkout tree-ish -- pathspecs)
|
|
1124
|
+
# args_def = Arguments.define do
|
|
1125
|
+
# flag_option :force
|
|
1126
|
+
# operand :tree_ish, required: true, allow_nil: true
|
|
1127
|
+
# end_of_options
|
|
1128
|
+
# operand :pathspecs, repeatable: true
|
|
1129
|
+
# end
|
|
1130
|
+
# args_def.bind('HEAD', 'file.txt').to_a # => ['HEAD', '--', 'file.txt']
|
|
1131
|
+
# args_def.bind('HEAD').to_a # => ['HEAD'] # (no --, nothing after it)
|
|
1132
|
+
# args_def.bind(nil, 'file.txt').to_a # => ['--', 'file.txt']
|
|
1133
|
+
# args_def.bind(nil).to_a # => []
|
|
1134
|
+
#
|
|
1135
|
+
# @example Custom terminator (git rev-parse --end-of-options)
|
|
1136
|
+
# args_def = Arguments.define do
|
|
1137
|
+
# flag_option :verify
|
|
1138
|
+
# end_of_options as: '--end-of-options'
|
|
1139
|
+
# operand :args, repeatable: true
|
|
1140
|
+
# end
|
|
1141
|
+
# args_def.bind('HEAD').to_a # => ['--end-of-options', 'HEAD']
|
|
1142
|
+
# args_def.bind.to_a # => []
|
|
1143
|
+
#
|
|
1144
|
+
# @param as [String] the CLI token to emit as the options terminator
|
|
1145
|
+
# (default `'--'`). Some commands use a different terminator; for example,
|
|
1146
|
+
# `git rev-parse` uses `'--end-of-options'`.
|
|
1147
|
+
#
|
|
1148
|
+
# @return [void]
|
|
1149
|
+
#
|
|
1150
|
+
# @raise [ArgumentError] if called more than once per definition block
|
|
1151
|
+
#
|
|
1152
|
+
# @raise [ArgumentError] if a flag-producing option is defined after this call
|
|
1153
|
+
def end_of_options(as: '--')
|
|
1154
|
+
raise ArgumentError, 'end_of_options cannot be declared twice' if @end_of_options_declared
|
|
1155
|
+
|
|
1156
|
+
@ordered_definitions << { kind: :end_of_options }
|
|
1157
|
+
@end_of_options_declared = true
|
|
1158
|
+
@end_of_options_as = as
|
|
1159
|
+
@past_separator = true
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
# Define a custom option with a custom builder block
|
|
1163
|
+
#
|
|
1164
|
+
# @example Custom transformation (e.g., formatting a Date value)
|
|
1165
|
+
# args_def = Arguments.define do
|
|
1166
|
+
# custom_option :since do |val|
|
|
1167
|
+
# val ? "--since=#{val.strftime('%Y-%m-%d')}" : nil
|
|
1168
|
+
# end
|
|
1169
|
+
# end
|
|
1170
|
+
# args_def.bind(since: Date.new(2024, 1, 1)).to_a # => ['--since=2024-01-01']
|
|
1171
|
+
# args_def.bind.to_a # => []
|
|
1172
|
+
#
|
|
1173
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
1174
|
+
#
|
|
1175
|
+
# @param required [Boolean] whether the option must be provided (key must exist in opts)
|
|
1176
|
+
#
|
|
1177
|
+
# @param allow_nil [Boolean] whether nil is allowed when required is true. Defaults to true.
|
|
1178
|
+
# When false with required: true, raises ArgumentError if value is nil.
|
|
1179
|
+
#
|
|
1180
|
+
# @return [void]
|
|
1181
|
+
#
|
|
1182
|
+
# @raise [ArgumentError] if defined after an `end_of_options` or `literal '--'` boundary
|
|
1183
|
+
#
|
|
1184
|
+
# @yield [value] block that receives the option value and returns the CLI argument(s)
|
|
1185
|
+
#
|
|
1186
|
+
# @yieldparam value [Object] the bound value for this option
|
|
1187
|
+
#
|
|
1188
|
+
# @yieldreturn [String, Array<String>, nil] the CLI argument(s) to emit;
|
|
1189
|
+
# nil or an empty array emits nothing
|
|
1190
|
+
#
|
|
1191
|
+
def custom_option(names, required: false, allow_nil: true, &block)
|
|
1192
|
+
register_option(names, type: :custom, builder: block, required: required, allow_nil: allow_nil)
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
# Define an execution option (not included in CLI output, forwarded to command execution)
|
|
1196
|
+
#
|
|
1197
|
+
# Execution options are omitted from the CLI argument array produced by {Bound#to_a}, but
|
|
1198
|
+
# their values are still accessible on the {Bound} object. This is useful for options that
|
|
1199
|
+
# control Ruby-side execution context (e.g., working directory) rather than git flags.
|
|
1200
|
+
#
|
|
1201
|
+
# @example Chdir option forwarded to execution context, not emitted as a CLI flag
|
|
1202
|
+
# args_def = Arguments.define do
|
|
1203
|
+
# flag_option :verbose
|
|
1204
|
+
# execution_option :chdir
|
|
1205
|
+
# end
|
|
1206
|
+
# bound = args_def.bind(verbose: true, chdir: '/tmp')
|
|
1207
|
+
# bound.to_a # => ['--verbose'] # :chdir is not included
|
|
1208
|
+
# bound[:chdir] # => '/tmp' # still accessible on the Bound object
|
|
1209
|
+
#
|
|
1210
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
1211
|
+
#
|
|
1212
|
+
# @return [void]
|
|
1213
|
+
def execution_option(names)
|
|
1214
|
+
register_option(names, type: :execution_option)
|
|
1215
|
+
end
|
|
1216
|
+
|
|
1217
|
+
# Declare that arguments conflict with each other (mutually exclusive)
|
|
1218
|
+
#
|
|
1219
|
+
# Each call to {#conflicts} defines a separate group of mutually exclusive
|
|
1220
|
+
# arguments. Names may refer to **options** (flag, value, flag-or-value, etc.)
|
|
1221
|
+
# or **operands** (positional arguments). When {#bind} is called, if more than
|
|
1222
|
+
# one argument in the same conflict group is "present", an ArgumentError is
|
|
1223
|
+
# raised.
|
|
1224
|
+
#
|
|
1225
|
+
# **Presence semantics** — an argument is present when its value is not `nil`,
|
|
1226
|
+
# `[]`, or `''`. `false` is always treated as absent for all option types.
|
|
1227
|
+
#
|
|
1228
|
+
# An ArgumentError is raised at definition time if any name given to
|
|
1229
|
+
# `conflicts` is not a known option or operand, catching typos early.
|
|
1230
|
+
#
|
|
1231
|
+
# The error message has the general form:
|
|
1232
|
+
#
|
|
1233
|
+
# "cannot specify :name1 and :name2"
|
|
1234
|
+
#
|
|
1235
|
+
# @example Option-only conflict group
|
|
1236
|
+
# args_def = Arguments.define do
|
|
1237
|
+
# flag_option :gpg_sign
|
|
1238
|
+
# flag_option :no_gpg_sign
|
|
1239
|
+
# flag_option :force
|
|
1240
|
+
# flag_option :no_force
|
|
1241
|
+
# conflicts :gpg_sign, :no_gpg_sign
|
|
1242
|
+
# conflicts :force, :no_force
|
|
1243
|
+
# end
|
|
1244
|
+
# args_def.bind(gpg_sign: true).to_a # => ['--gpg-sign']
|
|
1245
|
+
#
|
|
1246
|
+
# @example Mixed option and operand conflict
|
|
1247
|
+
# args_def = Arguments.define do
|
|
1248
|
+
# flag_option %i[merge m], as: '--merge'
|
|
1249
|
+
# operand :tree_ish, required: true, allow_nil: true
|
|
1250
|
+
# end_of_options
|
|
1251
|
+
# operand :paths, repeatable: true
|
|
1252
|
+
# conflicts :merge, :tree_ish
|
|
1253
|
+
# end
|
|
1254
|
+
# args_def.bind(nil, 'file.txt', merge: true).to_a # => ['--merge', '--', 'file.txt']
|
|
1255
|
+
# args_def.bind('main', 'file.txt', merge: true)
|
|
1256
|
+
# # => raise ArgumentError, 'cannot specify :merge and :tree_ish'
|
|
1257
|
+
#
|
|
1258
|
+
# @param names [Array<Symbol>] the option/operand names that conflict within
|
|
1259
|
+
# this group
|
|
1260
|
+
#
|
|
1261
|
+
# @return [void]
|
|
1262
|
+
#
|
|
1263
|
+
# @raise [ArgumentError] if any name is not a known option or operand
|
|
1264
|
+
#
|
|
1265
|
+
# @raise [ArgumentError] if more than one argument in the same conflict group
|
|
1266
|
+
# is present when binding arguments (during {#bind})
|
|
1267
|
+
def conflicts(*names)
|
|
1268
|
+
names.each do |name|
|
|
1269
|
+
sym = name.to_sym
|
|
1270
|
+
next if known_argument?(sym)
|
|
1271
|
+
|
|
1272
|
+
raise ArgumentError, "unknown argument :#{sym} in conflicts declaration"
|
|
1273
|
+
end
|
|
1274
|
+
@conflicts << names.map(&:to_sym)
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
# Declare that an exact combination of argument values is forbidden
|
|
1278
|
+
#
|
|
1279
|
+
# Each call to {#forbid_values} defines one forbidden tuple. A tuple matches
|
|
1280
|
+
# when **every** listed name is present (has a bound value after alias
|
|
1281
|
+
# normalization) **and** each value equals the declared value exactly (Ruby
|
|
1282
|
+
# `==`). When a tuple matches, {#bind} raises ArgumentError.
|
|
1283
|
+
#
|
|
1284
|
+
# This fills the gap left by {#conflicts}, which only checks *presence*.
|
|
1285
|
+
# `forbid_values` is useful for negatable flags whose combinations can be
|
|
1286
|
+
# semantically equivalent or contradictory depending on the actual boolean
|
|
1287
|
+
# values — presence-based exclusion would be too coarse.
|
|
1288
|
+
#
|
|
1289
|
+
# Names may refer to **options** (flag, value, flag-or-value, etc.) or
|
|
1290
|
+
# **operands** (positional arguments). Alias names are accepted and
|
|
1291
|
+
# canonicalized to their primary names.
|
|
1292
|
+
#
|
|
1293
|
+
# An ArgumentError is raised at **definition time** if any name is not a
|
|
1294
|
+
# known option or operand.
|
|
1295
|
+
#
|
|
1296
|
+
# The error message has the form:
|
|
1297
|
+
#
|
|
1298
|
+
# "cannot specify :name1=value1 with :name2=value2"
|
|
1299
|
+
#
|
|
1300
|
+
# @example Reject only the contradictory negatable flag combinations
|
|
1301
|
+
# args_def = Arguments.define do
|
|
1302
|
+
# flag_option :all, negatable: true
|
|
1303
|
+
# flag_option :ignore_removal, negatable: true
|
|
1304
|
+
# # --all --ignore-removal: contradictory (add ALL vs ignore removals)
|
|
1305
|
+
# forbid_values all: true, ignore_removal: true
|
|
1306
|
+
# # --no-all --no-ignore-removal: contradictory (ignore removals vs include removals)
|
|
1307
|
+
# forbid_values no_all: true, no_ignore_removal: true
|
|
1308
|
+
# end
|
|
1309
|
+
# # Contradictory tuples raise:
|
|
1310
|
+
# args_def.bind(all: true, ignore_removal: true)
|
|
1311
|
+
# # => raise ArgumentError, 'cannot specify :all=true with :ignore_removal=true'
|
|
1312
|
+
# args_def.bind(no_all: true, no_ignore_removal: true)
|
|
1313
|
+
# # => raise ArgumentError, 'cannot specify :no_all=true with :no_ignore_removal=true'
|
|
1314
|
+
# # Semantically compatible pairs are allowed:
|
|
1315
|
+
# args_def.bind(all: true, no_ignore_removal: true).to_a # => ['--all', '--no-ignore-removal']
|
|
1316
|
+
# args_def.bind(no_all: true, ignore_removal: true).to_a # => ['--no-all', '--ignore-removal']
|
|
1317
|
+
#
|
|
1318
|
+
# @param pairs [Hash{Symbol => Object}] keyword pairs mapping argument name to forbidden value
|
|
1319
|
+
#
|
|
1320
|
+
# Each key must be a known option or operand name. During {#bind}, an
|
|
1321
|
+
# `ArgumentError` is raised when all names are present and all values
|
|
1322
|
+
# exactly match the declared tuple.
|
|
1323
|
+
#
|
|
1324
|
+
# @option pairs [Object] :"argument_name" the value forbidden for the argument with that name
|
|
1325
|
+
#
|
|
1326
|
+
# @return [void]
|
|
1327
|
+
#
|
|
1328
|
+
# @raise [ArgumentError] if any name in `pairs` is not a known option or
|
|
1329
|
+
# operand
|
|
1330
|
+
#
|
|
1331
|
+
# @raise [ArgumentError] during {#bind} if all names are present and all
|
|
1332
|
+
# values exactly match the declared tuple
|
|
1333
|
+
def forbid_values(**pairs)
|
|
1334
|
+
raise ArgumentError, 'forbid_values must be given at least one name-value pair' if pairs.empty?
|
|
1335
|
+
|
|
1336
|
+
pairs.each_key do |name|
|
|
1337
|
+
sym = name.to_sym
|
|
1338
|
+
next if known_argument?(sym)
|
|
1339
|
+
|
|
1340
|
+
raise ArgumentError, "unknown argument :#{sym} in forbid_values declaration"
|
|
1341
|
+
end
|
|
1342
|
+
canonical = pairs.transform_keys { |k| @alias_map[k] || k }
|
|
1343
|
+
@forbidden_values << canonical
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
# Declare that at least one of the named arguments must be present when binding
|
|
1347
|
+
#
|
|
1348
|
+
# Each call to {#requires_one_of} defines an independent "at least one" group.
|
|
1349
|
+
# When {#bind} is called, if none of the arguments in the group is present,
|
|
1350
|
+
# an ArgumentError is raised.
|
|
1351
|
+
#
|
|
1352
|
+
# **Conditional form** — when `when:` is given, the check is only performed if
|
|
1353
|
+
# the named trigger argument is present. If the trigger is absent the group is
|
|
1354
|
+
# skipped entirely.
|
|
1355
|
+
#
|
|
1356
|
+
# **Presence semantics** — two slightly different rules apply:
|
|
1357
|
+
#
|
|
1358
|
+
# - *`when:` trigger* — the trigger is considered present when its value is
|
|
1359
|
+
# not `nil`, `false`, `[]`, or `''`. A flag set to `false` means absent,
|
|
1360
|
+
# so the trigger does **not** fire.
|
|
1361
|
+
# - *Satisfied-by check* — a group member is considered present when its
|
|
1362
|
+
# value is not `nil`, `false`, `[]`, or `''`. `false` is treated as absent
|
|
1363
|
+
# for all option types under the companion-key model.
|
|
1364
|
+
#
|
|
1365
|
+
# Names may refer to **options** (flag, value, flag-or-value, etc.) or
|
|
1366
|
+
# **operands** (positional arguments) interchangeably. Alias resolution happens
|
|
1367
|
+
# before the check, so supplying an alias for one of the named options counts
|
|
1368
|
+
# as that option being present.
|
|
1369
|
+
#
|
|
1370
|
+
# An ArgumentError is raised at definition time if any name (including the
|
|
1371
|
+
# `when:` trigger) is not a known option or operand, catching typos early.
|
|
1372
|
+
#
|
|
1373
|
+
# The error message has the general form (unconditional):
|
|
1374
|
+
#
|
|
1375
|
+
# "at least one of :name1, :name2 must be provided"
|
|
1376
|
+
#
|
|
1377
|
+
# The error message has the general form (conditional, `when:` given):
|
|
1378
|
+
#
|
|
1379
|
+
# ":trigger requires at least one of :name1, :name2"
|
|
1380
|
+
#
|
|
1381
|
+
# @example At-least-one of two keyword options (unconditional)
|
|
1382
|
+
# args_def = Arguments.define do
|
|
1383
|
+
# value_option :pathspec_from_file, inline: true
|
|
1384
|
+
# end_of_options
|
|
1385
|
+
# value_option :pathspec, as_operand: true, repeatable: true
|
|
1386
|
+
# requires_one_of :pathspec, :pathspec_from_file
|
|
1387
|
+
# end
|
|
1388
|
+
# args_def.bind(pathspec: ['file.txt']).to_a # => ['--', 'file.txt']
|
|
1389
|
+
# args_def.bind(pathspec_from_file: 'paths.txt').to_a
|
|
1390
|
+
# # => ['--pathspec-from-file=paths.txt']
|
|
1391
|
+
# args_def.bind
|
|
1392
|
+
# # => raise ArgumentError, 'at least one of :pathspec, :pathspec_from_file must be provided'
|
|
1393
|
+
#
|
|
1394
|
+
# @example Mixed option and operand group (unconditional)
|
|
1395
|
+
# args_def = Arguments.define do
|
|
1396
|
+
# flag_option :all
|
|
1397
|
+
# operand :paths, repeatable: true
|
|
1398
|
+
# requires_one_of :all, :paths
|
|
1399
|
+
# end
|
|
1400
|
+
# args_def.bind('file.txt').to_a # passes — :paths is present
|
|
1401
|
+
# args_def.bind(all: true).to_a # passes — :all is present
|
|
1402
|
+
# args_def.bind
|
|
1403
|
+
# # => raise ArgumentError, 'at least one of :all, :paths must be provided'
|
|
1404
|
+
#
|
|
1405
|
+
# @example Multiple independent groups (unconditional)
|
|
1406
|
+
# args_def = Arguments.define do
|
|
1407
|
+
# flag_option :commit
|
|
1408
|
+
# flag_option :all
|
|
1409
|
+
# value_option :pathspec_from_file, inline: true
|
|
1410
|
+
# end_of_options
|
|
1411
|
+
# value_option :pathspec, as_operand: true, repeatable: true
|
|
1412
|
+
# requires_one_of :commit, :all
|
|
1413
|
+
# requires_one_of :pathspec, :pathspec_from_file
|
|
1414
|
+
# end
|
|
1415
|
+
#
|
|
1416
|
+
# @example Conditional at-least-one-of group (`when:` form)
|
|
1417
|
+
# args_def = Arguments.define do
|
|
1418
|
+
# flag_option :annotate
|
|
1419
|
+
# value_option :message, inline: true
|
|
1420
|
+
# value_option :file, inline: true
|
|
1421
|
+
# requires_one_of :message, :file, when: :annotate
|
|
1422
|
+
# end
|
|
1423
|
+
# args_def.bind(annotate: true, message: 'v1.0').to_a # passes
|
|
1424
|
+
# args_def.bind(annotate: true)
|
|
1425
|
+
# # => raise ArgumentError, ':annotate requires at least one of :message, :file'
|
|
1426
|
+
# args_def.bind # trigger absent — no error
|
|
1427
|
+
#
|
|
1428
|
+
# @param names [Array<Symbol>] the option/operand names where at least one
|
|
1429
|
+
# must be present
|
|
1430
|
+
#
|
|
1431
|
+
# @option kwargs [Symbol] :when optional trigger argument; when given, the check is
|
|
1432
|
+
# only performed if the trigger argument is present
|
|
1433
|
+
#
|
|
1434
|
+
# @return [void]
|
|
1435
|
+
#
|
|
1436
|
+
# @raise [ArgumentError] if no names are given
|
|
1437
|
+
#
|
|
1438
|
+
# @raise [ArgumentError] if any name (or the `when:` trigger) is not a known
|
|
1439
|
+
# option or operand
|
|
1440
|
+
#
|
|
1441
|
+
# @raise [ArgumentError] if none of the arguments in the group is present
|
|
1442
|
+
# when binding arguments (and the trigger, if any, is present)
|
|
1443
|
+
def requires_one_of(*names, **kwargs)
|
|
1444
|
+
condition = kwargs.delete(:when)
|
|
1445
|
+
raise ArgumentError, "requires_one_of: unknown keyword arguments: #{kwargs.keys.inspect}" unless kwargs.empty?
|
|
1446
|
+
raise ArgumentError, 'requires_one_of must be given at least one argument name' if names.empty?
|
|
1447
|
+
|
|
1448
|
+
canonical_group = canonicalize_requires_names(names)
|
|
1449
|
+
canonical_condition = resolve_requires_condition(condition)
|
|
1450
|
+
@requires_one_of << { names: canonical_group, condition: canonical_condition, single: false }
|
|
1451
|
+
end
|
|
1452
|
+
|
|
1453
|
+
# Declare that exactly one of the named arguments must be present when binding
|
|
1454
|
+
#
|
|
1455
|
+
# This is a convenience composite that combines {#requires_one_of} (at least one
|
|
1456
|
+
# must be present) and {#conflicts} (at most one may be present). Use it when a
|
|
1457
|
+
# group of arguments is mutually exclusive *and* the caller must supply precisely
|
|
1458
|
+
# one of them.
|
|
1459
|
+
#
|
|
1460
|
+
# The call:
|
|
1461
|
+
#
|
|
1462
|
+
# requires_exactly_one_of :a, :b, :c
|
|
1463
|
+
#
|
|
1464
|
+
# is exactly equivalent to:
|
|
1465
|
+
#
|
|
1466
|
+
# requires_one_of :a, :b, :c
|
|
1467
|
+
# conflicts :a, :b, :c
|
|
1468
|
+
#
|
|
1469
|
+
# **Presence semantics** — inherits the rules from the constituent methods.
|
|
1470
|
+
# See {#requires_one_of} and {#conflicts} for the full details.
|
|
1471
|
+
#
|
|
1472
|
+
# An ArgumentError is raised at definition time if any name is not a known
|
|
1473
|
+
# option or operand, catching typos early.
|
|
1474
|
+
#
|
|
1475
|
+
# Error messages reuse the formats from the constituent methods:
|
|
1476
|
+
#
|
|
1477
|
+
# "at least one of :a, :b, :c must be provided" # zero present
|
|
1478
|
+
# "cannot specify :a and :b" # two or more present
|
|
1479
|
+
#
|
|
1480
|
+
# @example Mode flags where exactly one must be supplied
|
|
1481
|
+
# args_def = Arguments.define do
|
|
1482
|
+
# flag_option :mode_a
|
|
1483
|
+
# flag_option :mode_b
|
|
1484
|
+
# flag_option :mode_c
|
|
1485
|
+
# requires_exactly_one_of :mode_a, :mode_b, :mode_c
|
|
1486
|
+
# end
|
|
1487
|
+
# args_def.bind(mode_a: true).to_a # => ['--mode-a']
|
|
1488
|
+
# args_def.bind
|
|
1489
|
+
# # => raise ArgumentError, 'at least one of :mode_a, :mode_b, :mode_c must be provided'
|
|
1490
|
+
# args_def.bind(mode_a: true, mode_c: true)
|
|
1491
|
+
# # => raise ArgumentError, 'cannot specify :mode_a and :mode_c'
|
|
1492
|
+
#
|
|
1493
|
+
# @param names [Array<Symbol>] the option/operand names where exactly one
|
|
1494
|
+
# must be present
|
|
1495
|
+
#
|
|
1496
|
+
# @return [void]
|
|
1497
|
+
#
|
|
1498
|
+
# @raise [ArgumentError] if any name is not a known option or operand
|
|
1499
|
+
#
|
|
1500
|
+
# @raise [ArgumentError] at bind time if none of the arguments in the group is present
|
|
1501
|
+
#
|
|
1502
|
+
# @raise [ArgumentError] at bind time if more than one argument in the group is present
|
|
1503
|
+
def requires_exactly_one_of(*names)
|
|
1504
|
+
requires_one_of(*names)
|
|
1505
|
+
conflicts(*names)
|
|
1506
|
+
end
|
|
1507
|
+
|
|
1508
|
+
# Declare that *name* must be present whenever the trigger argument *when:* is present
|
|
1509
|
+
#
|
|
1510
|
+
# When {#bind} is called, if the trigger argument is present and *name* is absent,
|
|
1511
|
+
# an ArgumentError is raised. If the trigger is absent, the check is skipped.
|
|
1512
|
+
#
|
|
1513
|
+
# **Presence semantics** — two slightly different rules apply:
|
|
1514
|
+
#
|
|
1515
|
+
# - *`when:` trigger* — the trigger is considered present when its value is
|
|
1516
|
+
# not `nil`, `false`, `[]`, or `''`. A value of `false` is treated as
|
|
1517
|
+
# absent. If you need an explicit negative form for a negatable flag, use
|
|
1518
|
+
# its `no_<name>` companion key instead.
|
|
1519
|
+
# - *Required argument* — *name* is considered present when its value is
|
|
1520
|
+
# not `nil`, `false`, `[]`, or `''`.
|
|
1521
|
+
#
|
|
1522
|
+
# An ArgumentError is raised at definition time if either *name* or the `when:`
|
|
1523
|
+
# trigger is not a known option or operand, catching typos early.
|
|
1524
|
+
#
|
|
1525
|
+
# The error message has the form:
|
|
1526
|
+
#
|
|
1527
|
+
# ":trigger requires :name"
|
|
1528
|
+
#
|
|
1529
|
+
# @example Require pathspec_from_file when pathspec_file_nul is present
|
|
1530
|
+
# args_def = Arguments.define do
|
|
1531
|
+
# flag_option :pathspec_file_nul
|
|
1532
|
+
# value_option :pathspec_from_file, inline: true
|
|
1533
|
+
# requires :pathspec_from_file, when: :pathspec_file_nul
|
|
1534
|
+
# end
|
|
1535
|
+
# args_def.bind(pathspec_file_nul: true, pathspec_from_file: 'paths.txt').to_a
|
|
1536
|
+
# # => ['--pathspec-file-nul', '--pathspec-from-file=paths.txt']
|
|
1537
|
+
# args_def.bind(pathspec_file_nul: true)
|
|
1538
|
+
# # => raise ArgumentError, ':pathspec_file_nul requires :pathspec_from_file'
|
|
1539
|
+
# args_def.bind # trigger absent — no error
|
|
1540
|
+
#
|
|
1541
|
+
# @example Require dry_run when ignore_missing is present
|
|
1542
|
+
# args_def = Arguments.define do
|
|
1543
|
+
# flag_option :dry_run
|
|
1544
|
+
# flag_option :ignore_missing
|
|
1545
|
+
# requires :dry_run, when: :ignore_missing
|
|
1546
|
+
# end
|
|
1547
|
+
# args_def.bind(ignore_missing: true)
|
|
1548
|
+
# # => raise ArgumentError, ':ignore_missing requires :dry_run'
|
|
1549
|
+
#
|
|
1550
|
+
# @param name [Symbol] the option/operand name that must be present
|
|
1551
|
+
#
|
|
1552
|
+
# @option kwargs [Symbol] :when the trigger argument; when present, *name* must also be present
|
|
1553
|
+
#
|
|
1554
|
+
# @return [void]
|
|
1555
|
+
#
|
|
1556
|
+
# @raise [ArgumentError] if `when:` is not provided
|
|
1557
|
+
#
|
|
1558
|
+
# @raise [ArgumentError] if *name* or the `when:` trigger is not a known option
|
|
1559
|
+
# or operand
|
|
1560
|
+
#
|
|
1561
|
+
# @raise [ArgumentError] if the trigger is present and *name* is absent when
|
|
1562
|
+
# binding arguments
|
|
1563
|
+
def requires(name, **kwargs)
|
|
1564
|
+
condition = kwargs.delete(:when)
|
|
1565
|
+
raise ArgumentError, 'requires: `when:` keyword is required' unless condition
|
|
1566
|
+
raise ArgumentError, "requires: unknown keyword arguments: #{kwargs.keys.inspect}" unless kwargs.empty?
|
|
1567
|
+
|
|
1568
|
+
sym = name.to_sym
|
|
1569
|
+
validate_requires_name!(sym)
|
|
1570
|
+
canonical_trigger = resolve_requires_condition(condition)
|
|
1571
|
+
@requires_one_of << { names: [@alias_map[sym] || sym], condition: canonical_trigger, single: true }
|
|
1572
|
+
end
|
|
1573
|
+
|
|
1574
|
+
# rubocop:disable Layout/LineLength
|
|
1575
|
+
|
|
1576
|
+
# Restrict a value option to a fixed set of accepted strings
|
|
1577
|
+
#
|
|
1578
|
+
# Declares that the named option must only receive values from the given list
|
|
1579
|
+
# when a value is provided. Validation runs during {#bind}, after type checking.
|
|
1580
|
+
# `nil` and absent values are always skipped. Empty strings are skipped when
|
|
1581
|
+
# `allow_empty: true` is set on the option. For `repeatable: true` options
|
|
1582
|
+
# each element of the array is validated individually.
|
|
1583
|
+
#
|
|
1584
|
+
# @example Constrain chmod to '+x' or '-x'
|
|
1585
|
+
# args_def = Arguments.define do
|
|
1586
|
+
# value_option :chmod, inline: true
|
|
1587
|
+
# allowed_values :chmod, in: ['+x', '-x']
|
|
1588
|
+
# end
|
|
1589
|
+
# args_def.bind(chmod: '+x').to_a # => ['--chmod=+x']
|
|
1590
|
+
# args_def.bind(chmod: 'rx')
|
|
1591
|
+
# # => raise ArgumentError, 'Invalid value for :chmod: expected one of ["+x", "-x"], got "rx"'
|
|
1592
|
+
# args_def.bind.to_a # => [] # (absent — no error)
|
|
1593
|
+
#
|
|
1594
|
+
# @example Constrain cleanup to an enumerated set
|
|
1595
|
+
# args_def = Arguments.define do
|
|
1596
|
+
# value_option :cleanup, inline: true
|
|
1597
|
+
# allowed_values :cleanup, in: %w[verbatim whitespace strip]
|
|
1598
|
+
# end
|
|
1599
|
+
# args_def.bind(cleanup: 'verbatim').to_a # => ['--cleanup=verbatim']
|
|
1600
|
+
# args_def.bind(cleanup: 'compact')
|
|
1601
|
+
# # => raise ArgumentError, 'Invalid value for :cleanup: expected one of ["verbatim", "whitespace", "strip"], got "compact"'
|
|
1602
|
+
#
|
|
1603
|
+
# @example Repeatable option — each element is validated
|
|
1604
|
+
# args_def = Arguments.define do
|
|
1605
|
+
# value_option :strategy, inline: true, repeatable: true
|
|
1606
|
+
# allowed_values :strategy, in: %w[ours theirs]
|
|
1607
|
+
# end
|
|
1608
|
+
# args_def.bind(strategy: %w[ours theirs]).to_a
|
|
1609
|
+
# # => ['--strategy=ours', '--strategy=theirs']
|
|
1610
|
+
# args_def.bind(strategy: %w[ours other])
|
|
1611
|
+
# # => raise ArgumentError, 'Invalid value for :strategy: expected one of ["ours", "theirs"], got "other"'
|
|
1612
|
+
#
|
|
1613
|
+
# @param name [Symbol] the option name (primary or alias); must refer to a
|
|
1614
|
+
# previously defined {#value_option} or {#flag_or_value_option}
|
|
1615
|
+
#
|
|
1616
|
+
# @param in [#each] accepted values enumerable. Each value is coerced with
|
|
1617
|
+
# `to_s` and compared as a string.
|
|
1618
|
+
#
|
|
1619
|
+
# For {#flag_or_value_option} variants (including `negatable: true`),
|
|
1620
|
+
# boolean values (`true` / `false`) are skipped by this check because they
|
|
1621
|
+
# control flag-emission behavior rather than representing candidate string
|
|
1622
|
+
# values.
|
|
1623
|
+
#
|
|
1624
|
+
# @return [void]
|
|
1625
|
+
#
|
|
1626
|
+
# @raise [ArgumentError] if `name` is not a known option at definition time
|
|
1627
|
+
#
|
|
1628
|
+
# @raise [ArgumentError] if `name` refers to a non-value option (e.g., a flag)
|
|
1629
|
+
#
|
|
1630
|
+
# @raise [ArgumentError] during {#bind} if the bound value is not in the
|
|
1631
|
+
# accepted set, with a message of the form:
|
|
1632
|
+
# `"Invalid value for :name: expected one of [...], got \"actual\""`
|
|
1633
|
+
def allowed_values(name, in:)
|
|
1634
|
+
sym = name.to_sym
|
|
1635
|
+
defn = validate_allowed_values_definition!(sym)
|
|
1636
|
+
defn[:allowed_values] = coerce_allowed_values_set!(sym, binding.local_variable_get(:in))
|
|
1637
|
+
end
|
|
1638
|
+
|
|
1639
|
+
# rubocop:enable Layout/LineLength
|
|
1640
|
+
|
|
1641
|
+
# Define an operand (positional argument in Ruby terminology)
|
|
1642
|
+
#
|
|
1643
|
+
# Operands are mapped to values following Ruby method signature
|
|
1644
|
+
# semantics. Required operands before a repeatable are filled left-to-right,
|
|
1645
|
+
# required operands after a repeatable are filled from the end, and the
|
|
1646
|
+
# repeatable gets whatever remains in the middle.
|
|
1647
|
+
#
|
|
1648
|
+
# @example Required operand (like `def clone(repository)`)
|
|
1649
|
+
# args_def = Arguments.define do
|
|
1650
|
+
# operand :repository, required: true
|
|
1651
|
+
# end
|
|
1652
|
+
# args_def.bind('https://github.com/user/repo').to_a
|
|
1653
|
+
# # => ['https://github.com/user/repo']
|
|
1654
|
+
#
|
|
1655
|
+
# @example Optional operand with default (like `def log(commit = 'HEAD')`)
|
|
1656
|
+
# args_def = Arguments.define do
|
|
1657
|
+
# operand :commit, default: 'HEAD'
|
|
1658
|
+
# end
|
|
1659
|
+
# args_def.bind().to_a # => ['HEAD']
|
|
1660
|
+
# args_def.bind('main').to_a # => ['main']
|
|
1661
|
+
#
|
|
1662
|
+
# @example Repeatable operand (like `def add(*paths)`)
|
|
1663
|
+
# args_def = Arguments.define do
|
|
1664
|
+
# operand :paths, repeatable: true
|
|
1665
|
+
# end
|
|
1666
|
+
# args_def.bind('file1', 'file2', 'file3').to_a
|
|
1667
|
+
# # => ['file1', 'file2', 'file3']
|
|
1668
|
+
#
|
|
1669
|
+
# @example Required repeatable with at least one value (like `def rm(*paths)` with validation)
|
|
1670
|
+
# args_def = Arguments.define do
|
|
1671
|
+
# operand :paths, repeatable: true, required: true
|
|
1672
|
+
# end
|
|
1673
|
+
# args_def.bind() #=> raise ArgumentError, "at least one value is required for paths"
|
|
1674
|
+
# args_def.bind('file1').to_a # => ['file1']
|
|
1675
|
+
#
|
|
1676
|
+
# @example git mv pattern (like `def mv(*sources, destination)`)
|
|
1677
|
+
# args_def = Arguments.define do
|
|
1678
|
+
# operand :sources, repeatable: true, required: true
|
|
1679
|
+
# operand :destination, required: true
|
|
1680
|
+
# end
|
|
1681
|
+
# args_def.bind('src1', 'src2', 'dest').to_a # => ['src1', 'src2', 'dest']
|
|
1682
|
+
# args_def.bind('src', 'dest').to_a # => ['src', 'dest']
|
|
1683
|
+
#
|
|
1684
|
+
# @example Optional before variadic with required after (like `def foo(a = 'default', *middle, b)`)
|
|
1685
|
+
# args_def = Arguments.define do
|
|
1686
|
+
# operand :a, default: 'default_a'
|
|
1687
|
+
# operand :middle, repeatable: true
|
|
1688
|
+
# operand :b, required: true
|
|
1689
|
+
# end
|
|
1690
|
+
# args_def.bind('x').to_a # => ['default_a', 'x']
|
|
1691
|
+
# args_def.bind('x', 'y').to_a # => ['x', 'y']
|
|
1692
|
+
# args_def.bind('x', 'm', 'y').to_a # => ['x', 'm', 'y']
|
|
1693
|
+
#
|
|
1694
|
+
# @example Operand after end_of_options boundary (pathspec after --)
|
|
1695
|
+
# args_def = Arguments.define do
|
|
1696
|
+
# flag_option :force
|
|
1697
|
+
# end_of_options
|
|
1698
|
+
# operand :paths, repeatable: true
|
|
1699
|
+
# end
|
|
1700
|
+
# args_def.bind('file1', 'file2', force: true).to_a
|
|
1701
|
+
# # => ['--force', '--', 'file1', 'file2']
|
|
1702
|
+
#
|
|
1703
|
+
# @example Complex pattern (like `def diff(commit1, commit2 = nil, *paths)`)
|
|
1704
|
+
# args_def = Arguments.define do
|
|
1705
|
+
# operand :commit1, required: true
|
|
1706
|
+
# operand :commit2
|
|
1707
|
+
# end_of_options
|
|
1708
|
+
# operand :paths, repeatable: true
|
|
1709
|
+
# end
|
|
1710
|
+
# args_def.bind('HEAD~1').to_a # => ['HEAD~1']
|
|
1711
|
+
# args_def.bind('HEAD~1', 'HEAD').to_a # => ['HEAD~1', 'HEAD']
|
|
1712
|
+
# args_def.bind('HEAD~1', 'HEAD', 'file.rb').to_a
|
|
1713
|
+
# # => ['HEAD~1', 'HEAD', '--', 'file.rb']
|
|
1714
|
+
#
|
|
1715
|
+
# @example Required operand that allows nil (like `git checkout [tree-ish] -- paths`)
|
|
1716
|
+
# args_def = Arguments.define do
|
|
1717
|
+
# operand :tree_ish, required: true, allow_nil: true
|
|
1718
|
+
# end_of_options
|
|
1719
|
+
# operand :paths, repeatable: true
|
|
1720
|
+
# end
|
|
1721
|
+
# args_def.bind(nil, 'file1.txt', 'file2.txt').to_a
|
|
1722
|
+
# # => ['--', 'file1.txt', 'file2.txt']
|
|
1723
|
+
# args_def.bind('HEAD', 'file.rb').to_a
|
|
1724
|
+
# # => ['HEAD', '--', 'file.rb']
|
|
1725
|
+
# args_def.bind(nil, 'file.rb').to_a
|
|
1726
|
+
# # => ['--', 'file.rb']
|
|
1727
|
+
#
|
|
1728
|
+
# @param name [Symbol] the operand name (used in error messages)
|
|
1729
|
+
#
|
|
1730
|
+
# @param required [Boolean] whether the argument is required. For repeatable
|
|
1731
|
+
# operands, this means at least one value must be provided.
|
|
1732
|
+
#
|
|
1733
|
+
# @param repeatable [Boolean] whether the argument accepts multiple values
|
|
1734
|
+
# (like Ruby's splat operator *args). Only one repeatable operand is
|
|
1735
|
+
# allowed per definition; attempting to define a second will raise an
|
|
1736
|
+
# ArgumentError.
|
|
1737
|
+
#
|
|
1738
|
+
# @param default [Object] the default value if not provided. For repeatable
|
|
1739
|
+
# operands, this should be an array (e.g., `default: ['.']`).
|
|
1740
|
+
#
|
|
1741
|
+
# @param allow_nil [Boolean] whether nil is a valid value for a required
|
|
1742
|
+
# operand. When true, nil consumes the operand slot but is omitted
|
|
1743
|
+
# from output. This is useful for commands like `git checkout` where
|
|
1744
|
+
# the tree-ish is required to consume a slot but may be nil to restore
|
|
1745
|
+
# from the index. Defaults to false.
|
|
1746
|
+
#
|
|
1747
|
+
# @param skip_cli [Boolean] whether this operand participates in binding,
|
|
1748
|
+
# validation, and accessors but is omitted from CLI argv emission.
|
|
1749
|
+
# Defaults to false.
|
|
1750
|
+
#
|
|
1751
|
+
# @return [void]
|
|
1752
|
+
#
|
|
1753
|
+
# @raise [ArgumentError] during {#bind} if the operand appears before a '--'
|
|
1754
|
+
# boundary (or no boundary exists) and the bound value starts with '-'
|
|
1755
|
+
def operand(name, required: false, repeatable: false, default: nil, allow_nil: false,
|
|
1756
|
+
skip_cli: false)
|
|
1757
|
+
validate_single_repeatable!(name) if repeatable
|
|
1758
|
+
add_operand_definition(name, required, repeatable, default, allow_nil, skip_cli)
|
|
1759
|
+
end
|
|
1760
|
+
|
|
1761
|
+
# Bind positionals and options, returning a Bound object with accessor methods
|
|
1762
|
+
#
|
|
1763
|
+
# Unlike the internal build method which returns a raw Array, this method
|
|
1764
|
+
# returns a {Bound} object that:
|
|
1765
|
+
# - Provides accessor methods for all defined options and positional arguments
|
|
1766
|
+
# - Automatically normalizes option aliases to their canonical names
|
|
1767
|
+
# - Supports splatting via `to_ary` for seamless use with `command(*bound)`
|
|
1768
|
+
#
|
|
1769
|
+
# @example Simple splatting (same behavior as build)
|
|
1770
|
+
# def call(*, **)
|
|
1771
|
+
# @execution_context.command_capturing(*ARGS.bind(*, **))
|
|
1772
|
+
# end
|
|
1773
|
+
#
|
|
1774
|
+
# @example Inspecting options before command execution
|
|
1775
|
+
# args_def = Arguments.define do
|
|
1776
|
+
# flag_option :force
|
|
1777
|
+
# flag_option :remotes, as: ['-r', '--remotes']
|
|
1778
|
+
# operand :branch_names, repeatable: true
|
|
1779
|
+
# end
|
|
1780
|
+
# bound_args = args_def.bind('branch1', 'branch2', force: true, remotes: true)
|
|
1781
|
+
# bound_args.force? # => true
|
|
1782
|
+
# bound_args.remotes? # => true
|
|
1783
|
+
# bound_args.branch_names # => ['branch1', 'branch2']
|
|
1784
|
+
#
|
|
1785
|
+
# @example Hash-style access for reserved names
|
|
1786
|
+
# args_def = Arguments.define do
|
|
1787
|
+
# value_option :hash
|
|
1788
|
+
# end
|
|
1789
|
+
# bound_args = args_def.bind(hash: 'abc123')
|
|
1790
|
+
# bound_args[:hash] # => 'abc123'
|
|
1791
|
+
#
|
|
1792
|
+
# @param positionals [Array] positional argument values
|
|
1793
|
+
#
|
|
1794
|
+
# @param opts [Hash] the keyword options
|
|
1795
|
+
#
|
|
1796
|
+
# @option opts [Object] :"option_name" value for any option registered in this argument definition
|
|
1797
|
+
#
|
|
1798
|
+
# @return [Bound] a frozen object with accessor methods for all arguments
|
|
1799
|
+
#
|
|
1800
|
+
# @raise [ArgumentError] if unsupported options are provided or validation fails
|
|
1801
|
+
#
|
|
1802
|
+
# @raise [ArgumentError] if an operand value before a '--' boundary starts with '-'
|
|
1803
|
+
def bind(*positionals, **opts)
|
|
1804
|
+
normalized_opts = validate_and_normalize_options!(opts)
|
|
1805
|
+
allocated_positionals = allocate_and_validate_positionals(positionals)
|
|
1806
|
+
validate_bind_inputs!(normalized_opts, allocated_positionals)
|
|
1807
|
+
|
|
1808
|
+
args_array = build_ordered_arguments(allocated_positionals, normalized_opts)
|
|
1809
|
+
options_hash = build_options_hash(normalized_opts)
|
|
1810
|
+
execution_option_names = option_names_by_type(:execution_option)
|
|
1811
|
+
flag_names = option_names_by_type(:flag)
|
|
1812
|
+
|
|
1813
|
+
Bound.new(args_array, options_hash, allocated_positionals, execution_option_names, flag_names)
|
|
1814
|
+
end
|
|
1815
|
+
|
|
1816
|
+
# Option types allowed after a '--' separator boundary (they do not produce CLI flags)
|
|
1817
|
+
OPTION_TYPES_AFTER_SEPARATOR = %i[value_as_operand execution_option].freeze
|
|
1818
|
+
|
|
1819
|
+
# Sentinel object placed in the build array by an :end_of_options definition.
|
|
1820
|
+
# It is later replaced by the stored `as:` value (default `'--'`) if any element
|
|
1821
|
+
# follows it, or stripped if it is last.
|
|
1822
|
+
# Uses Object identity comparison (== is not overridden) so it can never collide
|
|
1823
|
+
# with the literal string '--' or any other real argument value.
|
|
1824
|
+
END_OF_OPTIONS_MARKER = Object.new.freeze
|
|
1825
|
+
private_constant :END_OF_OPTIONS_MARKER
|
|
1826
|
+
|
|
1827
|
+
# Option types that accept a string value — eligible for `allowed_values` constraints
|
|
1828
|
+
VALUE_OPTION_TYPES_FOR_ALLOWED_VALUES = %i[
|
|
1829
|
+
value inline_value value_as_operand
|
|
1830
|
+
flag_or_value flag_or_inline_value
|
|
1831
|
+
].freeze
|
|
1832
|
+
|
|
1833
|
+
# The subset of VALUE_OPTION_TYPES_FOR_ALLOWED_VALUES whose boolean values
|
|
1834
|
+
# carry semantic meaning (true = emit flag, false = suppress flag) and must
|
|
1835
|
+
# skip allowed_values validation rather than being compared against the set.
|
|
1836
|
+
FLAG_OR_VALUE_OPTION_TYPES = %i[
|
|
1837
|
+
flag_or_value flag_or_inline_value
|
|
1838
|
+
].freeze
|
|
1839
|
+
|
|
1840
|
+
private
|
|
1841
|
+
|
|
1842
|
+
# Run all cross-field validations on bound inputs
|
|
1843
|
+
#
|
|
1844
|
+
# @param normalized_opts [Hash] normalized keyword options
|
|
1845
|
+
#
|
|
1846
|
+
# @param allocated_positionals [Hash] allocated positional arguments
|
|
1847
|
+
#
|
|
1848
|
+
# @return [void]
|
|
1849
|
+
#
|
|
1850
|
+
def validate_bind_inputs!(normalized_opts, allocated_positionals)
|
|
1851
|
+
validate_no_option_like_operands!(allocated_positionals)
|
|
1852
|
+
validate_conflicts!(normalized_opts, allocated_positionals)
|
|
1853
|
+
validate_forbidden_values!(normalized_opts, allocated_positionals)
|
|
1854
|
+
validate_requires_one_of!(normalized_opts, allocated_positionals)
|
|
1855
|
+
end
|
|
1856
|
+
|
|
1857
|
+
# Collect option names whose definition type is one of the given types
|
|
1858
|
+
#
|
|
1859
|
+
# @param types [Array<Symbol>] the option types to match
|
|
1860
|
+
#
|
|
1861
|
+
# @return [Array<Symbol>] matching option names
|
|
1862
|
+
#
|
|
1863
|
+
def option_names_by_type(*types)
|
|
1864
|
+
@option_definitions.each_with_object([]) do |(name, definition), names|
|
|
1865
|
+
names << name if types.include?(definition[:type])
|
|
1866
|
+
end
|
|
1867
|
+
end
|
|
1868
|
+
|
|
1869
|
+
# Validate and normalize keyword options
|
|
1870
|
+
#
|
|
1871
|
+
# @param opts [Hash] raw keyword options
|
|
1872
|
+
#
|
|
1873
|
+
# @option opts [Object] :"option_name" value for any option registered in this argument definition
|
|
1874
|
+
#
|
|
1875
|
+
# @return [Hash] normalized options with aliases resolved
|
|
1876
|
+
#
|
|
1877
|
+
# @raise [ArgumentError] if options are unsupported, conflicting, or invalid
|
|
1878
|
+
#
|
|
1879
|
+
def validate_and_normalize_options!(opts)
|
|
1880
|
+
validate_unsupported_options!(opts)
|
|
1881
|
+
validate_conflicting_aliases!(opts)
|
|
1882
|
+
normalized_opts = normalize_aliases(opts)
|
|
1883
|
+
validate_required_options!(normalized_opts)
|
|
1884
|
+
validate_option_values!(normalized_opts)
|
|
1885
|
+
normalized_opts
|
|
1886
|
+
end
|
|
1887
|
+
|
|
1888
|
+
# Build a hash of all option values for the Bound object
|
|
1889
|
+
#
|
|
1890
|
+
# @param normalized_opts [Hash] the normalized options
|
|
1891
|
+
#
|
|
1892
|
+
# @return [Hash{Symbol => Object}] option values with defaults applied
|
|
1893
|
+
def build_options_hash(normalized_opts)
|
|
1894
|
+
result = {}
|
|
1895
|
+
@option_definitions.each_key do |name|
|
|
1896
|
+
result[name] = normalized_opts.key?(name) ? normalized_opts[name] : default_option_value(name)
|
|
1897
|
+
end
|
|
1898
|
+
result
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
# Get the default value for an option when not provided
|
|
1902
|
+
#
|
|
1903
|
+
# @param name [Symbol] the option name
|
|
1904
|
+
#
|
|
1905
|
+
# @return [Object] the default value (false for flags, nil for values)
|
|
1906
|
+
def default_option_value(name)
|
|
1907
|
+
definition = @option_definitions[name]
|
|
1908
|
+
case definition[:type]
|
|
1909
|
+
when :flag
|
|
1910
|
+
false
|
|
1911
|
+
end
|
|
1912
|
+
end
|
|
1913
|
+
|
|
1914
|
+
# Determine the internal option type based on inline and as_operand modifiers
|
|
1915
|
+
#
|
|
1916
|
+
# @param inline [Boolean] whether to use inline format (--flag=value)
|
|
1917
|
+
#
|
|
1918
|
+
# @param as_operand [Boolean] whether to output as operand (positional argument)
|
|
1919
|
+
#
|
|
1920
|
+
# @return [Symbol] the internal option type
|
|
1921
|
+
#
|
|
1922
|
+
def determine_value_option_type(inline, as_operand)
|
|
1923
|
+
if as_operand
|
|
1924
|
+
:value_as_operand
|
|
1925
|
+
elsif inline
|
|
1926
|
+
:inline_value
|
|
1927
|
+
else
|
|
1928
|
+
:value
|
|
1929
|
+
end
|
|
1930
|
+
end
|
|
1931
|
+
|
|
1932
|
+
# Validate value modifier combinations
|
|
1933
|
+
#
|
|
1934
|
+
# @param names [Symbol, Array<Symbol>] the option name(s)
|
|
1935
|
+
#
|
|
1936
|
+
# @param inline [Boolean] whether inline: true was specified
|
|
1937
|
+
#
|
|
1938
|
+
# @param as_operand [Boolean] whether as_operand: true was specified
|
|
1939
|
+
#
|
|
1940
|
+
# @raise [ArgumentError] if invalid modifier combination is used
|
|
1941
|
+
#
|
|
1942
|
+
def validate_value_modifiers!(names, inline, as_operand)
|
|
1943
|
+
primary = Array(names).first
|
|
1944
|
+
raise ArgumentError, "inline: and as_operand: cannot both be true for :#{primary}" if inline && as_operand
|
|
1945
|
+
end
|
|
1946
|
+
|
|
1947
|
+
# Register an option with optional aliases
|
|
1948
|
+
#
|
|
1949
|
+
# @param names [Symbol, Array<Symbol>] the option name(s), first is primary
|
|
1950
|
+
#
|
|
1951
|
+
# @param definition [Hash] the option definition
|
|
1952
|
+
#
|
|
1953
|
+
# @option definition [Symbol] :type the DSL option type (e.g. `:flag`, `:value`)
|
|
1954
|
+
#
|
|
1955
|
+
# @option definition [String, Array<String>, nil] :as the CLI flag name override
|
|
1956
|
+
#
|
|
1957
|
+
# @option definition [Class, nil] :expected_type the required Ruby class for the value
|
|
1958
|
+
#
|
|
1959
|
+
# @option definition [Proc, nil] :validator a callable that validates the value
|
|
1960
|
+
#
|
|
1961
|
+
# @option definition [Boolean] :required (false) whether the option is required
|
|
1962
|
+
#
|
|
1963
|
+
# @option definition [Boolean] :allow_nil whether nil is a valid value
|
|
1964
|
+
#
|
|
1965
|
+
# @option definition [Proc, nil] :builder a custom argument builder callable
|
|
1966
|
+
#
|
|
1967
|
+
# @option definition [Integer, nil] :max_times maximum repeat count for flag options
|
|
1968
|
+
#
|
|
1969
|
+
# @option definition [Boolean] :allow_empty whether empty values should be emitted
|
|
1970
|
+
#
|
|
1971
|
+
# @option definition [Boolean] :repeatable whether the option accepts multiple values
|
|
1972
|
+
#
|
|
1973
|
+
# @option definition [String] :key_separator separator used by key-value options
|
|
1974
|
+
#
|
|
1975
|
+
# @return [void]
|
|
1976
|
+
#
|
|
1977
|
+
def register_option(names, **definition)
|
|
1978
|
+
keys = Array(names)
|
|
1979
|
+
primary = keys.first
|
|
1980
|
+
definition[:aliases] = keys
|
|
1981
|
+
validate_no_duplicate_aliases!(keys)
|
|
1982
|
+
validate_no_companion_collision!(keys)
|
|
1983
|
+
validate_option_after_separator!(definition[:type], primary)
|
|
1984
|
+
validate_as_parameter!(definition, primary)
|
|
1985
|
+
apply_type_validator!(definition, primary)
|
|
1986
|
+
store_option(primary, keys, definition)
|
|
1987
|
+
end
|
|
1988
|
+
|
|
1989
|
+
# Store option in the option definitions and alias map
|
|
1990
|
+
#
|
|
1991
|
+
# @param primary [Symbol] the primary option name
|
|
1992
|
+
#
|
|
1993
|
+
# @param keys [Array<Symbol>] all alias keys including the primary
|
|
1994
|
+
#
|
|
1995
|
+
# @param definition [Hash] the option definition hash
|
|
1996
|
+
#
|
|
1997
|
+
# @return [void]
|
|
1998
|
+
#
|
|
1999
|
+
# @api private
|
|
2000
|
+
def store_option(primary, keys, definition)
|
|
2001
|
+
@option_definitions[primary] = definition
|
|
2002
|
+
keys.each { |key| @alias_map[key] = primary }
|
|
2003
|
+
@ordered_definitions << { kind: :option, name: primary }
|
|
2004
|
+
end
|
|
2005
|
+
|
|
2006
|
+
# Raise if any of `keys` collides with a previously synthesized `no_<name>`
|
|
2007
|
+
# companion entry. This catches the case where a user declares
|
|
2008
|
+
# `flag_option :foo, negatable: true` followed by `flag_option :no_foo`.
|
|
2009
|
+
#
|
|
2010
|
+
# @param keys [Array<Symbol>] the option alias keys to check
|
|
2011
|
+
#
|
|
2012
|
+
# @return [void]
|
|
2013
|
+
#
|
|
2014
|
+
# @raise [ArgumentError] if any key is already registered as a negatable companion
|
|
2015
|
+
#
|
|
2016
|
+
# @api private
|
|
2017
|
+
def validate_no_companion_collision!(keys)
|
|
2018
|
+
keys.each do |key|
|
|
2019
|
+
next unless @negatable_companions.include?(key)
|
|
2020
|
+
|
|
2021
|
+
raise ArgumentError,
|
|
2022
|
+
"option key :#{key} is already registered as a negatable companion"
|
|
2023
|
+
end
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
# Raise if the `keys` array contains duplicate entries
|
|
2027
|
+
#
|
|
2028
|
+
# Duplicate aliases in a single declaration (e.g. `flag_option %i[foo foo]`)
|
|
2029
|
+
# are a programming mistake and would silently overwrite each other in
|
|
2030
|
+
# `@alias_map`. Catching them at definition time makes the error obvious.
|
|
2031
|
+
#
|
|
2032
|
+
# @param keys [Array<Symbol>] the option alias keys to check for duplicates
|
|
2033
|
+
#
|
|
2034
|
+
# @return [void]
|
|
2035
|
+
#
|
|
2036
|
+
# @raise [ArgumentError] if the keys array contains duplicate entries
|
|
2037
|
+
#
|
|
2038
|
+
# @api private
|
|
2039
|
+
def validate_no_duplicate_aliases!(keys)
|
|
2040
|
+
seen = Set.new
|
|
2041
|
+
keys.each do |key|
|
|
2042
|
+
raise ArgumentError, "duplicate alias key :#{key} in option definition" unless seen.add?(key)
|
|
2043
|
+
end
|
|
2044
|
+
end
|
|
2045
|
+
|
|
2046
|
+
# Validate that max_times is nil or an Integer >= 2
|
|
2047
|
+
#
|
|
2048
|
+
# @param option_name [Symbol] the option name (for error messages)
|
|
2049
|
+
#
|
|
2050
|
+
# @param max_times [Integer, nil] the max_times value to validate
|
|
2051
|
+
#
|
|
2052
|
+
# @return [void]
|
|
2053
|
+
#
|
|
2054
|
+
# @raise [ArgumentError] if max_times is not nil and not an Integer >= 2
|
|
2055
|
+
#
|
|
2056
|
+
# @api private
|
|
2057
|
+
def validate_max_times!(option_name, max_times)
|
|
2058
|
+
return if max_times.nil?
|
|
2059
|
+
|
|
2060
|
+
return if max_times.is_a?(Integer) && max_times >= 2
|
|
2061
|
+
|
|
2062
|
+
raise ArgumentError, "max_times for :#{option_name} must be an Integer >= 2"
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2065
|
+
# Register two companion :flag entries for a negatable flag option
|
|
2066
|
+
#
|
|
2067
|
+
# Registers a positive entry for `names` and a boolean-only negative entry for
|
|
2068
|
+
# `:no_<primary>`. An automatic conflict is added so that both being
|
|
2069
|
+
# true at bind time raises ArgumentError.
|
|
2070
|
+
#
|
|
2071
|
+
# @param names [Symbol, Array<Symbol>] the option name(s); first is primary
|
|
2072
|
+
#
|
|
2073
|
+
# @param as [String, nil] the CLI flag string, or nil to derive from name
|
|
2074
|
+
#
|
|
2075
|
+
# @param required [Boolean] whether at least one of the pair must be provided
|
|
2076
|
+
#
|
|
2077
|
+
# @param allow_nil [Boolean] whether nil is a permitted bound value
|
|
2078
|
+
#
|
|
2079
|
+
# @param max_times [Integer, nil] maximum number of times the flag may be repeated
|
|
2080
|
+
#
|
|
2081
|
+
# @return [void]
|
|
2082
|
+
#
|
|
2083
|
+
# @api private
|
|
2084
|
+
def register_negatable_flag_pair(names, as:, required:, allow_nil:, max_times:)
|
|
2085
|
+
primary = Array(names).first
|
|
2086
|
+
validate_negatable_allow_nil!(primary, required: required, allow_nil: allow_nil)
|
|
2087
|
+
prepare_negatable!(primary, names, as)
|
|
2088
|
+
|
|
2089
|
+
register_option(names, type: :flag, as: as, expected_type: nil, validator: nil,
|
|
2090
|
+
required: false, allow_nil: allow_nil, max_times: max_times)
|
|
2091
|
+
register_negative_companion(primary, as: as, required: required)
|
|
2092
|
+
end
|
|
2093
|
+
|
|
2094
|
+
# Register a positive flag-or-value entry and a boolean-only negative companion
|
|
2095
|
+
# entry for a negatable flag-or-value option
|
|
2096
|
+
#
|
|
2097
|
+
# @param names [Symbol, Array<Symbol>] the option name(s); first is primary
|
|
2098
|
+
#
|
|
2099
|
+
# @param as [String, nil] the CLI flag string, or nil to derive from name
|
|
2100
|
+
#
|
|
2101
|
+
# @param type [Class, Array<Class>, nil] expected Ruby type(s) for the value
|
|
2102
|
+
#
|
|
2103
|
+
# @param inline [Boolean] whether to use inline format (--flag=value)
|
|
2104
|
+
#
|
|
2105
|
+
# @param repeatable [Boolean] whether the option may be given multiple times
|
|
2106
|
+
#
|
|
2107
|
+
# @param required [Boolean] whether at least one of the pair must be provided
|
|
2108
|
+
#
|
|
2109
|
+
# @param allow_nil [Boolean] whether nil is a permitted bound value
|
|
2110
|
+
#
|
|
2111
|
+
# @return [void]
|
|
2112
|
+
#
|
|
2113
|
+
# @api private
|
|
2114
|
+
def register_negatable_flag_or_value_pair(names, as:, type:, inline:, repeatable:, required:, allow_nil:)
|
|
2115
|
+
primary = Array(names).first
|
|
2116
|
+
validate_negatable_allow_nil!(primary, required: required, allow_nil: allow_nil)
|
|
2117
|
+
prepare_negatable!(primary, names, as)
|
|
2118
|
+
|
|
2119
|
+
positive_type = inline ? :flag_or_inline_value : :flag_or_value
|
|
2120
|
+
register_option(names, type: positive_type, as: as, expected_type: type,
|
|
2121
|
+
repeatable: repeatable, required: false, allow_nil: allow_nil)
|
|
2122
|
+
register_negative_companion(primary, as: as, required: required)
|
|
2123
|
+
end
|
|
2124
|
+
|
|
2125
|
+
# Run shared validations for a negatable option before registering either side
|
|
2126
|
+
#
|
|
2127
|
+
# @param primary [Symbol] the primary option name
|
|
2128
|
+
#
|
|
2129
|
+
# @param names [Symbol, Array<Symbol>] the option name(s) including aliases
|
|
2130
|
+
#
|
|
2131
|
+
# @param as [String, nil] the CLI flag string, or nil to derive from name
|
|
2132
|
+
#
|
|
2133
|
+
# @return [void]
|
|
2134
|
+
#
|
|
2135
|
+
# @raise [ArgumentError] if any negatable precondition is violated
|
|
2136
|
+
#
|
|
2137
|
+
# @api private
|
|
2138
|
+
def prepare_negatable!(primary, names, as)
|
|
2139
|
+
validate_negatable_primary_key!(primary)
|
|
2140
|
+
validate_negatable_as_not_array!(primary, as)
|
|
2141
|
+
validate_negatable_as_long_form!(primary, as)
|
|
2142
|
+
no_name = :"no_#{primary}"
|
|
2143
|
+
validate_no_negatable_collision!(no_name)
|
|
2144
|
+
validate_no_companion_in_alias_list!(no_name, Array(names))
|
|
2145
|
+
end
|
|
2146
|
+
|
|
2147
|
+
# Register the synthesized `no_<primary>` flag entry, the auto-conflict, and
|
|
2148
|
+
# (when `required: true`) the auto requires_one_of group
|
|
2149
|
+
#
|
|
2150
|
+
# @param primary [Symbol] the primary option name
|
|
2151
|
+
#
|
|
2152
|
+
# @param as [String, nil] the positive CLI flag string used to derive the negative form
|
|
2153
|
+
#
|
|
2154
|
+
# @param required [Boolean] whether an auto requires_one_of group should be added
|
|
2155
|
+
#
|
|
2156
|
+
# @return [void]
|
|
2157
|
+
#
|
|
2158
|
+
# @api private
|
|
2159
|
+
def register_negative_companion(primary, as:, required:)
|
|
2160
|
+
no_name = :"no_#{primary}"
|
|
2161
|
+
positive_flag = as || default_arg_spec(primary)
|
|
2162
|
+
negative_flag = negate_flag(positive_flag)
|
|
2163
|
+
|
|
2164
|
+
register_option(no_name, type: :flag, as: negative_flag, expected_type: nil, validator: nil,
|
|
2165
|
+
required: false, allow_nil: true)
|
|
2166
|
+
@negatable_companions << no_name
|
|
2167
|
+
@conflicts << [primary, no_name]
|
|
2168
|
+
@requires_one_of << { names: [primary, no_name], condition: nil, single: false } if required
|
|
2169
|
+
end
|
|
2170
|
+
|
|
2171
|
+
# Raise if `allow_nil: false` is combined with `negatable: true` and `required: true`
|
|
2172
|
+
#
|
|
2173
|
+
# When `negatable: true` and `required: true`, the "required" constraint is enforced
|
|
2174
|
+
# by an auto `requires_one_of` group (either the primary or its `no_<name>` companion
|
|
2175
|
+
# must be present). Because the primary option is internally registered with
|
|
2176
|
+
# `required: false`, the `allow_nil: false` nil-check never runs, making the
|
|
2177
|
+
# combination silently misleading. Fail at definition time instead.
|
|
2178
|
+
#
|
|
2179
|
+
# @param key [Symbol] the primary option name (for the error message)
|
|
2180
|
+
#
|
|
2181
|
+
# @param required [Boolean] whether the option is required
|
|
2182
|
+
#
|
|
2183
|
+
# @param allow_nil [Boolean] whether nil is allowed
|
|
2184
|
+
#
|
|
2185
|
+
# @return [void]
|
|
2186
|
+
#
|
|
2187
|
+
# @raise [ArgumentError] if `required: true` and `allow_nil: false` are combined
|
|
2188
|
+
# with `negatable: true`
|
|
2189
|
+
#
|
|
2190
|
+
def validate_negatable_allow_nil!(key, required:, allow_nil:)
|
|
2191
|
+
return unless required && allow_nil == false
|
|
2192
|
+
|
|
2193
|
+
raise ArgumentError,
|
|
2194
|
+
"allow_nil: false cannot be used with negatable: true and required: true on :#{key} " \
|
|
2195
|
+
'(nil is caught by the auto requires_one_of group, not allow_nil)'
|
|
2196
|
+
end
|
|
2197
|
+
|
|
2198
|
+
# Validate that the primary key is snake_case so a meaningful no_ companion can be generated
|
|
2199
|
+
#
|
|
2200
|
+
# @param key [Symbol] the primary option name to validate
|
|
2201
|
+
#
|
|
2202
|
+
# @return [void]
|
|
2203
|
+
#
|
|
2204
|
+
# @raise [ArgumentError] if key is not snake_case
|
|
2205
|
+
#
|
|
2206
|
+
# @api private
|
|
2207
|
+
def validate_negatable_primary_key!(key)
|
|
2208
|
+
return if key.to_s.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
2209
|
+
|
|
2210
|
+
raise ArgumentError,
|
|
2211
|
+
"negatable: true requires a snake_case primary key, got :#{key} " \
|
|
2212
|
+
"(would generate :no_#{key} which is not a meaningful negative form)"
|
|
2213
|
+
end
|
|
2214
|
+
|
|
2215
|
+
# Raise if as: is an Array when negatable: true
|
|
2216
|
+
#
|
|
2217
|
+
# Arrays for as: are not compatible with negatable: true regardless of the
|
|
2218
|
+
# underlying option type — the synthesized +--no-<flag>+ form has no sensible
|
|
2219
|
+
# mapping when the positive form expands to multiple CLI tokens.
|
|
2220
|
+
#
|
|
2221
|
+
# @param primary [Symbol] the primary option name (for error messages)
|
|
2222
|
+
#
|
|
2223
|
+
# @param as [Object] the as: parameter value to check
|
|
2224
|
+
#
|
|
2225
|
+
# @return [void]
|
|
2226
|
+
#
|
|
2227
|
+
# @raise [ArgumentError] if as: is an Array
|
|
2228
|
+
#
|
|
2229
|
+
# @api private
|
|
2230
|
+
def validate_negatable_as_not_array!(primary, as)
|
|
2231
|
+
return unless as.is_a?(Array)
|
|
2232
|
+
|
|
2233
|
+
raise ArgumentError,
|
|
2234
|
+
"arrays for as: parameter cannot be combined with negatable: true (option :#{primary})"
|
|
2235
|
+
end
|
|
2236
|
+
|
|
2237
|
+
# Raise if as: is given as a short-form flag (e.g. +-S+) when negatable: true.
|
|
2238
|
+
# Negation requires a long-form flag because the synthesized companion is
|
|
2239
|
+
# always +--no-<flag>+; deriving it from a short flag would yield a
|
|
2240
|
+
# nonexistent git form like +--no-S+.
|
|
2241
|
+
#
|
|
2242
|
+
# @param primary [Symbol] the primary option name (for error messages)
|
|
2243
|
+
#
|
|
2244
|
+
# @param as [String, nil] the as: parameter value to check
|
|
2245
|
+
#
|
|
2246
|
+
# @return [void]
|
|
2247
|
+
#
|
|
2248
|
+
# @raise [ArgumentError] if as: is not nil and not a long-form flag
|
|
2249
|
+
#
|
|
2250
|
+
# @api private
|
|
2251
|
+
def validate_negatable_as_long_form!(primary, as)
|
|
2252
|
+
return if as.nil?
|
|
2253
|
+
return if as.is_a?(String) && as.start_with?('--')
|
|
2254
|
+
|
|
2255
|
+
raise ArgumentError,
|
|
2256
|
+
"negatable: true requires a long-form (--flag) value for as: on :#{primary}, got #{as.inspect}"
|
|
2257
|
+
end
|
|
2258
|
+
|
|
2259
|
+
# Raise if the generated no_ companion key is already registered
|
|
2260
|
+
#
|
|
2261
|
+
# @param no_name [Symbol] the synthesized companion key (e.g. :no_force)
|
|
2262
|
+
#
|
|
2263
|
+
# @return [void]
|
|
2264
|
+
#
|
|
2265
|
+
# @raise [ArgumentError] if the companion key is already in the alias map
|
|
2266
|
+
#
|
|
2267
|
+
# @api private
|
|
2268
|
+
def validate_no_negatable_collision!(no_name)
|
|
2269
|
+
return unless @alias_map.key?(no_name)
|
|
2270
|
+
|
|
2271
|
+
raise ArgumentError,
|
|
2272
|
+
"negatable: true would register :#{no_name} but that key is already registered"
|
|
2273
|
+
end
|
|
2274
|
+
|
|
2275
|
+
# Raise if the synthesized companion key appears in the same declaration's alias list
|
|
2276
|
+
#
|
|
2277
|
+
# This catches e.g. `flag_option %i[foo no_foo], negatable: true` where `:no_foo`
|
|
2278
|
+
# is listed as an alias and would be silently overwritten when the companion is
|
|
2279
|
+
# registered, corrupting `@alias_map` and `@option_definitions`.
|
|
2280
|
+
#
|
|
2281
|
+
# @param no_name [Symbol] the synthesized companion key (e.g. :no_force)
|
|
2282
|
+
#
|
|
2283
|
+
# @param keys [Array<Symbol>] the alias list from the current declaration
|
|
2284
|
+
#
|
|
2285
|
+
# @return [void]
|
|
2286
|
+
#
|
|
2287
|
+
# @raise [ArgumentError] if the companion key is already listed as an alias
|
|
2288
|
+
#
|
|
2289
|
+
# @api private
|
|
2290
|
+
def validate_no_companion_in_alias_list!(no_name, keys)
|
|
2291
|
+
return unless keys.include?(no_name)
|
|
2292
|
+
|
|
2293
|
+
raise ArgumentError,
|
|
2294
|
+
"negatable: true would register :#{no_name} as a companion, but :#{no_name} " \
|
|
2295
|
+
'is already listed as an alias in the same declaration'
|
|
2296
|
+
end
|
|
2297
|
+
|
|
2298
|
+
# Validate that flag-producing options are not defined after a '--' boundary
|
|
2299
|
+
#
|
|
2300
|
+
# @param type [Symbol] the option type
|
|
2301
|
+
#
|
|
2302
|
+
# @param option_name [Symbol] the primary option name
|
|
2303
|
+
#
|
|
2304
|
+
# @return [void]
|
|
2305
|
+
#
|
|
2306
|
+
# @raise [ArgumentError] if a flag-producing option is defined after '--'
|
|
2307
|
+
#
|
|
2308
|
+
def validate_option_after_separator!(type, option_name)
|
|
2309
|
+
return unless @past_separator
|
|
2310
|
+
return if OPTION_TYPES_AFTER_SEPARATOR.include?(type)
|
|
2311
|
+
|
|
2312
|
+
raise ArgumentError,
|
|
2313
|
+
"option :#{option_name} cannot be defined after a '--' separator " \
|
|
2314
|
+
'boundary because its flags would be treated as operands by git'
|
|
2315
|
+
end
|
|
2316
|
+
|
|
2317
|
+
# Apply a type-checking validator to a definition when expected_type is set
|
|
2318
|
+
#
|
|
2319
|
+
# @param definition [Hash] the option definition hash (mutated in place)
|
|
2320
|
+
#
|
|
2321
|
+
# @param option_name [Symbol] the option name (for error messages)
|
|
2322
|
+
#
|
|
2323
|
+
# @return [void]
|
|
2324
|
+
#
|
|
2325
|
+
# @raise [ArgumentError] if both `expected_type` and `validator` are specified
|
|
2326
|
+
#
|
|
2327
|
+
# @api private
|
|
2328
|
+
def apply_type_validator!(definition, option_name)
|
|
2329
|
+
return unless definition[:expected_type]
|
|
2330
|
+
|
|
2331
|
+
if definition[:validator]
|
|
2332
|
+
raise ArgumentError,
|
|
2333
|
+
"cannot specify both type: and validator: for :#{option_name}"
|
|
2334
|
+
end
|
|
2335
|
+
|
|
2336
|
+
definition[:validator] = create_type_validator(option_name, definition[:expected_type])
|
|
2337
|
+
end
|
|
2338
|
+
|
|
2339
|
+
# Validate that array as: values are only used with flag-type options
|
|
2340
|
+
#
|
|
2341
|
+
# @param definition [Hash] the option definition hash
|
|
2342
|
+
#
|
|
2343
|
+
# @param option_name [Symbol] the primary option name (for error messages)
|
|
2344
|
+
#
|
|
2345
|
+
# @return [void]
|
|
2346
|
+
#
|
|
2347
|
+
# @raise [ArgumentError] if as: is an Array and the type is not :flag
|
|
2348
|
+
#
|
|
2349
|
+
# @api private
|
|
2350
|
+
def validate_as_parameter!(definition, option_name)
|
|
2351
|
+
return unless definition[:as].is_a?(Array)
|
|
2352
|
+
|
|
2353
|
+
return if definition[:type] == :flag
|
|
2354
|
+
|
|
2355
|
+
type = definition[:type]
|
|
2356
|
+
raise ArgumentError,
|
|
2357
|
+
"arrays for as: parameter are only supported for flag types, not :#{type} (option :#{option_name})"
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
# Build arguments by iterating over definitions in their defined order
|
|
2361
|
+
#
|
|
2362
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
2363
|
+
#
|
|
2364
|
+
# @param normalized_opts [Hash] normalized keyword options
|
|
2365
|
+
#
|
|
2366
|
+
# @return [Array<String>] the command-line arguments
|
|
2367
|
+
#
|
|
2368
|
+
def build_ordered_arguments(allocated_positionals, normalized_opts)
|
|
2369
|
+
args = []
|
|
2370
|
+
|
|
2371
|
+
@ordered_definitions.each do |entry|
|
|
2372
|
+
if entry[:kind] == :end_of_options
|
|
2373
|
+
args << END_OF_OPTIONS_MARKER
|
|
2374
|
+
else
|
|
2375
|
+
build_entry(args, entry, normalized_opts, allocated_positionals)
|
|
2376
|
+
end
|
|
2377
|
+
end
|
|
2378
|
+
|
|
2379
|
+
resolve_end_of_options_marker(args)
|
|
2380
|
+
end
|
|
2381
|
+
|
|
2382
|
+
# Build a single definition entry and append to args
|
|
2383
|
+
#
|
|
2384
|
+
# @param args [Array<String>] the argument array to append to
|
|
2385
|
+
#
|
|
2386
|
+
# @param entry [Hash] the definition entry with :kind and name/flag
|
|
2387
|
+
#
|
|
2388
|
+
# @param normalized_opts [Hash] normalized keyword options
|
|
2389
|
+
#
|
|
2390
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
2391
|
+
#
|
|
2392
|
+
# @return [void]
|
|
2393
|
+
#
|
|
2394
|
+
def build_entry(args, entry, normalized_opts, allocated_positionals)
|
|
2395
|
+
case entry[:kind]
|
|
2396
|
+
when :static
|
|
2397
|
+
args << entry[:flag]
|
|
2398
|
+
when :option
|
|
2399
|
+
build_option(args, entry[:name], @option_definitions[entry[:name]], normalized_opts[entry[:name]])
|
|
2400
|
+
when :operand
|
|
2401
|
+
build_single_positional(args, entry[:name], allocated_positionals)
|
|
2402
|
+
# simplecov:disable this case should be unreachable
|
|
2403
|
+
else
|
|
2404
|
+
raise ArgumentError, "unknown entry kind: #{entry[:kind].inspect}"
|
|
2405
|
+
end
|
|
2406
|
+
# simplecov:enable
|
|
2407
|
+
end
|
|
2408
|
+
|
|
2409
|
+
# Replace the END_OF_OPTIONS_MARKER with the stored `as:` value if any element
|
|
2410
|
+
# follows it, or strip it
|
|
2411
|
+
#
|
|
2412
|
+
# @param args [Array] the built argument array (may contain END_OF_OPTIONS_MARKER)
|
|
2413
|
+
#
|
|
2414
|
+
# @return [Array<String>] the argument array with the marker resolved
|
|
2415
|
+
#
|
|
2416
|
+
def resolve_end_of_options_marker(args)
|
|
2417
|
+
idx = args.index(END_OF_OPTIONS_MARKER)
|
|
2418
|
+
return args unless idx
|
|
2419
|
+
|
|
2420
|
+
if idx == args.size - 1
|
|
2421
|
+
args.delete_at(idx) # nothing follows — strip
|
|
2422
|
+
else
|
|
2423
|
+
args[idx] = @end_of_options_as # something follows — make it real
|
|
2424
|
+
end
|
|
2425
|
+
args
|
|
2426
|
+
end
|
|
2427
|
+
|
|
2428
|
+
# Allocate positionals and perform validation, returning the allocation hash
|
|
2429
|
+
#
|
|
2430
|
+
# @param positionals [Array] positional argument values
|
|
2431
|
+
#
|
|
2432
|
+
# @return [Hash] allocation of positional names to values
|
|
2433
|
+
#
|
|
2434
|
+
def allocate_and_validate_positionals(positionals)
|
|
2435
|
+
positionals = normalize_positionals(positionals)
|
|
2436
|
+
allocation, consumed_count = allocate_positionals(positionals)
|
|
2437
|
+
|
|
2438
|
+
@operand_definitions.each do |definition|
|
|
2439
|
+
value = allocation[definition[:name]]
|
|
2440
|
+
validate_required_positional(value, definition)
|
|
2441
|
+
validate_no_nil_values!(value, definition)
|
|
2442
|
+
end
|
|
2443
|
+
|
|
2444
|
+
check_unexpected_positionals(positionals, consumed_count)
|
|
2445
|
+
allocation
|
|
2446
|
+
end
|
|
2447
|
+
|
|
2448
|
+
# Build a single positional argument
|
|
2449
|
+
#
|
|
2450
|
+
# @param args [Array<String>] the argument array to append to
|
|
2451
|
+
#
|
|
2452
|
+
# @param name [Symbol] the positional argument name
|
|
2453
|
+
#
|
|
2454
|
+
# @param allocation [Hash] the allocated positional values
|
|
2455
|
+
#
|
|
2456
|
+
# @return [void]
|
|
2457
|
+
#
|
|
2458
|
+
def build_single_positional(args, name, allocation)
|
|
2459
|
+
definition = @operand_definitions.find { |d| d[:name] == name }
|
|
2460
|
+
return if definition[:skip_cli]
|
|
2461
|
+
|
|
2462
|
+
value = allocation[name]
|
|
2463
|
+
append_positional_to_args(args, value, definition)
|
|
2464
|
+
end
|
|
2465
|
+
|
|
2466
|
+
# Raise if a second repeatable operand is declared
|
|
2467
|
+
#
|
|
2468
|
+
# @param name [Symbol] the name of the operand being added
|
|
2469
|
+
#
|
|
2470
|
+
# @return [void]
|
|
2471
|
+
#
|
|
2472
|
+
# @raise [ArgumentError] if a repeatable operand is already registered
|
|
2473
|
+
#
|
|
2474
|
+
# @api private
|
|
2475
|
+
def validate_single_repeatable!(name)
|
|
2476
|
+
existing_repeatable = @operand_definitions.find { |d| d[:repeatable] }
|
|
2477
|
+
return unless existing_repeatable
|
|
2478
|
+
|
|
2479
|
+
raise ArgumentError,
|
|
2480
|
+
"only one repeatable operand is allowed; :#{existing_repeatable[:name]} is already repeatable, " \
|
|
2481
|
+
"cannot add :#{name} as repeatable"
|
|
2482
|
+
end
|
|
2483
|
+
|
|
2484
|
+
# Append a positional operand definition to the internal arrays
|
|
2485
|
+
#
|
|
2486
|
+
# @param name [Symbol] the operand name
|
|
2487
|
+
#
|
|
2488
|
+
# @param required [Boolean] whether the operand is required
|
|
2489
|
+
#
|
|
2490
|
+
# @param repeatable [Boolean] whether the operand may accept multiple values
|
|
2491
|
+
#
|
|
2492
|
+
# @param default [Object] the default value when the operand is not provided
|
|
2493
|
+
#
|
|
2494
|
+
# @param allow_nil [Boolean] whether nil is a permitted value
|
|
2495
|
+
#
|
|
2496
|
+
# @param skip_cli [Boolean] whether to omit this operand from the CLI array
|
|
2497
|
+
#
|
|
2498
|
+
# @return [void]
|
|
2499
|
+
#
|
|
2500
|
+
# @api private
|
|
2501
|
+
def add_operand_definition(name, required, repeatable, default, allow_nil, skip_cli)
|
|
2502
|
+
@operand_definitions << {
|
|
2503
|
+
name: name, required: required, repeatable: repeatable,
|
|
2504
|
+
default: default, allow_nil: allow_nil, skip_cli: skip_cli
|
|
2505
|
+
}
|
|
2506
|
+
@ordered_definitions << { kind: :operand, name: name }
|
|
2507
|
+
end
|
|
2508
|
+
|
|
2509
|
+
# Maps option types to their build strategy (Symbol method name or lambda)
|
|
2510
|
+
#
|
|
2511
|
+
# @api private
|
|
2512
|
+
BUILDERS = {
|
|
2513
|
+
flag: :build_flag,
|
|
2514
|
+
value: lambda do |args, arg_spec, value, definition|
|
|
2515
|
+
if definition[:repeatable]
|
|
2516
|
+
Array(value).each { |v| args << arg_spec << v.to_s }
|
|
2517
|
+
else
|
|
2518
|
+
args << arg_spec << value.to_s
|
|
2519
|
+
end
|
|
2520
|
+
end,
|
|
2521
|
+
inline_value: :build_inline_value,
|
|
2522
|
+
flag_or_inline_value: :build_flag_or_inline_value,
|
|
2523
|
+
flag_or_value: :build_flag_or_value,
|
|
2524
|
+
value_as_operand: lambda do |args, _, value, definition|
|
|
2525
|
+
# Validate array usage when repeatable is false
|
|
2526
|
+
if value.is_a?(Array) && !definition[:repeatable]
|
|
2527
|
+
raise ArgumentError,
|
|
2528
|
+
"value_as_operand :#{definition[:aliases].first} requires repeatable: true to accept an array"
|
|
2529
|
+
end
|
|
2530
|
+
|
|
2531
|
+
# Validate no nil values in array
|
|
2532
|
+
if definition[:repeatable] && value.is_a?(Array) && value.any?(&:nil?)
|
|
2533
|
+
raise ArgumentError,
|
|
2534
|
+
"nil values are not allowed in value_as_operand :#{definition[:aliases].first}"
|
|
2535
|
+
end
|
|
2536
|
+
|
|
2537
|
+
# Add values as positional arguments
|
|
2538
|
+
if definition[:repeatable]
|
|
2539
|
+
Array(value).each { |v| args << v.to_s }
|
|
2540
|
+
else
|
|
2541
|
+
args << value.to_s
|
|
2542
|
+
end
|
|
2543
|
+
end,
|
|
2544
|
+
key_value: :build_key_value,
|
|
2545
|
+
inline_key_value: :build_inline_key_value,
|
|
2546
|
+
custom: lambda do |args, _, value, definition|
|
|
2547
|
+
result = definition[:builder]&.call(value)
|
|
2548
|
+
result.is_a?(Array) ? args.concat(result) : (args << result if result)
|
|
2549
|
+
end,
|
|
2550
|
+
execution_option: ->(*) {}
|
|
2551
|
+
}.freeze
|
|
2552
|
+
private_constant :BUILDERS
|
|
2553
|
+
|
|
2554
|
+
# Build a single option's CLI tokens and append them to args
|
|
2555
|
+
#
|
|
2556
|
+
# @param args [Array<String>] the argument array to append to
|
|
2557
|
+
#
|
|
2558
|
+
# @param name [Symbol] the canonical option name
|
|
2559
|
+
#
|
|
2560
|
+
# @param definition [Hash] the option definition hash
|
|
2561
|
+
#
|
|
2562
|
+
# @param value [Object] the bound value for the option
|
|
2563
|
+
#
|
|
2564
|
+
# @return [void]
|
|
2565
|
+
#
|
|
2566
|
+
# @api private
|
|
2567
|
+
def build_option(args, name, definition, value)
|
|
2568
|
+
return if should_skip_option?(value, definition)
|
|
2569
|
+
|
|
2570
|
+
arg_spec = definition[:as] || default_arg_spec(name)
|
|
2571
|
+
builder = BUILDERS[definition[:type]]
|
|
2572
|
+
if builder.is_a?(Symbol)
|
|
2573
|
+
send(builder, args, arg_spec, value, definition)
|
|
2574
|
+
else
|
|
2575
|
+
builder.call(args, arg_spec, value, definition)
|
|
2576
|
+
end
|
|
2577
|
+
end
|
|
2578
|
+
|
|
2579
|
+
# Generate the default argument specification based on option name length
|
|
2580
|
+
#
|
|
2581
|
+
# POSIX convention: single-character options use single dash (-f),
|
|
2582
|
+
# multi-character options use double dash (--force)
|
|
2583
|
+
#
|
|
2584
|
+
# @param name [Symbol] the option name
|
|
2585
|
+
#
|
|
2586
|
+
# @return [String] the argument specification (e.g., '-f' or '--force')
|
|
2587
|
+
#
|
|
2588
|
+
def default_arg_spec(name)
|
|
2589
|
+
name_str = name.to_s.tr('_', '-')
|
|
2590
|
+
name_str.length == 1 ? "-#{name_str}" : "--#{name_str}"
|
|
2591
|
+
end
|
|
2592
|
+
|
|
2593
|
+
# Check if an argument specification is for a short (single-character) option
|
|
2594
|
+
#
|
|
2595
|
+
# @param arg_spec [String] the argument specification
|
|
2596
|
+
#
|
|
2597
|
+
# @return [Boolean] true if this is a short option (single dash, single char)
|
|
2598
|
+
#
|
|
2599
|
+
def short_option?(arg_spec)
|
|
2600
|
+
arg_spec.is_a?(String) && arg_spec.match?(/\A-[^-]\z/)
|
|
2601
|
+
end
|
|
2602
|
+
|
|
2603
|
+
# Build key_value option tokens and append to args
|
|
2604
|
+
#
|
|
2605
|
+
# @param args [Array<String>] the argument array to append to
|
|
2606
|
+
#
|
|
2607
|
+
# @param arg_spec [String] the CLI flag string
|
|
2608
|
+
#
|
|
2609
|
+
# @param value [Hash, Array] the key-value input
|
|
2610
|
+
#
|
|
2611
|
+
# @param definition [Hash] the option definition hash
|
|
2612
|
+
#
|
|
2613
|
+
# @return [void]
|
|
2614
|
+
#
|
|
2615
|
+
# @api private
|
|
2616
|
+
def build_key_value(args, arg_spec, value, definition)
|
|
2617
|
+
sep = definition[:key_separator] || '='
|
|
2618
|
+
option_name = definition[:aliases].first
|
|
2619
|
+
normalize_key_value_pairs(value).each do |pair|
|
|
2620
|
+
validate_key_value_pair_size!(pair, option_name)
|
|
2621
|
+
k, v = pair
|
|
2622
|
+
validate_key_value_key!(k, sep, option_name)
|
|
2623
|
+
validate_key_value_value!(v, option_name)
|
|
2624
|
+
args << arg_spec << (v.nil? ? k.to_s : "#{k}#{sep}#{v}")
|
|
2625
|
+
end
|
|
2626
|
+
end
|
|
2627
|
+
|
|
2628
|
+
# Build inline_key_value option tokens and append to args
|
|
2629
|
+
#
|
|
2630
|
+
# @param args [Array<String>] the argument array to append to
|
|
2631
|
+
#
|
|
2632
|
+
# @param arg_spec [String] the CLI flag string
|
|
2633
|
+
#
|
|
2634
|
+
# @param value [Hash, Array] the key-value input
|
|
2635
|
+
#
|
|
2636
|
+
# @param definition [Hash] the option definition hash
|
|
2637
|
+
#
|
|
2638
|
+
# @return [void]
|
|
2639
|
+
#
|
|
2640
|
+
# @api private
|
|
2641
|
+
def build_inline_key_value(args, arg_spec, value, definition)
|
|
2642
|
+
sep = definition[:key_separator] || '='
|
|
2643
|
+
option_name = definition[:aliases].first
|
|
2644
|
+
normalize_key_value_pairs(value).each do |pair|
|
|
2645
|
+
validate_key_value_pair_size!(pair, option_name)
|
|
2646
|
+
k, v = pair
|
|
2647
|
+
validate_key_value_key!(k, sep, option_name)
|
|
2648
|
+
validate_key_value_value!(v, option_name)
|
|
2649
|
+
args << "#{arg_spec}=#{v.nil? ? k.to_s : "#{k}#{sep}#{v}"}"
|
|
2650
|
+
end
|
|
2651
|
+
end
|
|
2652
|
+
|
|
2653
|
+
# Build inline value option with POSIX-compliant formatting
|
|
2654
|
+
#
|
|
2655
|
+
# Short options (single-char) use no separator: -n3
|
|
2656
|
+
# Long options (multi-char) use = separator: --name=value
|
|
2657
|
+
#
|
|
2658
|
+
# @param args [Array<String>] the argument array to append to
|
|
2659
|
+
#
|
|
2660
|
+
# @param arg_spec [String] the CLI flag string
|
|
2661
|
+
#
|
|
2662
|
+
# @param value [Object] the bound value
|
|
2663
|
+
#
|
|
2664
|
+
# @param definition [Hash] the option definition hash
|
|
2665
|
+
#
|
|
2666
|
+
# @return [void]
|
|
2667
|
+
#
|
|
2668
|
+
# @api private
|
|
2669
|
+
def build_inline_value(args, arg_spec, value, definition)
|
|
2670
|
+
sep = inline_value_separator(arg_spec)
|
|
2671
|
+
if definition[:repeatable]
|
|
2672
|
+
Array(value).each { |v| args << "#{arg_spec}#{sep}#{v}" }
|
|
2673
|
+
else
|
|
2674
|
+
args << "#{arg_spec}#{sep}#{value}"
|
|
2675
|
+
end
|
|
2676
|
+
end
|
|
2677
|
+
|
|
2678
|
+
# Build flag or inline value option with POSIX-compliant formatting
|
|
2679
|
+
#
|
|
2680
|
+
# @param args [Array<String>] the argument array to append to
|
|
2681
|
+
#
|
|
2682
|
+
# @param arg_spec [String] the CLI flag string
|
|
2683
|
+
#
|
|
2684
|
+
# @param value [Boolean, Object] the bound value
|
|
2685
|
+
#
|
|
2686
|
+
# @param definition [Hash] the option definition hash
|
|
2687
|
+
#
|
|
2688
|
+
# @return [void]
|
|
2689
|
+
#
|
|
2690
|
+
# @api private
|
|
2691
|
+
def build_flag_or_inline_value(args, arg_spec, value, definition)
|
|
2692
|
+
each_flag_or_value_value(value, definition, 'flag_or_inline_value') do |v|
|
|
2693
|
+
next if v == false
|
|
2694
|
+
|
|
2695
|
+
args << (v == true ? arg_spec : "#{arg_spec}#{inline_value_separator(arg_spec)}#{v}")
|
|
2696
|
+
end
|
|
2697
|
+
end
|
|
2698
|
+
|
|
2699
|
+
# Build flag or value option
|
|
2700
|
+
#
|
|
2701
|
+
# @param args [Array<String>] the argument array to append to
|
|
2702
|
+
#
|
|
2703
|
+
# @param arg_spec [String] the CLI flag string
|
|
2704
|
+
#
|
|
2705
|
+
# @param value [Boolean, Object] the bound value
|
|
2706
|
+
#
|
|
2707
|
+
# @param definition [Hash] the option definition hash
|
|
2708
|
+
#
|
|
2709
|
+
# @return [void]
|
|
2710
|
+
#
|
|
2711
|
+
# @api private
|
|
2712
|
+
def build_flag_or_value(args, arg_spec, value, definition)
|
|
2713
|
+
each_flag_or_value_value(value, definition, 'flag_or_value') do |v|
|
|
2714
|
+
next if v == false
|
|
2715
|
+
|
|
2716
|
+
if v == true
|
|
2717
|
+
args << arg_spec
|
|
2718
|
+
else
|
|
2719
|
+
args << arg_spec << v.to_s
|
|
2720
|
+
end
|
|
2721
|
+
end
|
|
2722
|
+
end
|
|
2723
|
+
|
|
2724
|
+
# Iterate over each individual flag-or-value element, validating and yielding
|
|
2725
|
+
#
|
|
2726
|
+
# @param value [Object] the bound value (scalar or array for repeatable)
|
|
2727
|
+
#
|
|
2728
|
+
# @param definition [Hash] the option definition hash
|
|
2729
|
+
#
|
|
2730
|
+
# @param option_type [String] the option type name (for error messages)
|
|
2731
|
+
#
|
|
2732
|
+
# @return [void]
|
|
2733
|
+
#
|
|
2734
|
+
# @yield [value] each validated element
|
|
2735
|
+
#
|
|
2736
|
+
# @yieldparam value [Object] the validated scalar value (true, false, or a non-nil object)
|
|
2737
|
+
#
|
|
2738
|
+
# @api private
|
|
2739
|
+
def each_flag_or_value_value(value, definition, option_type)
|
|
2740
|
+
values = definition[:repeatable] ? Array(value) : [value]
|
|
2741
|
+
values.each do |v|
|
|
2742
|
+
validate_flag_or_value_type!(v, option_type)
|
|
2743
|
+
yield v
|
|
2744
|
+
end
|
|
2745
|
+
end
|
|
2746
|
+
|
|
2747
|
+
# Validate that a flag_or_value element is not nil.
|
|
2748
|
+
#
|
|
2749
|
+
# Boolean values (true/false) control flag presence/absence. Any other non-nil
|
|
2750
|
+
# object is accepted and converted to a CLI argument string via `#to_s`.
|
|
2751
|
+
# Nil is rejected only within repeatable arrays — non-repeatable nil values are
|
|
2752
|
+
# filtered out earlier by `should_skip_option?` and never reach here.
|
|
2753
|
+
#
|
|
2754
|
+
# @param value [Object] the element value to validate
|
|
2755
|
+
#
|
|
2756
|
+
# @param option_type [String] the option type name (for error messages)
|
|
2757
|
+
#
|
|
2758
|
+
# @return [void]
|
|
2759
|
+
#
|
|
2760
|
+
# @raise [ArgumentError] if value is nil
|
|
2761
|
+
#
|
|
2762
|
+
# @api private
|
|
2763
|
+
def validate_flag_or_value_type!(value, option_type)
|
|
2764
|
+
return unless value.nil?
|
|
2765
|
+
|
|
2766
|
+
raise ArgumentError,
|
|
2767
|
+
"Invalid value for #{option_type}: nil is not allowed as an array element; " \
|
|
2768
|
+
'expected true, false, or a non-nil object that responds to #to_s'
|
|
2769
|
+
end
|
|
2770
|
+
|
|
2771
|
+
# Determine the separator to use for inline values based on option type
|
|
2772
|
+
#
|
|
2773
|
+
# POSIX convention:
|
|
2774
|
+
# - Short options (single dash, single char like -n): no separator (-n3)
|
|
2775
|
+
# - Long options (double dash like --name): = separator (--name=value)
|
|
2776
|
+
#
|
|
2777
|
+
# @param arg_spec [String] the argument specification
|
|
2778
|
+
#
|
|
2779
|
+
# @return [String] empty string ('') for short options, '=' for long options;
|
|
2780
|
+
# never returns nil, safe to concatenate directly
|
|
2781
|
+
#
|
|
2782
|
+
def inline_value_separator(arg_spec)
|
|
2783
|
+
short_option?(arg_spec) ? '' : '='
|
|
2784
|
+
end
|
|
2785
|
+
|
|
2786
|
+
# Build flag option tokens and append to args
|
|
2787
|
+
#
|
|
2788
|
+
# @param args [Array<String>] the argument array to append to
|
|
2789
|
+
#
|
|
2790
|
+
# @param arg_spec [String, Array<String>] the CLI flag string(s)
|
|
2791
|
+
#
|
|
2792
|
+
# @param value [Boolean, Integer] the bound flag value
|
|
2793
|
+
#
|
|
2794
|
+
# @param definition [Hash] the option definition hash
|
|
2795
|
+
#
|
|
2796
|
+
# @return [void]
|
|
2797
|
+
#
|
|
2798
|
+
# @api private
|
|
2799
|
+
def build_flag(args, arg_spec, value, definition)
|
|
2800
|
+
count = normalize_flag_value!(value, definition)
|
|
2801
|
+
append_repeated_flag(args, arg_spec, count)
|
|
2802
|
+
end
|
|
2803
|
+
|
|
2804
|
+
# Append a flag to the args array the given number of times
|
|
2805
|
+
#
|
|
2806
|
+
# @param args [Array<String>] the argument array to append to
|
|
2807
|
+
#
|
|
2808
|
+
# @param arg_spec [String, Array<String>] the CLI flag string(s)
|
|
2809
|
+
#
|
|
2810
|
+
# @param count [Integer] the number of times to append the flag
|
|
2811
|
+
#
|
|
2812
|
+
# @return [void]
|
|
2813
|
+
#
|
|
2814
|
+
# @api private
|
|
2815
|
+
def append_repeated_flag(args, arg_spec, count)
|
|
2816
|
+
return if count <= 0
|
|
2817
|
+
|
|
2818
|
+
count.times do
|
|
2819
|
+
arg_spec.is_a?(Array) ? args.concat(arg_spec) : args << arg_spec
|
|
2820
|
+
end
|
|
2821
|
+
end
|
|
2822
|
+
|
|
2823
|
+
# Normalize a flag bound value to a repeat count integer
|
|
2824
|
+
#
|
|
2825
|
+
# @param value [Boolean, Integer, nil] the bound flag value
|
|
2826
|
+
#
|
|
2827
|
+
# @param definition [Hash] the option definition hash
|
|
2828
|
+
#
|
|
2829
|
+
# @return [Integer] the number of times the flag should be emitted
|
|
2830
|
+
#
|
|
2831
|
+
# @raise [ArgumentError] if the value is not valid for this flag type
|
|
2832
|
+
#
|
|
2833
|
+
# @api private
|
|
2834
|
+
def normalize_flag_value!(value, definition)
|
|
2835
|
+
return 1 if value == true
|
|
2836
|
+
return 0 if value.nil? || value == false
|
|
2837
|
+
|
|
2838
|
+
option_name = definition[:aliases].first
|
|
2839
|
+
max_times = definition[:max_times]
|
|
2840
|
+
|
|
2841
|
+
raise_flag_type_boolean_error!(value, definition) if max_times.nil?
|
|
2842
|
+
|
|
2843
|
+
return normalize_flag_integer_value!(value, option_name, max_times) if value.is_a?(Integer)
|
|
2844
|
+
|
|
2845
|
+
raise ArgumentError, "Invalid value for :#{option_name}: expected true, false, or a positive Integer"
|
|
2846
|
+
end
|
|
2847
|
+
|
|
2848
|
+
# Raise a type error for a flag option that received a non-boolean value without max_times
|
|
2849
|
+
#
|
|
2850
|
+
# @param value [Object] the invalid value
|
|
2851
|
+
#
|
|
2852
|
+
# @param definition [Hash] the option definition hash
|
|
2853
|
+
#
|
|
2854
|
+
# @return [void]
|
|
2855
|
+
#
|
|
2856
|
+
# @raise [ArgumentError] always
|
|
2857
|
+
#
|
|
2858
|
+
# @api private
|
|
2859
|
+
def raise_flag_type_boolean_error!(value, definition)
|
|
2860
|
+
raise_flag_boolean_error!(definition[:aliases].first, value)
|
|
2861
|
+
end
|
|
2862
|
+
|
|
2863
|
+
# Raise an ArgumentError describing a non-boolean value for a boolean-only flag option
|
|
2864
|
+
#
|
|
2865
|
+
# @param option_name [Symbol] the option name (for the error message)
|
|
2866
|
+
#
|
|
2867
|
+
# @param value [Object] the invalid value received
|
|
2868
|
+
#
|
|
2869
|
+
# @return [void]
|
|
2870
|
+
#
|
|
2871
|
+
# @raise [ArgumentError] always
|
|
2872
|
+
#
|
|
2873
|
+
# @api private
|
|
2874
|
+
def raise_flag_boolean_error!(option_name, value)
|
|
2875
|
+
raise ArgumentError,
|
|
2876
|
+
"flag_option :#{option_name} expects a boolean value, got #{value.inspect} (#{value.class})"
|
|
2877
|
+
end
|
|
2878
|
+
|
|
2879
|
+
# Validate and return an integer flag repeat count within the max_times limit
|
|
2880
|
+
#
|
|
2881
|
+
# @param value [Integer] the integer value to validate
|
|
2882
|
+
#
|
|
2883
|
+
# @param option_name [Symbol] the option name (for error messages)
|
|
2884
|
+
#
|
|
2885
|
+
# @param max_times [Integer] the maximum allowed repeat count
|
|
2886
|
+
#
|
|
2887
|
+
# @return [Integer] the validated repeat count
|
|
2888
|
+
#
|
|
2889
|
+
# @raise [ArgumentError] if value is not a positive Integer or exceeds max_times
|
|
2890
|
+
#
|
|
2891
|
+
# @api private
|
|
2892
|
+
def normalize_flag_integer_value!(value, option_name, max_times)
|
|
2893
|
+
raise ArgumentError, "Invalid value for :#{option_name}: expected a positive Integer" if value <= 0
|
|
2894
|
+
|
|
2895
|
+
raise_max_times_exceeded!(option_name, value, max_times) if value > max_times
|
|
2896
|
+
|
|
2897
|
+
value
|
|
2898
|
+
end
|
|
2899
|
+
|
|
2900
|
+
# Raise an ArgumentError when a flag repeat count exceeds max_times
|
|
2901
|
+
#
|
|
2902
|
+
# @param option_name [Symbol] the option name (for the error message)
|
|
2903
|
+
#
|
|
2904
|
+
# @param value [Integer] the value that exceeded the limit
|
|
2905
|
+
#
|
|
2906
|
+
# @param max_times [Integer] the configured maximum
|
|
2907
|
+
#
|
|
2908
|
+
# @return [void]
|
|
2909
|
+
#
|
|
2910
|
+
# @raise [ArgumentError] always
|
|
2911
|
+
#
|
|
2912
|
+
# @api private
|
|
2913
|
+
def raise_max_times_exceeded!(option_name, value, max_times)
|
|
2914
|
+
raise ArgumentError,
|
|
2915
|
+
"#{option_name}: #{value} exceeds max_times: #{max_times} for :#{option_name}"
|
|
2916
|
+
end
|
|
2917
|
+
|
|
2918
|
+
# Negate a flag by adding --no- prefix
|
|
2919
|
+
#
|
|
2920
|
+
# For short options (-f), expands to --no-f
|
|
2921
|
+
# For long options (--force), transforms to --no-force
|
|
2922
|
+
#
|
|
2923
|
+
# @param arg_spec [String] the argument specification
|
|
2924
|
+
#
|
|
2925
|
+
# @return [String] the negated flag
|
|
2926
|
+
#
|
|
2927
|
+
def negate_flag(arg_spec)
|
|
2928
|
+
if short_option?(arg_spec)
|
|
2929
|
+
# -f => --no-f
|
|
2930
|
+
"--no-#{arg_spec[1]}"
|
|
2931
|
+
else
|
|
2932
|
+
# --force => --no-force
|
|
2933
|
+
arg_spec.sub(/\A--/, '--no-')
|
|
2934
|
+
end
|
|
2935
|
+
end
|
|
2936
|
+
|
|
2937
|
+
# Determine whether an option value should be skipped during CLI build
|
|
2938
|
+
#
|
|
2939
|
+
# @param value [Object] the bound option value
|
|
2940
|
+
#
|
|
2941
|
+
# @param definition [Hash] the option definition hash
|
|
2942
|
+
#
|
|
2943
|
+
# @return [Boolean] true if the option should be omitted from the CLI array
|
|
2944
|
+
#
|
|
2945
|
+
# @api private
|
|
2946
|
+
def should_skip_option?(value, definition)
|
|
2947
|
+
return true if value.nil?
|
|
2948
|
+
return true if value == false && %i[flag_or_inline_value flag_or_value].include?(definition[:type])
|
|
2949
|
+
return skip_value_as_operand_array?(value, definition) if value.is_a?(Array)
|
|
2950
|
+
|
|
2951
|
+
value.respond_to?(:empty?) && value.empty? && !definition[:allow_empty]
|
|
2952
|
+
end
|
|
2953
|
+
|
|
2954
|
+
# For value_as_operand, empty arrays always skip regardless of allow_empty
|
|
2955
|
+
# (allow_empty only applies to empty strings, not empty arrays)
|
|
2956
|
+
#
|
|
2957
|
+
# @param value [Array] the array value to evaluate
|
|
2958
|
+
#
|
|
2959
|
+
# @param definition [Hash] the option definition hash
|
|
2960
|
+
#
|
|
2961
|
+
# @return [Boolean] true if the option should be skipped
|
|
2962
|
+
#
|
|
2963
|
+
# @api private
|
|
2964
|
+
def skip_value_as_operand_array?(value, definition)
|
|
2965
|
+
return value.empty? if definition[:type] == :value_as_operand
|
|
2966
|
+
|
|
2967
|
+
value.empty? && !definition[:allow_empty]
|
|
2968
|
+
end
|
|
2969
|
+
|
|
2970
|
+
# Normalize key-value input to an array of [key, value] pairs
|
|
2971
|
+
#
|
|
2972
|
+
# Accepts:
|
|
2973
|
+
# - Hash: { 'key' => 'value' } or { 'key' => ['v1', 'v2'] }
|
|
2974
|
+
# - Array of arrays: [['key', 'value'], ['key2', 'value2']]
|
|
2975
|
+
# - Single array pair: ['key', 'value']
|
|
2976
|
+
#
|
|
2977
|
+
# @param value [Hash, Array] the input value
|
|
2978
|
+
#
|
|
2979
|
+
# @return [Array<Array>] array of [key, value] pairs
|
|
2980
|
+
#
|
|
2981
|
+
def normalize_key_value_pairs(value)
|
|
2982
|
+
case value
|
|
2983
|
+
when Hash then normalize_hash_to_pairs(value)
|
|
2984
|
+
when Array then normalize_array_to_pairs(value)
|
|
2985
|
+
else
|
|
2986
|
+
raise ArgumentError,
|
|
2987
|
+
"key_value option must be a Hash or Array, got #{value.class}"
|
|
2988
|
+
end
|
|
2989
|
+
end
|
|
2990
|
+
|
|
2991
|
+
# Normalize a Hash to an array of [key, value] pairs
|
|
2992
|
+
#
|
|
2993
|
+
# @param hash [Hash] the input hash
|
|
2994
|
+
#
|
|
2995
|
+
# @return [Array<Array>] array of [key, value] pairs
|
|
2996
|
+
#
|
|
2997
|
+
# @api private
|
|
2998
|
+
def normalize_hash_to_pairs(hash)
|
|
2999
|
+
hash.flat_map do |k, v|
|
|
3000
|
+
v.is_a?(Array) ? v.map { |val| [k, val] } : [[k, v]]
|
|
3001
|
+
end
|
|
3002
|
+
end
|
|
3003
|
+
|
|
3004
|
+
# Normalize an Array to an array of [key, value] pairs
|
|
3005
|
+
#
|
|
3006
|
+
# @param array [Array] the input array
|
|
3007
|
+
#
|
|
3008
|
+
# @return [Array<Array>] array of [key, value] pairs
|
|
3009
|
+
#
|
|
3010
|
+
# @raise [ArgumentError] if the array is not a pair or array of pairs
|
|
3011
|
+
#
|
|
3012
|
+
# @api private
|
|
3013
|
+
def normalize_array_to_pairs(array)
|
|
3014
|
+
# Check if it's a single [key, value] pair or array of pairs
|
|
3015
|
+
if array.size == 2 && !array.first.is_a?(Array)
|
|
3016
|
+
[array]
|
|
3017
|
+
elsif array.any? { |e| !e.is_a?(Array) }
|
|
3018
|
+
# Flat array with non-pair elements (e.g., ['a', 'b', 'c'])
|
|
3019
|
+
raise ArgumentError, 'key_value array input must be a [key, value] pair or array of pairs'
|
|
3020
|
+
else
|
|
3021
|
+
array
|
|
3022
|
+
end
|
|
3023
|
+
end
|
|
3024
|
+
|
|
3025
|
+
# Validate that a key-value pair array has at most 2 elements
|
|
3026
|
+
#
|
|
3027
|
+
# @param pair [Array] the pair to validate
|
|
3028
|
+
#
|
|
3029
|
+
# @param option_name [Symbol] the option name for error messages
|
|
3030
|
+
#
|
|
3031
|
+
# @return [void]
|
|
3032
|
+
#
|
|
3033
|
+
# @raise [ArgumentError] if pair has more than 2 elements
|
|
3034
|
+
#
|
|
3035
|
+
# @api private
|
|
3036
|
+
def validate_key_value_pair_size!(pair, option_name)
|
|
3037
|
+
return unless pair.is_a?(Array) && pair.size > 2
|
|
3038
|
+
|
|
3039
|
+
raise ArgumentError,
|
|
3040
|
+
"key_value :#{option_name} pair #{pair.inspect} has too many elements (expected [key, value])"
|
|
3041
|
+
end
|
|
3042
|
+
|
|
3043
|
+
# Validate a key for key_value options
|
|
3044
|
+
#
|
|
3045
|
+
# @param key [Object] the key to validate
|
|
3046
|
+
#
|
|
3047
|
+
# @param separator [String] the key-value separator
|
|
3048
|
+
#
|
|
3049
|
+
# @param option_name [Symbol] the option name for error messages
|
|
3050
|
+
#
|
|
3051
|
+
# @return [void]
|
|
3052
|
+
#
|
|
3053
|
+
# @raise [ArgumentError] if key is nil, empty, or contains the separator
|
|
3054
|
+
#
|
|
3055
|
+
# @api private
|
|
3056
|
+
def validate_key_value_key!(key, separator, option_name)
|
|
3057
|
+
key_str = key.to_s
|
|
3058
|
+
raise ArgumentError, "key_value :#{option_name} requires a non-empty key" if key.nil? || key_str.empty?
|
|
3059
|
+
|
|
3060
|
+
return unless key_str.include?(separator)
|
|
3061
|
+
|
|
3062
|
+
raise ArgumentError,
|
|
3063
|
+
"key_value :#{option_name} key #{key_str.inspect} cannot contain the separator #{separator.inspect}"
|
|
3064
|
+
end
|
|
3065
|
+
|
|
3066
|
+
# Validate a value for key_value options
|
|
3067
|
+
#
|
|
3068
|
+
# @param value [Object] the value to validate
|
|
3069
|
+
#
|
|
3070
|
+
# @param option_name [Symbol] the option name for error messages
|
|
3071
|
+
#
|
|
3072
|
+
# @return [void]
|
|
3073
|
+
#
|
|
3074
|
+
# @raise [ArgumentError] if value is a Hash or Array (non-scalar)
|
|
3075
|
+
#
|
|
3076
|
+
# @api private
|
|
3077
|
+
def validate_key_value_value!(value, option_name)
|
|
3078
|
+
return if value.nil?
|
|
3079
|
+
return unless value.is_a?(Hash) || value.is_a?(Array)
|
|
3080
|
+
|
|
3081
|
+
raise ArgumentError,
|
|
3082
|
+
"key_value :#{option_name} value must be a scalar (String, Symbol, Numeric, nil), " \
|
|
3083
|
+
"got #{value.class}: #{value.inspect}"
|
|
3084
|
+
end
|
|
3085
|
+
|
|
3086
|
+
# Flatten a single-element array wrapper if the first element is itself an array
|
|
3087
|
+
#
|
|
3088
|
+
# @param positionals [Array] the raw positional values from the splat
|
|
3089
|
+
#
|
|
3090
|
+
# @return [Array] normalized positionals array
|
|
3091
|
+
#
|
|
3092
|
+
# @api private
|
|
3093
|
+
def normalize_positionals(positionals)
|
|
3094
|
+
# Flatten if first element is an array (allows both splat and array syntax)
|
|
3095
|
+
positionals = positionals.first if positionals.size == 1 && positionals.first.is_a?(Array)
|
|
3096
|
+
Array(positionals)
|
|
3097
|
+
end
|
|
3098
|
+
|
|
3099
|
+
# Allocate positional arguments to definitions following Ruby semantics
|
|
3100
|
+
# Returns [allocation_hash, consumed_count] where consumed_count is the
|
|
3101
|
+
# number of non-nil positionals that were consumed by definitions.
|
|
3102
|
+
#
|
|
3103
|
+
# @param positionals [Array] the positional argument values
|
|
3104
|
+
#
|
|
3105
|
+
# @return [Array(Hash, Integer)] tuple of allocation hash and consumed count
|
|
3106
|
+
#
|
|
3107
|
+
# @api private
|
|
3108
|
+
def allocate_positionals(positionals)
|
|
3109
|
+
OperandAllocator.new(@operand_definitions).allocate(positionals)
|
|
3110
|
+
end
|
|
3111
|
+
|
|
3112
|
+
# Append a positional argument's value(s) to the CLI args array
|
|
3113
|
+
#
|
|
3114
|
+
# @param args [Array<String>] the argument array to append to
|
|
3115
|
+
#
|
|
3116
|
+
# @param value [Object] the positional value
|
|
3117
|
+
#
|
|
3118
|
+
# @param definition [Hash] the operand definition hash
|
|
3119
|
+
#
|
|
3120
|
+
# @return [void]
|
|
3121
|
+
#
|
|
3122
|
+
# @api private
|
|
3123
|
+
def append_positional_to_args(args, value, definition)
|
|
3124
|
+
return if positional_value_empty?(value, definition)
|
|
3125
|
+
|
|
3126
|
+
append_positional_value(args, value, definition[:repeatable])
|
|
3127
|
+
end
|
|
3128
|
+
|
|
3129
|
+
# Return true if a positional value is empty and should be skipped
|
|
3130
|
+
#
|
|
3131
|
+
# @param value [Object] the positional value
|
|
3132
|
+
#
|
|
3133
|
+
# @param definition [Hash] the operand definition hash
|
|
3134
|
+
#
|
|
3135
|
+
# @return [Boolean] true if the value should be skipped
|
|
3136
|
+
#
|
|
3137
|
+
# @api private
|
|
3138
|
+
def positional_value_empty?(value, definition)
|
|
3139
|
+
return true if value.nil?
|
|
3140
|
+
|
|
3141
|
+
definition[:repeatable] && value.respond_to?(:empty?) && value.empty?
|
|
3142
|
+
end
|
|
3143
|
+
|
|
3144
|
+
# Append a positional value to the args array, expanding arrays when repeatable
|
|
3145
|
+
#
|
|
3146
|
+
# @param args [Array<String>] the argument array to append to
|
|
3147
|
+
#
|
|
3148
|
+
# @param value [Object] the positional value
|
|
3149
|
+
#
|
|
3150
|
+
# @param repeatable [Boolean] whether the operand accepts multiple values
|
|
3151
|
+
#
|
|
3152
|
+
# @return [void]
|
|
3153
|
+
#
|
|
3154
|
+
# @api private
|
|
3155
|
+
def append_positional_value(args, value, repeatable)
|
|
3156
|
+
if repeatable
|
|
3157
|
+
args.concat(Array(value).map(&:to_s))
|
|
3158
|
+
else
|
|
3159
|
+
args << value.to_s
|
|
3160
|
+
end
|
|
3161
|
+
end
|
|
3162
|
+
|
|
3163
|
+
# Raise if more positional values were provided than definitions can absorb
|
|
3164
|
+
#
|
|
3165
|
+
# @param positionals [Array] the full list of provided positional values
|
|
3166
|
+
#
|
|
3167
|
+
# @param consumed_count [Integer] the number of positionals consumed by definitions
|
|
3168
|
+
#
|
|
3169
|
+
# @return [void]
|
|
3170
|
+
#
|
|
3171
|
+
# @raise [ArgumentError] if extra positional values remain after allocation
|
|
3172
|
+
#
|
|
3173
|
+
# @api private
|
|
3174
|
+
def check_unexpected_positionals(positionals, consumed_count)
|
|
3175
|
+
provided_count = positionals.compact.size
|
|
3176
|
+
|
|
3177
|
+
return if provided_count <= consumed_count
|
|
3178
|
+
|
|
3179
|
+
unexpected_count = provided_count - consumed_count
|
|
3180
|
+
unexpected = positionals.compact.last(unexpected_count)
|
|
3181
|
+
raise ArgumentError, "Unexpected positional arguments: #{unexpected.join(', ')}"
|
|
3182
|
+
end
|
|
3183
|
+
|
|
3184
|
+
# Raise if a required positional argument is missing or empty
|
|
3185
|
+
#
|
|
3186
|
+
# @param value [Object] the allocated positional value
|
|
3187
|
+
#
|
|
3188
|
+
# @param definition [Hash] the operand definition hash
|
|
3189
|
+
#
|
|
3190
|
+
# @return [void]
|
|
3191
|
+
#
|
|
3192
|
+
# @raise [ArgumentError] if a required positional is missing
|
|
3193
|
+
#
|
|
3194
|
+
# @api private
|
|
3195
|
+
def validate_required_positional(value, definition)
|
|
3196
|
+
return unless definition[:required]
|
|
3197
|
+
return if definition[:allow_nil] && value.nil?
|
|
3198
|
+
return unless value_empty?(value)
|
|
3199
|
+
|
|
3200
|
+
raise ArgumentError, "at least one value is required for #{definition[:name]}" if definition[:repeatable]
|
|
3201
|
+
|
|
3202
|
+
raise ArgumentError, "#{definition[:name]} is required"
|
|
3203
|
+
end
|
|
3204
|
+
|
|
3205
|
+
# Raise if a repeatable positional array contains nil values
|
|
3206
|
+
#
|
|
3207
|
+
# @param value [Object] the allocated positional value
|
|
3208
|
+
#
|
|
3209
|
+
# @param definition [Hash] the operand definition hash
|
|
3210
|
+
#
|
|
3211
|
+
# @return [void]
|
|
3212
|
+
#
|
|
3213
|
+
# @raise [ArgumentError] if the array contains nil elements
|
|
3214
|
+
#
|
|
3215
|
+
# @api private
|
|
3216
|
+
def validate_no_nil_values!(value, definition)
|
|
3217
|
+
return unless definition[:repeatable]
|
|
3218
|
+
return if value.nil? # Allow nil as "not provided"
|
|
3219
|
+
|
|
3220
|
+
# For repeatable positionals, check if array contains any nil values
|
|
3221
|
+
values = Array(value)
|
|
3222
|
+
return unless values.any?(&:nil?)
|
|
3223
|
+
|
|
3224
|
+
raise ArgumentError, "nil values are not allowed in repeatable positional argument: #{definition[:name]}"
|
|
3225
|
+
end
|
|
3226
|
+
|
|
3227
|
+
# Reject operand values that look like command-line options
|
|
3228
|
+
#
|
|
3229
|
+
# Operands appearing before a '--' separator boundary (or all operands
|
|
3230
|
+
# if no boundary exists) are validated to ensure they don't start with
|
|
3231
|
+
# a hyphen, which could be misinterpreted as a git option.
|
|
3232
|
+
#
|
|
3233
|
+
# @param allocation [Hash{Symbol => Object}] the allocated operand values
|
|
3234
|
+
#
|
|
3235
|
+
# @return [void]
|
|
3236
|
+
#
|
|
3237
|
+
# @raise [ArgumentError] if any pre-separator operand value starts with '-'
|
|
3238
|
+
#
|
|
3239
|
+
def validate_no_option_like_operands!(allocation)
|
|
3240
|
+
pre_separator_operands = operand_names_before_separator
|
|
3241
|
+
pre_separator_operands.each do |name|
|
|
3242
|
+
value = allocation[name]
|
|
3243
|
+
check_operand_not_option_like(name, value)
|
|
3244
|
+
end
|
|
3245
|
+
end
|
|
3246
|
+
|
|
3247
|
+
# Determine which operands appear before any '--' separator boundary
|
|
3248
|
+
#
|
|
3249
|
+
# Walks the ordered definitions and collects operand names until hitting
|
|
3250
|
+
# a `literal '--'` or an `end_of_options` declaration. All operands after
|
|
3251
|
+
# any such boundary are excluded from option-like validation.
|
|
3252
|
+
#
|
|
3253
|
+
# @return [Array<Symbol>] operand names that need option-like validation
|
|
3254
|
+
#
|
|
3255
|
+
def operand_names_before_separator
|
|
3256
|
+
names = []
|
|
3257
|
+
@ordered_definitions.each do |defn|
|
|
3258
|
+
break if separator_boundary_active?(defn)
|
|
3259
|
+
|
|
3260
|
+
names << defn[:name] if defn[:kind] == :operand && !operand_skip_cli?(defn[:name])
|
|
3261
|
+
end
|
|
3262
|
+
names
|
|
3263
|
+
end
|
|
3264
|
+
|
|
3265
|
+
# Check if an operand is configured with skip_cli: true
|
|
3266
|
+
#
|
|
3267
|
+
# @param name [Symbol] the operand name
|
|
3268
|
+
#
|
|
3269
|
+
# @return [Boolean] true if operand has skip_cli enabled
|
|
3270
|
+
#
|
|
3271
|
+
def operand_skip_cli?(name)
|
|
3272
|
+
operand_def = @operand_definitions.find { |d| d[:name] == name }
|
|
3273
|
+
operand_def[:skip_cli] == true
|
|
3274
|
+
end
|
|
3275
|
+
|
|
3276
|
+
# Check if a definition represents an active '--' separator boundary
|
|
3277
|
+
#
|
|
3278
|
+
# A `literal '--'` is always active. An `end_of_options` entry is also always active,
|
|
3279
|
+
# even when its runtime `--` may be suppressed by {#resolve_end_of_options_marker}.
|
|
3280
|
+
#
|
|
3281
|
+
# @param defn [Hash] a definition entry from @ordered_definitions
|
|
3282
|
+
#
|
|
3283
|
+
# @return [Boolean] true if this definition is an active '--' boundary
|
|
3284
|
+
#
|
|
3285
|
+
def separator_boundary_active?(defn)
|
|
3286
|
+
return true if literal_separator_flag?(defn)
|
|
3287
|
+
return true if defn[:kind] == :end_of_options
|
|
3288
|
+
|
|
3289
|
+
false
|
|
3290
|
+
end
|
|
3291
|
+
|
|
3292
|
+
# Check if a definition is a literal '--' static flag
|
|
3293
|
+
#
|
|
3294
|
+
# @param defn [Hash] the entry definition
|
|
3295
|
+
#
|
|
3296
|
+
# @return [Boolean] true if the definition emits a literal `--` boundary
|
|
3297
|
+
#
|
|
3298
|
+
def literal_separator_flag?(defn)
|
|
3299
|
+
defn[:kind] == :static && defn[:flag] == '--'
|
|
3300
|
+
end
|
|
3301
|
+
|
|
3302
|
+
# Check that a single operand value does not look like a command-line option
|
|
3303
|
+
#
|
|
3304
|
+
# @param name [Symbol] the operand name
|
|
3305
|
+
#
|
|
3306
|
+
# @param value [Object] the operand value
|
|
3307
|
+
#
|
|
3308
|
+
# @return [void]
|
|
3309
|
+
#
|
|
3310
|
+
# @raise [ArgumentError] if the value starts with '-'
|
|
3311
|
+
#
|
|
3312
|
+
def check_operand_not_option_like(name, value)
|
|
3313
|
+
case value
|
|
3314
|
+
when String
|
|
3315
|
+
raise_option_like_error(name, value) if value.start_with?('-')
|
|
3316
|
+
when Array
|
|
3317
|
+
raise_option_like_array_error(name, value)
|
|
3318
|
+
end
|
|
3319
|
+
end
|
|
3320
|
+
|
|
3321
|
+
# Raise if a single operand string value looks like a command-line option
|
|
3322
|
+
#
|
|
3323
|
+
# @param name [Symbol] the operand name (for the error message)
|
|
3324
|
+
#
|
|
3325
|
+
# @param value [String] the option-like string value
|
|
3326
|
+
#
|
|
3327
|
+
# @return [void]
|
|
3328
|
+
#
|
|
3329
|
+
# @raise [ArgumentError] if the string value starts with '-'
|
|
3330
|
+
#
|
|
3331
|
+
# @api private
|
|
3332
|
+
def raise_option_like_error(name, value)
|
|
3333
|
+
raise ArgumentError, "operand :#{name} value '#{value}' looks like a command-line option"
|
|
3334
|
+
end
|
|
3335
|
+
|
|
3336
|
+
# Raise if any element of an operand array looks like a command-line option
|
|
3337
|
+
#
|
|
3338
|
+
# @param name [Symbol] the operand name (for the error message)
|
|
3339
|
+
#
|
|
3340
|
+
# @param values [Array] the array of operand values to check
|
|
3341
|
+
#
|
|
3342
|
+
# @return [void]
|
|
3343
|
+
#
|
|
3344
|
+
# @raise [ArgumentError] if any array element starts with '-'
|
|
3345
|
+
#
|
|
3346
|
+
# @api private
|
|
3347
|
+
def raise_option_like_array_error(name, values)
|
|
3348
|
+
invalid = values.select { |v| v.is_a?(String) && v.start_with?('-') }
|
|
3349
|
+
return if invalid.empty?
|
|
3350
|
+
|
|
3351
|
+
raise ArgumentError,
|
|
3352
|
+
"operand :#{name} contains option-like values: #{invalid.map { |v| "'#{v}'" }.join(', ')}"
|
|
3353
|
+
end
|
|
3354
|
+
|
|
3355
|
+
# Check if a positional value is empty (not provided)
|
|
3356
|
+
#
|
|
3357
|
+
# Only nil means "not provided" for positionals. Empty strings and empty
|
|
3358
|
+
# arrays are valid values that should be passed through.
|
|
3359
|
+
#
|
|
3360
|
+
# @param value [Object] the value to check
|
|
3361
|
+
#
|
|
3362
|
+
# @return [Boolean] true if the value is nil
|
|
3363
|
+
#
|
|
3364
|
+
def value_empty?(value)
|
|
3365
|
+
value.nil?
|
|
3366
|
+
end
|
|
3367
|
+
|
|
3368
|
+
# Raise if opts contains any keys not registered in the alias map
|
|
3369
|
+
#
|
|
3370
|
+
# @param opts [Hash] the raw keyword options to check
|
|
3371
|
+
#
|
|
3372
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3373
|
+
#
|
|
3374
|
+
# @return [void]
|
|
3375
|
+
#
|
|
3376
|
+
# @raise [ArgumentError] if any option key is not registered
|
|
3377
|
+
#
|
|
3378
|
+
# @api private
|
|
3379
|
+
def validate_unsupported_options!(opts)
|
|
3380
|
+
unsupported = opts.keys - @alias_map.keys
|
|
3381
|
+
return if unsupported.empty?
|
|
3382
|
+
|
|
3383
|
+
raise ArgumentError, "Unsupported options: #{unsupported.map(&:inspect).join(', ')}"
|
|
3384
|
+
end
|
|
3385
|
+
|
|
3386
|
+
# Raise if the caller provided more than one alias from the same option group
|
|
3387
|
+
#
|
|
3388
|
+
# @param opts [Hash] the raw keyword options
|
|
3389
|
+
#
|
|
3390
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3391
|
+
#
|
|
3392
|
+
# @return [void]
|
|
3393
|
+
#
|
|
3394
|
+
# @raise [ArgumentError] if conflicting aliases are both present
|
|
3395
|
+
#
|
|
3396
|
+
# @api private
|
|
3397
|
+
def validate_conflicting_aliases!(opts)
|
|
3398
|
+
@option_definitions.each_value do |definition|
|
|
3399
|
+
aliases = definition[:aliases]
|
|
3400
|
+
next unless aliases.size > 1
|
|
3401
|
+
|
|
3402
|
+
provided = aliases & opts.keys
|
|
3403
|
+
next unless provided.size > 1
|
|
3404
|
+
|
|
3405
|
+
raise ArgumentError, "Conflicting options: #{provided.map(&:inspect).join(' and ')}"
|
|
3406
|
+
end
|
|
3407
|
+
end
|
|
3408
|
+
|
|
3409
|
+
# Rekey opts so every alias maps to its primary option name
|
|
3410
|
+
#
|
|
3411
|
+
# @param opts [Hash] the raw keyword options
|
|
3412
|
+
#
|
|
3413
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3414
|
+
#
|
|
3415
|
+
# @return [Hash] a new hash with all keys resolved to primary names
|
|
3416
|
+
#
|
|
3417
|
+
# @api private
|
|
3418
|
+
def normalize_aliases(opts)
|
|
3419
|
+
opts.transform_keys { |key| @alias_map[key] || key }
|
|
3420
|
+
end
|
|
3421
|
+
|
|
3422
|
+
# Raise if any required options are missing or nil when allow_nil is false
|
|
3423
|
+
#
|
|
3424
|
+
# @param opts [Hash] the normalized keyword options
|
|
3425
|
+
#
|
|
3426
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3427
|
+
#
|
|
3428
|
+
# @return [void]
|
|
3429
|
+
#
|
|
3430
|
+
# @raise [ArgumentError] if required options are missing or nil
|
|
3431
|
+
#
|
|
3432
|
+
# @api private
|
|
3433
|
+
def validate_required_options!(opts)
|
|
3434
|
+
missing, nil_not_allowed = collect_required_option_errors(opts)
|
|
3435
|
+
raise_missing_options_error(missing) if missing.any?
|
|
3436
|
+
raise_nil_options_error(nil_not_allowed) if nil_not_allowed.any?
|
|
3437
|
+
end
|
|
3438
|
+
|
|
3439
|
+
# Collect missing and nil-not-allowed option names from opts
|
|
3440
|
+
#
|
|
3441
|
+
# @param opts [Hash] the normalized keyword options
|
|
3442
|
+
#
|
|
3443
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3444
|
+
#
|
|
3445
|
+
# @return [Array(Array<Symbol>, Array<Symbol>)] tuple of missing and nil-invalid names
|
|
3446
|
+
#
|
|
3447
|
+
# @api private
|
|
3448
|
+
def collect_required_option_errors(opts)
|
|
3449
|
+
missing = []
|
|
3450
|
+
nil_not_allowed = []
|
|
3451
|
+
@option_definitions.each do |name, definition|
|
|
3452
|
+
next unless definition[:required]
|
|
3453
|
+
|
|
3454
|
+
missing << name unless opts.key?(name)
|
|
3455
|
+
nil_not_allowed << name if opts.key?(name) && opts[name].nil? && definition[:allow_nil] == false
|
|
3456
|
+
end
|
|
3457
|
+
[missing, nil_not_allowed]
|
|
3458
|
+
end
|
|
3459
|
+
|
|
3460
|
+
# Raise an error listing required options that were not provided
|
|
3461
|
+
#
|
|
3462
|
+
# @param missing [Array<Symbol>] the names of missing required options
|
|
3463
|
+
#
|
|
3464
|
+
# @return [void]
|
|
3465
|
+
#
|
|
3466
|
+
# @raise [ArgumentError] always
|
|
3467
|
+
#
|
|
3468
|
+
# @api private
|
|
3469
|
+
def raise_missing_options_error(missing)
|
|
3470
|
+
raise ArgumentError, "Required options not provided: #{missing.map(&:inspect).join(', ')}"
|
|
3471
|
+
end
|
|
3472
|
+
|
|
3473
|
+
# Raise an error listing required options that were provided as nil
|
|
3474
|
+
#
|
|
3475
|
+
# @param nil_not_allowed [Array<Symbol>] the names of options given nil when not allowed
|
|
3476
|
+
#
|
|
3477
|
+
# @return [void]
|
|
3478
|
+
#
|
|
3479
|
+
# @raise [ArgumentError] always
|
|
3480
|
+
#
|
|
3481
|
+
# @api private
|
|
3482
|
+
def raise_nil_options_error(nil_not_allowed)
|
|
3483
|
+
raise ArgumentError, "Required options cannot be nil: #{nil_not_allowed.map(&:inspect).join(', ')}"
|
|
3484
|
+
end
|
|
3485
|
+
|
|
3486
|
+
# Run validators and allowed_values checks for each option present in opts
|
|
3487
|
+
#
|
|
3488
|
+
# @param opts [Hash] the normalized keyword options
|
|
3489
|
+
#
|
|
3490
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3491
|
+
#
|
|
3492
|
+
# @return [void]
|
|
3493
|
+
#
|
|
3494
|
+
# @api private
|
|
3495
|
+
def validate_option_values!(opts)
|
|
3496
|
+
@option_definitions.each do |name, definition|
|
|
3497
|
+
next unless opts.key?(name)
|
|
3498
|
+
|
|
3499
|
+
validate_single_option!(name, opts[name], definition)
|
|
3500
|
+
end
|
|
3501
|
+
end
|
|
3502
|
+
|
|
3503
|
+
# Run the validator and allowed_values check for a single option value
|
|
3504
|
+
#
|
|
3505
|
+
# @param name [Symbol] the option name
|
|
3506
|
+
#
|
|
3507
|
+
# @param value [Object] the option value to validate
|
|
3508
|
+
#
|
|
3509
|
+
# @param definition [Hash] the option definition hash
|
|
3510
|
+
#
|
|
3511
|
+
# @return [void]
|
|
3512
|
+
#
|
|
3513
|
+
# @raise [ArgumentError] if validation fails
|
|
3514
|
+
#
|
|
3515
|
+
# @api private
|
|
3516
|
+
def validate_single_option!(name, value, definition)
|
|
3517
|
+
run_validator!(name, value, definition[:validator]) if definition[:validator]
|
|
3518
|
+
check_allowed_values!(name, value, definition) if definition[:allowed_values]
|
|
3519
|
+
end
|
|
3520
|
+
|
|
3521
|
+
# Call the validator proc and raise if it returns an error
|
|
3522
|
+
#
|
|
3523
|
+
# @param name [Symbol] the option name (for the default error message)
|
|
3524
|
+
#
|
|
3525
|
+
# @param value [Object] the option value to validate
|
|
3526
|
+
#
|
|
3527
|
+
# @param validator [Proc] the validator callable
|
|
3528
|
+
#
|
|
3529
|
+
# @return [void]
|
|
3530
|
+
#
|
|
3531
|
+
# @raise [ArgumentError] if the validator returns any value other than `true`
|
|
3532
|
+
#
|
|
3533
|
+
# @api private
|
|
3534
|
+
def run_validator!(name, value, validator)
|
|
3535
|
+
result = validator.call(value)
|
|
3536
|
+
return if result == true
|
|
3537
|
+
|
|
3538
|
+
error_msg = result.is_a?(String) ? result : "Invalid value for option: #{name}"
|
|
3539
|
+
raise ArgumentError, error_msg
|
|
3540
|
+
end
|
|
3541
|
+
|
|
3542
|
+
# Check that the option value is within the declared allowed_values set
|
|
3543
|
+
#
|
|
3544
|
+
# @param name [Symbol] the option name
|
|
3545
|
+
#
|
|
3546
|
+
# @param value [Object] the option value to check
|
|
3547
|
+
#
|
|
3548
|
+
# @param definition [Hash] the option definition hash
|
|
3549
|
+
#
|
|
3550
|
+
# @return [void]
|
|
3551
|
+
#
|
|
3552
|
+
# @raise [ArgumentError] if the value is not in the allowed set
|
|
3553
|
+
#
|
|
3554
|
+
# @api private
|
|
3555
|
+
def check_allowed_values!(name, value, definition)
|
|
3556
|
+
allowed = definition[:allowed_values]
|
|
3557
|
+
type = definition[:type]
|
|
3558
|
+
if definition[:repeatable]
|
|
3559
|
+
check_repeatable_allowed_values!(name, value, allowed, definition[:allow_empty], type)
|
|
3560
|
+
else
|
|
3561
|
+
check_single_allowed_value!(name, value, allowed, definition[:allow_empty], type)
|
|
3562
|
+
end
|
|
3563
|
+
end
|
|
3564
|
+
|
|
3565
|
+
# Convert an allowed_values enumerable to a frozen array of strings
|
|
3566
|
+
#
|
|
3567
|
+
# @param sym [Symbol] the option name (for error messages)
|
|
3568
|
+
#
|
|
3569
|
+
# @param values [Enumerable] the allowed values to coerce
|
|
3570
|
+
#
|
|
3571
|
+
# @return [Array<String>] a frozen array of stringified allowed values
|
|
3572
|
+
#
|
|
3573
|
+
# @raise [ArgumentError] if values is not Enumerable or is empty
|
|
3574
|
+
#
|
|
3575
|
+
# @api private
|
|
3576
|
+
def coerce_allowed_values_set!(sym, values)
|
|
3577
|
+
unless values.respond_to?(:map)
|
|
3578
|
+
raise ArgumentError,
|
|
3579
|
+
"allowed_values :#{sym} expects an Enumerable for `in:`, got #{values.class}"
|
|
3580
|
+
end
|
|
3581
|
+
arr = values.map(&:to_s)
|
|
3582
|
+
raise ArgumentError, "allowed_values :#{sym} must specify at least one allowed value" if arr.empty?
|
|
3583
|
+
|
|
3584
|
+
arr.freeze
|
|
3585
|
+
end
|
|
3586
|
+
|
|
3587
|
+
# Look up and validate the option definition for an allowed_values declaration
|
|
3588
|
+
#
|
|
3589
|
+
# @param sym [Symbol] the option or alias name
|
|
3590
|
+
#
|
|
3591
|
+
# @return [Hash] the option definition hash
|
|
3592
|
+
#
|
|
3593
|
+
# @raise [ArgumentError] if sym is unknown or not a value option type
|
|
3594
|
+
#
|
|
3595
|
+
# @api private
|
|
3596
|
+
def validate_allowed_values_definition!(sym)
|
|
3597
|
+
primary = @alias_map[sym]
|
|
3598
|
+
defn = primary && @option_definitions[primary]
|
|
3599
|
+
unless defn
|
|
3600
|
+
raise ArgumentError, ":#{sym} is not a value option" if @operand_definitions.any? { |d| d[:name] == sym }
|
|
3601
|
+
|
|
3602
|
+
raise ArgumentError, "unknown argument :#{sym} in allowed_values declaration"
|
|
3603
|
+
end
|
|
3604
|
+
unless VALUE_OPTION_TYPES_FOR_ALLOWED_VALUES.include?(defn[:type])
|
|
3605
|
+
raise ArgumentError, ":#{sym} is not a value option"
|
|
3606
|
+
end
|
|
3607
|
+
|
|
3608
|
+
defn
|
|
3609
|
+
end
|
|
3610
|
+
|
|
3611
|
+
# Check each element of a repeatable option value against the allowed set
|
|
3612
|
+
#
|
|
3613
|
+
# @param name [Symbol] the option name
|
|
3614
|
+
#
|
|
3615
|
+
# @param values [Object] the repeatable value (array or scalar)
|
|
3616
|
+
#
|
|
3617
|
+
# @param allowed [Array<String>] the allowed string values
|
|
3618
|
+
#
|
|
3619
|
+
# @param allow_empty [Boolean] whether empty strings are permitted
|
|
3620
|
+
#
|
|
3621
|
+
# @param type [Symbol] the option type (for boolean skip logic)
|
|
3622
|
+
#
|
|
3623
|
+
# @return [void]
|
|
3624
|
+
#
|
|
3625
|
+
# @raise [ArgumentError] if any element is not in the allowed set
|
|
3626
|
+
#
|
|
3627
|
+
# @api private
|
|
3628
|
+
def check_repeatable_allowed_values!(name, values, allowed, allow_empty, type)
|
|
3629
|
+
Array(values).each do |v|
|
|
3630
|
+
next if skip_allowed_values_check?(v, allow_empty, type)
|
|
3631
|
+
|
|
3632
|
+
unless allowed.include?(v.to_s)
|
|
3633
|
+
raise ArgumentError,
|
|
3634
|
+
"Invalid value for :#{name}: expected one of #{allowed.inspect}, got #{v.inspect}"
|
|
3635
|
+
end
|
|
3636
|
+
end
|
|
3637
|
+
end
|
|
3638
|
+
|
|
3639
|
+
# Check a single option value against the allowed set
|
|
3640
|
+
#
|
|
3641
|
+
# @param name [Symbol] the option name
|
|
3642
|
+
#
|
|
3643
|
+
# @param value [Object] the value to check
|
|
3644
|
+
#
|
|
3645
|
+
# @param allowed [Array<String>] the allowed string values
|
|
3646
|
+
#
|
|
3647
|
+
# @param allow_empty [Boolean] whether empty strings are permitted
|
|
3648
|
+
#
|
|
3649
|
+
# @param type [Symbol] the option type (for boolean skip logic)
|
|
3650
|
+
#
|
|
3651
|
+
# @return [void]
|
|
3652
|
+
#
|
|
3653
|
+
# @raise [ArgumentError] if the value is not in the allowed set
|
|
3654
|
+
#
|
|
3655
|
+
# @api private
|
|
3656
|
+
def check_single_allowed_value!(name, value, allowed, allow_empty, type)
|
|
3657
|
+
return if skip_allowed_values_check?(value, allow_empty, type)
|
|
3658
|
+
|
|
3659
|
+
return if allowed.include?(value.to_s)
|
|
3660
|
+
|
|
3661
|
+
raise ArgumentError,
|
|
3662
|
+
"Invalid value for :#{name}: expected one of #{allowed.inspect}, got #{value.inspect}"
|
|
3663
|
+
end
|
|
3664
|
+
|
|
3665
|
+
# Return true if an allowed_values check should be bypassed for this value
|
|
3666
|
+
#
|
|
3667
|
+
# @param value [Object] the value being checked
|
|
3668
|
+
#
|
|
3669
|
+
# @param allow_empty [Boolean] whether empty strings should be skipped
|
|
3670
|
+
#
|
|
3671
|
+
# @param type [Symbol] the option type (for flag_or_value boolean skip)
|
|
3672
|
+
#
|
|
3673
|
+
# @return [Boolean] true if the check should be skipped
|
|
3674
|
+
#
|
|
3675
|
+
# @api private
|
|
3676
|
+
def skip_allowed_values_check?(value, allow_empty, type)
|
|
3677
|
+
return true if value.nil?
|
|
3678
|
+
# Only skip boolean values for flag_or_value option types where true/false carry
|
|
3679
|
+
# semantic meaning (true = emit flag, false = suppress flag). For plain value
|
|
3680
|
+
# options, a boolean is an invalid value and should fail the allowed_values check.
|
|
3681
|
+
return true if [true, false].include?(value) && FLAG_OR_VALUE_OPTION_TYPES.include?(type)
|
|
3682
|
+
return true if value.to_s.empty? && allow_empty
|
|
3683
|
+
|
|
3684
|
+
false
|
|
3685
|
+
end
|
|
3686
|
+
|
|
3687
|
+
# Build a lambda that validates a value is one of the expected types
|
|
3688
|
+
#
|
|
3689
|
+
# @param option_name [Symbol] the option name (for error messages)
|
|
3690
|
+
#
|
|
3691
|
+
# @param expected_type [Class, Array<Class>] the expected Ruby type(s)
|
|
3692
|
+
#
|
|
3693
|
+
# @return [Proc] a validator lambda
|
|
3694
|
+
#
|
|
3695
|
+
# @api private
|
|
3696
|
+
def create_type_validator(option_name, expected_type)
|
|
3697
|
+
types = Array(expected_type)
|
|
3698
|
+
|
|
3699
|
+
lambda do |value|
|
|
3700
|
+
return true if value.nil? # nil values are universally skipped by should_skip_option?
|
|
3701
|
+
return true if types.any? { |t| value.is_a?(t) }
|
|
3702
|
+
|
|
3703
|
+
# Generate a helpful error message
|
|
3704
|
+
type_names = types.map(&:name).join(' or ')
|
|
3705
|
+
actual_type = value.class.name
|
|
3706
|
+
"The :#{option_name} option must be a #{type_names}, but was a #{actual_type}"
|
|
3707
|
+
end
|
|
3708
|
+
end
|
|
3709
|
+
|
|
3710
|
+
# Raise if any conflicting argument names are both present in the bound values
|
|
3711
|
+
#
|
|
3712
|
+
# @param opts [Hash] normalized keyword options
|
|
3713
|
+
#
|
|
3714
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3715
|
+
#
|
|
3716
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3717
|
+
#
|
|
3718
|
+
# @return [void]
|
|
3719
|
+
#
|
|
3720
|
+
# @raise [ArgumentError] if conflicting arguments are both present
|
|
3721
|
+
#
|
|
3722
|
+
# @api private
|
|
3723
|
+
def validate_conflicts!(opts, allocated_positionals = {})
|
|
3724
|
+
@conflicts.each do |conflict_group|
|
|
3725
|
+
provided = conflict_group.select { |name| conflict_present?(name, opts, allocated_positionals) }
|
|
3726
|
+
next if provided.size <= 1
|
|
3727
|
+
|
|
3728
|
+
formatted = provided.map { |name| ":#{name}" }.join(' and ')
|
|
3729
|
+
raise ArgumentError, "cannot specify #{formatted}"
|
|
3730
|
+
end
|
|
3731
|
+
end
|
|
3732
|
+
|
|
3733
|
+
# Return true if a named argument should be counted as present during conflict checking
|
|
3734
|
+
#
|
|
3735
|
+
# For registered keyword options only looks in opts; positional slots use
|
|
3736
|
+
# allocated_positionals. This prevents a positional operand that shares a
|
|
3737
|
+
# name with a keyword option from spuriously triggering keyword conflicts.
|
|
3738
|
+
#
|
|
3739
|
+
# @param name [Symbol] the argument name to check
|
|
3740
|
+
#
|
|
3741
|
+
# @param opts [Hash] normalized keyword options
|
|
3742
|
+
#
|
|
3743
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3744
|
+
#
|
|
3745
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3746
|
+
#
|
|
3747
|
+
# @return [Boolean] true if the named argument has a present bound value
|
|
3748
|
+
#
|
|
3749
|
+
# @api private
|
|
3750
|
+
def conflict_present?(name, opts, allocated_positionals)
|
|
3751
|
+
canonical_name = @alias_map[name] || name
|
|
3752
|
+
value = if @option_definitions.key?(canonical_name)
|
|
3753
|
+
opts[canonical_name]
|
|
3754
|
+
else
|
|
3755
|
+
allocated_positionals[canonical_name]
|
|
3756
|
+
end
|
|
3757
|
+
argument_present?(value)
|
|
3758
|
+
end
|
|
3759
|
+
|
|
3760
|
+
# Validate that no bound values match a forbidden exact-value tuple
|
|
3761
|
+
#
|
|
3762
|
+
# @param opts [Hash] normalized keyword options (aliases already resolved)
|
|
3763
|
+
#
|
|
3764
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3765
|
+
#
|
|
3766
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3767
|
+
#
|
|
3768
|
+
# @return [void]
|
|
3769
|
+
#
|
|
3770
|
+
# @raise [ArgumentError] if all names in a forbidden tuple are present with
|
|
3771
|
+
# their declared values
|
|
3772
|
+
#
|
|
3773
|
+
def validate_forbidden_values!(opts, allocated_positionals = {})
|
|
3774
|
+
@forbidden_values.each do |tuple|
|
|
3775
|
+
next unless forbidden_tuple_matches?(tuple, opts, allocated_positionals)
|
|
3776
|
+
|
|
3777
|
+
formatted = tuple.map { |name, value| ":#{name}=#{value.inspect}" }.join(' with ')
|
|
3778
|
+
raise ArgumentError, "cannot specify #{formatted}"
|
|
3779
|
+
end
|
|
3780
|
+
end
|
|
3781
|
+
|
|
3782
|
+
# Return true if every name in the tuple has a bound value equal to the
|
|
3783
|
+
# declared forbidden value.
|
|
3784
|
+
#
|
|
3785
|
+
# The check only fires when the key is actually present (bound) — an absent
|
|
3786
|
+
# key never triggers a forbidden-values match.
|
|
3787
|
+
#
|
|
3788
|
+
# @param tuple [Hash{Symbol => Object}] canonical name → forbidden value
|
|
3789
|
+
#
|
|
3790
|
+
# @param opts [Hash] normalized keyword options
|
|
3791
|
+
#
|
|
3792
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3793
|
+
#
|
|
3794
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3795
|
+
#
|
|
3796
|
+
# @return [Boolean] true if all tuple entries match the bound values
|
|
3797
|
+
#
|
|
3798
|
+
def forbidden_tuple_matches?(tuple, opts, allocated_positionals)
|
|
3799
|
+
tuple.all? do |name, forbidden_value|
|
|
3800
|
+
if opts.key?(name)
|
|
3801
|
+
opts[name] == forbidden_value
|
|
3802
|
+
elsif allocated_positionals.key?(name)
|
|
3803
|
+
allocated_positionals[name] == forbidden_value
|
|
3804
|
+
else
|
|
3805
|
+
false
|
|
3806
|
+
end
|
|
3807
|
+
end
|
|
3808
|
+
end
|
|
3809
|
+
|
|
3810
|
+
# Validate conditional and unconditional requires_one_of groups
|
|
3811
|
+
#
|
|
3812
|
+
# Each entry in @requires_one_of is a Hash with keys:
|
|
3813
|
+
# :names — Array of canonical argument names that must collectively satisfy
|
|
3814
|
+
# the at-least-one constraint
|
|
3815
|
+
# :condition — canonical trigger name (Symbol), or nil for unconditional groups
|
|
3816
|
+
# :single — true when declared via `requires` (affects error message wording)
|
|
3817
|
+
#
|
|
3818
|
+
# @param opts [Hash] normalized keyword options (aliases already resolved)
|
|
3819
|
+
#
|
|
3820
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3821
|
+
#
|
|
3822
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3823
|
+
#
|
|
3824
|
+
# @return [void]
|
|
3825
|
+
#
|
|
3826
|
+
# @raise [ArgumentError] if none of the arguments in any applicable group is present
|
|
3827
|
+
#
|
|
3828
|
+
def validate_requires_one_of!(opts, allocated_positionals = {})
|
|
3829
|
+
@requires_one_of.each do |entry|
|
|
3830
|
+
validate_requires_one_of_entry!(entry, opts, allocated_positionals)
|
|
3831
|
+
end
|
|
3832
|
+
end
|
|
3833
|
+
|
|
3834
|
+
# Validate a single requires_one_of entry
|
|
3835
|
+
#
|
|
3836
|
+
# @param entry [Hash] the group entry with :names, :condition, :single keys
|
|
3837
|
+
#
|
|
3838
|
+
# @param opts [Hash] normalized keyword options
|
|
3839
|
+
#
|
|
3840
|
+
# @param allocated_positionals [Hash] the allocated positional values
|
|
3841
|
+
#
|
|
3842
|
+
# @option opts [Object] :"option_name" value for any registered option name
|
|
3843
|
+
#
|
|
3844
|
+
# @return [void]
|
|
3845
|
+
#
|
|
3846
|
+
def validate_requires_one_of_entry!(entry, opts, allocated_positionals)
|
|
3847
|
+
condition = entry[:condition]
|
|
3848
|
+
|
|
3849
|
+
return if condition && !conflict_present?(condition, opts, allocated_positionals)
|
|
3850
|
+
|
|
3851
|
+
names = entry[:names]
|
|
3852
|
+
return if names.any? { |n| conflict_present?(n, opts, allocated_positionals) }
|
|
3853
|
+
|
|
3854
|
+
raise ArgumentError, requires_one_of_error_message(names, condition, entry[:single])
|
|
3855
|
+
end
|
|
3856
|
+
|
|
3857
|
+
# Build the error message for a failed requires_one_of check
|
|
3858
|
+
#
|
|
3859
|
+
# @param names [Array<Symbol>] the required argument names
|
|
3860
|
+
#
|
|
3861
|
+
# @param condition [Symbol, nil] the trigger argument name, or nil for unconditional
|
|
3862
|
+
#
|
|
3863
|
+
# @param single [Boolean] true when declared via `requires` (single required arg)
|
|
3864
|
+
#
|
|
3865
|
+
# @return [String] the error message
|
|
3866
|
+
#
|
|
3867
|
+
def requires_one_of_error_message(names, condition, single)
|
|
3868
|
+
formatted = names.map { |name| ":#{name}" }.join(', ')
|
|
3869
|
+
return "at least one of #{formatted} must be provided" unless condition
|
|
3870
|
+
return ":#{condition} requires #{formatted}" if single
|
|
3871
|
+
|
|
3872
|
+
":#{condition} requires at least one of #{formatted}"
|
|
3873
|
+
end
|
|
3874
|
+
|
|
3875
|
+
# Validate a single name used in a requires_one_of declaration
|
|
3876
|
+
#
|
|
3877
|
+
# @param sym [Symbol] the name to validate
|
|
3878
|
+
#
|
|
3879
|
+
# @return [void]
|
|
3880
|
+
#
|
|
3881
|
+
# @raise [ArgumentError] if sym is not a known option or operand
|
|
3882
|
+
#
|
|
3883
|
+
def validate_requires_one_of_name!(sym)
|
|
3884
|
+
raise ArgumentError, "unknown argument :#{sym} in requires_one_of declaration" unless known_argument?(sym)
|
|
3885
|
+
end
|
|
3886
|
+
|
|
3887
|
+
# Validate a single name used in a requires or conditional requires_one_of declaration
|
|
3888
|
+
#
|
|
3889
|
+
# @param sym [Symbol] the name to validate
|
|
3890
|
+
#
|
|
3891
|
+
# @return [void]
|
|
3892
|
+
#
|
|
3893
|
+
# @raise [ArgumentError] if sym is not a known option or operand
|
|
3894
|
+
#
|
|
3895
|
+
def validate_requires_name!(sym)
|
|
3896
|
+
raise ArgumentError, "unknown argument :#{sym} in requires declaration" unless known_argument?(sym)
|
|
3897
|
+
end
|
|
3898
|
+
|
|
3899
|
+
# Canonicalize an array of argument names for a requires_one_of group
|
|
3900
|
+
#
|
|
3901
|
+
# Validates each name, resolves aliases to their primary name, and deduplicates.
|
|
3902
|
+
# For options, canonical name comes from alias_map; for positional-only operands
|
|
3903
|
+
# the name is used directly.
|
|
3904
|
+
#
|
|
3905
|
+
# @param names [Array<Symbol, String>] raw argument names
|
|
3906
|
+
#
|
|
3907
|
+
# @return [Array<Symbol>] canonical, deduplicated names
|
|
3908
|
+
#
|
|
3909
|
+
def canonicalize_requires_names(names)
|
|
3910
|
+
names.map do |name|
|
|
3911
|
+
sym = name.to_sym
|
|
3912
|
+
validate_requires_one_of_name!(sym)
|
|
3913
|
+
@alias_map[sym] || sym
|
|
3914
|
+
end.uniq
|
|
3915
|
+
end
|
|
3916
|
+
|
|
3917
|
+
# Validate and canonicalize the `when:` condition for requires/requires_one_of
|
|
3918
|
+
#
|
|
3919
|
+
# @param condition [Symbol, nil] the raw trigger argument name
|
|
3920
|
+
#
|
|
3921
|
+
# @return [Symbol, nil] canonical trigger name, or nil when condition is nil
|
|
3922
|
+
#
|
|
3923
|
+
def resolve_requires_condition(condition)
|
|
3924
|
+
return nil unless condition
|
|
3925
|
+
|
|
3926
|
+
trigger_sym = condition.to_sym
|
|
3927
|
+
validate_requires_name!(trigger_sym)
|
|
3928
|
+
@alias_map[trigger_sym] || trigger_sym
|
|
3929
|
+
end
|
|
3930
|
+
|
|
3931
|
+
# Return true if the given name refers to a defined option or operand
|
|
3932
|
+
#
|
|
3933
|
+
# @param name [Symbol] the argument name to look up
|
|
3934
|
+
#
|
|
3935
|
+
# @return [Boolean] true if the name is an option, alias, or operand
|
|
3936
|
+
def known_argument?(name)
|
|
3937
|
+
@alias_map.key?(name) || @operand_definitions.any? { |d| d[:name] == name }
|
|
3938
|
+
end
|
|
3939
|
+
|
|
3940
|
+
# Return true if a conflict-group value should be considered "present"
|
|
3941
|
+
#
|
|
3942
|
+
# A value is absent (not present) when it is nil, false, an empty array,
|
|
3943
|
+
# or an empty string. All other values — including non-empty arrays — are
|
|
3944
|
+
# present, regardless of their contents. This keeps validation consistent
|
|
3945
|
+
# with CLI emission: repeatable options (value_option, inline_value, etc.)
|
|
3946
|
+
# emit tokens for non-empty arrays even when every element is '' or false.
|
|
3947
|
+
#
|
|
3948
|
+
# @param value [Object] the argument value to test
|
|
3949
|
+
#
|
|
3950
|
+
# @return [Boolean] true if the value counts as present for validation
|
|
3951
|
+
def argument_present?(value)
|
|
3952
|
+
return false if value.nil?
|
|
3953
|
+
return false if value == false
|
|
3954
|
+
return false if value == []
|
|
3955
|
+
return false if value == ''
|
|
3956
|
+
|
|
3957
|
+
true
|
|
3958
|
+
end
|
|
3959
|
+
|
|
3960
|
+
# Bound arguments object returned by {Arguments#bind}
|
|
3961
|
+
#
|
|
3962
|
+
# Provides accessor methods for all defined options and positional arguments,
|
|
3963
|
+
# with automatic normalization of aliases to their canonical names.
|
|
3964
|
+
#
|
|
3965
|
+
# For every `flag_option`, both a plain accessor (e.g. `bound.force`) and a
|
|
3966
|
+
# `?`-suffixed predicate alias (e.g. `bound.force?`) are generated, following
|
|
3967
|
+
# Ruby convention for boolean predicates. Plain accessors are kept for backward
|
|
3968
|
+
# compatibility. `value_option` fields only receive plain accessors.
|
|
3969
|
+
#
|
|
3970
|
+
# **Reserved-name exception:** if the `?`-suffixed name conflicts with a name
|
|
3971
|
+
# in {RESERVED_NAMES} (e.g. `nil?`, `frozen?`), the predicate alias is *not*
|
|
3972
|
+
# generated to avoid overriding built-in `Object` methods. Use hash-style
|
|
3973
|
+
# access (`bound[:nil]`) when the flag name is reserved.
|
|
3974
|
+
#
|
|
3975
|
+
# @example Accessing bound arguments
|
|
3976
|
+
# args_def = Arguments.define do
|
|
3977
|
+
# flag_option :force
|
|
3978
|
+
# flag_option :remotes, as: ['-r', '--remotes']
|
|
3979
|
+
# operand :branch_names, repeatable: true
|
|
3980
|
+
# end
|
|
3981
|
+
# bound = args_def.bind('branch1', 'branch2', force: true, remotes: true)
|
|
3982
|
+
# bound.force # => true
|
|
3983
|
+
# bound.force? # => true # ? alias for flag_option
|
|
3984
|
+
# bound.remotes # => true
|
|
3985
|
+
# bound.remotes? # => true # ? alias for flag_option
|
|
3986
|
+
# bound.branch_names # => ['branch1', 'branch2']
|
|
3987
|
+
#
|
|
3988
|
+
# @example Splatting for command execution
|
|
3989
|
+
# args_def = Arguments.define do
|
|
3990
|
+
# flag_option :force
|
|
3991
|
+
# operand :file
|
|
3992
|
+
# end
|
|
3993
|
+
# bound = args_def.bind('test.txt', force: true)
|
|
3994
|
+
# bound.to_a # => ['--force', 'test.txt']
|
|
3995
|
+
#
|
|
3996
|
+
# @example Hash-style access for reserved names
|
|
3997
|
+
# args_def = Arguments.define do
|
|
3998
|
+
# value_option :hash
|
|
3999
|
+
# end
|
|
4000
|
+
# bound = args_def.bind(hash: 'abc123')
|
|
4001
|
+
# bound[:hash] # => 'abc123'
|
|
4002
|
+
#
|
|
4003
|
+
# @api private
|
|
4004
|
+
#
|
|
4005
|
+
class Bound
|
|
4006
|
+
# Names that cannot have accessor methods defined (would override Object methods)
|
|
4007
|
+
RESERVED_NAMES = (Object.instance_methods + [:to_ary]).freeze
|
|
4008
|
+
|
|
4009
|
+
# Canonical frozen empty hash returned by {#execution_options} when no
|
|
4010
|
+
# non-nil execution options are present.
|
|
4011
|
+
#
|
|
4012
|
+
# @return [Hash{Symbol => Object}] frozen empty execution options hash
|
|
4013
|
+
EMPTY_EXECUTION_OPTIONS = {}.freeze
|
|
4014
|
+
|
|
4015
|
+
# Execution options and values for command execution.
|
|
4016
|
+
#
|
|
4017
|
+
# Includes only options declared via {Arguments#execution_option} and
|
|
4018
|
+
# excludes options with nil values.
|
|
4019
|
+
#
|
|
4020
|
+
# @return [Hash{Symbol => Object}] frozen hash of execution option values
|
|
4021
|
+
attr_reader :execution_options
|
|
4022
|
+
|
|
4023
|
+
# Initialize a new frozen Bound object with accessor methods for all defined arguments
|
|
4024
|
+
#
|
|
4025
|
+
# @param args_array [Array<String>] the CLI argument array (frozen)
|
|
4026
|
+
#
|
|
4027
|
+
# @param options [Hash{Symbol => Object}] normalized options hash (frozen)
|
|
4028
|
+
#
|
|
4029
|
+
# @param positionals [Hash{Symbol => Object}] positional arguments hash (frozen)
|
|
4030
|
+
#
|
|
4031
|
+
# @param execution_option_names [Array<Symbol>] option names declared via {Arguments#execution_option}
|
|
4032
|
+
#
|
|
4033
|
+
# @param flag_names [Array<Symbol>] option names declared via {Arguments#flag_option}
|
|
4034
|
+
#
|
|
4035
|
+
# @option options [Object] :"option_name" bound value for any registered option name
|
|
4036
|
+
#
|
|
4037
|
+
def initialize(args_array, options, positionals, execution_option_names = [], flag_names = [])
|
|
4038
|
+
@args_array = args_array.freeze
|
|
4039
|
+
@options = options.freeze
|
|
4040
|
+
@positionals = positionals.freeze
|
|
4041
|
+
@execution_options = build_execution_options(execution_option_names)
|
|
4042
|
+
|
|
4043
|
+
# Define accessor methods (skip reserved names)
|
|
4044
|
+
@options.each_key { |name| define_accessor(name, @options) }
|
|
4045
|
+
@positionals.each_key { |name| define_accessor(name, @positionals) }
|
|
4046
|
+
define_flag_predicate_accessors(flag_names)
|
|
4047
|
+
|
|
4048
|
+
freeze
|
|
4049
|
+
end
|
|
4050
|
+
|
|
4051
|
+
# Returns the CLI arguments array for splatting
|
|
4052
|
+
#
|
|
4053
|
+
# This enables direct splatting: `command(*bound_args)`.
|
|
4054
|
+
#
|
|
4055
|
+
# Operands declared with `skip_cli: true` are intentionally excluded.
|
|
4056
|
+
#
|
|
4057
|
+
# @return [Array<String>] the CLI arguments
|
|
4058
|
+
def to_ary
|
|
4059
|
+
@args_array
|
|
4060
|
+
end
|
|
4061
|
+
|
|
4062
|
+
# Returns the CLI arguments array for splatting
|
|
4063
|
+
#
|
|
4064
|
+
# Ruby's splat operator in array literals uses `to_a` for expansion.
|
|
4065
|
+
# This enables: `['git', 'branch', *bound_args]`.
|
|
4066
|
+
#
|
|
4067
|
+
# Operands declared with `skip_cli: true` are intentionally excluded.
|
|
4068
|
+
#
|
|
4069
|
+
# @return [Array<String>] the CLI arguments
|
|
4070
|
+
def to_a
|
|
4071
|
+
@args_array
|
|
4072
|
+
end
|
|
4073
|
+
|
|
4074
|
+
# Hash-style access to option and positional values
|
|
4075
|
+
#
|
|
4076
|
+
# Use this for reserved names (like :hash, :class) that cannot have
|
|
4077
|
+
# accessor methods defined.
|
|
4078
|
+
#
|
|
4079
|
+
# @param key [Symbol] the option or positional name
|
|
4080
|
+
#
|
|
4081
|
+
# @return [Object, nil] the value, or nil if not found
|
|
4082
|
+
def [](key)
|
|
4083
|
+
return @options[key] if @options.key?(key)
|
|
4084
|
+
return @positionals[key] if @positionals.key?(key)
|
|
4085
|
+
|
|
4086
|
+
nil
|
|
4087
|
+
end
|
|
4088
|
+
|
|
4089
|
+
private
|
|
4090
|
+
|
|
4091
|
+
# Build the execution_options hash from the given option names
|
|
4092
|
+
#
|
|
4093
|
+
# @param execution_option_names [Array<Symbol>] option names declared as execution options
|
|
4094
|
+
#
|
|
4095
|
+
# @return [Hash{Symbol => Object}] frozen hash of non-nil execution option values
|
|
4096
|
+
#
|
|
4097
|
+
# @api private
|
|
4098
|
+
def build_execution_options(execution_option_names)
|
|
4099
|
+
result = execution_option_names.each_with_object({}) do |name, values|
|
|
4100
|
+
value = @options[name]
|
|
4101
|
+
values[name] = value unless value.nil?
|
|
4102
|
+
end
|
|
4103
|
+
|
|
4104
|
+
result.empty? ? EMPTY_EXECUTION_OPTIONS : result.freeze
|
|
4105
|
+
end
|
|
4106
|
+
|
|
4107
|
+
# Define an accessor method for the given name
|
|
4108
|
+
#
|
|
4109
|
+
# For `flag_option` names, a `?`-suffixed predicate alias is also defined
|
|
4110
|
+
# by {#initialize} after all plain accessors have been set up.
|
|
4111
|
+
#
|
|
4112
|
+
# @param name [Symbol] the option or positional name
|
|
4113
|
+
#
|
|
4114
|
+
# @param source [Hash] the hash to read from (@options or @positionals)
|
|
4115
|
+
#
|
|
4116
|
+
def define_accessor(name, source)
|
|
4117
|
+
return if RESERVED_NAMES.include?(name)
|
|
4118
|
+
|
|
4119
|
+
define_singleton_method(name) { source[name] }
|
|
4120
|
+
end
|
|
4121
|
+
|
|
4122
|
+
# Define `?`-suffixed predicate aliases for each flag option
|
|
4123
|
+
#
|
|
4124
|
+
# Skips any name whose `?` form appears in {RESERVED_NAMES} and skips
|
|
4125
|
+
# names that are not present in the options hash.
|
|
4126
|
+
#
|
|
4127
|
+
# @param flag_names [Array<Symbol>] flag option names
|
|
4128
|
+
#
|
|
4129
|
+
def define_flag_predicate_accessors(flag_names)
|
|
4130
|
+
flag_names.each do |name|
|
|
4131
|
+
predicate_name = :"#{name}?"
|
|
4132
|
+
next if RESERVED_NAMES.include?(predicate_name)
|
|
4133
|
+
|
|
4134
|
+
define_singleton_method(predicate_name) { flag_predicate?(@options[name]) }
|
|
4135
|
+
end
|
|
4136
|
+
end
|
|
4137
|
+
|
|
4138
|
+
# Return true if a flag value represents a truthy flag state
|
|
4139
|
+
#
|
|
4140
|
+
# @param value [Boolean, Integer, nil] the flag value
|
|
4141
|
+
#
|
|
4142
|
+
# @return [Boolean] true if the flag value represents an enabled flag
|
|
4143
|
+
#
|
|
4144
|
+
# @api private
|
|
4145
|
+
def flag_predicate?(value)
|
|
4146
|
+
return value.positive? if value.is_a?(Integer)
|
|
4147
|
+
|
|
4148
|
+
value == true
|
|
4149
|
+
end
|
|
4150
|
+
end
|
|
4151
|
+
end
|
|
4152
|
+
|
|
4153
|
+
# Allocates operand (positional argument) values to definitions following Ruby semantics.
|
|
4154
|
+
#
|
|
4155
|
+
# This class handles the complex logic of mapping positional values to their
|
|
4156
|
+
# definitions, supporting required, optional, and repeatable operands.
|
|
4157
|
+
#
|
|
4158
|
+
# @api private
|
|
4159
|
+
class OperandAllocator
|
|
4160
|
+
# @param definitions [Array<Hash>] operand definitions
|
|
4161
|
+
def initialize(definitions)
|
|
4162
|
+
@definitions = definitions
|
|
4163
|
+
end
|
|
4164
|
+
|
|
4165
|
+
# Allocate values to definitions
|
|
4166
|
+
#
|
|
4167
|
+
# @param values [Array] the positional argument values
|
|
4168
|
+
#
|
|
4169
|
+
# @return [Array(Hash, Integer)] tuple of allocation hash and consumed count
|
|
4170
|
+
def allocate(values)
|
|
4171
|
+
allocation = {}
|
|
4172
|
+
repeatable_index = @definitions.find_index { |d| d[:repeatable] }
|
|
4173
|
+
|
|
4174
|
+
consumed = if repeatable_index.nil?
|
|
4175
|
+
allocate_without_repeatable(values, allocation)
|
|
4176
|
+
else
|
|
4177
|
+
allocate_with_repeatable(values, allocation, repeatable_index)
|
|
4178
|
+
end
|
|
4179
|
+
|
|
4180
|
+
[allocation, consumed]
|
|
4181
|
+
end
|
|
4182
|
+
|
|
4183
|
+
private
|
|
4184
|
+
|
|
4185
|
+
# Allocate when there's no repeatable positional, following Ruby semantics:
|
|
4186
|
+
# - Required positionals at the END are reserved first
|
|
4187
|
+
# - Leading positionals get remaining values left-to-right
|
|
4188
|
+
# - Optional positionals are skipped when there aren't enough values
|
|
4189
|
+
#
|
|
4190
|
+
# @param values [Array] the positional argument values
|
|
4191
|
+
#
|
|
4192
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4193
|
+
#
|
|
4194
|
+
# @return [Integer] the number of non-nil positionals consumed
|
|
4195
|
+
#
|
|
4196
|
+
# @api private
|
|
4197
|
+
def allocate_without_repeatable(values, allocation)
|
|
4198
|
+
trailing = count_trailing_required
|
|
4199
|
+
leading_defs = @definitions[0...(@definitions.size - trailing)]
|
|
4200
|
+
trailing_defs = @definitions[(@definitions.size - trailing)..]
|
|
4201
|
+
|
|
4202
|
+
values_for_leading = [values.size - trailing, 0].max
|
|
4203
|
+
leading_values = values[0...values_for_leading]
|
|
4204
|
+
trailing_values = values[values_for_leading..]
|
|
4205
|
+
|
|
4206
|
+
consumed = allocate_leading(allocation, leading_defs, leading_values)
|
|
4207
|
+
consumed + allocate_trailing(allocation, trailing_defs, trailing_values)
|
|
4208
|
+
end
|
|
4209
|
+
|
|
4210
|
+
# Count the number of trailing required definitions
|
|
4211
|
+
#
|
|
4212
|
+
# @return [Integer] number of required definitions at the end of @definitions
|
|
4213
|
+
#
|
|
4214
|
+
# @api private
|
|
4215
|
+
def count_trailing_required
|
|
4216
|
+
count = 0
|
|
4217
|
+
@definitions.reverse_each do |d|
|
|
4218
|
+
break unless required?(d)
|
|
4219
|
+
|
|
4220
|
+
count += 1
|
|
4221
|
+
end
|
|
4222
|
+
count
|
|
4223
|
+
end
|
|
4224
|
+
|
|
4225
|
+
# Return true if a definition is required (required and has no default)
|
|
4226
|
+
#
|
|
4227
|
+
# @param definition [Hash] the operand definition hash
|
|
4228
|
+
#
|
|
4229
|
+
# @return [Boolean] true if the operand must be provided and has no default
|
|
4230
|
+
#
|
|
4231
|
+
# @api private
|
|
4232
|
+
def required?(definition)
|
|
4233
|
+
definition[:required] && definition[:default].nil?
|
|
4234
|
+
end
|
|
4235
|
+
|
|
4236
|
+
# Allocate leading positionals (those before any trailing required)
|
|
4237
|
+
# Required positionals consume values; optional ones only consume if extras available
|
|
4238
|
+
#
|
|
4239
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4240
|
+
#
|
|
4241
|
+
# @param definitions [Array<Hash>] the leading operand definitions
|
|
4242
|
+
#
|
|
4243
|
+
# @param values [Array] the values available for leading allocation
|
|
4244
|
+
#
|
|
4245
|
+
# @return [Integer] the number of non-nil positionals consumed
|
|
4246
|
+
#
|
|
4247
|
+
# @api private
|
|
4248
|
+
def allocate_leading(allocation, definitions, values)
|
|
4249
|
+
return 0 if definitions.empty?
|
|
4250
|
+
|
|
4251
|
+
state = LeadingAllocationState.new(definitions, values, method(:required?))
|
|
4252
|
+
state.allocate(allocation)
|
|
4253
|
+
end
|
|
4254
|
+
|
|
4255
|
+
# Allocate trailing required positionals from the end of the values array
|
|
4256
|
+
#
|
|
4257
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4258
|
+
#
|
|
4259
|
+
# @param definitions [Array<Hash>] the trailing operand definitions
|
|
4260
|
+
#
|
|
4261
|
+
# @param values [Array] the values remaining for trailing allocation
|
|
4262
|
+
#
|
|
4263
|
+
# @return [Integer] the number of non-nil positionals consumed
|
|
4264
|
+
#
|
|
4265
|
+
# @api private
|
|
4266
|
+
def allocate_trailing(allocation, definitions, values)
|
|
4267
|
+
consumed = 0
|
|
4268
|
+
definitions.each_with_index do |definition, index|
|
|
4269
|
+
allocation[definition[:name]] = index < values.size ? values[index] : definition[:default]
|
|
4270
|
+
consumed += 1 if index < values.size
|
|
4271
|
+
end
|
|
4272
|
+
consumed
|
|
4273
|
+
end
|
|
4274
|
+
|
|
4275
|
+
# Allocate values when a repeatable definition is present
|
|
4276
|
+
#
|
|
4277
|
+
# @param values [Array] the positional argument values
|
|
4278
|
+
#
|
|
4279
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4280
|
+
#
|
|
4281
|
+
# @param repeatable_index [Integer] the index of the repeatable definition
|
|
4282
|
+
#
|
|
4283
|
+
# @return [Integer] total non-nil positionals consumed
|
|
4284
|
+
#
|
|
4285
|
+
# @api private
|
|
4286
|
+
def allocate_with_repeatable(values, allocation, repeatable_index)
|
|
4287
|
+
parts = split_around_repeatable(repeatable_index)
|
|
4288
|
+
slices = calculate_repeatable_slices(values, parts)
|
|
4289
|
+
|
|
4290
|
+
pre_consumed = allocate_pre_repeatable_smart(allocation, parts[:pre], slices[:pre_values])
|
|
4291
|
+
repeatable_consumed = allocate_repeatable(
|
|
4292
|
+
allocation, parts[:repeatable], values, slices[:var_start], slices[:var_end]
|
|
4293
|
+
)
|
|
4294
|
+
post_consumed = allocate_post_repeatable(allocation, parts[:post], values, slices[:post_start])
|
|
4295
|
+
|
|
4296
|
+
pre_consumed + repeatable_consumed + post_consumed
|
|
4297
|
+
end
|
|
4298
|
+
|
|
4299
|
+
# Calculate start/end indices for pre, repeatable, and post slices
|
|
4300
|
+
#
|
|
4301
|
+
# @param values [Array] the full positional values array
|
|
4302
|
+
#
|
|
4303
|
+
# @param parts [Hash] the pre/repeatable/post definition parts
|
|
4304
|
+
#
|
|
4305
|
+
# @return [Hash] slice indices: :pre_values, :var_start, :var_end, :post_start
|
|
4306
|
+
#
|
|
4307
|
+
# @api private
|
|
4308
|
+
def calculate_repeatable_slices(values, parts)
|
|
4309
|
+
post_required_count = count_required(parts[:post])
|
|
4310
|
+
pre_available = [values.size - post_required_count, 0].max
|
|
4311
|
+
pre_end = [pre_available, parts[:pre].size].min
|
|
4312
|
+
post_start = [values.size - parts[:post].size, pre_end].max
|
|
4313
|
+
|
|
4314
|
+
{
|
|
4315
|
+
pre_values: values[0...pre_end],
|
|
4316
|
+
var_start: pre_end,
|
|
4317
|
+
var_end: post_start,
|
|
4318
|
+
post_start: post_start
|
|
4319
|
+
}
|
|
4320
|
+
end
|
|
4321
|
+
|
|
4322
|
+
# Count required definitions in the given list
|
|
4323
|
+
#
|
|
4324
|
+
# @param definitions [Array<Hash>] the operand definitions to count
|
|
4325
|
+
#
|
|
4326
|
+
# @return [Integer] the number of required definitions
|
|
4327
|
+
#
|
|
4328
|
+
# @api private
|
|
4329
|
+
def count_required(definitions)
|
|
4330
|
+
definitions.count { |d| required?(d) }
|
|
4331
|
+
end
|
|
4332
|
+
|
|
4333
|
+
# Split @definitions into pre, repeatable, and post parts
|
|
4334
|
+
#
|
|
4335
|
+
# @param repeatable_index [Integer] the index of the repeatable definition
|
|
4336
|
+
#
|
|
4337
|
+
# @return [Hash] parts hash with :pre, :repeatable, and :post keys
|
|
4338
|
+
#
|
|
4339
|
+
# @api private
|
|
4340
|
+
def split_around_repeatable(repeatable_index)
|
|
4341
|
+
{
|
|
4342
|
+
pre: @definitions[0...repeatable_index],
|
|
4343
|
+
repeatable: @definitions[repeatable_index],
|
|
4344
|
+
post: @definitions[(repeatable_index + 1)..]
|
|
4345
|
+
}
|
|
4346
|
+
end
|
|
4347
|
+
|
|
4348
|
+
# Allocate pre-repeatable positionals with Ruby-like semantics
|
|
4349
|
+
# (required get values first, optional only if extra values available)
|
|
4350
|
+
#
|
|
4351
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4352
|
+
#
|
|
4353
|
+
# @param definitions [Array<Hash>] the pre-repeatable operand definitions
|
|
4354
|
+
#
|
|
4355
|
+
# @param values [Array] the values available for pre-repeatable allocation
|
|
4356
|
+
#
|
|
4357
|
+
# @return [Integer] the number of non-nil positionals consumed
|
|
4358
|
+
#
|
|
4359
|
+
# @api private
|
|
4360
|
+
def allocate_pre_repeatable_smart(allocation, definitions, values)
|
|
4361
|
+
return 0 if definitions.empty?
|
|
4362
|
+
|
|
4363
|
+
state = LeadingAllocationState.new(definitions, values, method(:required?))
|
|
4364
|
+
state.allocate(allocation)
|
|
4365
|
+
end
|
|
4366
|
+
|
|
4367
|
+
# Allocate the repeatable definition's slice of values
|
|
4368
|
+
#
|
|
4369
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4370
|
+
#
|
|
4371
|
+
# @param definition [Hash] the repeatable operand definition
|
|
4372
|
+
#
|
|
4373
|
+
# @param values [Array] the full values array
|
|
4374
|
+
#
|
|
4375
|
+
# @param start_idx [Integer] the start index of the repeatable slice
|
|
4376
|
+
#
|
|
4377
|
+
# @param end_idx [Integer] the end index (exclusive) of the repeatable slice
|
|
4378
|
+
#
|
|
4379
|
+
# @return [Integer] the number of non-nil values consumed by the repeatable
|
|
4380
|
+
#
|
|
4381
|
+
# @api private
|
|
4382
|
+
def allocate_repeatable(allocation, definition, values, start_idx, end_idx)
|
|
4383
|
+
repeatable_values = values[start_idx...end_idx] || []
|
|
4384
|
+
allocation[definition[:name]] =
|
|
4385
|
+
if repeatable_values.empty? || repeatable_values.all?(&:nil?)
|
|
4386
|
+
definition[:default]
|
|
4387
|
+
else
|
|
4388
|
+
repeatable_values
|
|
4389
|
+
end
|
|
4390
|
+
repeatable_values.compact.size
|
|
4391
|
+
end
|
|
4392
|
+
|
|
4393
|
+
# Allocate post-repeatable positionals from the end of the values array
|
|
4394
|
+
#
|
|
4395
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4396
|
+
#
|
|
4397
|
+
# @param definitions [Array<Hash>] the post-repeatable operand definitions
|
|
4398
|
+
#
|
|
4399
|
+
# @param values [Array] the full values array
|
|
4400
|
+
#
|
|
4401
|
+
# @param post_start [Integer] the index where post definitions begin
|
|
4402
|
+
#
|
|
4403
|
+
# @return [Integer] the number of non-nil positionals consumed
|
|
4404
|
+
#
|
|
4405
|
+
# @api private
|
|
4406
|
+
def allocate_post_repeatable(allocation, definitions, values, post_start)
|
|
4407
|
+
consumed = 0
|
|
4408
|
+
definitions.each_with_index do |definition, offset|
|
|
4409
|
+
pos_index = post_start + offset
|
|
4410
|
+
value = pos_index < values.size ? values[pos_index] : nil
|
|
4411
|
+
allocation[definition[:name]] = value.nil? ? definition[:default] : value
|
|
4412
|
+
consumed += 1 if pos_index < values.size && !values[pos_index].nil?
|
|
4413
|
+
end
|
|
4414
|
+
consumed
|
|
4415
|
+
end
|
|
4416
|
+
|
|
4417
|
+
# Encapsulates state for allocating leading positionals
|
|
4418
|
+
#
|
|
4419
|
+
# @api private
|
|
4420
|
+
class LeadingAllocationState
|
|
4421
|
+
# Initialize allocation state for the given leading definitions and values
|
|
4422
|
+
#
|
|
4423
|
+
# @param definitions [Array<Hash>] the leading operand definitions
|
|
4424
|
+
#
|
|
4425
|
+
# @param values [Array] the values available for allocation
|
|
4426
|
+
#
|
|
4427
|
+
# @param required_check [Method] callable returning true if a definition is required
|
|
4428
|
+
#
|
|
4429
|
+
# @return [void]
|
|
4430
|
+
#
|
|
4431
|
+
def initialize(definitions, values, required_check)
|
|
4432
|
+
@definitions = definitions
|
|
4433
|
+
@values = values
|
|
4434
|
+
@required_check = required_check
|
|
4435
|
+
@required_count = definitions.count { |d| required_check.call(d) }
|
|
4436
|
+
@extra_for_optionals = [values.size - @required_count, 0].max
|
|
4437
|
+
@val_idx = 0
|
|
4438
|
+
@opt_idx = 0
|
|
4439
|
+
@consumed = 0
|
|
4440
|
+
end
|
|
4441
|
+
|
|
4442
|
+
# Allocates leading positional values and returns consumed non-nil count
|
|
4443
|
+
#
|
|
4444
|
+
# @param allocation [Hash{Symbol => Object}] allocation hash to populate
|
|
4445
|
+
#
|
|
4446
|
+
# @return [Integer] number of non-nil positional values consumed
|
|
4447
|
+
#
|
|
4448
|
+
def allocate(allocation)
|
|
4449
|
+
@definitions.each { |definition| allocate_one(allocation, definition) }
|
|
4450
|
+
@consumed
|
|
4451
|
+
end
|
|
4452
|
+
|
|
4453
|
+
private
|
|
4454
|
+
|
|
4455
|
+
# Allocate a single definition, dispatching to required or optional branch
|
|
4456
|
+
#
|
|
4457
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4458
|
+
#
|
|
4459
|
+
# @param definition [Hash] the operand definition to allocate
|
|
4460
|
+
#
|
|
4461
|
+
# @return [void]
|
|
4462
|
+
#
|
|
4463
|
+
# @api private
|
|
4464
|
+
def allocate_one(allocation, definition)
|
|
4465
|
+
if @required_check.call(definition)
|
|
4466
|
+
allocate_required(allocation, definition)
|
|
4467
|
+
else
|
|
4468
|
+
allocate_optional(allocation, definition)
|
|
4469
|
+
end
|
|
4470
|
+
end
|
|
4471
|
+
|
|
4472
|
+
# Allocate a required definition, consuming the next value unconditionally
|
|
4473
|
+
#
|
|
4474
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4475
|
+
#
|
|
4476
|
+
# @param definition [Hash] the operand definition to allocate
|
|
4477
|
+
#
|
|
4478
|
+
# @return [void]
|
|
4479
|
+
#
|
|
4480
|
+
# @api private
|
|
4481
|
+
def allocate_required(allocation, definition)
|
|
4482
|
+
allocation[definition[:name]] = value_or_default(definition)
|
|
4483
|
+
@consumed += 1 if @val_idx < @values.size
|
|
4484
|
+
@val_idx += 1
|
|
4485
|
+
end
|
|
4486
|
+
|
|
4487
|
+
# Allocate an optional definition, consuming only if extra values remain
|
|
4488
|
+
#
|
|
4489
|
+
# @param allocation [Hash] the allocation hash to populate
|
|
4490
|
+
#
|
|
4491
|
+
# @param definition [Hash] the operand definition to allocate
|
|
4492
|
+
#
|
|
4493
|
+
# @return [void]
|
|
4494
|
+
#
|
|
4495
|
+
# @api private
|
|
4496
|
+
def allocate_optional(allocation, definition)
|
|
4497
|
+
if @opt_idx < @extra_for_optionals
|
|
4498
|
+
allocation[definition[:name]] = value_or_default(definition)
|
|
4499
|
+
@consumed += 1
|
|
4500
|
+
@val_idx += 1
|
|
4501
|
+
else
|
|
4502
|
+
allocation[definition[:name]] = definition[:default]
|
|
4503
|
+
end
|
|
4504
|
+
@opt_idx += 1
|
|
4505
|
+
end
|
|
4506
|
+
|
|
4507
|
+
# Return the current value from @values or the definition default if exhausted
|
|
4508
|
+
#
|
|
4509
|
+
# @param definition [Hash] the operand definition
|
|
4510
|
+
#
|
|
4511
|
+
# @return [Object] the value or default
|
|
4512
|
+
#
|
|
4513
|
+
# @api private
|
|
4514
|
+
def value_or_default(definition)
|
|
4515
|
+
@val_idx < @values.size ? @values[@val_idx] : definition[:default]
|
|
4516
|
+
end
|
|
4517
|
+
end
|
|
4518
|
+
end
|
|
4519
|
+
# rubocop:enable Metrics/ParameterLists
|
|
4520
|
+
end
|
|
4521
|
+
end
|