class Git::Commands::Arguments

This class provides a DSL for mapping Ruby method arguments to git command-line arguments.

## Overview

This class provides a DSL for defining how arguments passed to {#bind} should be mapped to git CLI argument arrays. The process follows four phases:

  1. Definition of expected CLI arguments and their constraints

  2. Binding of method arguments to the definition

  3. Validation of values against argument constraints

  4. Building of the CLI argument array

See {Git::Commands::Init} for a usage example.

For example, defining arguments for a command:

“‘ruby # 1. Definition of expected CLI arguments and their constraints args_def = Arguments.define do

flag_option :force
value_option :branch
operand :repository, required: true

end

# 2. Binding of method arguments to the definition # 3. Validation of values against argument constraints args = args_def.bind(‘github.com/user/repo’, force: true, branch: ‘main’)

# 4. Building of the CLI argument array args.to_a # => [‘–force’, ‘–branch’, ‘main’, ‘github.com/user/repo’]

# Bonus: accessing bound values args.force? # => true args.branch # => ‘main’ args.repository # => ‘github.com/user/repo’ “‘

## Terminology

This class bridges CLI and Ruby interfaces. While both use the term “arguments” for values passed to commands/methods, they differ in terminology for specific argument types:

| CLI (POSIX) | Ruby Interface | Description | | ———————- | ———————- | ————————————————— | | argument specification | DSL definition | Declared command inputs and constraints | | arguments | arguments | Values passed when calling a command/method | | operands | positional arguments | Arguments identified by position | | options | keyword arguments | Arguments identified by name (‘–force` / `force:`) |

The following sections explain each interface in detail.

### CLI Interface (POSIX)

An **argument specification** declares what command inputs are accepted and their constraints.

For example:

“‘text git branch (–set-upstream-to=<upstream>|-u <upstream>) [<branch-name>] “`

When a command is invoked, arguments are the values passed to it:

For example:

“‘shell git branch –set-upstream-to=origin/main main “`

### Ruby Interface

A **DSL definition** declares what arguments the {#bind} method accepts and how they map to CLI arguments.

For example:

“‘ruby Arguments.define do

literal 'branch'
value_option %i[set_upstream_to u], inline: true  # primary name with short alias :u
operand :branch_name

end “‘

When {#bind} is called, arguments are the values passed to it:

For example:

“‘ruby args_def.bind(’main’, set_upstream_to: ‘origin/main’) “‘

Calling {Bound#to_a} on the bound result produces the CLI argument array:

“‘ruby args_def.bind(’main’, set_upstream_to: ‘origin/main’).to_a # => [‘branch’, ‘–set-upstream-to=origin/main’, ‘main’] “‘

## Design

The class operates in two stages:

  1. **Definition stage**: DSL methods ({#flag_option}, {#value_option}, {#operand}, etc.) record argument definitions in internal data structures.

  2. **Bind stage**: {#bind} binds Ruby values and validates them against constraints, returning a {Bound} object.

The returned {Bound} object provides accessor methods for the bound values and handles the building phase, converting bound values to CLI arguments via {Bound#to_a}.

Key internal components:

## Argument Ordering

Arguments are rendered in the exact order they are defined in the DSL block, regardless of type (options, operands, or static flags). This is important for git commands where argument order matters, such as when using ‘–` to separate options from pathspecs.

Use {#end_of_options} to emit ‘–` only when at least one following operand produces output, or {#literal} with `’–‘` when `–` must always be present.

@example Ordering example (end_of_options emits ‘–’ only when path is present)

args_def = Arguments.define do
  operand :ref
  end_of_options
  operand :path
end
args_def.bind('HEAD', 'file.txt').to_a  # => ['HEAD', '--', 'file.txt']
args_def.bind('HEAD').to_a              # => ['HEAD']  # (no trailing --)

## Short Option Detection

Option names are automatically formatted using POSIX conventions:

For inline values (‘inline: true`), the separator also follows POSIX conventions:

Negated flags always use double-dash format (e.g., ‘-f` → `–no-f` when false).

The ‘as:` parameter can override this automatic detection when needed.

@example Short option detection

args_def = Arguments.define do
  flag_option :f                          # true → '-f'
  flag_option :force                      # true → '--force'
  value_option :n, inline: true           # 3 → '-n3'
  value_option :name, inline: true        # 'test' → '--name=test'
end

args_def.bind(f: true, force: true, n: 3, name: 'test').to_a
# => ['-f', '--force', '-n3', '--name=test']

@example Explicit override with ‘as:`

args_def = Arguments.define do
  flag_option :f, as: '--force'
end
args_def.bind(f: true).to_a  # => ['--force']

## Option Types

The DSL supports several option types with modifiers:

### Primary Option Types

{#value_option} supports a ‘repeatable: true` parameter that allows the option to accept an array of values. This repeats the flag for each value (or outputs each as an operand when using `as_operand: true`):

Repeatable options:

“‘ruby value_option :config, repeatable: true # config: [’a=b’, ‘c=d’] => [‘–config’, ‘a=b’, ‘–config’, ‘c=d’]

value_option :sort, inline: true, repeatable: true # sort: [‘refname’, ‘-committerdate’] => [‘–sort=refname’, ‘–sort=-committerdate’]

end_of_options value_option :pathspecs, as_operand: true, repeatable: true # pathspecs: [‘file1.txt’, ‘file2.txt’] => [‘–’, ‘file1.txt’, ‘file2.txt’] “‘

## Common Option Parameters

Most option types support parameters that affect **input validation** (checked during {#bind}):

Note that {#literal} and {#execution_option} do not support these validation parameters.

These parameters affect **output generation** (what CLI arguments are produced):

@example Required option with non-nil value

args_def = Arguments.define do
  value_option :upstream, inline: true, required: true, allow_nil: false
end
args_def.bind() #=> raise ArgumentError, "Required options not provided: :upstream"
args_def.bind(upstream: nil) #=> raise ArgumentError, "Required options cannot be nil: :upstream"
args_def.bind(upstream: 'origin').to_a  # => ['--upstream=origin']

@example Required option allowing nil (default)

args_def = Arguments.define do
  value_option :branch, inline: true, required: true
end
args_def.bind() #=> raise ArgumentError, "Required options not provided: :branch"
args_def.bind(branch: nil).to_a  # => []
args_def.bind(branch: 'main').to_a  # => ['--branch=main']

## Operands (Positional Arguments)

Operands are mapped using Ruby-like semantics:

  1. Post-repeatable required operands are reserved first (from the end)

  2. Pre-repeatable operands are filled with remaining values (required first, then optional)

  3. Optional operands (with defaults) get values only if extras are available

  4. Repeatable operand gets whatever is left in the middle

This matches Ruby’s parameter binding behavior, including patterns like ‘def foo(a = default, *rest, b)` where the required `b` is filled before optional `a`.

@example Simple operand (like ‘git clone <repository>`)

args_def = Arguments.define do
  literal 'clone'
  operand :repository, required: true
end
args_def.bind('https://github.com/user/repo').to_a
# => ['clone', 'https://github.com/user/repo']

@example Repeatable operand (like ‘git add <paths>…`)

args_def = Arguments.define do
  literal 'add'
  operand :paths, repeatable: true
end
args_def.bind('file1', 'file2', 'file3').to_a
# => ['add', 'file1', 'file2', 'file3']

@example git mv pattern (like ‘git mv <sources>… <destination>`)

args_def = Arguments.define do
  literal 'mv'
  operand :sources, repeatable: true, required: true
  operand :destination, required: true
end
args_def.bind('src1', 'src2', 'dest').to_a  # => ['mv', 'src1', 'src2', 'dest']

## Nil Handling for Operands

When nil values are allowed (see ‘required:` and `allow_nil:` above), they have special output behavior:

@example Nil value omitted from output

args = Arguments.define do
  operand :tree_ish, required: true, allow_nil: true
  operand :paths, repeatable: true
end.bind(nil, 'file1', 'file2')
args.to_a     # => ['file1', 'file2']
args.tree_ish # => nil
args.paths    # => ['file1', 'file2']

## Option-like Operand Rejection

Operands that appear before a ‘–` separator boundary in the argument definition are automatically validated to ensure their values don’t start with ‘-`. This prevents user-supplied strings like `’-s’‘ from being misinterpreted as git flags when passed as positional arguments.

The ‘–` boundary can come from:

Operands after the ‘–` boundary are not validated (they represent paths/filenames which may legitimately start with `-`). If no `–` boundary exists in the definition, all operands are validated.

@example Operands before and after ‘–’ end_of_options boundary

args_def = Arguments.define do
  operand :commit1
  operand :commit2
  end_of_options
  operand :paths, repeatable: true
end
args_def.bind('-s') #=> raise ArgumentError, "operand :commit1 value '-s' looks like a command-line option"
args_def.bind('HEAD', 'HEAD~1', '-file.txt').to_a
# => ['HEAD', 'HEAD~1', '--', '-file.txt']

@example All operands validated when no ‘–’ boundary exists

args_def = Arguments.define do
  operand :path1, required: true
  operand :path2, required: true
end
args_def.bind('-s', 'file.txt')
#=> raise ArgumentError, "operand :path1 value '-s' looks like a command-line option"

## Options After Separator

Options that produce CLI flags (e.g. ‘flag_option`, `value_option`, `key_value_option`, `custom_option`) cannot be defined after a `–` separator boundary. Git treats everything after `–` as operands, so flags emitted there would be misinterpreted.

Only ‘value_option` with `as_operand: true` and `execution_option` are allowed after the boundary because they do not produce flag-prefixed output.

For example, this will raise ‘ArgumentError` during definition:

Arguments.define do
  literal '--'
  flag_option :verbose
end #=> raises ArgumentError

@example Allowed: value_option as_operand after ‘–’

Arguments.define do
  literal '--'
  value_option :paths, as_operand: true, repeatable: true
end

## Type Validation

The ‘type:` parameter provides declarative type validation for option values. When validation fails, an ArgumentError is raised with a descriptive message.

@example Single type validation

args_def = Arguments.define do
  value_option :date, type: String, inline: true
end
args_def.bind(date: "2024-01-01").to_a  # => ['--date=2024-01-01']
args_def.bind(date: 12345) #=> raise ArgumentError, "The :date option must be a String, but was a Integer"

@example Multiple type validation (allows any of the specified types)

args_def = Arguments.define do
  value_option :timeout, type: [Integer, Float], inline: true
end
args_def.bind(timeout: 30).to_a    # => ['--timeout=30']
args_def.bind(timeout: 30.5).to_a  # => ['--timeout=30.5']
args_def.bind(timeout: "30")
#=> raise ArgumentError, "The :timeout option must be a Integer or Float, but was a String"

## String Conversion

During the build phase, value-bearing option types (‘value_option`, `flag_or_value_option`, `key_value_option`) and `operand` definitions convert their bound values to CLI argument strings by calling `#to_s`. This means any object with a meaningful `#to_s` implementation — `Integer`, `Float`, `Pathname`, etc. — can be passed as a value without the DSL needing to know about the type.

Note that ‘flag_option` values control *presence or absence* of a flag and are not stringified. `custom_option` builders receive the raw value and are responsible for producing CLI strings themselves.

The ‘type:` parameter does not affect this conversion; it only validates the Ruby class of the value before stringification.

@example Numeric values are stringified automatically

args_def = Arguments.define do
  value_option :depth, inline: true
  value_option :jobs,  inline: true
end
args_def.bind(depth: 5, jobs: 4).to_a  # => ['--depth=5', '--jobs=4']

@example Pathname is also accepted (no type: needed)

args_def = Arguments.define do
  operand :path, required: true
end
args_def.bind(Pathname.new('/tmp/foo')).to_a  # => ['/tmp/foo']

## Conflict Detection

Use {#conflicts} to declare mutually exclusive arguments. Names may refer to options (flag, value, flag-or-value, etc.) or operands (positional arguments) interchangeably. When {#bind} is called, if more than one argument in a conflict group is “present”, an ArgumentError is raised.

An argument is considered present when its value is not ‘nil`, `false`, `[]`, or `”`.

@example Option vs option conflict

args_def = Arguments.define do
  flag_option :force
  flag_option :force_force
  conflicts :force, :force_force
end
args_def.bind(force: true, force_force: true) #=> raise ArgumentError, "cannot specify :force and :force_force"

@example Mixed option and operand conflict

args_def = Arguments.define do
  flag_option %i[merge m], as: '--merge'
  operand :tree_ish, required: true, allow_nil: true
  conflicts :merge, :tree_ish
end
args_def.bind('main', merge: true)  #=> raise ArgumentError, "cannot specify :merge and :tree_ish"
args_def.bind(nil, merge: true).to_a  # => ['--merge']

## Forbidden Value Combinations

{#conflicts} is presence-based — it cannot distinguish between semantically equivalent and contradictory combinations of negatable flags. Use {#forbid_values} to declare specific **exact-value tuples** that are invalid.

A ‘forbid_values` declaration matches only when every listed name has a bound value equal to the declared value (Ruby `==`). Only matching tuples raise ArgumentError; all other value combinations are permitted. Names may be options or operands; aliases are canonicalized before comparison.

This is most useful for negatable flags where some value-pairings are contradictory but others are semantically equivalent and should remain valid.

The error message has the form:

"cannot specify :name1=value1 with :name2=value2"

@example Reject contradictory pairs without blocking equivalent ones

args_def = Arguments.define do
  flag_option :all,            negatable: true
  flag_option :ignore_removal, negatable: true
  forbid_values all: true,    ignore_removal: true         # --all --ignore-removal: contradictory
  forbid_values no_all: true, no_ignore_removal: true     # --no-all --no-ignore-removal: contradictory
end
args_def.bind(all: true,    ignore_removal: true)
  #=> raise ArgumentError, 'cannot specify :all=true with :ignore_removal=true'
args_def.bind(all: true,    no_ignore_removal: true).to_a  # => ['--all', '--no-ignore-removal']
args_def.bind(no_all: true, ignore_removal: true).to_a     # => ['--no-all', '--ignore-removal']

## At-Least-One Presence Validation

Use {#requires_one_of} to declare groups of arguments where at least one must be present. Names may refer to options (flag, value, flag-or-value, etc.) or operands (positional arguments) interchangeably. When {#bind} is called, if none of the arguments in a group is present, an ArgumentError is raised.

@example Requiring at least one path source (options only)

args_def = Arguments.define do
  value_option :pathspec_from_file, inline: true
  end_of_options
  value_option :pathspec, as_operand: true, repeatable: true
  requires_one_of :pathspec, :pathspec_from_file
end
args_def.bind
  #=> raise ArgumentError, 'at least one of :pathspec, :pathspec_from_file must be provided'
args_def.bind(pathspec: ['file.txt']).to_a  # => ['--', 'file.txt']

@example Mixed option and operand group

args_def = Arguments.define do
  flag_option :all
  operand :paths, repeatable: true
  requires_one_of :all, :paths
end
args_def.bind
  #=> raise ArgumentError, 'at least one of :all, :paths must be provided'
args_def.bind('file.txt').to_a  # => ['file.txt']

## Conditional Argument Requirements

Use {#requires} and the ‘when:` form of {#requires_one_of} to declare that an argument (or at least one of a group) must be present **only when** a specific trigger argument is present. These constraints are evaluated during {#bind}: if the trigger is absent the check is skipped entirely.

An ArgumentError is raised at definition time if either the required name(s) or the trigger name are not known arguments, catching typos early.

@example Single conditional requirement

args_def = Arguments.define do
  flag_option :pathspec_file_nul
  value_option :pathspec_from_file, inline: true
  requires :pathspec_from_file, when: :pathspec_file_nul
end
args_def.bind(pathspec_file_nul: true, pathspec_from_file: 'paths.txt').to_a
# => ['--pathspec-file-nul', '--pathspec-from-file=paths.txt']
args_def.bind(pathspec_file_nul: true)
#=> raise ArgumentError, ':pathspec_file_nul requires :pathspec_from_file'
args_def.bind  # trigger absent — no error

@example Conditional at-least-one-of group

args_def = Arguments.define do
  flag_option :annotate
  value_option :message, inline: true
  value_option :file, inline: true
  requires_one_of :message, :file, when: :annotate
end
args_def.bind(annotate: true, message: 'v1.0').to_a  # => ['--annotate', '--message=v1.0']
args_def.bind(annotate: true)
#=> raise ArgumentError, ':annotate requires at least one of :message, :file'
args_def.bind  # trigger absent — no error

## Value Constraints

In addition to presence-based validation ({#conflicts}, {#requires_one_of}, and {#requires}) and value-combination constraints ({#forbid_values}), you can restrict the *set of acceptable values* for any value-type option using {#allowed_values}. If a bound value falls outside the configured set, {#bind} raises ArgumentError with a descriptive message.

This is typically used to model git options that accept only a fixed list of modes or strategies.

@example Restricting option values

args_def = Arguments.define do
  value_option :strategy, inline: true
  allowed_values :strategy, in: %w[ours theirs]
end
args_def.bind(strategy: 'ours').to_a      # => ['--strategy=ours']
args_def.bind(strategy: 'theirs').to_a # => ['--strategy=theirs']
args_def.bind(strategy: 'rebase')
# => raise ArgumentError, 'Invalid value for :strategy: expected one of ["ours", "theirs"], got "rebase"'

@api private