lexopt

package module
v0.0.0-...-5de19e9 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2024 License: MIT Imports: 7 Imported by: 0

README

Go lexopt

🐿️ Rust's lexopt crate ported to Go

Usage

package main

import (
    "errors"
    "fmt"
    "log"
    "os"
    "strconv"
    "strings"

    . "github.com/jcbhmr/go-lexopt/prelude"
    "github.com/jcbhmr/go-lexopt"
)

type args struct {
    thing string
    number uint32
    shout bool
}

func parseArgs() (args, error) {
    var thing *string = nil
    var number uint32 = 1
    var shout bool = false
    parser := lexopt.ParserFromEnv()
    for {
        arg, ok, err := parser.Next()
        if err != nil {
            return args{}, err
        }
        if !ok {
            break
        }
        argShort, argShortOk := arg.(Short)
        argLong, argLongOk := arg.(Long)
        if (argShortOk && argShort == 'n') || (argLongOk && argLong == "number") {
            value, err := parser.Value()
            if err != nil {
                return args{}, err
            }
            number, err = strconv.ParseUint(value, 10, 32)
            if err != nil {
                return args{}, err
            }
        } else if v, ok := arg.(Long); ok && v == "shout" {
            shout = true
        } else if val, ok := arg.(Value); thing == nil && ok {
            thing = &val
        } else if v, ok := arg.(Long); ok && v == "help" {
            fmt.Println("Usage: hello [-n|--number=NUM] [--shout] THING")
            os.Exit(0)
        } else {
            return args{}, arg.Unexpected()
        }
    }
    if thing == nil {
        return args{}, errors.New("missing argument THING")
    }
    return args{
        thing: *thing,
        number: number,
        shout: shout,
    }, nil
}

func main() {
    args, err := parseArgs()
    if err != nil {
        log.Fatal(err)
    }
    message := fmt.Sprintf("Hello %s!", args.thing)
    if args.shout {
        message = strings.ToUpper(message)
    }
    for i := uint32(0); i < args.number; i++ {
        fmt.Println(message)
    }
}

