1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-06-29 01:12:24 +08:00

Merge pull request #2785 from ipfs/feature/auto-synopsis

Add synopsis autogenerator
This commit is contained in:
Jeromy Johnson
2016-06-28 12:05:15 -07:00
committed by GitHub
15 changed files with 124 additions and 49 deletions

View File

@ -78,7 +78,6 @@ const longHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}} {{.Indent}}{{template "usage" .}}
{{if .Synopsis}}SYNOPSIS {{if .Synopsis}}SYNOPSIS
{{.Synopsis}} {{.Synopsis}}
{{end}}{{if .Arguments}}ARGUMENTS {{end}}{{if .Arguments}}ARGUMENTS
@ -163,6 +162,9 @@ func LongHelp(rootName string, root *cmds.Command, path []string, out io.Writer)
if len(fields.Subcommands) == 0 { if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n") fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
} }
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc) // trim the extra newlines (see TrimNewlines doc)
fields.TrimNewlines() fields.TrimNewlines()
@ -206,6 +208,9 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
if len(fields.Subcommands) == 0 { if len(fields.Subcommands) == 0 {
fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n") fields.Subcommands = strings.Join(subcommandText(cmd, rootName, path), "\n")
} }
if len(fields.Synopsis) == 0 {
fields.Synopsis = generateSynopsis(cmd, pathStr)
}
// trim the extra newlines (see TrimNewlines doc) // trim the extra newlines (see TrimNewlines doc)
fields.TrimNewlines() fields.TrimNewlines()
@ -216,6 +221,54 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
return shortHelpTemplate.Execute(out, fields) return shortHelpTemplate.Execute(out, fields)
} }
func generateSynopsis(cmd *cmds.Command, path string) string {
res := path
for _, opt := range cmd.Options {
valopt, ok := cmd.Helptext.SynopsisOptionsValues[opt.Names()[0]]
if !ok {
valopt = opt.Names()[0]
}
sopt := ""
for i, n := range opt.Names() {
pre := "-"
if len(n) > 1 {
pre = "--"
}
if opt.Type() == cmds.Bool && opt.DefaultVal() == true {
pre = "--"
sopt = fmt.Sprintf("%s%s=false", pre, n)
break
} else {
if i == 0 {
if opt.Type() == cmds.Bool {
sopt = fmt.Sprintf("%s%s", pre, n)
} else {
sopt = fmt.Sprintf("%s%s=<%s>", pre, n, valopt)
}
} else {
sopt = fmt.Sprintf("%s | %s%s", sopt, pre, n)
}
}
}
res = fmt.Sprintf("%s [%s]", res, sopt)
}
if len(cmd.Arguments) > 0 {
res = fmt.Sprintf("%s [--]", res)
}
for _, arg := range cmd.Arguments {
sarg := fmt.Sprintf("<%s>", arg.Name)
if arg.Variadic {
sarg = sarg + "..."
}
if !arg.Required {
sarg = fmt.Sprintf("[%s]", sarg)
}
res = fmt.Sprintf("%s %s", res, sarg)
}
return strings.Trim(res, " ")
}
func argumentText(cmd *cmds.Command) []string { func argumentText(cmd *cmds.Command) []string {
lines := make([]string, len(cmd.Arguments)) lines := make([]string, len(cmd.Arguments))

View File

@ -0,0 +1,45 @@
package cli
import (
"strings"
"testing"
cmds "github.com/ipfs/go-ipfs/commands"
)
func TestSynopsisGenerator(t *testing.T) {
command := &cmds.Command{
Arguments: []cmds.Argument{
cmds.StringArg("required", true, false, ""),
cmds.StringArg("variadic", false, true, ""),
},
Options: []cmds.Option{
cmds.StringOption("opt", "o", "Option"),
},
Helptext: cmds.HelpText{
SynopsisOptionsValues: map[string]string{
"opt": "OPTION",
},
},
}
syn := generateSynopsis(command, "cmd")
t.Logf("Synopsis is: %s", syn)
if !strings.HasPrefix(syn, "cmd ") {
t.Fatal("Synopsis should start with command name")
}
if !strings.Contains(syn, "[--opt=<OPTION> | -o]") {
t.Fatal("Synopsis should contain option descriptor")
}
if !strings.Contains(syn, "<required>") {
t.Fatal("Synopsis should contain required argument")
}
if !strings.Contains(syn, "<variadic>...") {
t.Fatal("Synopsis should contain variadic argument")
}
if !strings.Contains(syn, "[<variadic>...]") {
t.Fatal("Synopsis should contain optional argument")
}
if !strings.Contains(syn, "[--]") {
t.Fatal("Synopsis should contain options finalizer")
}
}

View File

@ -38,7 +38,7 @@ type HelpText struct {
// required // required
Tagline string // used in <cmd usage> Tagline string // used in <cmd usage>
ShortDescription string // used in DESCRIPTION ShortDescription string // used in DESCRIPTION
Synopsis string // showcasing the cmd SynopsisOptionsValues map[string]string // mappings for synopsis generator
// optional - whole section overrides // optional - whole section overrides
Usage string // overrides USAGE section Usage string // overrides USAGE section
@ -46,6 +46,7 @@ type HelpText struct {
Options string // overrides OPTIONS section Options string // overrides OPTIONS section
Arguments string // overrides ARGUMENTS section Arguments string // overrides ARGUMENTS section
Subcommands string // overrides SUBCOMMANDS section Subcommands string // overrides SUBCOMMANDS section
Synopsis string // overrides SYNOPSIS field
} }
// Command is a runnable command, with input arguments and options (flags). // Command is a runnable command, with input arguments and options (flags).

View File

@ -189,7 +189,7 @@ const (
// options that are used by this package // options that are used by this package
var OptionEncodingType = StringOption(EncLong, EncShort, "The encoding type the output should be encoded with (json, xml, or text)") var OptionEncodingType = StringOption(EncLong, EncShort, "The encoding type the output should be encoded with (json, xml, or text)")
var OptionRecursivePath = BoolOption(RecLong, RecShort, "Add directory paths recursively") var OptionRecursivePath = BoolOption(RecLong, RecShort, "Add directory paths recursively").Default(false)
var OptionStreamChannels = BoolOption(ChanOpt, "Stream channel output") var OptionStreamChannels = BoolOption(ChanOpt, "Stream channel output")
var OptionTimeout = StringOption(TimeoutOpt, "set a global timeout on the command") var OptionTimeout = StringOption(TimeoutOpt, "set a global timeout on the command")

View File

@ -22,7 +22,7 @@ Lists running and recently run commands.
res.SetOutput(req.InvocContext().ReqLog.Report()) res.SetOutput(req.InvocContext().ReqLog.Report())
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.BoolOption("v", "verbose", "Print extra information.").Default(false), cmds.BoolOption("verbose", "v", "Print extra information.").Default(false),
}, },
Subcommands: map[string]*cmds.Command{ Subcommands: map[string]*cmds.Command{
"clear": clearInactiveCmd, "clear": clearInactiveCmd,

View File

@ -40,7 +40,7 @@ operations.
`, `,
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.BoolOption("f", "flush", "Flush target and ancestors after write. Default: true."), cmds.BoolOption("flush", "f", "Flush target and ancestors after write. Default: true."),
}, },
Subcommands: map[string]*cmds.Command{ Subcommands: map[string]*cmds.Command{
"read": FilesReadCmd, "read": FilesReadCmd,
@ -397,8 +397,8 @@ Examples:
cmds.StringArg("path", true, false, "Path to file to be read."), cmds.StringArg("path", true, false, "Path to file to be read."),
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.IntOption("o", "offset", "Byte offset to begin reading from."), cmds.IntOption("offset", "o", "Byte offset to begin reading from."),
cmds.IntOption("n", "count", "Maximum number of bytes to read."), cmds.IntOption("count", "n", "Maximum number of bytes to read."),
}, },
Run: func(req cmds.Request, res cmds.Response) { Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode() n, err := req.InvocContext().GetNode()
@ -565,10 +565,10 @@ stat' on the file or any of its ancestors.
cmds.FileArg("data", true, false, "Data to write.").EnableStdin(), cmds.FileArg("data", true, false, "Data to write.").EnableStdin(),
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.IntOption("o", "offset", "Byte offset to begin writing at."), cmds.IntOption("offset", "o", "Byte offset to begin writing at."),
cmds.BoolOption("e", "create", "Create the file if it does not exist."), cmds.BoolOption("create", "e", "Create the file if it does not exist."),
cmds.BoolOption("t", "truncate", "Truncate the file to size zero before writing."), cmds.BoolOption("truncate", "t", "Truncate the file to size zero before writing."),
cmds.IntOption("n", "count", "Maximum number of bytes to read."), cmds.IntOption("count", "n", "Maximum number of bytes to read."),
}, },
Run: func(req cmds.Request, res cmds.Response) { Run: func(req cmds.Request, res cmds.Response) {
path, err := checkPath(req.Arguments()[0]) path, err := checkPath(req.Arguments()[0])
@ -678,7 +678,7 @@ Examples:
cmds.StringArg("path", true, false, "Path to dir to make."), cmds.StringArg("path", true, false, "Path to dir to make."),
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.BoolOption("p", "parents", "No error if existing, make parent directories as needed."), cmds.BoolOption("parents", "p", "No error if existing, make parent directories as needed."),
}, },
Run: func(req cmds.Request, res cmds.Response) { Run: func(req cmds.Request, res cmds.Response) {
n, err := req.InvocContext().GetNode() n, err := req.InvocContext().GetNode()
@ -758,7 +758,7 @@ Remove files or directories.
cmds.StringArg("path", true, true, "File to remove."), cmds.StringArg("path", true, true, "File to remove."),
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.BoolOption("r", "recursive", "Recursively remove directories."), cmds.BoolOption("recursive", "r", "Recursively remove directories."),
}, },
Run: func(req cmds.Request, res cmds.Response) { Run: func(req cmds.Request, res cmds.Response) {
nd, err := req.InvocContext().GetNode() nd, err := req.InvocContext().GetNode()

View File

@ -16,9 +16,6 @@ import (
var MountCmd = &cmds.Command{ var MountCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Mounts IPFS to the filesystem (read-only).", Tagline: "Mounts IPFS to the filesystem (read-only).",
Synopsis: `
ipfs mount [-f <ipfs mount path>] [-n <ipns mount path>]
`,
ShortDescription: ` ShortDescription: `
Mount ipfs at a read-only mountpoint on the OS (default: /ipfs and /ipns). Mount ipfs at a read-only mountpoint on the OS (default: /ipfs and /ipns).
All ipfs objects will be accessible under that directory. Note that the All ipfs objects will be accessible under that directory. Note that the

View File

@ -251,7 +251,7 @@ to a file containing 'bar', and returns the hash of the new object.
cmds.StringArg("ref", true, false, "IPFS object to add link to."), cmds.StringArg("ref", true, false, "IPFS object to add link to."),
}, },
Options: []cmds.Option{ Options: []cmds.Option{
cmds.BoolOption("p", "create", "Create intermediary nodes.").Default(false), cmds.BoolOption("create", "p", "Create intermediary nodes.").Default(false),
}, },
Run: func(req cmds.Request, res cmds.Response) { Run: func(req cmds.Request, res cmds.Response) {
nd, err := req.InvocContext().GetNode() nd, err := req.InvocContext().GetNode()

View File

@ -29,7 +29,6 @@ type PingResult struct {
var PingCmd = &cmds.Command{ var PingCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Send echo request packets to IPFS hosts.", Tagline: "Send echo request packets to IPFS hosts.",
Synopsis: "ipfs ping <peerId> [--count <int>| -n]",
ShortDescription: ` ShortDescription: `
'ipfs ping' is a tool to test sending data to other nodes. It finds nodes 'ipfs ping' is a tool to test sending data to other nodes. It finds nodes
via the routing system, sends pings, waits for pongs, and prints out round- via the routing system, sends pings, waits for pongs, and prints out round-

View File

@ -20,9 +20,7 @@ const (
var Root = &cmds.Command{ var Root = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Global p2p merkle-dag filesystem.", Tagline: "Global p2p merkle-dag filesystem.",
Synopsis: ` Synopsis: "ipfs [--config=<config> | -c] [--debug=<debug> | -D] [--help=<help>] [-h=<h>] [--local=<local> | -L] [--api=<api>] <command> ...",
ipfs [<flags>] <command> [<arg>] ...
`,
Subcommands: ` Subcommands: `
BASIC COMMANDS BASIC COMMANDS
init Initialize ipfs local configuration init Initialize ipfs local configuration

View File

@ -19,7 +19,6 @@ import (
var StatsCmd = &cmds.Command{ var StatsCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Query ipfs statistics.", Tagline: "Query ipfs statistics.",
Synopsis: "ipfs stats <command>",
ShortDescription: `'ipfs stats' is a set of commands to help look at statistics ShortDescription: `'ipfs stats' is a set of commands to help look at statistics
for your ipfs node. for your ipfs node.
`, `,
@ -37,9 +36,6 @@ for your ipfs node.`,
var statBwCmd = &cmds.Command{ var statBwCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Print ipfs bandwidth information.", Tagline: "Print ipfs bandwidth information.",
Synopsis: `ipfs stats bw [--peer <peerId> | -p] [--proto <protocol> | -t] [--poll]
[--interval <timeInterval> | -i]
`,
ShortDescription: `'ipfs stats bw' prints bandwidth information for the ipfs daemon. ShortDescription: `'ipfs stats bw' prints bandwidth information for the ipfs daemon.
It displays: TotalIn, TotalOut, RateIn, RateOut. It displays: TotalIn, TotalOut, RateIn, RateOut.
`, `,

View File

@ -36,7 +36,6 @@ type LsOutput struct {
var LsCmd = &cmds.Command{ var LsCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "List directory contents for Unix filesystem objects.", Tagline: "List directory contents for Unix filesystem objects.",
Synopsis: "ipfs file ls <path>",
ShortDescription: ` ShortDescription: `
Displays the contents of an IPFS or IPNS object(s) at the given path. Displays the contents of an IPFS or IPNS object(s) at the given path.

View File

@ -5,7 +5,6 @@ import cmds "github.com/ipfs/go-ipfs/commands"
var UnixFSCmd = &cmds.Command{ var UnixFSCmd = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "Interact with ipfs objects representing Unix filesystems.", Tagline: "Interact with ipfs objects representing Unix filesystems.",
Synopsis: "ipfs file <command>",
ShortDescription: ` ShortDescription: `
'ipfs file' provides a familiar interface to file systems represented 'ipfs file' provides a familiar interface to file systems represented
by IPFS objects, which hides IPFS implementation details like layout by IPFS objects, which hides IPFS implementation details like layout

View File

@ -35,7 +35,7 @@ test_expect_success "ipfs help succeeds" '
test_expect_success "ipfs help output looks good" ' test_expect_success "ipfs help output looks good" '
egrep -i "^Usage" help.txt >/dev/null && egrep -i "^Usage" help.txt >/dev/null &&
egrep "ipfs .* <command>" help.txt >/dev/null || egrep "ipfs <command>" help.txt >/dev/null ||
test_fsh cat help.txt test_fsh cat help.txt
' '

View File

@ -8,19 +8,6 @@ test_description="Test add and cat commands"
. lib/test-lib.sh . lib/test-lib.sh
client_err_add() {
printf "$@\n\n"
echo 'USAGE
ipfs add <path>... - Add a file to ipfs.
Adds contents of <path> to ipfs. Use -r to add directories.
Note that directories are added recursively, to form the ipfs
MerkleDAG.
Use '"'"'ipfs add --help'"'"' for more information about this command.
'
}
test_add_cat_file() { test_add_cat_file() {
test_expect_success "ipfs add succeeds" ' test_expect_success "ipfs add succeeds" '
echo "Hello Worlds!" >mountdir/hello.txt && echo "Hello Worlds!" >mountdir/hello.txt &&
@ -176,9 +163,10 @@ test_add_named_pipe() {
test_expect_success "useful error message when adding a named pipe" ' test_expect_success "useful error message when adding a named pipe" '
mkfifo named-pipe && mkfifo named-pipe &&
test_expect_code 1 ipfs add named-pipe 2>actual && test_expect_code 1 ipfs add named-pipe 2>actual &&
client_err_add "Error: Unrecognized file type for named-pipe: $(generic_stat named-pipe)" >expected &&
rm named-pipe && rm named-pipe &&
test_cmp expected actual grep "Error: Unrecognized file type for named-pipe: $(generic_stat named-pipe)" actual &&
grep USAGE actual &&
grep "ipfs add" actual
' '
test_expect_success "useful error message when recursively adding a named pipe" ' test_expect_success "useful error message when recursively adding a named pipe" '