A pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent
core of Ruby's OptionParser
(stdlib optparse) — the argv-parsing engine. It models the option
specifications declared with on(...), tokenizes an argv, performs
long/short/abbreviation matching, handles =/bundled/--[no-]/optional-argument
forms, coerces values, and reports the full OptionParser::ParseError taxonomy —
matching MRI 4.0.5 byte-for-byte.
It is the OptionParser backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime.
What it is — and isn't. Modeling the option specs, tokenizing argv, matching long/short/abbreviated names, coercing arguments (Integer/Float/Array/custom lists) and computing the error taxonomy is fully deterministic and needs no interpreter, so it lives here as pure Go. The per-match Ruby blocks that
on(...)registers do need a Ruby interpreter and stay in the consumer (e.g. rbgo): this library parses argv and returns the ordered matches + coerced values + leftover operands + MRI-exact errors, and the host dispatches the blocks over those matches.
Faithful port of MRI's lib/optparse.rb parsing engine, validated against the
ruby binary on every supported platform:
- All flag forms — long
--name, short-n, both-n, --name. - Argument styles — required
--name VALUE/-n VALUE, optional--name [VALUE],=-joined--name=VALUE/-n=VALUE, glued short-nVALUE. - Bundled shorts —
-xvf, with the last switch in a bundle taking its argument. - Negation —
--[no-]flagmatches both--flag(true) and--no-flag(false). - Abbreviation & completion — unique-prefix long abbreviation (
--verb→--verbose), short→long completion (-n→--name), withAmbiguousOptionon a tie. - Coercion —
Integer(decimal +0x/0b/0o,_separators, signed, arbitrary precision),Float,Array(comma-split),String, and custom candidate lists / value maps with prefix completion. - Parse modes —
parse!/permute!(options anywhere),order!(stop at the first operand, or a callback per operand), andgetopts. - Terminators —
--ends option parsing; a bare-is an operand. - Full error tree —
InvalidOption,MissingArgument,InvalidArgument,AmbiguousOption,AmbiguousArgument,NeedlessArgument, each with MRI-exactmessage/reason/argsandrecover. - Help layout —
help/to_s/summarizewith MRI's column alignment, separators, andon_headordering.
CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and
green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le,
s390x).
package main
import (
"fmt"
"github.com/go-ruby-optparse/optparse"
)
func main() {
p := optparse.New()
p.Banner = "Usage: tool [options] FILE..."
// Declare options from MRI `on(...)` flag strings (+ optional coercion).
verbose := p.Define([]string{"-v", "--verbose", "run verbosely"}, "", nil, nil)
count := p.Define([]string{"--count N", "how many"}, optparse.CoerceInteger, nil, nil)
mode := p.Define([]string{"--mode M", "fast|slow"},
optparse.CoerceList, []string{"fast", "slow"}, nil)
matches, rest, err := p.ParseBang([]string{"-v", "--count", "0x10", "--mode", "fa", "file.txt"})
if err != nil {
// err is a *optparse.ParseError with MRI-exact Error()/Class()/Args.
fmt.Println(err) // e.g. "invalid argument: --count xx"
return
}
// Dispatch: in rbgo each match's SpecIndex selects the Ruby block to call.
for _, m := range matches {
switch m.SpecIndex {
case verbose:
fmt.Println("verbose:", m.Value) // true
case count:
fmt.Println("count:", m.Value) // int64(16)
case mode:
fmt.Println("mode:", m.Value) // "fast"
}
}
fmt.Println("operands:", rest) // ["file.txt"]
fmt.Print(p.Help())
}MakeSpec builds an optparse.Spec directly from on(...) strings; On /
OnHead register a Spec and return its index; Order, Permute and Getopts
cover the remaining MRI parse entry points.
The test suite is differential: every case is parsed by both this engine and
the same pure-Ruby OptionParser that go-embedded-ruby ships (extracted into
testdata/optionparser.rb), and the serialized result — matches, coerced values,
leftovers, or the error class/reason/args/message — is compared byte-for-byte. The
Ruby oracle self-skips where ruby is absent (and binds $stdout to binary so
Windows never injects CRLF); a parallel set of deterministic, ruby-free tests
locks the same expectations and reaches 100% coverage on its own, so the
no-ruby and qemu CI lanes stay green.
go test ./... # full suite (uses ruby if present)
go test -race -coverprofile=cover.out ./... # race + coverage
go tool cover -func=cover.out | tail -1 # total: 100.0%BSD-3-Clause. See LICENSE.