Let's walk through this:

  • We start parsing with lexopt.ParserFromEnv()
  • We call parser.Next() in a loop to get all the arguments until they run out
  • We use if statements to structurally match on the arguments. Short and Long indicate an option.
  • To get the value that belongs to an option (like 10 in -n 10) we call parser.Value().
    • This returns a standard string.
    • For convenience, import . "github.com/jcbhmr/go-lexopt/prelude" adds a `

Documentation

Overview

Example (Cargo)
/*
A very partial unfaithful implementation of cargo's command line.

This showcases some harier patterns, like subcommands and custom value parsing.
*/
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/jcbhmr/go-lexopt"
	. "github.com/jcbhmr/go-lexopt/prelude"
)

const help = "cargo [+toolchain] [OPTIONS] [SUBCOMMAND]"

func main() {
	os.Args = []string{"cargo", "+nightly", "--verbose", "--color=never", "install", "hello", "--root", "/home/octocat/project1", "--jobs", "8"}

	log.SetFlags(0)

	settings := globalSettings{
		toolchain: "stable",
		color:     colorAuto,
		offline:   false,
		quiet:     false,
		verbose:   false,
	}

	parser := lexopt.ParserFromEnv()
	for {
		arg, ok, err := parser.Next()
		fmt.Printf("parser=%#+v, arg=%#+v, ok=%#+v, err=%#+v\n", parser, arg, ok, err)
		if err != nil {
			log.Fatal(err)
		}
		if !ok {
			break
		}
		if argV, ok := arg.(Long); ok && argV.A == "color" {
			colorText, err := parser.Value()
			if err != nil {
				log.Fatal(err)
			}
			color, err2 := colorFromStr(colorText)
			if err2 != "" {
				err = &lexopt.ErrorCustom{errors.New(err2)}
				log.Fatal(err)
			}
			settings.color = color
		} else if argV, ok := arg.(Long); ok && argV.A == "offline" {
			settings.offline = true
		} else if argV, ok := arg.(Long); ok && argV.A == "quiet" {
			settings.quiet = true
			settings.verbose = false
		} else if argV, ok := arg.(Long); ok && argV.A == "verbose" {
			settings.quiet = false
			settings.verbose = true
		} else if argV, ok := arg.(Long); ok && argV.A == "help" {
			fmt.Println(help)
			os.Exit(0)
		} else if value, ok := arg.(Value); ok {
			if strings.HasPrefix(value.A, "+") {
				settings.toolchain = value.A[1:]
			} else if value.A == "install" {
				err := install(settings, parser)
				if err != nil {
					log.Fatal(err)
				}
				return
			} else {
				log.Fatalf("Unknown subcommand '%v'", value.A)
			}
		} else {
			log.Fatal(arg.Unexpected())
		}
	}

	fmt.Println(help)

}

type globalSettings struct {
	toolchain string
	color     color
	offline   bool
	quiet     bool
	verbose   bool
}

func install(settings globalSettings, parser *lexopt.Parser) lexopt.Error {
	var package_ *string = nil
	var root *string = nil
	var jobs uint16 = getNoOfCPUs()

	for {
		arg, ok, err := parser.Next()
		if err != nil {
			return err
		}
		if !ok {
			break
		}
		argShort, argShortOk := arg.(Short)
		argLong, argLongOk := arg.(Long)
		if value, ok := arg.(Value); package_ == nil && ok {
			package_ = &value.A
		} else if (arg == Long{"root"}) {
			rootText, err := parser.Value()
			if err != nil {
				return err
			}
			root = &rootText
		} else if (argShortOk && argShort.A == 'j') || (argLongOk && argLong.A == "jobs") {
			jobsText, err := parser.Value()
			if err != nil {
				return err
			}
			jobs64, err2 := strconv.ParseUint(jobsText, 10, 16)
			if err2 != nil {
				return &lexopt.ErrorCustom{err2}
			}
			jobs = uint16(jobs64)
		} else if argV, ok := arg.(Long); ok && argV.A == "help" {
			fmt.Println("cargo install [OPTIONS] CRATE")
			os.Exit(0)
		} else {
			return arg.Unexpected()
		}
	}

	fmt.Printf("Settings: %#+v\n", settings)
	if package_ == nil {
		return &lexopt.ErrorCustom{errors.New("missing CRATE argument")}
	}
	fmt.Printf("Installing %v into %#v with %v jobs\n", *package_, root, jobs)

	return nil
}

type color uint8

const (
	colorAuto color = iota
	colorAlways
	colorNever
)

func colorFromStr(s string) (color, string) {
	switch strings.ToLower(s) {
	case "auto":
		return colorAuto, ""
	case "always":
		return colorAlways, ""
	case "never":
		return colorNever, ""
	default:
		return 0, fmt.Sprintf("Invalid style '%v' [pick from: auto, always, never]", s)
	}
}

func getNoOfCPUs() uint16 {
	return 4
}
Output:

Settings: lexopt_test.globalSettings{toolchain:"nightly", color:0, offline:false, quiet:false, verbose:true}
Installing hello into /home/octocat/project1 with 8 jobs
Example (Hello)
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/jcbhmr/go-lexopt"
	. "github.com/jcbhmr/go-lexopt/prelude"
)

type args struct {
	thing  string
	number uint32
	shout  bool
}

func parseArgs() (args, error) {
	var thing *string = nil
	var number uint32 = 1
	var shout bool = false
	parser := lexopt.ParserFromEnv()
	for {
		arg, ok, err := parser.Next()
		if err != nil {
			return args{}, err
		}
		if !ok {
			break
		}
		if (arg == Short{'n'}) || (arg == Long{"number"}) {
			numberText, err := parser.Value()
			if err != nil {
				return args{}, err
			}
			number64, err2 := strconv.ParseUint(numberText, 10, 32)
			if err2 != nil {
				return args{}, err2
			}
			number = uint32(number64)
		} else if (arg == Long{"shout"}) {
			shout = true
		} else if val, ok := arg.(Value); thing == nil && ok {
			v := val.A
			thing = &v
		} else if (arg == Long{"help"}) {
			fmt.Println("Usage: hello [-n|--number=NUM] [--shout] THING")
			os.Exit(0)
		} else {
			return args{}, arg.Unexpected()
		}
	}

	if thing == nil {
		return args{}, errors.New("missing argument THING")
	}
	return args{
		thing:  *thing,
		number: number,
		shout:  shout,
	}, nil
}

func main() {
	os.Args = []string{"hello", "--number=3", "--shout", "Alan Turing"}

	log.SetFlags(0)

	args, err := parseArgs()
	if err != nil {
		log.Fatal(err)
	}
	message := fmt.Sprintf("Hello %s!", args.thing)
	if args.shout {
		message = strings.ToUpper(message)
	}
	for i := uint32(0); i < args.number; i++ {
		fmt.Println(message)
	}

}
Output:

HELLO ALAN TURING!
Example (Nonstandard)
/*
Some programs accept options with an unusual syntax. For example, tail
accepts -13 as an alias for -n 13.

This program shows how to use parser.TryRawArgs() to handle them
manually.

(Note: actual tail implementations handle it slightly differently! This
is just an example.)
*/
package main

import (
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/jcbhmr/go-lexopt"
	. "github.com/jcbhmr/go-lexopt/prelude"
)

func parseDashnum(parser *lexopt.Parser) (uint64, bool) {
	raw, ok := parser.TryRawArgs()
	if !ok {
		return 0, false
	}
	arg, ok := raw.Peek()
	if !ok {
		return 0, false
	}
	num, err := strconv.ParseUint(strings.TrimPrefix(arg, "-"), 10, 64)
	if err != nil {
		return 0, false
	}
	raw.Next() // Consume the argument we just parsed
	return num, true
}

func main() {
	os.Args = []string{"nonstandard", "-13", "--number=42", "--follow", "file.txt"}

	log.SetFlags(0)

	parser := lexopt.ParserFromEnv()
	for {
		if num, ok := parseDashnum(parser); ok {
			log.Printf("Got number %v", num)
		} else {
			arg, ok, err := parser.Next()
			if err != nil {
				log.Fatal(err)
			}
			if ok {
				argShort, argShortOk := arg.(Short)
				argLong, argLongOk := arg.(Long)
				if (argShortOk && argShort.A == 'f') || (argLongOk && argLong.A == "follow") {
					log.Println("Got --follow")
				} else if (argShortOk && argShort.A == 'n') || (argLongOk && argLong.A == "number") {
					numText, err := parser.Value()
					if err != nil {
						log.Fatal(err)
					}
					num, err2 := strconv.ParseUint(numText, 10, 64)
					if err2 != nil {
						log.Fatal(err2)
					}
					log.Printf("Got number %v", num)
				} else if argV, ok := arg.(Value); ok {
					log.Printf("Got file %v", argV.A)
				}
			} else {
				break
			}
		}
	}
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Arg

type Arg interface {
	Unexpected() Error
	// contains filtered or unexported methods
}

type ArgLong

type ArgLong struct {
	A string
}

func (ArgLong) Unexpected

func (a ArgLong) Unexpected() Error

type ArgShort

type ArgShort struct {
	A rune
}

func (ArgShort) Unexpected

func (a ArgShort) Unexpected() Error

type ArgValue

type ArgValue struct {
	A string
}

func (ArgValue) Unexpected

func (a ArgValue) Unexpected() Error

type Error

type Error interface {
	fmt.Stringer
	fmt.GoStringer
	error
	Unwrap() error
	// contains filtered or unexported methods
}

type ErrorCustom

type ErrorCustom struct {
	A error
}

func (*ErrorCustom) Error

func (e *ErrorCustom) Error() string

func (*ErrorCustom) GoString

func (e *ErrorCustom) GoString() string

func (*ErrorCustom) String

func (e *ErrorCustom) String() string

func (*ErrorCustom) Unwrap

func (e *ErrorCustom) Unwrap() error

type ErrorMissingValue

type ErrorMissingValue struct {
	Option *string
}

func (*ErrorMissingValue) Error

func (e *ErrorMissingValue) Error() string

func (*ErrorMissingValue) GoString

func (e *ErrorMissingValue) GoString() string

func (*ErrorMissingValue) String

func (e *ErrorMissingValue) String() string

func (*ErrorMissingValue) Unwrap

func (e *ErrorMissingValue) Unwrap() error

type ErrorNonUnicodeValue

type ErrorNonUnicodeValue struct {
	A string
}

func (*ErrorNonUnicodeValue) Error

func (e *ErrorNonUnicodeValue) Error() string

func (*ErrorNonUnicodeValue) GoString

func (e *ErrorNonUnicodeValue) GoString() string

func (*ErrorNonUnicodeValue) String

func (e *ErrorNonUnicodeValue) String() string

func (*ErrorNonUnicodeValue) Unwrap

func (e *ErrorNonUnicodeValue) Unwrap() error

type ErrorParsingFailed

type ErrorParsingFailed struct {
	Value  string
	Error2 error
}

func (*ErrorParsingFailed) Error

func (e *ErrorParsingFailed) Error() string

func (*ErrorParsingFailed) GoString

func (e *ErrorParsingFailed) GoString() string

func (*ErrorParsingFailed) String

func (e *ErrorParsingFailed) String() string

func (*ErrorParsingFailed) Unwrap

func (e *ErrorParsingFailed) Unwrap() error

type ErrorUnexpectedArgument

type ErrorUnexpectedArgument struct {
	A string
}

func (*ErrorUnexpectedArgument) Error

func (e *ErrorUnexpectedArgument) Error() string

func (*ErrorUnexpectedArgument) GoString

func (e *ErrorUnexpectedArgument) GoString() string

func (*ErrorUnexpectedArgument) String

func (e *ErrorUnexpectedArgument) String() string

func (*ErrorUnexpectedArgument) Unwrap

func (e *ErrorUnexpectedArgument) Unwrap() error

type ErrorUnexpectedOption

type ErrorUnexpectedOption struct {
	A string
}

func (*ErrorUnexpectedOption) Error

func (e *ErrorUnexpectedOption) Error() string

func (*ErrorUnexpectedOption) GoString

func (e *ErrorUnexpectedOption) GoString() string

func (*ErrorUnexpectedOption) String

func (e *ErrorUnexpectedOption) String() string

func (*ErrorUnexpectedOption) Unwrap

func (e *ErrorUnexpectedOption) Unwrap() error

type ErrorUnexpectedValue

type ErrorUnexpectedValue struct {
	Option string
	Value  string
}

func (*ErrorUnexpectedValue) Error

func (e *ErrorUnexpectedValue) Error() string

func (*ErrorUnexpectedValue) GoString

func (e *ErrorUnexpectedValue) GoString() string

func (*ErrorUnexpectedValue) String

func (e *ErrorUnexpectedValue) String() string

func (*ErrorUnexpectedValue) Unwrap

func (e *ErrorUnexpectedValue) Unwrap() error

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

A parser for command line arguments.

func ParserFromArgs

func ParserFromArgs(args iter.Seq[string]) *Parser

Create a parser from an iterator that does **not** include a binary name.

The iterator is consumed immediately.

.BinName() will return (T, false). Consider using ParserFromIter() instead.

func ParserFromEnv

func ParserFromEnv() *Parser

Create a parser from the environment using os.Args.

This is the usual way to create a Parser.

func ParserFromIter

func ParserFromIter(args iter.Seq[string]) *Parser

Create a parser from an iterator. This is useful for testing among other things.

The first item from the iterator **must** be the binary name, as from os.Args.

The iterator is consumed immediately.

Example

args := []string{"myapp", "-n", "10", "./foo.bar"}
parser := lexopt.ParserFromIter(slices.Values(args))

func (*Parser) BinName

func (p *Parser) BinName() (string, bool)

The name of the command, as in the zeroth argument of the process.

This is intended for use in messages. If the name is not valid unicode it will be sanitized with replacement characters as by strings.ToValidUTF8.

To get the current executable, use os.Executable.

Example

parser := lexopt.ParserFromEnv()
binName, ok := parser.BinName()
if !ok {
    binName = "myapp"
}
fmt.Printf("%v: Some message", binName)

func (*Parser) Next

func (p *Parser) Next() (Arg, bool, Error)

Get the next option or positional argument.

A return value of (nil, false, nil) means there are no more arguments.

Errors

ErrorUnexpectedValue is returned if the last option had a value that hasn't been consumed, as in --option=value or -o=value.

It's possible to continue parsing after this error (but this is rarely useful).

func (*Parser) OptionalValue

func (p *Parser) OptionalValue() (string, bool)

Get a value only if it's concatenated to an option, as in -ovalue or --option=value or -o=value, but not -o value or --option value.

func (*Parser) RawArgs

func (p *Parser) RawArgs() (*RawArgs, Error)

Take raw arguments from the original command line.

This returns an iterator of strings. Any arguments that are not consumed are kept, so you can continue parsing after you're done with the iterator.

TODO: To inspect an argument without consuming it, use rawArgs.Peek() or rawArgs.AsSlice().

Errors

Returns an ErrorUnexpectedValue if the last option had a left-over argument, as in --option=value, -ovalue, or if it was midway through an option chan, as in -abc. The iterator only yields whole arguments. To avoid this, use TryRawArgs().

After this error the method is guarenteed to succeed, as it consumes the rest of the argument.

# Example As soon as a free-standing argument is found, consume the other arguments as-is, and build them into a command.

parser := lexopt.ParserFromArgs([]string{"-x", "echo", "-n", "'Hello world'"})
for {
    arg, ok, err := parser.Next()
    if err != nil {
        return err
    }
    if !ok {
        break
    }
    if prog, ok := arg.(Value); ok {
        rawArgsIter, err := parser.RawArgs()
        if err != nil {
            return err
        }
        args := slices.Collect(rawArgsIter)
        command := exec.Command(prog, args...)
    }
}

func (*Parser) TryRawArgs

func (p *Parser) TryRawArgs() (*RawArgs, bool)

Take raw arguments from the original command line, *if* the current argument has finished processing.

Unlike .RawArgs() this does not consume any value in case of a left-over argument. This makes it safe to call at any time.

It returns (nil, false) exactly when .OptionalValue() would return (T, true).

Note: If no arguments are left then it returns an empty iterator (not (nil, false)).

# Example Process arguments of the form -123 as numbers. For a complete runnable version of this example, see example_nonstandard_test.go.

parser := lexopt.ParserFromArgs([]string{"-13"})
parseDashnum := func (parser *lexopt.Parser) (uint64, bool) {
    raw, ok := parser.TryRawArgs()
    if !ok {
        return 0, false
    }
    arg, ok := raw.Peek()
    if !ok {
        return 0, false
    }
    num, err := strconv.ParseUint(strings.TrimLeft(arg, "-"), 10, 64)
    if err != nil {
        return 0, false
    }
    raw.Next()
    return num, true
}

for {
    if num, ok := parseDashnum(parser); ok {
        fmt.Printf("Got number %v\n", num)
    } else {
        arg, ok, err := parser.Next()
        if err != nil {
            log.Fatal(err)
        }
        if ok {
            // ...
        } else {
            break
        }
    }
}

func (*Parser) Value

func (p *Parser) Value() (string, Error)

Get a value for an option.

This function should normally be called right after seeing an option that expects a value, with positional arguments being collected using parser.Next().

A value is collected even if it looks like an option (i.e., starts with -).

Errors

An ErrorMissingValue is returned if the end of the command line is reached.

func (*Parser) Values

func (p *Parser) Values() (*ValuesIter, Error)

Gather multiple values for an option.

This is used for options that take multiple arguments, such as a --command flag that's invoked as app --command echo 'Hello world'.

It will gather arguments until another option is found, or -- is found, or the end of the command line is reached. This differs from .Value(), which takes a value even if it looks like an option.

An equals sign (=) will limit this to a single value. That means -a=b c and --opt=b c will only yield "b" while -a b c and --opt b c will yield "b" and "c".

# Errors If not at least one value is found then ErrorMissingValue is returned.

Example

parser := lexopt.ParserFromArgs([]string{"a", "b", "-x", "one", "two", "three", "four"})
argumentsIter, err := parser.Values()
if err != nil {
    return err
}
arguments := slices.Collect(argumentsIter)
require.Equal(t, []string{"a", "b"}, arguments)
parser.Next()
valuesIter, err := parser.Values()
if err != nil {
    return err
}
next, _ := iter.Pull(valuesIter)
atMostThreeFiles := []string{}
for i := 0; i < 3; i++ {
    value, ok := next()
    if !ok {
        break
    }
    atMostThreeFiles = append(atMostThreeFiles, value)
}
rawArgsIter, err := parser.RawArgs()
if err != nil {
    return err
}
rawArgs := slices.Collect(rawArgsIter)
require.Equal(t, rawArgs, []string{"four"})
valuesIter, err = parser.Values()
if err != nil {
    return err
}
for v := range valuesIter {
    // ...
}

type RawArgs

type RawArgs struct {
	// contains filtered or unexported fields
}

func (*RawArgs) All

func (r *RawArgs) All(yield func(string) bool)

func (*RawArgs) AsSlice

func (r *RawArgs) AsSlice() []string

func (*RawArgs) Next

func (r *RawArgs) Next() (string, bool)

func (*RawArgs) NextIf

func (r *RawArgs) NextIf(func_ func(string) bool) (string, bool)

func (*RawArgs) Peek

func (r *RawArgs) Peek() (string, bool)

type ValuesIter

type ValuesIter struct {
	// contains filtered or unexported fields
}

func (*ValuesIter) All

func (v *ValuesIter) All(yield func(string) bool)

func (*ValuesIter) Next

func (v *ValuesIter) Next() (string, bool)

Directories

Path Synopsis
A small prelude for processing arguments.
A small prelude for processing arguments.

Jump to

Keyboard shortcuts

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