package flags
import "io"
// IOInjector is an interface for dependency injection of input and output.
type IOInjector interface {
Inject(in io.Reader, out io.Writer, err io.Writer)
}
// IOInject is meant to be embedded in Commands that which to have dependency
// injection for standard input, standard output, and standard error.
type IOInject struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
}
// Inject fulfills the IOInjector interface.
func (i *IOInject) Inject(in io.Reader, out io.Writer, err io.Writer) {
i.Stdin = in
i.Stdout = out
i.Stderr = err
}
package compile
import (
"reflect"
"strings"
"unicode/utf8"
)
type parameters struct {
optional []OptionalParam
required []RequiredParam
}
func Compile(cmd interface{}) ([]OptionalParam, []RequiredParam) {
v := reflect.Indirect(reflect.ValueOf(cmd))
if v.Kind() != reflect.Struct {
panic("program bug: invalid use of flags: command is not a structure")
}
if !v.CanAddr() {
panic("program bug: invalid use of flags: command is not addressable")
}
parameters := compile(v, parameters{})
checkDuplicateNames(parameters.optional)
checkRequiredParamSlices(parameters.required)
return parameters.optional, parameters.required
}
func compile(value reflect.Value, params parameters) parameters {
t := value.Type()
for i := 0; i < t.NumField(); i++ {
if tag, ok := reflect.StructTag(t.Field(i).Tag).Lookup("flags"); ok {
params = compileField(value.Field(i), t.Field(i), tag, params)
continue
} else if value := reflect.Indirect(value.Field(i)); value.Kind() == reflect.Struct {
params = compile(value, params)
}
}
return params
}
func compileField(value reflect.Value, field reflect.StructField, tag string, params parameters) parameters {
shortName, longName := "", ""
if !value.CanSet() {
panic("program bug: incorrect flags specification: field '" + field.Name + "' is unexported")
}
if tag != "" {
for _, spec := range strings.Split(tag, ",") {
if strings.HasPrefix(spec, "--") {
if longName != "" {
panic("program bug: incorrect flags specification: long name specified twice")
}
if len(spec) <= 2 {
panic("program bug: incorrect flags specification: long name is too short")
}
longName = spec
} else if strings.HasPrefix(spec, "-") {
if shortName != "" {
panic("program bug: incorrect flags specification: short name specified twice")
}
if utf8.RuneCountInString(spec) != 2 {
panic("program bug: incorrect flags specification: short name is too long")
}
shortName = spec
} else {
panic("program bug: unrecognized flags struct tag: " + spec)
}
}
}
if shortName != "" || longName != "" {
params.optional = append(params.optional, OptionalParam{
Name: strings.ToLower(field.Name),
Value: value,
Short: shortName,
Long: longName,
Env: reflect.StructTag(field.Tag).Get("env"),
IsBoolFlag: value.Kind() == reflect.Bool,
Description: reflect.StructTag(field.Tag).Get("description"),
})
} else {
params.required = append(params.required, RequiredParam{
Name: strings.ToLower(field.Name),
Value: value,
Description: reflect.StructTag(field.Tag).Get("description"),
})
}
return params
}
package compile
import (
"reflect"
)
type OptionalParam struct {
Name string
Value reflect.Value
Short string
Long string
Env string
IsBoolFlag bool
Description string
}
func checkDuplicateNames(params []OptionalParam) {
names := map[string]struct{}{}
for i := range params {
if params[i].Short != "" {
if _, ok := names[params[i].Short]; ok {
panic("program bug: invalid use of flags: name is duplicated: " + params[i].Short)
}
names[params[i].Short] = struct{}{}
}
if params[i].Long != "" {
if _, ok := names[params[i].Long]; ok {
panic("program bug: invalid use of flags: name is duplicated: " + params[i].Long)
}
names[params[i].Long] = struct{}{}
}
}
}
func FindOptionalParam(params []OptionalParam, key string) int {
for i := range params {
if params[i].Short == key || params[i].Long == key {
return i
}
}
return -1
}
func UsesEnvironmentVariables(params []OptionalParam) bool {
for _, v := range params {
if v.Env != "" {
return true
}
}
return false
}
package compile
import "reflect"
type RequiredParam struct {
Name string
Value reflect.Value
Description string
}
func checkRequiredParamSlices(params []RequiredParam) {
if len(params) < 2 {
return
}
for _, v := range params[:len(params)-1] {
if v.Value.Kind() == reflect.Slice {
panic("program bug: invalid use of flags: only last required parameter can be a slice")
}
}
}
package completion
import (
"fmt"
"strings"
)
// AddCompletion prints the possible completion if it matches the prefix.
func AddCompletion(value, description string, prefix string) {
if strings.HasPrefix(value, prefix) {
fmt.Println(value)
}
}
package completion
import (
"os"
"strconv"
"strings"
"git.sr.ht/~rj/flags/internal/compile"
)
// Env contains the information passed by the shell in environment variables.
type Env struct {
Line string
Point int
Type int
Key int
}
func (env *Env) Init() bool {
env.Line = os.Getenv("COMP_LINE")
point, ok := getenvInt("COMP_POINT")
if !ok {
return false
}
env.Point = point
typ, ok := getenvInt("COMP_TYPE")
if !ok {
return false
}
env.Type = typ
key, ok := getenvInt("COMP_KEY")
if !ok {
return false
}
env.Key = key
return true
}
func (env *Env) LeadingWords() []string {
words := strings.Fields(env.Line[:env.Point])
if env.Line[env.Point-1] == ' ' {
words = append(words, "")
}
return words
}
func (env *Env) WriteCompletions(command interface{}, words []string) {
optparams, reqparams := compile.Compile(command)
assignField, allowOpt, reqparamspos := parseCompletion(words)
word := words[len(words)-1]
// Current state is to assign to the previous field.
if assignField {
prev := words[len(words)-2]
ndx := compile.FindOptionalParam(optparams, prev)
if ndx < 0 {
return
}
if !optparams[ndx].IsBoolFlag {
AddCompletion(optparams[ndx].Value.String(), optparams[ndx].Description, word)
return
}
}
// Assign within the field
if ndx := strings.IndexByte(word, '='); allowOpt && strings.HasPrefix(word, "-") && ndx >= 0 {
opt := compile.FindOptionalParam(optparams, word[:ndx])
if opt >= 0 {
AddCompletion(optparams[opt].Value.String(), optparams[opt].Description, word[ndx+1:])
}
return
}
if allowOpt {
for i := range optparams {
AddCompletion(optparams[i].Short, optparams[i].Description, word)
AddCompletion(optparams[i].Long, optparams[i].Description, word)
}
}
if reqparamspos < len(reqparams) {
value := reqparams[reqparamspos].Value.String()
if len(value) > 0 {
AddCompletion(value, reqparams[reqparamspos].Description, words[len(words)-1])
}
}
}
func getenvInt(key string) (int, bool) {
value := os.Getenv(key)
i, err := strconv.Atoi(value)
return i, err == nil
}
package completion
import (
"strings"
"unicode/utf8"
)
func parseCompletion(args []string) (bool, bool, int) {
// This parsing algorithm should match the top-level parsing algorithm to
// ensure that completions match. However, reproduced here since we don't
// need to update any parameters, and are more interested in the parse
// state.
assignField := false
reqparamspos := 0
allowoptparam := true
for i := 0; i < len(args)-1; i++ {
if assignField {
assignField = false
} else if strings.HasPrefix(args[i], "--") && allowoptparam {
// Handle long name arguments.
if args[i] == "--" {
// User has indicated that all remaining arguments are to be
// treated as required params.
allowoptparam = false
} else if pos := strings.Index(args[i], "="); pos > 0 {
// Do nothing.
} else {
assignField = true
}
} else if strings.HasPrefix(args[i], "-") && allowoptparam {
// Handle short name arguments
if pos := strings.Index(args[i], "="); pos > 0 {
// Do nothing.
} else if utf8.RuneCountInString(args[i]) > 2 {
// Short names are never longer then the dash and their rune.
// The user has either combined multiple boolean flags into a
// single argument, or the value is also included.
// Do nothing.
} else {
assignField = true
}
} else {
reqparamspos++
}
}
return assignField, allowoptparam, reqparamspos
}
package completion
import (
"fmt"
"io"
)
func BashScript(w io.Writer, bin, name string) error {
// Bash completion links back into the executable, using the special
// command-line flag. If completion fails, fall back on defaults provided by
// the shell.
_, err := fmt.Fprintf(w, "complete -C \"%s --completion \" -o bashdefault -o default %s", bin, name)
return err
}
func ZshScript(w io.Writer, bin, name string) error {
_, err := fmt.Fprintf(w, "complete -o nospace -C \"%s --completion \" %s", bin, name)
return err
}
func FishScript(w io.Writer, bin, name string) error {
const script = `function __complete_%[2]s
set -lx COMP_LINE (commandline -p)
test -z (commandline -ct)
and set COMP_LINE "$COMP_LINE "
set -lx COMP_POINT (commandline --cursor -p)
set -lx COMP_TYPE 9
set -lx COMP_KEY 9
%[1]s --completion
end
complete -f -c %[2]s -a "(__complete_%[2]s)"
`
_, err := fmt.Fprintf(w, script, bin, name)
return err
}
package parse
import (
"reflect"
"strconv"
)
// Value is the interface to the dynamic value stored in a flag.
type Value interface {
String() string
Set(string) error
}
// AssignValue parses the string ans assigns the value to the field. The parse
// will be selected based on the the field's type. The field must either be a
// non-nil pointer, or an addressable and settable value.
func AssignValue(field reflect.Value, text string) error {
// Automatically dereference pointers when assign values.
field = reflect.Indirect(field)
// If the type supports the Value interface, defer to the supplied
// implementation.
if setter, ok := field.Addr().Interface().(Value); ok {
if err := setter.Set(text); err != nil {
return &ConversionError{
text: text,
err: err,
}
}
return nil
}
switch field.Kind() {
case reflect.Bool:
if b, err := strconv.ParseBool(text); err == nil {
field.SetBool(b)
} else {
return &ConversionError{
text: text,
err: err,
}
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if i, err := strconv.ParseInt(text, 0, int(field.Type().Size()*8)); err == nil {
field.SetInt(i)
} else {
return &ConversionError{
text: text,
err: err,
}
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if i, err := strconv.ParseUint(text, 0, int(field.Type().Size()*8)); err == nil {
field.SetUint(i)
} else {
return &ConversionError{
text: text,
err: err,
}
}
case reflect.Float32, reflect.Float64:
if i, err := strconv.ParseFloat(text, int(field.Type().Size()*8)); err == nil {
field.SetFloat(i)
} else {
return &ConversionError{
text: text,
err: err,
}
}
case reflect.Complex64, reflect.Complex128:
if i, err := strconv.ParseComplex(text, int(field.Type().Size()*8)); err == nil {
field.SetComplex(i)
} else {
return &ConversionError{
text: text,
err: err,
}
}
case reflect.String:
field.SetString(text)
case reflect.Slice:
elem := reflect.New(field.Type().Elem())
if err := AssignValue(elem, text); err != nil {
return err
}
field.Set(reflect.Append(field, elem.Elem()))
default:
panic("program bug: field type not supported by flags")
}
return nil
}
package parse
type ConversionError struct {
text string
err error
}
func (e *ConversionError) Error() string {
return "could not convert '" + e.text + "': " + e.err.Error()
}
func (e *ConversionError) Unwrap() error {
return e.err
}
package table
import (
"fmt"
"io"
"os"
)
type Table struct {
columns int
rows [][]string
}
func NewTable(columns int) Table {
return Table{
columns: columns,
}
}
func (t *Table) AddRow(cells ...string) {
if len(cells) != t.columns {
panic("programming bug: incorrect number of cells in call to AddRow")
}
t.rows = append(t.rows, cells)
}
func (t *Table) Print() error {
return t.Fprint(os.Stdout)
}
func (t *Table) Fprint(w io.Writer) error {
widths := make([]int, t.columns)
for i := range t.rows {
for j, v := range t.rows[i] {
if newLen := len(v); newLen > widths[j] {
widths[j] = newLen
}
}
}
for _, v := range t.rows {
for j, v := range v {
if j < t.columns-1 {
fmt.Fprintf(w, "%-*s ", roundUp(widths[j]), v)
} else {
fmt.Fprintf(w, "%s\n", v)
}
}
}
return nil
}
func roundUp(cols int) int {
// Integer math to round to nearest 4*n+2.
return ((cols+1)/4+1)*4 - 2
}
package flags
import (
"os"
"path/filepath"
)
// Option specifies additionial metadata to help with printing usage
// information, or with parsing command-line arguments.
type Option func(*config)
type config struct {
name string
description string
version string
args []string
testing bool
exec string
}
func (c *config) initCommand(command interface{}) {
if describer, ok := command.(Describer); ok {
c.description = describer.Description()
}
}
func (c *config) initOptions(options []Option) {
c.name = filepath.Base(os.Args[0])
c.args = os.Args
for _, v := range options {
v(c)
}
}
func (c *config) executable() (string, error) {
if c.exec != "" {
return c.exec, nil
}
return os.Executable()
}
func (c *config) exit(code int) {
if !c.testing {
os.Exit(code)
}
}
// Name sets the name of the program, for use with printing usage.
func Name(name string) Option {
return func(c *config) {
c.name = name
}
}
// Description sets a description of the program, to be printed after basic
// usage information. The description can span multiple paragraphs (separated
// by a single newline).
func Description(desc string) Option {
return func(c *config) {
c.description = desc
}
}
// Version sets a version string for the program. Setting a version will enable
// additional standard options so that the user can print version information.
func Version(version string) Option {
return func(c *config) {
c.version = version
}
}
// Args overrides the command-line arguments used for parsing. The arguments
// should start with the program name (similar to os.Args).
func Args(args ...string) Option {
return func(c *config) {
c.args = args
}
}
// Executable overrides the return for calls to os.Executable.
func Executable(executable string) Option {
return func(c *config) {
c.exec = executable
}
}
// Testing sets a flag to indicate that calls to os.Exit should be skipped.
func Testing(testing bool) Option {
return func(c *config) {
c.testing = testing
}
}
package flags
import (
"errors"
"os"
"reflect"
"strings"
"unicode/utf8"
"git.sr.ht/~rj/flags/internal/compile"
"git.sr.ht/~rj/flags/internal/parse"
)
// Parse parses command-line flags, and stores the result in the struct. The
// first command should be a pointer to a struct whose fields have tags to
// specify the argument names.
//
// Parse expects to see only the command-line arguments. For example, one could
// use os.Args[1:].
func Parse(command interface{}, args []string) error {
optparams, reqparams := compile.Compile(command)
for _, v := range optparams {
if v.Env != "" {
if value, ok := os.LookupEnv(v.Env); ok {
err := parse.AssignValue(v.Value, value)
if err != nil {
return err
}
}
}
}
reqparamspos := 0
allowoptparam := true
for i := 0; i < len(args); i++ {
if strings.HasPrefix(args[i], "--") && allowoptparam {
// Handle long name arguments.
if args[i] == "--" {
// User has indicated that all remaining arguments are to be
// treated as required params.
allowoptparam = false
} else if pos := strings.Index(args[i], "="); pos > 0 {
// User has separated the name and the argument by an equal
// sign. Split the argument at the equal sign to extract the
// key and the value.
err := assignOptField(optparams, args[i][:pos], args[i][pos+1:])
if err != nil {
return err
}
} else {
// User has separated the name and the argument by space. The
// value for the parameter should be in the following argument.
adjust, err := assignOptField2(optparams, args[i], args[i+1:])
if err != nil {
return err
}
// May need to change iterator if we consumed an extra argument.
i += adjust
}
} else if strings.HasPrefix(args[i], "-") && allowoptparam {
// Handle short name arguments
if pos := strings.Index(args[i], "="); pos > 0 {
// User has separated the name and the argument by an equal
// sign. Split the argument at the equal sign to extract the
// key and the value.
err := assignOptField(optparams, args[i][:pos], args[i][pos+1:])
if err != nil {
return err
}
} else if utf8.RuneCountInString(args[i]) > 2 {
// Short names are never longer then the dash and their rune.
// The user has either combined multiple boolean flags into a
// single argument, or the value is also included.
if index := compile.FindOptionalParam(optparams, args[i][:2]); index >= 0 && optparams[index].IsBoolFlag {
err := parse.AssignValue(optparams[index].Value, "true")
if err != nil {
return err
}
for _, v := range args[i][2:] {
err := assignOptField(optparams, "-"+string(v), "true")
if err != nil {
return err
}
}
} else {
// Combined key and value.
err := assignOptField(optparams, args[i][:2], args[i][2:])
if err != nil {
return err
}
}
} else {
// User has separated the name and the argument by space. The
// value for the parameter should be in the following argument.
adjust, err := assignOptField2(optparams, args[i], args[i+1:])
if err != nil {
return err
}
i += adjust
}
} else {
if len(reqparams) == 0 {
return errors.New("no positional arguments are accepted")
}
if reqparamspos >= len(reqparams) {
if reqparams[len(reqparams)-1].Value.Kind() == reflect.Slice {
reqparamspos = len(reqparams) - 1
} else {
return errors.New("too many positional arguments")
}
}
err := parse.AssignValue(reqparams[reqparamspos].Value, args[i])
if err != nil {
return err
}
reqparamspos++
}
}
if reqparamspos < len(reqparams) {
return errors.New("too few positional arguments")
}
return nil
}
func assignOptField(params []compile.OptionalParam, key, value string) error {
index := compile.FindOptionalParam(params, key)
if index < 0 {
return errors.New("argument not recognized: " + key)
}
return parse.AssignValue(params[index].Value, value)
}
func assignOptField2(params []compile.OptionalParam, key string, rest []string) (int, error) {
index := compile.FindOptionalParam(params, key)
if index < 0 {
return 0, errors.New("argument not recognized: " + key)
}
if params[index].IsBoolFlag {
return 0, parse.AssignValue(params[index].Value, "true")
}
if len(rest) == 0 {
return 0, errors.New("argument incomplete: " + key)
}
return 1, parse.AssignValue(params[index].Value, rest[0])
}
package flags
import (
"fmt"
"io"
"reflect"
"sort"
"strings"
"git.sr.ht/~rj/flags/internal/compile"
"git.sr.ht/~rj/flags/internal/table"
"git.sr.ht/~rj/flags/internal/term"
"git.sr.ht/~rj/sgr"
"git.sr.ht/~rj/sgr/wcwidth"
)
func printUsage(w io.Writer, commands map[string]Command, cfg *config) {
f := sgr.NewFormatterForWriter(w)
// Headline usage
fmt.Fprint(w, f.Bold("Usage: "), cfg.name,
" <", f.Italic("command"), "> [", f.Italic("options"), "]... <", f.Italic("arguments"), ">...\n")
printStandardOptions(w, cfg)
// Description, if provided
printDescription(w, cfg.description)
// Commands
printSection(w, f, "Available commands")
table := table.NewTable(3)
for _, name := range sortCommandNames(commands) {
description := ""
if describer, ok := commands[name].(Describer); ok {
description = describer.Description()
}
table.AddRow("", name, description)
}
table.Fprint(w)
}
func sortCommandNames(commands map[string]Command) []string {
names := make([]string, 0, len(commands))
for key := range commands {
names = append(names, key)
}
sort.Strings(names)
return names
}
// PrintSingleUsage generates and prints the usage for a command to the writer.
// It prints the same message as would happen in a call to RunSingle, if the
// arguments requested help.
func PrintSingleUsage(w io.Writer, command interface{}, options ...Option) {
cfg := config{}
cfg.initCommand(command)
cfg.initOptions(options)
printSingleUsage(w, command, &cfg)
}
func printSingleUsage(w io.Writer, command interface{}, cfg *config) {
f := sgr.NewFormatterForWriter(w)
optparams, reqparams := compile.Compile(command)
// Headline usage
fmt.Fprintf(w, "%s %s", f.Bold("Usage:"), cfg.name)
printOptionalParameters(w, f, optparams)
printRequiredParameters(w, f, reqparams)
fmt.Fprint(w, "\n")
printStandardOptions(w, cfg)
// Sections
printDescription(w, cfg.description)
printRequiredSection(w, f, reqparams)
printOptionalSection(w, f, optparams)
printEnvironmentSection(w, f, optparams)
}
func printCommandUsage(w io.Writer, command Command, cfg *config) {
f := sgr.NewFormatterForWriter(w)
optparams, reqparams := compile.Compile(command)
// Headline usage
fmt.Fprint(w, "Usage: ", cfg.name, " ", cfg.args[1])
printOptionalParameters(w, f, optparams)
printRequiredParameters(w, f, reqparams)
fmt.Fprint(w, "\n")
fmt.Fprint(w, " ", cfg.name, " ", cfg.args[1], " (--help | -h)\n")
// Sections
printDescription(w, cfg.description)
printRequiredSection(w, f, reqparams)
printOptionalSection(w, f, optparams)
printEnvironmentSection(w, f, optparams)
}
func printOptionalParameters(w io.Writer, f *sgr.Formatter, params []compile.OptionalParam) {
if len(params) > 0 {
fmt.Fprint(w, " [", f.Italic("options"), "]...")
}
}
func printRequiredParameters(w io.Writer, f *sgr.Formatter, params []compile.RequiredParam) {
for _, v := range params {
fmt.Fprint(w, " <", f.Italic(v.Name), ">")
if v.Value.Kind() == reflect.Slice {
fmt.Fprint(w, "...")
}
}
}
func printStandardOptions(w io.Writer, cfg *config) {
fmt.Fprintf(w, " %s (--help | -h)\n", cfg.name)
if cfg.version != "" {
fmt.Fprintf(w, " %s (--version | -v)\n", cfg.name)
}
}
func printDescription(w io.Writer, text string) {
if text == "" {
return
}
width, _ := term.MustGetSize(w)
for p, rest, _ := cut(text, "\n"); p != ""; p, rest, _ = cut(rest, "\n") {
printParagraph(w, width, 0, p)
}
}
func cut(s, sep string) (before, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
func printRequiredSection(w io.Writer, f *sgr.Formatter, reqparams []compile.RequiredParam) {
if len(reqparams) == 0 {
return
}
printSection(w, f, "Required Arguments")
width, _ := term.MustGetSize(w)
for _, v := range reqparams {
// Print top line
if v.Value.Kind() == reflect.Slice {
fmt.Fprint(w, " <", f.Italic(v.Name), ">...")
} else {
fmt.Fprint(w, " <", f.Italic(v.Name), ">")
}
// Print the option's description
printParagraph(w, width, 8, v.Description)
}
}
func printOptionalSection(w io.Writer, f *sgr.Formatter, optparams []compile.OptionalParam) {
if len(optparams) == 0 {
return
}
printSection(w, f, "Optional Arguments")
width, _ := term.MustGetSize(w)
for _, v := range optparams {
description := v.Description
if value := reflect.Indirect(v.Value); !value.IsZero() {
if value.Kind() == reflect.String {
description = fmt.Sprintf("%s (default %q)", description, value.String())
} else {
description = fmt.Sprintf("%s (default %v)", description, value.Interface())
}
}
// Print top line
fmt.Fprint(w, " ")
if v.Short != "" {
if v.IsBoolFlag {
fmt.Fprintf(w, "%s", v.Short)
} else {
fmt.Fprintf(w, "%s=<%s>", v.Short, f.Italic(v.Value.Type().String()))
}
}
if v.Long != "" {
if v.Short != "" {
fmt.Fprintf(w, ", ")
}
if v.IsBoolFlag {
fmt.Fprintf(w, "%s", v.Long)
} else {
fmt.Fprintf(w, "%s=<%s>", v.Long, f.Italic(v.Value.Type().String()))
}
}
// Print the option's description
if description != "" {
printParagraph(w, width, 8, description)
} else {
fmt.Fprint(w, "\n")
}
}
}
func printEnvironmentSection(w io.Writer, f *sgr.Formatter, optparams []compile.OptionalParam) {
// Environment
if !compile.UsesEnvironmentVariables(optparams) {
return
}
printSection(w, f, "Environment")
width, _ := term.MustGetSize(w)
for _, v := range optparams {
if v.Env == "" {
continue
}
// Print top line
fmt.Fprint(w, " ", v.Env)
// Print the option's description
printParagraph(w, width, 8, v.Description)
fmt.Fprint(w, " See: ")
if v.Short != "" {
fmt.Fprint(w, v.Short)
}
if v.Long != "" {
if v.Short != "" {
fmt.Fprint(w, ", ")
}
fmt.Fprint(w, v.Long)
}
fmt.Fprint(w, "\n")
}
}
func printSection(w io.Writer, f *sgr.Formatter, text string) {
fmt.Fprintf(w, "\n%s\n", f.Boldf("%s:", text))
}
func printParagraph(w io.Writer, width, leftPad int, text string) {
if width < 20 {
width = 20
}
padding := strings.Repeat(" ", leftPad)
line, _, rest := wcwidth.ParagraphLineBreak(width-leftPad, text)
fmt.Fprint(w, "\n", padding, line, "\n")
for rest != "" {
line, _, rest = wcwidth.ParagraphLineBreak(width, rest)
fmt.Fprint(w, padding, line, "\n")
}
}
package flags
import (
"errors"
"fmt"
"os"
"strings"
"git.sr.ht/~rj/flags/internal/completion"
)
// Command is a subcommand.
type Command interface {
Run() error
}
// Describer allows a command to set a description for itself, useful when print usage.
type Describer interface {
Command
Description() string
}
// Run parses the command-line to select and configure a Command. It then runs
// that command.
//
// Run will check for standard options to see if the user has requested help, or
// possibly to print version information. When a standard option is present,
// Run will handle the option and then exit the program.
//
// If there are any errors parsing the command line, or when executing the
// command, Run will abort the program.
func Run(commands map[string]Command, options ...Option) {
cfg := config{}
cfg.initOptions(options)
if len(cfg.args) <= 1 {
printUsage(os.Stderr, commands, &cfg)
cfg.exit(1)
return
}
if len(cfg.args) == 2 && (cfg.args[1] == "-h" || cfg.args[1] == "--help") {
printUsage(os.Stdout, commands, &cfg)
cfg.exit(0)
return
}
if len(cfg.args) == 2 && cfg.version != "" && (cfg.args[1] == "-v" || cfg.args[1] == "--version") {
printVersion(os.Stdout, &cfg)
cfg.exit(0)
return
}
if len(cfg.args) >= 2 && cfg.args[1] == "--completion" {
// Load the environment variables with the parameters for the completion.
comp := completion.Env{}
if !comp.Init() {
fmt.Fprintf(os.Stderr, "error: completion requested, but environment variables not set\n")
cfg.exit(1)
return
}
// Break the command line up into 'words' (i.e. individual arguments)
words := comp.LeadingWords()
// If the user is trying to complete the second word, then the options
// are the standard args and the commands.
if len(words) == 2 {
// No need to sort the commands. The shell will do that action.
for key := range commands {
completion.AddCompletion(key, "", words[1])
}
// Add the --help and --version.
addCompletionStandardArgs(&cfg, words)
cfg.exit(0)
return
}
// Stop further completions if the user has requested help or the
// version.
if len(words) > 2 && isStandardArg(&cfg, words[1]) {
cfg.exit(0)
return
}
command, ok := commands[words[1]]
if !ok {
cfg.exit(1)
return
}
if len(words) == 3 {
completion.AddCompletion("--help", "", words[2])
}
// Start the completion handling for the command.
comp.WriteCompletions(command, words[2:])
cfg.exit(0)
return
}
if len(cfg.args) == 2 && strings.HasPrefix(cfg.args[1], "--completion=") {
if err := runCompletionCommand(&cfg, cfg.args[1][13:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
}
cfg.exit(0)
return
}
cmd, ok := commands[cfg.args[1]]
if !ok {
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cfg.args[1])
cfg.exit(1)
return
}
if len(cfg.args) == 3 && (cfg.args[2] == "-h" || cfg.args[2] == "--help") {
cfg.initCommand(cmd)
printCommandUsage(os.Stdout, cmd, &cfg)
cfg.exit(0)
return
}
err := runCommand(cmd, cfg.args[2:])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
return
}
}
// RunSingle parses the command-line to configure a Command. It then runs that
// command.
//
// RunSingle will check for standard options to see if the user has requested help, or
// possibly to print version information. When a standard option is present,
// RunSingle will handle the option and then exit the program.
//
// If there are any errors parsing the command line, or when executing the
// command, Run will abort the program.
func RunSingle(command Command, options ...Option) {
cfg := config{}
cfg.initOptions(options)
if len(cfg.args) == 2 && (cfg.args[1] == "-h" || cfg.args[1] == "--help") {
printSingleUsage(os.Stdout, command, &cfg)
cfg.exit(0)
return
}
if len(cfg.args) == 2 && cfg.version != "" && (cfg.args[1] == "-v" || cfg.args[1] == "--version") {
printVersion(os.Stdout, &cfg)
cfg.exit(0)
return
}
if len(cfg.args) >= 2 && cfg.args[1] == "--completion" {
// Load the environment variables with the parameters for the completion.
comp := completion.Env{}
if !comp.Init() {
fmt.Fprintf(os.Stderr, "error: completion requested, but environment variables not set\n")
cfg.exit(1)
return
}
// Break the command line up into 'words' (i.e. individual arguments)
words := comp.LeadingWords()
// If the user is trying to complete the second word, then we need to
// add suggestions for --help and --version.
if len(words) == 2 {
addCompletionStandardArgs(&cfg, words)
}
// Conversely, if the user has already requested help, then stop further processing.
if len(words) > 2 && isStandardArg(&cfg, words[1]) {
cfg.exit(0)
return
}
comp.WriteCompletions(command, words[1:])
cfg.exit(0)
return
}
if len(cfg.args) >= 2 && strings.HasPrefix(cfg.args[1], "--completion=") {
if err := runCompletionCommand(&cfg, cfg.args[1][13:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
}
cfg.exit(0)
return
}
err := runCommand(command, cfg.args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
return
}
}
func runCommand(command Command, args []string) error {
if err := Parse(command, args); err != nil {
return err
}
if injector, ok := command.(IOInjector); ok {
injector.Inject(os.Stdin, os.Stdout, os.Stderr)
}
return command.Run()
}
func runCompletionCommand(cfg *config, command string) error {
bin, err := cfg.executable()
if err != nil {
return err
}
switch command {
case "bash":
return completion.BashScript(os.Stdout, bin, cfg.name)
case "fish":
return completion.FishScript(os.Stdout, bin, cfg.name)
case "share":
file, err := os.OpenFile("/usr/share/bash-completion/completions/"+cfg.name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
return completion.BashScript(file, bin, cfg.name)
case "zsh":
return completion.ZshScript(os.Stdout, bin, cfg.name)
default:
return errors.New("unrecognized completion request: " + command)
}
}
func addCompletionStandardArgs(cfg *config, words []string) {
completion.AddCompletion("--help", "", words[1])
if cfg.version != "" {
completion.AddCompletion("--version", "", words[1])
}
}
func isStandardArg(cfg *config, s string) bool {
if s == "--help" || s == "-h" {
return true
}
if cfg.version != "" && (s == "--version" || s == "-v") {
return true
}
return false
}
package flags
import (
"fmt"
"io"
)
func printVersion(w io.Writer, cfg *config) {
fmt.Fprintf(w, "%s (%s)\n", cfg.name, cfg.version)
}