Skip to content

Commit

Permalink
Merge pull request #2785 from ipfs/feature/auto-synopsis
Browse files Browse the repository at this point in the history
Add synopsis autogenerator
  • Loading branch information
whyrusleeping committed Jun 28, 2016
2 parents 80fcac8 + 1c0f18d commit 7fa88fd
Show file tree
Hide file tree
Showing 15 changed files with 124 additions and 49 deletions.
55 changes: 54 additions & 1 deletion commands/cli/helptext.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ const longHelpFormat = `USAGE
{{.Indent}}{{template "usage" .}}
{{if .Synopsis}}SYNOPSIS
{{.Synopsis}}
{{end}}{{if .Arguments}}ARGUMENTS
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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))

Expand Down
45 changes: 45 additions & 0 deletions commands/cli/helptext_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
7 changes: 4 additions & 3 deletions commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@ 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
LongDescription string // overrides DESCRIPTION section
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).
Expand Down
2 changes: 1 addition & 1 deletion commands/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
2 changes: 1 addition & 1 deletion core/commands/active.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
18 changes: 9 additions & 9 deletions core/commands/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
3 changes: 0 additions & 3 deletions core/commands/mount_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion core/commands/object/patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 1 addition & 2 deletions core/commands/ping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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-
Expand Down
6 changes: 2 additions & 4 deletions core/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions core/commands/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`,
Expand All @@ -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.
`,
Expand Down
1 change: 0 additions & 1 deletion core/commands/unixfs/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions core/commands/unixfs/unixfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion test/sharness/t0010-basic-commands.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
'

Expand Down
18 changes: 3 additions & 15 deletions test/sharness/t0040-add-and-cat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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" '
Expand Down

0 comments on commit 7fa88fd

Please sign in to comment.