1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-06-28 00:39:31 +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" .}}
{{if .Synopsis}}SYNOPSIS
{{.Synopsis}}
{{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 {
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)
fields.TrimNewlines()
@ -206,6 +208,9 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
if len(fields.Subcommands) == 0 {
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)
fields.TrimNewlines()
@ -216,6 +221,54 @@ func ShortHelp(rootName string, root *cmds.Command, path []string, out io.Writer
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 {
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

@ -36,9 +36,9 @@ type MarshalerMap map[EncodingType]Marshaler
// text follows formats similar to man pages, but not exactly the same.
type HelpText struct {
// required
Tagline string // used in <cmd usage>
ShortDescription string // used in DESCRIPTION
Synopsis string // showcasing the cmd
Tagline string // used in <cmd usage>
ShortDescription string // used in DESCRIPTION
SynopsisOptionsValues map[string]string // mappings for synopsis generator
// optional - whole section overrides
Usage string // overrides USAGE section
@ -46,6 +46,7 @@ type HelpText struct {
Options string // overrides OPTIONS section
Arguments string // overrides ARGUMENTS section
Subcommands string // overrides SUBCOMMANDS section
Synopsis string // overrides SYNOPSIS field
}
// 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
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 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())
},
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{
"clear": clearInactiveCmd,

View File

@ -40,7 +40,7 @@ operations.
`,
},
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{
"read": FilesReadCmd,
@ -397,8 +397,8 @@ Examples:
cmds.StringArg("path", true, false, "Path to file to be read."),
},
Options: []cmds.Option{
cmds.IntOption("o", "offset", "Byte offset to begin reading from."),
cmds.IntOption("n", "count", "Maximum number of bytes to read."),
cmds.IntOption("offset", "o", "Byte offset to begin reading from."),
cmds.IntOption("count", "n", "Maximum number of bytes to read."),
},
Run: func(req cmds.Request, res cmds.Response) {
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(),
},
Options: []cmds.Option{
cmds.IntOption("o", "offset", "Byte offset to begin writing at."),
cmds.BoolOption("e", "create", "Create the file if it does not exist."),
cmds.BoolOption("t", "truncate", "Truncate the file to size zero before writing."),
cmds.IntOption("n", "count", "Maximum number of bytes to read."),
cmds.IntOption("offset", "o", "Byte offset to begin writing at."),
cmds.BoolOption("create", "e", "Create the file if it does not exist."),
cmds.BoolOption("truncate", "t", "Truncate the file to size zero before writing."),
cmds.IntOption("count", "n", "Maximum number of bytes to read."),
},
Run: func(req cmds.Request, res cmds.Response) {
path, err := checkPath(req.Arguments()[0])
@ -678,7 +678,7 @@ Examples:
cmds.StringArg("path", true, false, "Path to dir to make."),
},
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) {
n, err := req.InvocContext().GetNode()
@ -758,7 +758,7 @@ Remove files or directories.
cmds.StringArg("path", true, true, "File to remove."),
},
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) {
nd, err := req.InvocContext().GetNode()

View File

@ -16,9 +16,6 @@ import (
var MountCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Mounts IPFS to the filesystem (read-only).",
Synopsis: `
ipfs mount [-f <ipfs mount path>] [-n <ipns mount path>]
`,
ShortDescription: `
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

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."),
},
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) {
nd, err := req.InvocContext().GetNode()

View File

@ -28,8 +28,7 @@ type PingResult struct {
var PingCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Send echo request packets to IPFS hosts.",
Synopsis: "ipfs ping <peerId> [--count <int>| -n]",
Tagline: "Send echo request packets to IPFS hosts.",
ShortDescription: `
'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-

View File

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

View File

@ -18,8 +18,7 @@ import (
var StatsCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Query ipfs statistics.",
Synopsis: "ipfs stats <command>",
Tagline: "Query ipfs statistics.",
ShortDescription: `'ipfs stats' is a set of commands to help look at statistics
for your ipfs node.
`,
@ -37,9 +36,6 @@ for your ipfs node.`,
var statBwCmd = &cmds.Command{
Helptext: cmds.HelpText{
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.
It displays: TotalIn, TotalOut, RateIn, RateOut.
`,

View File

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

View File

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

View File

@ -35,7 +35,7 @@ test_expect_success "ipfs help succeeds" '
test_expect_success "ipfs help output looks good" '
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
'

View File

@ -8,19 +8,6 @@ test_description="Test add and cat commands"
. 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_expect_success "ipfs add succeeds" '
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" '
mkfifo named-pipe &&
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 &&
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" '