argsieve

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 21, 2026 License: GPL-3.0 Imports: 7 Imported by: 0

README

argsieve

Go argument parsing with known/unknown flag separation for CLI wrappers.

CI Go Reference

Installation

go get github.com/ivoronin/argsieve

Usage

type Options struct {
    Verbose bool   `short:"v" long:"verbose"`
    Config  string `short:"c" long:"config"`
}

var opts Options

// Sift: extract known flags, pass through unknown
remaining, positional, err := argsieve.Sift(&opts, os.Args[1:], []string{"-o"}, nil)

// Parse: strict mode, error on unknown flags
positional, err := argsieve.Parse(&opts, os.Args[1:], nil)

Configuration

Both Sift and Parse accept an optional *Config parameter (pass nil for defaults).

RequirePositionalDelimiter

Require all positional arguments to appear after --:

cfg := &argsieve.Config{RequirePositionalDelimiter: true}
positional, err := argsieve.Parse(&opts, os.Args[1:], cfg)

// "-v filename"     → error: positional before "--"
// "-v -- filename"  → OK: positional after delimiter

See pkg.go.dev for full API documentation.

License

GPL-3.0

Documentation

Overview

Package argsieve provides argument parsing with known/unknown flag separation.

argsieve is designed for CLI wrapper applications that need to intercept some flags while passing others through to an underlying command.

Two Parsing Modes

Sift extracts known flags and passes unknown flags through - ideal for CLI wrappers that need to handle some flags while forwarding others to an underlying command.

Parse is strict mode that errors on any unknown flag - use this when building standalone CLI tools.

Configuration

Both Sift and Parse accept an optional Config parameter. Pass nil to use defaults.

Use [Config.RequirePositionalDelimiter] to require that all positional arguments appear after the "--" delimiter:

cfg := &argsieve.Config{RequirePositionalDelimiter: true}
positional, err := argsieve.Parse(&opts, args, cfg)
// "-v filename" → error: positional before "--"
// "-v -- filename" → OK: positional after delimiter

Use [Config.StopAtFirstPositional] to stop flag parsing at the first positional argument (POSIX-style):

cfg := &argsieve.Config{StopAtFirstPositional: true}
positional, err := argsieve.Parse(&opts, args, cfg)
// "-v file -d" → -v parsed, ["file", "-d"] are positional

Struct Tags

Define flags using struct tags:

type Options struct {
    Region  string `short:"r" long:"region"`
    Verbose bool   `short:"v" long:"verbose"`
}

Supported Flag Formats

  • Short flags: `-v`, `-r value`, `-rvalue`, `-vdr` (chained bools)
  • Long flags: `--verbose`, `--region value`, `--region=value`
  • Terminator: `--` (everything after is positional)

Supported Field Types

  • `bool`: flag presence sets true (no value required)
  • `string`: requires a value
  • encoding.TextUnmarshaler: custom parsing (pointer types are nil when absent)

Embedded Structs

Flags can be organized using embedded structs:

type CommonFlags struct {
    Verbose bool `short:"v"`
}
type Options struct {
    CommonFlags
    Output string `short:"o"`
}

Error Handling

Parse errors are wrapped with ErrParse for easy detection:

if errors.Is(err, argsieve.ErrParse) {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}
Example
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Config  string `short:"c" long:"config"`
		Verbose bool   `short:"v" long:"verbose"`
	}

	var opts Options
	args := []string{"-v", "--config", "app.yaml", "file1.txt", "file2.txt"}

	remaining, positional, err := argsieve.Sift(&opts, args, nil, nil)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Config: %s\n", opts.Config)
	fmt.Printf("Verbose: %t\n", opts.Verbose)
	fmt.Printf("Remaining: %v\n", remaining)
	fmt.Printf("Positional: %v\n", positional)
}
Output:

Config: app.yaml
Verbose: true
Remaining: []
Positional: [file1.txt file2.txt]

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrParse = errors.New("argument parsing error")

ErrParse indicates a parsing error such as a missing value for a flag that requires one, or (in strict mode) an unknown flag.

Use errors.Is to check for parsing errors:

if errors.Is(err, argsieve.ErrParse) {
    // Handle parsing error
}

Functions

func Parse

func Parse(target any, args []string, cfg *Config) (positional []string, err error)

Parse parses args into target in strict mode, returning only positional arguments.

Unlike Sift, Parse returns an error if any unknown flags are encountered. Use this for standalone CLI tools where all flags should be defined.

The cfg parameter allows optional configuration. Pass nil to use defaults. When cfg.RequirePositionalDelimiter is true, positional arguments must appear after the "--" delimiter or an error is returned.

Example:

type Options struct {
    Output  string `short:"o" long:"output"`
    Verbose bool   `short:"v"`
}
var opts Options
positional, err := argsieve.Parse(&opts, os.Args[1:], nil)
if errors.Is(err, argsieve.ErrParse) {
    // Handle unknown flag or missing value
}

Panics if target is not a pointer to struct or if any tagged field has an unsupported type.

Example
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Output string `short:"o" long:"output"`
		Force  bool   `short:"f" long:"force"`
	}

	var opts Options
	args := []string{"-f", "--output", "result.txt", "input.txt"}

	positional, err := argsieve.Parse(&opts, args, nil)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Output: %s\n", opts.Output)
	fmt.Printf("Force: %t\n", opts.Force)
	fmt.Printf("Files: %v\n", positional)
}
Output:

Output: result.txt
Force: true
Files: [input.txt]
Example (ErrorHandling)
package main

