package main
import (
"git.sr.ht/~rj/flags"
)
const (
description = "Command makeman ingests the JSON output from a program built with git.sr.ht/~rj/flags, and generates man pages." //nolint:lll
)
var (
version = "development"
)
func main() {
flags.RunSingle(
&MakeMan{
Input: "-", // Default input is stdin.
Output: ".", // Default output files are in current directory.
Format: FormatTroff,
},
flags.Description(description),
flags.Version(version),
)
}
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"git.sr.ht/~rj/flags/internal/compile"
)
type MakeMan struct {
Input string `flags:"" description:"Input file with flags JSON."`
Output string `flags:"-o,--output" description:"Output directory for man pages."`
Format Format `flags:"-f,--format" description:"Select the documentation format."`
Verbose bool `flags:"-v,--verbose" description:"Flag for verbose output."`
}
func (cmd *MakeMan) Run() error {
// Get JSON decoder.
dec, closer, err := cmd.NewInputDecoder()
if err != nil {
return err
}
defer closer()
// Ingest the JSON
data := Data{}
err = dec.Decode(&data)
if err != nil {
return err
}
// Print the main man page
if len(data.Commands) != 0 {
err = cmd.PrintMan(&data)
} else {
err = cmd.PrintSingleMan(&data)
}
if err != nil {
return err
}
// Print man pages for commands.
for i := range data.Commands {
err := cmd.PrintCommandMan(&data, &data.Commands[i])
if err != nil {
return err
}
}
return nil
}
func (cmd *MakeMan) NewInputDecoder() (*json.Decoder, func(), error) {
if cmd.Input == "-" {
return json.NewDecoder(os.Stdin), func() {}, nil
}
file, err := os.Open(cmd.Input)
if err != nil {
return nil, nil, err
}
return json.NewDecoder(file), func() { file.Close() }, nil
}
func (cmd *MakeMan) Info(a ...interface{}) {
if !cmd.Verbose {
return
}
fmt.Println(a...)
}
func (cmd *MakeMan) GetFormatter() Formatter {
switch cmd.Format {
case FormatTroff:
return (*TroffFormatter)(nil)
case FormatMarkdown:
return &MarkdownFormatter{1}
default:
panic("internal error")
}
}
func (cmd *MakeMan) PrintMan(data *Data) error {
filename := filepath.Join(cmd.Output, data.Name+cmd.Format.Ext())
cmd.Info("writing output:", filename)
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer file.Close()
f := cmd.GetFormatter()
f.Title(file, data.Name)
printNameSection(file, f, data.Name, data.Version)
f.Section(file, "Synopsis")
f.Paragraph(file, f.Bold(data.Name), " ", f.Italic("command"),
" [", f.Italic("options..."), "] ",
f.Italic("arguments..."))
printStandardOptions(file, f, data.Name, data.Version)
if data.Description != "" {
f.Section(file, "Description")
f.Paragraph(file, data.Description)
}
f.Section(file, "Commands")
for _, cmd := range data.Commands {
if cmd.Description != "" {
f.Definition(file, cmd.Name, cmd.Description)
} else {
f.Paragraph(file, cmd.Name)
}
}
f.Section(file, "Author")
f.Paragraph(file, "NA")
f.Section(file, "Reporting Bugs")
f.Paragraph(file, "NA")
f.Section(file, "Copyright")
f.Paragraph(file, "NA")
f.Section(file, "See also")
f.Paragraph(file, "NA")
return nil
}
func (cmd *MakeMan) PrintCommandMan(data *Data, item *CommandData) error {
filename := filepath.Join(cmd.Output, data.Name+"-"+item.Name+cmd.Format.Ext())
cmd.Info("writing output:", filename)
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer file.Close()
f := cmd.GetFormatter()
f.Title(file, data.Name)
printNameSection(file, f, data.Name+" "+item.Name, data.Version)
printSynopsisSection(file, f, data.Name+" "+item.Name, item)
printStandardOptions(file, f, data.Name+" "+item.Name, "")
printDescriptionSection(file, f, item.Description)
printOptionsSection(file, f, item.ReqParams, item.OptParams)
printEnvironmentSection(file, f, item.OptParams)
f.Section(file, "Author")
f.Paragraph(file, "NA")
f.Section(file, "Reporting Bugs")
f.Paragraph(file, "NA")
f.Section(file, "Copyright")
f.Paragraph(file, "NA")
f.Section(file, "See also")
f.Paragraph(file, "NA")
return nil
}
func (cmd *MakeMan) PrintSingleMan(data *Data) error {
filename := filepath.Join(cmd.Output, data.Name+cmd.Format.Ext())
cmd.Info("writing output:", filename)
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer file.Close()
f := cmd.GetFormatter()
f.Title(file, data.Name)
printNameSection(file, f, data.Name, data.Version)
printSynopsisSection(file, f, data.Name, &CommandData{
ReqParams: data.ReqParams,
OptParams: data.OptParams,
})
printStandardOptions(file, f, data.Name, data.Version)
printDescriptionSection(file, f, data.Description)
printOptionsSection(file, f, data.ReqParams, data.OptParams)
printEnvironmentSection(file, f, data.OptParams)
f.Section(file, "Author")
f.Paragraph(file, "NA")
f.Section(file, "Reporting Bugs")
f.Paragraph(file, "NA")
f.Section(file, "Copyright")
f.Paragraph(file, "NA")
f.Section(file, "See also")
f.Paragraph(file, "NA")
return nil
}
func printRequiredParameter(w io.Writer, f Formatter, p *compile.RequiredParam) {
if p.Description != "" {
f.Definition(w, f.RequiredParameter(p), p.Description)
} else {
f.Paragraph(w, f.RequiredParameter(p))
}
}
func printOptionalParameter(w io.Writer, f Formatter, p *compile.OptionalParam) {
if p.Description != "" {
f.Definition(w, f.OptionalParameter(p), p.Description)
} else {
f.Paragraph(w, f.OptionalParameter(p))
}
}
func printNameSection(w io.Writer, f Formatter, name string, version string) {
f.Section(w, "Name")
if version != "" {
f.Paragraph(w, name+" ("+version+")")
} else {
f.Paragraph(w, name)
}
}
func printSynopsisSection(w io.Writer, f Formatter, name string, data *CommandData) {
f.Section(w, "Synopsis")
elem := []interface{}{f.Bold(name), " "}
for i := range data.OptParams {
elem = append(elem, f.OptionalParameter(&data.OptParams[i]), " ")
}
for i := range data.ReqParams {
elem = append(elem, f.RequiredParameter(&data.ReqParams[i]), " ")
}
f.Paragraph(w, elem...)
}
func printStandardOptions(w io.Writer, f Formatter, name, version string) {
f.Paragraph(w, f.Bold(name), " (--help | -h)")
if version != "" {
f.Paragraph(w, f.Bold(name), " (--version | -v)")
}
}
func printDescriptionSection(w io.Writer, f Formatter, description string) {
if description != "" {
f.Section(w, "Description")
f.Paragraph(w, description)
}
}
func printOptionsSection(w io.Writer, f Formatter, req []compile.RequiredParam, opt []compile.OptionalParam) {
if len(req) == 0 && len(opt) == 0 {
return
}
f.Section(w, "Options")
for _, v := range req {
v := v
printRequiredParameter(w, f, &v)
}
for _, v := range opt {
v := v
printOptionalParameter(w, f, &v)
}
}
func printEnvironmentSection(w io.Writer, f Formatter, optParams []compile.OptionalParam) {
haveEnvironmentSection := func(params []compile.OptionalParam) bool {
for i := range params {
if params[i].Env != "" {
return true
}
}
return false
}
if !haveEnvironmentSection(optParams) {
return
}
f.Section(w, "Environment")
for i := range optParams {
if optParams[i].Env == "" {
continue
}
f.Definition(w, optParams[i].Env, "See option ", optParams[i].Short, " ", optParams[i].Long, ".")
}
}
package main
import (
"fmt"
"io"
"strings"
"git.sr.ht/~rj/flags/internal/compile"
)
type MarkdownFormatter struct {
level int
}
func (f *MarkdownFormatter) Title(w io.Writer, name string) {
fmt.Fprint(w, strings.Repeat("#", f.level), " ", name, "\n\n")
}
func (f *MarkdownFormatter) Section(w io.Writer, name string) {
fmt.Fprint(w, strings.Repeat("#", f.level+1), " ", name, "\n\n")
}
func (*MarkdownFormatter) Paragraph(w io.Writer, a ...interface{}) {
for i := range a {
if s, ok := a[i].(string); ok {
a[i] = strings.ReplaceAll(s, "\n", "\n\n")
}
}
fmt.Fprint(w, a...)
fmt.Fprint(w, "\n\n")
}
func (*MarkdownFormatter) Definition(w io.Writer, name interface{}, a ...interface{}) {
for i := range a {
if s, ok := a[i].(string); ok {
a[i] = strings.ReplaceAll(s, "\n", "\n\n")
}
}
fmt.Fprint(w, name, ": ")
fmt.Fprint(w, a...)
fmt.Fprint(w, "\n\n")
}
func (*MarkdownFormatter) Bold(text string) interface{} {
return markdownBold(text)
}
type markdownBold string
func (s markdownBold) Format(f fmt.State, verb rune) {
fmt.Fprint(f, "**", string(s), "**")
}
func (*MarkdownFormatter) Italic(text string) interface{} {
return markdownItalic(text)
}
type markdownItalic string
func (s markdownItalic) Format(f fmt.State, verb rune) {
fmt.Fprint(f, "*", string(s), "*")
}
func (*MarkdownFormatter) OptionalParameter(p *compile.OptionalParam) interface{} {
return markdownOptional{p}
}
type markdownOptional struct{ *compile.OptionalParam }
func (s markdownOptional) Format(f fmt.State, verb rune) {
p := s.OptionalParam
if p.Short != "" {
if p.IsBool {
fmt.Fprint(f, "[**", p.Short, "**]")
} else {
fmt.Fprint(f, "[**", p.Short, "**=*", p.Name, "*]")
}
if p.Long != "" {
fmt.Fprint(f, " ")
}
}
if p.Long != "" {
fmt.Fprint(f, "[**", p.Long, "**=*", p.Name, "*]")
}
}
func (*MarkdownFormatter) RequiredParameter(p *compile.RequiredParam) interface{} {
return markdownRequired{p}
}
type markdownRequired struct{ *compile.RequiredParam }
func (s markdownRequired) Format(f fmt.State, verb rune) {
p := s.RequiredParam
if p.IsSlice {
fmt.Fprint(f, "*", p.Name, "...*")
} else {
fmt.Fprint(f, "*", p.Name, "*")
}
}
package main
import (
"fmt"
"io"
"strings"
"git.sr.ht/~rj/flags/internal/compile"
)
type TroffFormatter struct{}
func (*TroffFormatter) Title(w io.Writer, name string) {
const section = 1
fmt.Fprint(w, ".TH ", name, " ", section, "\n")
}
func (*TroffFormatter) Section(w io.Writer, name string) {
fmt.Fprint(w, ".SH ", strings.ToUpper(name), "\n")
}
func (*TroffFormatter) Paragraph(w io.Writer, a ...interface{}) {
for i := range a {
if s, ok := a[i].(string); ok {
a[i] = strings.ReplaceAll(strings.ReplaceAll(s, "-", "\\-"), "\n", "\n.PP\n")
}
}
fmt.Fprint(w, ".PP\n")
fmt.Fprint(w, a...)
fmt.Fprint(w, "\n")
}
func (*TroffFormatter) Definition(w io.Writer, name interface{}, a ...interface{}) {
for i := range a {
if s, ok := a[i].(string); ok {
a[i] = strings.ReplaceAll(strings.ReplaceAll(s, "-", "\\-"), "\n", "\n.PP\n")
}
}
fmt.Fprint(w, ".PP\n", name, "\n.RS\n.PP\n")
fmt.Fprint(w, a...)
fmt.Fprint(w, "\n.RE\n")
}
func (*TroffFormatter) Bold(text string) interface{} {
return troffBold(text)
}
type troffBold string
func (s troffBold) Format(f fmt.State, verb rune) {
fmt.Fprint(f, "\\fB", string(s), "\\fR")
}
func (*TroffFormatter) Italic(text string) interface{} {
return troffItalic(text)
}
type troffItalic string
func (s troffItalic) Format(f fmt.State, verb rune) {
fmt.Fprint(f, "\\fI", string(s), "\\fR")
}
func (*TroffFormatter) OptionalParameter(p *compile.OptionalParam) interface{} {
return troffOptional{p}
}
type troffOptional struct{ *compile.OptionalParam }
func (s troffOptional) Format(f fmt.State, verb rune) {
p := s.OptionalParam
if p.Short != "" {
if p.IsBool {
fmt.Fprint(f, "[\\fB", strings.Replace(p.Short, "-", "\\-", -1), "\\fR]")
} else {
fmt.Fprint(f, "[\\fB", strings.Replace(p.Short, "-", "\\-", -1), "=\\fR\\fI", p.Name, "\\fR]")
}
if p.Long != "" {
fmt.Fprint(f, " ")
}
}
if p.Long != "" {
fmt.Fprint(f, "[\\fB", strings.Replace(p.Long, "-", "\\-", -1), "=\\fR\\fI", p.Name, "\\fR]")
}
}
func (*TroffFormatter) RequiredParameter(p *compile.RequiredParam) interface{} {
return troffRequired{p}
}
type troffRequired struct{ *compile.RequiredParam }
func (s troffRequired) Format(f fmt.State, verb rune) {
p := s.RequiredParam
if p.IsSlice {
fmt.Fprint(f, "\\fI", p.Name, "...\\fR")
} else {
fmt.Fprint(f, "\\fI", p.Name, "\\fR")
}
}
package main
import (
"errors"
"io"
"strings"
"git.sr.ht/~rj/flags/internal/compile"
)
type Data struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Commands []CommandData `json:"commands"`
OptParams []compile.OptionalParam `json:"optional"`
ReqParams []compile.RequiredParam `json:"required"`
}
type CommandData struct {
Name string `json:"name"`
Description string `json:"description"`
OptParams []compile.OptionalParam `json:"optional"`
ReqParams []compile.RequiredParam `json:"required"`
}
type Formatter interface {
Title(w io.Writer, title string)
Section(w io.Writer, name string)
Paragraph(w io.Writer, a ...interface{})
Definition(w io.Writer, name interface{}, a ...interface{})
Bold(text string) interface{}
Italic(text string) interface{}
OptionalParameter(*compile.OptionalParam) interface{}
RequiredParameter(*compile.RequiredParam) interface{}
}
type Format uint
const (
FormatTroff Format = iota + 1
FormatMarkdown
)
func (f Format) Ext() string {
switch f {
case FormatTroff:
return ".1"
case FormatMarkdown:
return ".md"
default:
panic("internal error")
}
}
func (f Format) String() string {
switch f {
case FormatTroff:
return "Troff"
case FormatMarkdown:
return "Markdown"
default:
panic("internal error")
}
}
func (f *Format) Set(value string) error {
switch strings.ToLower(value) {
case "troff":
*f = FormatTroff
return nil
case "markdown", "md":
*f = FormatMarkdown
return nil
}
return errors.New("unrecognized value for format: " + value)
}
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 wish 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 := 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: field.Tag.Get("env"),
IsBool: value.Kind() == reflect.Bool,
Description: field.Tag.Get("description"),
})
} else {
params.required = append(params.required, RequiredParam{
Name: strings.ToLower(field.Name),
Value: value,
IsSlice: value.Kind() == reflect.Slice,
Description: field.Tag.Get("description"),
})
}
return params
}
package compile
import (
"reflect"
)
type OptionalParam struct {
Name string `json:"name"`
Value reflect.Value `json:"-"`
Short string `json:"short"`
Long string `json:"long"`
Env string `json:"env,omitempty"`
IsBool bool `json:"isbool,omitempty"`
Description string `json:"description"`
}
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 `json:"name"`
Value reflect.Value `json:"-"`
IsSlice bool `json:"isslice,omitempty"`
Description string `json:"description"`
}
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) { //nolint:revive
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].IsBool {
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 {
// Instead of writing --key=value, the user has written --key value.
// Parsing will need to look for value in next argument.
assignField = true
}
} else if strings.HasPrefix(args[i], "-") && allowoptparam {
// Handle short name arguments
if pos := strings.Index(args[i], "="); pos > 0 {
// Do nothing.
assignField = false
} 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.
assignField = false
} 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 editdistance
// Levenshtein returns the minimum number of operations required to convert s1 to
// s2. Operations include inserstion, deletions, and substitutions.
func Levenshtein(s1, s2 string) int {
rs1 := []rune(s1)
rs2 := []rune(s2)
m, n := len(rs1), len(rs2)
prev := 0 // Stores dp[i-1][j-1]
curr := make([]int, n+1) // Stores dp[i][j-1]
// and dp[i][j]
for j := range curr {
curr[j] = j
}
for i := 1; i <= m; i++ {
prev = curr[0]
curr[0] = i
for j := 1; j <= n; j++ {
temp := curr[j]
if s1[i-1] == s2[j-1] {
curr[j] = prev
} else {
curr[j] = 1 + min(curr[j-1], prev, curr[j])
}
prev = temp
}
}
return curr[n]
}
func min(a, b, c int) int {
if a < b {
if a < c {
return a
}
return c
}
if b < c {
return b
}
return c
}
package parse
import (
"encoding"
"reflect"
"strconv"
"time"
)
// 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 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
}
// If the type supports the TextUnmarshaler interface, defer to the supplied
// implementation.
if setter, ok := field.Addr().Interface().(encoding.TextUnmarshaler); ok {
if err := setter.UnmarshalText([]byte(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 _, ok := field.Interface().(time.Duration); ok {
// It would be nice if time.Duration supported UnmarshalText, like
// time.Time, but it does not.
dur, err := time.ParseDuration(text)
if err != nil {
return &ConversionError{
text: text,
err: err,
}
}
field.SetInt(int64(dur))
} else 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 term
import (
"io"
"os"
"golang.org/x/term"
)
// GetSize returns the visible dimensions of the given terminal.
//
// These dimensions don't include any scrollback buffer height.
func GetSize(w io.Writer) (width, height int, err error) {
if file, ok := w.(*os.File); ok {
return term.GetSize(int(file.Fd()))
}
return 80, 25, nil
}
// MustGetSize returns the visible dimensions of the given terminal.
//
// If there is an error determining the terminal size, this function returns a
// default size.
func MustGetSize(w io.Writer) (width, height int) {
width, height, err := GetSize(w)
if err != nil {
return 80, 25
}
return width, height
}
package teststdio
import (
"bytes"
"fmt"
"io"
"os"
"testing"
)
func CaptureStderr(t *testing.T) (chan string, func()) {
t.Helper()
// Capture stdout.
stderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("could not capture standard err: %s", err)
}
os.Stderr = w
outC := make(chan string)
go func() {
defer r.Close()
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\n", err)
os.Exit(1)
}
outC <- buf.String()
}()
return outC, func() {
w.Close()
os.Stderr = stderr
}
}
package teststdio
import (
"bytes"
"fmt"
"io"
"os"
"testing"
)
func CaptureStdout(t *testing.T) (chan string, func()) {
t.Helper()
// Capture stdout.
stdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("could not capture standard out: %s", err)
}
os.Stdout = w
outC := make(chan string)
go func() {
defer r.Close()
var buf bytes.Buffer
_, err := io.Copy(&buf, r)
if err != nil {
fmt.Fprintf(os.Stderr, "testing: copying pipe: %v\n", err)
os.Exit(1)
}
outC <- buf.String()
}()
return outC, func() {
w.Close()
os.Stdout = stdout
}
}
package flags
import (
"encoding/json"
"io"
"os"
"sort"
"git.sr.ht/~rj/flags/internal/compile"
)
func printJSON(w io.Writer, cmds map[string]Command, cfg *config) error {
if cfg.exec == "" {
cfg.exec, _ = os.Executable()
}
type commandData struct {
Name string `json:"name"`
Description string `json:"description"`
OptParams []compile.OptionalParam `json:"optional"`
ReqParams []compile.RequiredParam `json:"required"`
}
data := struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Commands []commandData `json:"commands"`
}{
Name: cfg.name,
Description: cfg.description,
Version: cfg.version,
}
getDescription := func(cmd Command) string {
if d, ok := cmd.(Describer); ok {
return d.Description()
}
return ""
}
names := make([]string, 0, len(cmds))
for name := range cmds {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
value := cmds[name]
optparams, reqparams := compile.Compile(value)
data.Commands = append(data.Commands, commandData{
Name: name,
Description: getDescription(value),
OptParams: optparams,
ReqParams: reqparams,
})
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(&data)
}
func printSingleJSON(w io.Writer, cmd Command, cfg *config) error {
optparams, reqparams := compile.Compile(cmd)
if cfg.exec == "" {
cfg.exec, _ = os.Executable()
}
data := struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
OptParams []compile.OptionalParam `json:"optional"`
ReqParams []compile.RequiredParam `json:"required"`
}{
Name: cfg.name,
Description: cfg.description,
Version: cfg.version,
OptParams: optparams,
ReqParams: reqparams,
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(&data)
}
package flags
import (
"os"
"path/filepath"
"runtime/debug"
"strings"
)
// 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) {
// Supply default values, where available.
c.name = filepath.Base(os.Args[0])
c.args = os.Args
if info, ok := debug.ReadBuildInfo(); ok {
c.version = strings.TrimPrefix(
strings.TrimSuffix(info.Main.Version, ")"),
"(",
)
}
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].IsBool {
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 were provided")
}
}
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 were provided")
}
return nil
}
func assignOptField(params []compile.OptionalParam, key, value string) error {
index := compile.FindOptionalParam(params, key)
if index < 0 {
return errors.New("argument name 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 name not recognized: " + key)
}
if params[index].IsBool {
return 0, parse.AssignValue(params[index].Value, "true")
}
if len(rest) == 0 {
return 0, errors.New("optional argument missing value: " + 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/editdistance"
"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.Fprint(w, 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, f.Bold("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.IsSlice {
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.IsBool {
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.IsBool {
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")
}
}
func printSuggestedCommands(w io.Writer, cmds map[string]Command, arg1 string) {
names := []string(nil)
for name := range cmds {
if editdistance.Levenshtein(name, arg1) <= 2 {
names = append(names, name)
}
}
if len(names) == 0 {
return
}
sort.Strings(names)
fmt.Fprintf(w, "\nThe most similar commands available are:\n")
table := table.NewTable(3)
for _, name := range names {
description := ""
if describer, ok := cmds[name].(Describer); ok {
description = describer.Description()
}
table.AddRow("", name, description)
}
table.Fprint(w)
}
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.
//
// If the selected command supports the IOInjector interface, it will be called
// with the standard streams (os.Stdin, os.Stdout, and os.Stderr).
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] == "--help=json" {
err := printJSON(os.Stdout, commands, &cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
return
}
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, "error: unknown command: %s\n", cfg.args[1])
printSuggestedCommands(os.Stderr, commands, 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.
//
// If the command supports the IOInjector interface, it will be called
// with the standard streams (os.Stdin, os.Stdout, and os.Stderr).
func RunSingle(command Command, options ...Option) {
cfg := config{}
cfg.initCommand(command)
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] == "--help=json" {
err := printSingleJSON(os.Stdout, command, &cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %s\n", err)
cfg.exit(1)
return
}
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)
}