diff --git a/cmd/grpcurl/go1_10.go b/cmd/grpcurl/go1_10.go new file mode 100644 index 0000000..ff96b0e --- /dev/null +++ b/cmd/grpcurl/go1_10.go @@ -0,0 +1,9 @@ +// +build go1.10 + +package main + +func indent() string { + // In Go 1.10 and up, the flag package automatically + // adds the right indentation. + return "" +} diff --git a/cmd/grpcurl/go1_9.go b/cmd/grpcurl/go1_9.go new file mode 100644 index 0000000..8a2a30d --- /dev/null +++ b/cmd/grpcurl/go1_9.go @@ -0,0 +1,9 @@ +// +build !go1.10 + +package main + +func indent() string { + // In Go 1.9 and older, we need to add indentation + // after newlines in the flag doc strings. + return " \t" +} diff --git a/cmd/grpcurl/grpcurl.go b/cmd/grpcurl/grpcurl.go index a1347b4..0d6093d 100644 --- a/cmd/grpcurl/grpcurl.go +++ b/cmd/grpcurl/grpcurl.go @@ -1,4 +1,4 @@ -// Command grpcurl makes GRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor +// Command grpcurl makes gRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor // file, protobuf sources, or service reflection to translate JSON or text request data into the // appropriate protobuf messages and vice versa for presenting the response contents. package main @@ -40,109 +40,110 @@ var ( isUnixSocket func() bool // nil when run on non-unix platform - help = flag.Bool("help", false, - `Print usage instructions and exit.`) - printVersion = flag.Bool("version", false, - `Print version.`) - plaintext = flag.Bool("plaintext", false, - `Use plain-text HTTP/2 when connecting to server (no TLS).`) - insecure = flag.Bool("insecure", false, - `Skip server certificate and domain verification. (NOT SECURE!). Not - valid with -plaintext option.`) - cacert = flag.String("cacert", "", - `File containing trusted root certificates for verifying the server. - Ignored if -insecure is specified.`) - cert = flag.String("cert", "", - `File containing client certificate (public key), to present to the - server. Not valid with -plaintext option. Must also provide -key option.`) - key = flag.String("key", "", - `File containing client private key, to present to the server. Not valid - with -plaintext option. Must also provide -cert option.`) + help = flag.Bool("help", false, prettify(` + Print usage instructions and exit.`)) + printVersion = flag.Bool("version", false, prettify(` + Print version.`)) + plaintext = flag.Bool("plaintext", false, prettify(` + Use plain-text HTTP/2 when connecting to server (no TLS).`)) + insecure = flag.Bool("insecure", false, prettify(` + Skip server certificate and domain verification. (NOT SECURE!) Not + valid with -plaintext option.`)) + cacert = flag.String("cacert", "", prettify(` + File containing trusted root certificates for verifying the server. + Ignored if -insecure is specified.`)) + cert = flag.String("cert", "", prettify(` + File containing client certificate (public key), to present to the + server. Not valid with -plaintext option. Must also provide -key option.`)) + key = flag.String("key", "", prettify(` + File containing client private key, to present to the server. Not valid + with -plaintext option. Must also provide -cert option.`)) protoset multiString protoFiles multiString importPaths multiString addlHeaders multiString rpcHeaders multiString reflHeaders multiString - authority = flag.String("authority", "", - `:authority pseudo header value to be passed along with underlying HTTP/2 - requests. It defaults to 'host [ ":" port ]' part of the target url.`) - data = flag.String("d", "", - `Data for request contents. If the value is '@' then the request contents - are read from stdin. For calls that accept a stream of requests, the - contents should include all such request messages concatenated together - (optionally separated by whitespace).`) - format = flag.String("format", "json", - `The format of request data. The allowed values are 'json' or 'text'. For - 'json', the input data must be in JSON format. Multiple request values may - be concatenated (messages with a JSON representation other than object - must be separated by whitespace, such as a newline). For 'text', the input - data must be in the protobuf text format, in which case multiple request - values must be separated by the "record separate" ASCII character: 0x1E. - The stream should not end in a record separator. If it does, it will be - interpreted as a final, blank message after the separator.`) - connectTimeout = flag.String("connect-timeout", "", - `The maximum time, in seconds, to wait for connection to be established. - Defaults to 10 seconds.`) - keepaliveTime = flag.String("keepalive-time", "", - `If present, the maximum idle time in seconds, after which a keepalive - probe is sent. If the connection remains idle and no keepalive response - is received for this same period then the connection is closed and the - operation fails.`) - maxTime = flag.String("max-time", "", - `The maximum total time the operation can take. This is useful for - preventing batch jobs that use grpcurl from hanging due to slow or bad - network links or due to incorrect stream method usage.`) - emitDefaults = flag.Bool("emit-defaults", false, - `Emit default values for JSON-encoded responses.`) - msgTemplate = flag.Bool("msg-template", false, - `When describing messages, show a template of input data.`) - verbose = flag.Bool("v", false, - `Enable verbose output.`) - serverName = flag.String("servername", "", "Override servername when validating TLS certificate.") + authority = flag.String("authority", "", prettify(` + Value of :authority pseudo-header to be use with underlying HTTP/2 + requests. It defaults to the given address.`)) + data = flag.String("d", "", prettify(` + Data for request contents. If the value is '@' then the request contents + are read from stdin. For calls that accept a stream of requests, the + contents should include all such request messages concatenated together + (possibly delimited; see -format).`)) + format = flag.String("format", "json", prettify(` + The format of request data. The allowed values are 'json' or 'text'. For + 'json', the input data must be in JSON format. Multiple request values + may be concatenated (messages with a JSON representation other than + object must be separated by whitespace, such as a newline). For 'text', + the input data must be in the protobuf text format, in which case + multiple request values must be separated by the "record separator" + ASCII character: 0x1E. The stream should not end in a record separator. + If it does, it will be interpreted as a final, blank message after the + separator.`)) + connectTimeout = flag.String("connect-timeout", "", prettify(` + The maximum time, in seconds, to wait for connection to be established. + Defaults to 10 seconds.`)) + keepaliveTime = flag.String("keepalive-time", "", prettify(` + If present, the maximum idle time in seconds, after which a keepalive + probe is sent. If the connection remains idle and no keepalive response + is received for this same period then the connection is closed and the + operation fails.`)) + maxTime = flag.String("max-time", "", prettify(` + The maximum total time the operation can take. This is useful for + preventing batch jobs that use grpcurl from hanging due to slow or bad + network links or due to incorrect stream method usage.`)) + emitDefaults = flag.Bool("emit-defaults", false, prettify(` + Emit default values for JSON-encoded responses.`)) + msgTemplate = flag.Bool("msg-template", false, prettify(` + When describing messages, show a template of input data.`)) + verbose = flag.Bool("v", false, prettify(` + Enable verbose output.`)) + serverName = flag.String("servername", "", prettify(` + Override server name when validating TLS certificate.`)) ) func init() { - flag.Var(&addlHeaders, "H", - `Additional headers in 'name: value' format. May specify more than one - via multiple flags. These headers will also be included in reflection - requests requests to a server.`) - flag.Var(&rpcHeaders, "rpc-header", - `Additional RPC headers in 'name: value' format. May specify more than - one via multiple flags. These headers will *only* be used when invoking - the requested RPC method. They are excluded from reflection requests.`) - flag.Var(&reflHeaders, "reflect-header", - `Additional reflection headers in 'name: value' format. May specify more - than one via multiple flags. These headers will only be used during - reflection requests and will be excluded when invoking the requested RPC - method.`) - flag.Var(&protoset, "protoset", - `The name of a file containing an encoded FileDescriptorSet. This file's - contents will be used to determine the RPC schema instead of querying - for it from the remote server via the GRPC reflection API. When set: the - 'list' action lists the services found in the given descriptors (vs. - those exposed by the remote server), and the 'describe' action describes - symbols found in the given descriptors. May specify more than one via - multiple -protoset flags. It is an error to use both -protoset and - -proto flags.`) - flag.Var(&protoFiles, "proto", - `The name of a proto source file. Source files given will be used to - determine the RPC schema instead of querying for it from the remote - server via the GRPC reflection API. When set: the 'list' action lists - the services found in the given files and their imports (vs. those - exposed by the remote server), and the 'describe' action describes - symbols found in the given files. May specify more than one via - multiple -proto flags. Imports will be resolved using the given - -import-path flags. Multiple proto files can be specified by specifying - multiple -proto flags. It is an error to use both -protoset and -proto - flags.`) - flag.Var(&importPaths, "import-path", - `The path to a directory from which proto sources can be imported, - for use with -proto flags. Multiple import paths can be configured by - specifying multiple -import-path flags. Paths will be searched in the - order given. If no import paths are given, all files (including all - imports) must be provided as -proto flags, and grpcurl will attempt to - resolve all import statements from the set of file names given.`) + flag.Var(&addlHeaders, "H", prettify(` + Additional headers in 'name: value' format. May specify more than one + via multiple flags. These headers will also be included in reflection + requests requests to a server.`)) + flag.Var(&rpcHeaders, "rpc-header", prettify(` + Additional RPC headers in 'name: value' format. May specify more than + one via multiple flags. These headers will *only* be used when invoking + the requested RPC method. They are excluded from reflection requests.`)) + flag.Var(&reflHeaders, "reflect-header", prettify(` + Additional reflection headers in 'name: value' format. May specify more + than one via multiple flags. These headers will *only* be used during + reflection requests and will be excluded when invoking the requested RPC + method.`)) + flag.Var(&protoset, "protoset", prettify(` + The name of a file containing an encoded FileDescriptorSet. This file's + contents will be used to determine the RPC schema instead of querying + for it from the remote server via the gRPC reflection API. When set: the + 'list' action lists the services found in the given descriptors (vs. + those exposed by the remote server), and the 'describe' action describes + symbols found in the given descriptors. May specify more than one via + multiple -protoset flags. It is an error to use both -protoset and + -proto flags.`)) + flag.Var(&protoFiles, "proto", prettify(` + The name of a proto source file. Source files given will be used to + determine the RPC schema instead of querying for it from the remote + server via the gRPC reflection API. When set: the 'list' action lists + the services found in the given files and their imports (vs. those + exposed by the remote server), and the 'describe' action describes + symbols found in the given files. May specify more than one via multiple + -proto flags. Imports will be resolved using the given -import-path + flags. Multiple proto files can be specified by specifying multiple + -proto flags. It is an error to use both -protoset and -proto flags.`)) + flag.Var(&importPaths, "import-path", prettify(` + The path to a directory from which proto sources can be imported, for + use with -proto flags. Multiple import paths can be configured by + specifying multiple -import-path flags. Paths will be searched in the + order given. If no import paths are given, all files (including all + imports) must be provided as -proto flags, and grpcurl will attempt to + resolve all import statements from the set of file names given.`)) } type multiString []string @@ -184,6 +185,9 @@ func main() { if *format != "json" && *format != "text" { fail(nil, "The -format option must be 'json' or 'text.") } + if *emitDefaults && *format != "json" { + warn("The -emit-defaults is only used when using json format.") + } args := flag.Args() @@ -496,8 +500,8 @@ func usage() { fmt.Fprintf(os.Stderr, `Usage: %s [flags] [address] [list|describe] [symbol] -The 'host:port' is only optional when used with 'list' or 'describe' and a -protoset flag is provided. +The 'address' is only optional when used with 'list' or 'describe' and a +protoset or proto flag is provided. If 'list' is indicated, the symbol (if present) should be a fully-qualified service name. If present, all methods of that service are listed. If not @@ -509,17 +513,36 @@ is given then the descriptors for all exposed or known services are shown. If neither verb is present, the symbol must be a fully-qualified method name in 'service/method' or 'service.method' format. In this case, the request body will -be used to invoke the named method. If no body is given, an empty instance of -the method's request type will be sent. +be used to invoke the named method. If no body is given but one is required +(i.e. the method is unary or server-streaming), an empty instance of the +method's request type will be sent. The address will typically be in the form "host:port" where host can be an IP address or a hostname and port is a numeric port or service name. If an IPv6 address is given, it must be surrounded by brackets, like "[2001:db8::1]". For Unix variants, if a -unix=true flag is present, then the address must be the path to the domain socket. + +Available flags: `, os.Args[0]) flag.PrintDefaults() +} +func prettify(docString string) string { + parts := strings.Split(docString, "\n") + + // cull empty lines and also remove trailing and leading spaces + // from each line in the doc string + j := 0 + for _, part := range parts { + if part == "" { + continue + } + parts[j] = strings.TrimSpace(part) + j++ + } + + return strings.Join(parts[:j], "\n"+indent()) } func warn(msg string, args ...interface{}) { diff --git a/cmd/grpcurl/indent_test.go b/cmd/grpcurl/indent_test.go new file mode 100644 index 0000000..52f123a --- /dev/null +++ b/cmd/grpcurl/indent_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "bytes" + "flag" + "testing" +) + +func TestFlagDocIndent(t *testing.T) { + // Tests the prettify() and indent() function. The indent() function + // differs by Go version, due to differences in "flags" package across + // versions. Run with multiple versions of Go to ensure that doc output + // is properly indented, regardless of Go version. + + var fs flag.FlagSet + var buf bytes.Buffer + fs.SetOutput(&buf) + + fs.String("foo", "", prettify(` + This is a flag doc string. + It has multiple lines. + More than two, actually.`)) + fs.Int("bar", 100, prettify(`This is a simple flag doc string.`)) + fs.Bool("baz", false, prettify(` + This is another long doc string. + It also has multiple lines. But not as long as the first one.`)) + + fs.PrintDefaults() + + expected := + ` -bar int + This is a simple flag doc string. (default 100) + -baz + This is another long doc string. + It also has multiple lines. But not as long as the first one. + -foo string + This is a flag doc string. + It has multiple lines. + More than two, actually. +` + + actual := buf.String() + if actual != expected { + t.Errorf("Flag output had wrong indentation.\nExpecting:\n%s\nGot:\n%s", expected, actual) + } +} diff --git a/cmd/grpcurl/unix.go b/cmd/grpcurl/unix.go index c3bbed5..3f4f5c7 100644 --- a/cmd/grpcurl/unix.go +++ b/cmd/grpcurl/unix.go @@ -5,8 +5,8 @@ package main import "flag" var ( - unix = flag.Bool("unix", false, - `Indicates that the server address is the path to a Unix domain socket.`) + unix = flag.Bool("unix", false, prettify(` + Indicates that the server address is the path to a Unix domain socket.`)) ) func init() {