import (
	"errors"
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Output string `short:"o" long:"output"`
	}

	var opts Options
	args := []string{"--unknown-flag"}

	_, err := argsieve.Parse(&opts, args, nil)
	if errors.Is(err, argsieve.ErrParse) {
		fmt.Println("Parse error:", err)
	}
}
Output:

Parse error: argument parsing error: unknown option --unknown-flag
Example (TextUnmarshaler)
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

// LogLevel demonstrates encoding.TextUnmarshaler for custom flag types.
type LogLevel string

func (l *LogLevel) UnmarshalText(text []byte) error {
	switch string(text) {
	case "info", "debug", "error":
		*l = LogLevel(text)
	default:
		return fmt.Errorf("unknown log level: %q", text)
	}
	return nil
}

func main() {
	type Options struct {
		Level   LogLevel `short:"l" long:"level"`
		Verbose bool     `short:"v"`
	}

	var opts Options
	args := []string{"-v", "--level", "debug"}

	_, err := argsieve.Parse(&opts, args, nil)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Level: %s\n", opts.Level)
	fmt.Printf("Verbose: %t\n", opts.Verbose)
}
Output:

Level: debug
Verbose: true

func Sift

func Sift(target any, args []string, passthroughWithArg []string, cfg *Config) (remaining, positional []string, err error)

Sift extracts known flags from args into target, returning unknown flags and positional arguments separately.

This is the primary function for CLI wrapper applications. Known flags (those matching struct tags) are parsed into target. Unknown flags are returned in remaining, allowing you to forward them to another command.

The passthroughWithArg parameter lists unknown flags that consume a value. Without this hint, an unknown flag's value would be treated as positional.

The cfg parameter allows optional configuration. Pass nil to use defaults. When cfg.RequirePositionalDelimiter is true, positional arguments must appear after the "--" delimiter or an error is returned.

Example:

type Options struct {
    Config string `short:"c" long:"config"`
    Debug  bool   `short:"d"`
}
var opts Options
remaining, positional, err := argsieve.Sift(&opts, os.Args[1:], []string{"-x"}, nil)
// opts.Config contains the parsed value
// remaining holds unknown flags like ["-x", "value"]
// positional holds non-flag arguments

Panics if target is not a pointer to struct or if any tagged field has an unsupported type.

Example
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Config string `short:"c" long:"config"`
	}

	var opts Options
	args := []string{"-c", "app.yaml", "-x", "extra-value", "target"}

	// -x takes a value, so list it in passthroughWithArg
	remaining, positional, _ := argsieve.Sift(&opts, args, []string{"-x"}, nil)

	fmt.Printf("Config: %s\n", opts.Config)
	fmt.Printf("Passthrough: %v\n", remaining)
	fmt.Printf("Positional: %v\n", positional)
}
Output:

Config: app.yaml
Passthrough: [-x extra-value]
Positional: [target]
Example (ChainedFlags)
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Verbose bool   `short:"v"`
		Debug   bool   `short:"d"`
		Level   string `short:"l"`
	}

	var opts Options
	// -vdl combines -v, -d, and -l with attached value
	args := []string{"-vdlinfo"}

	if _, _, err := argsieve.Sift(&opts, args, nil, nil); err != nil {
		panic(err)
	}

	fmt.Printf("Verbose: %t, Debug: %t, Level: %s\n", opts.Verbose, opts.Debug, opts.Level)
}
Output:

Verbose: true, Debug: true, Level: info
Example (Passthrough)
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Debug bool `short:"d" long:"debug"`
	}

	var opts Options
	args := []string{"-d", "-L", "8080:localhost:80", "--unknown", "value", "target"}

	// List flags that consume values so they're captured correctly
	remaining, positional, _ := argsieve.Sift(&opts, args, []string{"-L", "--unknown"}, nil)

	fmt.Printf("Debug: %t\n", opts.Debug)
	fmt.Printf("Passthrough: %v\n", remaining)
	fmt.Printf("Positional: %v\n", positional)
}
Output:

Debug: true
Passthrough: [-L 8080:localhost:80 --unknown value]
Positional: [target]

Types

type Config added in v0.0.3

type Config struct {
	// RequirePositionalDelimiter when true requires all positional arguments
	// to appear after the "--" delimiter. Positional arguments before "--"
	// will cause a parse error.
	RequirePositionalDelimiter bool

	// StopAtFirstPositional when true stops flag parsing at the first
	// positional argument. All subsequent arguments are treated as positional,
	// even if they look like flags.
	StopAtFirstPositional bool
}

Config holds optional settings for argument parsing. Pass nil to use defaults.

Example (RequirePositionalDelimiter)
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Verbose bool `short:"v" long:"verbose"`
	}

	var opts Options
	// With RequirePositionalDelimiter, positionals must come after "--"
	args := []string{"-v", "--", "file1.txt", "file2.txt"}

	cfg := &argsieve.Config{RequirePositionalDelimiter: true}
	positional, err := argsieve.Parse(&opts, args, cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Verbose: %t\n", opts.Verbose)
	fmt.Printf("Files: %v\n", positional)
}
Output:

Verbose: true
Files: [file1.txt file2.txt]
Example (StopAtFirstPositional)
package main

import (
	"fmt"

	"github.com/ivoronin/argsieve"
)

func main() {
	type Options struct {
		Verbose bool `short:"v" long:"verbose"`
	}

	var opts Options
	// With StopAtFirstPositional, flags after "cmd" become positional
	args := []string{"-v", "cmd", "-x", "--flag"}

	cfg := &argsieve.Config{StopAtFirstPositional: true}
	positional, err := argsieve.Parse(&opts, args, cfg)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Verbose: %t\n", opts.Verbose)
	fmt.Printf("Positional: %v\n", positional)
}
Output:

Verbose: true
Positional: [cmd -x --flag]

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL