vendor: Update dependencies

Update to the latest available version of our dependencies.

Signed-off-by: Rodrigo Chiossi <rodrigo.chiossi@intel.com>
This commit is contained in:
Rodrigo Chiossi
2019-07-01 21:56:51 +02:00
committed by Rodrigo Chiossi
parent 5228ca5649
commit c02e87e6aa
99 changed files with 7892 additions and 5159 deletions
Generated
+29 -11
View File
@@ -2,44 +2,62 @@
[[projects]]
digest = "1:e4b30804a381d7603b8a344009987c1ba351c26043501b23b8c7ce21f0b67474"
name = "github.com/BurntSushi/toml"
packages = ["."]
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
version = "v0.3.0"
pruneopts = ""
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
version = "v0.3.1"
[[projects]]
digest = "1:03edf882162b807cdf1bc558c66226167fa2f8eb44359eac2eeb3794a91cb168"
name = "github.com/go-ini/ini"
packages = ["."]
revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a"
version = "v1.32.0"
pruneopts = ""
revision = "c85607071cf08ca1adaf48319cd1aa322e81d8c1"
version = "v1.42.0"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = ""
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
digest = "1:1d7e1867c49a6dd9856598ef7c3123604ea3daabf5b83f303ff457bcbc410b1d"
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
pruneopts = ""
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
digest = "1:0c63b3c7ad6d825a898f28cb854252a3b29d37700c68a117a977263f5ec94efe"
name = "github.com/spf13/cobra"
packages = ["."]
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
version = "v0.0.1"
pruneopts = ""
revision = "f2b07da1e2c38d5f12845a4f607e2e1018cbb1f5"
version = "v0.0.5"
[[projects]]
digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66"
name = "github.com/spf13/pflag"
packages = ["."]
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
pruneopts = ""
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
version = "v1.0.3"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "54f12c04bf30404052834c768761deef34ce0e0d44c5ca84126cad8f32ec391d"
input-imports = [
"github.com/BurntSushi/toml",
"github.com/go-ini/ini",
"github.com/pkg/errors",
"github.com/spf13/cobra",
"github.com/spf13/pflag",
]
solver-name = "gps-cdcl"
solver-version = 1
+4 -4
View File
@@ -22,16 +22,16 @@
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
version = "0.8.1"
[[constraint]]
name = "github.com/spf13/cobra"
version = "0.0.1"
version = "0.0.5"
[[constraint]]
name = "github.com/go-ini/ini"
version = "1.32.0"
version = "1.42.0"
[[constraint]]
name = "github.com/BurntSushi/toml"
version = "v0.3.0"
version = "v0.3.1"
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+17 -10
View File
@@ -1,14 +1,21 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
The MIT License (MIT)
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Copyright (c) 2013 TOML authors
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+14
View File
@@ -16,6 +16,8 @@ age = 250
andrew = "gallant"
kait = "brady"
now = 1987-07-05T05:45:00Z
nowEast = 2017-06-22T16:15:21+08:00
nowWest = 2017-06-22T02:14:36-06:00
yesOrNo = true
pi = 3.14
colors = [
@@ -38,6 +40,8 @@ cauchy = "cat 2"
Pi float64
YesOrNo bool
Now time.Time
NowEast time.Time
NowWest time.Time
Andrew string
Kait string
My map[string]cats
@@ -53,11 +57,21 @@ cauchy = "cat 2"
if err != nil {
panic(err)
}
nowEast, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T16:15:21+08:00")
if err != nil {
panic(err)
}
nowWest, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-06-22T02:14:36-06:00")
if err != nil {
panic(err)
}
var answer = simple{
Age: 250,
Andrew: "gallant",
Kait: "brady",
Now: now,
NowEast: nowEast,
NowWest: nowWest,
YesOrNo: true,
Pi: 3.14,
Colors: [][]string{
+1 -1
View File
@@ -775,7 +775,7 @@ func lexDatetime(lx *lexer) stateFn {
return lexDatetime
}
switch r {
case '-', 'T', ':', '.', 'Z':
case '-', 'T', ':', '.', 'Z', '+':
return lexDatetime
}
+4 -2
View File
@@ -1,15 +1,17 @@
sudo: false
language: go
go:
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
script:
script:
- go get golang.org/x/tools/cmd/cover
- go get github.com/smartystreets/goconvey
- mkdir -p $HOME/gopath/src/gopkg.in
- ln -s $HOME/gopath/src/github.com/go-ini/ini $HOME/gopath/src/gopkg.in/ini.v1
- cd $HOME/gopath/src/gopkg.in/ini.v1
- go test -v -cover -race
+1 -1
View File
@@ -12,4 +12,4 @@ vet:
go vet
coverage:
go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out
go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out
+12 -729
View File
@@ -1,15 +1,13 @@
INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://sourcegraph.com/github.com/go-ini/ini/-/badge.svg)](https://sourcegraph.com/github.com/go-ini/ini?badge)
INI [![Build Status](https://travis-ci.org/go-ini/ini.svg?branch=master)](https://travis-ci.org/go-ini/ini) [![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg)](https://sourcegraph.com/github.com/go-ini/ini)
===
![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
Package ini provides INI file read and write functionality in Go.
[简体中文](README_ZH.md)
## Features
## Feature
- Load multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites.
- Load from multiple data sources(`[]byte`, file and `io.ReadCloser`) with overwrites.
- Read with recursion values.
- Read with parent-child sections.
- Read with auto-increment key names.
@@ -22,741 +20,26 @@ Package ini provides INI file read and write functionality in Go.
## Installation
The minimum requirement of Go is **1.6**.
To use a tagged revision:
go get gopkg.in/ini.v1
```sh
$ go get gopkg.in/ini.v1
```
To use with latest changes:
go get github.com/go-ini/ini
```sh
$ go get github.com/go-ini/ini
```
Please add `-u` flag to update in the future.
### Testing
If you want to test on your machine, please apply `-t` flag:
go get -t gopkg.in/ini.v1
Please add `-u` flag to update in the future.
## Getting Started
### Loading from data sources
A **Data Source** is either raw data in type `[]byte`, a file name with type `string` or `io.ReadCloser`. You can load **as many data sources as you want**. Passing other types will simply return an error.
```go
cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data"))))
```
Or start with an empty object:
```go
cfg := ini.Empty()
```
When you cannot decide how many data sources to load at the beginning, you will still be able to **Append()** them later.
```go
err := cfg.Append("other file", []byte("other raw data"))
```
If you have a list of files with possibilities that some of them may not available at the time, and you don't know exactly which ones, you can use `LooseLoad` to ignore nonexistent files without returning error.
```go
cfg, err := ini.LooseLoad("filename", "filename_404")
```
The cool thing is, whenever the file is available to load while you're calling `Reload` method, it will be counted as usual.
#### Ignore cases of key name
When you do not care about cases of section and key names, you can use `InsensitiveLoad` to force all names to be lowercased while parsing.
```go
cfg, err := ini.InsensitiveLoad("filename")
//...
// sec1 and sec2 are the exactly same section object
sec1, err := cfg.GetSection("Section")
sec2, err := cfg.GetSection("SecTIOn")
// key1 and key2 are the exactly same key object
key1, err := sec1.GetKey("Key")
key2, err := sec2.GetKey("KeY")
```
#### MySQL-like boolean key
MySQL's configuration allows a key without value as follows:
```ini
[mysqld]
...
skip-host-cache
skip-name-resolve
```
By default, this is considered as missing value. But if you know you're going to deal with those cases, you can assign advanced load options:
```go
cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
```
The value of those keys are always `true`, and when you save to a file, it will keep in the same foramt as you read.
To generate such keys in your program, you could use `NewBooleanKey`:
```go
key, err := sec.NewBooleanKey("skip-host-cache")
```
#### Comment
Take care that following format will be treated as comment:
1. Line begins with `#` or `;`
2. Words after `#` or `;`
3. Words after section name (i.e words after `[some section name]`)
If you want to save a value with `#` or `;`, please quote them with ``` ` ``` or ``` """ ```.
Alternatively, you can use following `LoadOptions` to completely ignore inline comments:
```go
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini"))
```
### Working with sections
To get a section, you would need to:
```go
section, err := cfg.GetSection("section name")
```
For a shortcut for default section, just give an empty string as name:
```go
section, err := cfg.GetSection("")
```
When you're pretty sure the section exists, following code could make your life easier:
```go
section := cfg.Section("section name")
```
What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you.
To create a new section:
```go
err := cfg.NewSection("new section")
```
To get a list of sections or section names:
```go
sections := cfg.Sections()
names := cfg.SectionStrings()
```
### Working with keys
To get a key under a section:
```go
key, err := cfg.Section("").GetKey("key name")
```
Same rule applies to key operations:
```go
key := cfg.Section("").Key("key name")
```
To check if a key exists:
```go
yes := cfg.Section("").HasKey("key name")
```
To create a new key:
```go
err := cfg.Section("").NewKey("name", "value")
```
To get a list of keys or key names:
```go
keys := cfg.Section("").Keys()
names := cfg.Section("").KeyStrings()
```
To get a clone hash of keys and corresponding values:
```go
hash := cfg.Section("").KeysHash()
```
### Working with values
To get a string value:
```go
val := cfg.Section("").Key("key name").String()
```
To validate key value on the fly:
```go
val := cfg.Section("").Key("key name").Validate(func(in string) string {
if len(in) == 0 {
return "default"
}
return in
})
```
If you do not want any auto-transformation (such as recursive read) for the values, you can get raw value directly (this way you get much better performance):
```go
val := cfg.Section("").Key("key name").Value()
```
To check if raw value exists:
```go
yes := cfg.Section("").HasValue("test value")
```
To get value with types:
```go
// For boolean values:
// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
// false when value is: 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
v, err = cfg.Section("").Key("BOOL").Bool()
v, err = cfg.Section("").Key("FLOAT64").Float64()
v, err = cfg.Section("").Key("INT").Int()
v, err = cfg.Section("").Key("INT64").Int64()
v, err = cfg.Section("").Key("UINT").Uint()
v, err = cfg.Section("").Key("UINT64").Uint64()
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
v = cfg.Section("").Key("BOOL").MustBool()
v = cfg.Section("").Key("FLOAT64").MustFloat64()
v = cfg.Section("").Key("INT").MustInt()
v = cfg.Section("").Key("INT64").MustInt64()
v = cfg.Section("").Key("UINT").MustUint()
v = cfg.Section("").Key("UINT64").MustUint64()
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
// Methods start with Must also accept one argument for default value
// when key not found or fail to parse value to given type.
// Except method MustString, which you have to pass a default value.
v = cfg.Section("").Key("String").MustString("default")
v = cfg.Section("").Key("BOOL").MustBool(true)
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
v = cfg.Section("").Key("INT").MustInt(10)
v = cfg.Section("").Key("INT64").MustInt64(99)
v = cfg.Section("").Key("UINT").MustUint(3)
v = cfg.Section("").Key("UINT64").MustUint64(6)
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
```
What if my value is three-line long?
```ini
[advance]
ADDRESS = """404 road,
NotFound, State, 5000
Earth"""
```
Not a problem!
```go
cfg.Section("advance").Key("ADDRESS").String()
/* --- start ---
404 road,
NotFound, State, 5000
Earth
------ end --- */
```
That's cool, how about continuation lines?
```ini
[advance]
two_lines = how about \
continuation lines?
lots_of_lines = 1 \
2 \
3 \
4
```
Piece of cake!
```go
cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
```
Well, I hate continuation lines, how do I disable that?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{
IgnoreContinuation: true,
}, "filename")
```
Holy crap!
Note that single quotes around values will be stripped:
```ini
foo = "some value" // foo: some value
bar = 'some value' // bar: some value
```
Sometimes you downloaded file from [Crowdin](https://crowdin.com/) has values like the following (value is surrounded by double quotes and quotes in the value are escaped):
```ini
create_repo="created repository <a href=\"%s\">%s</a>"
```
How do you transform this to regular format automatically?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini"))
cfg.Section("<name of your section>").Key("create_repo").String()
// You got: created repository <a href="%s">%s</a>
```
That's all? Hmm, no.
#### Helper methods of working with values
To get value with given candidates:
```go
v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
```
Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
To validate value in a given range:
```go
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
```
##### Auto-split values into a slice
To use zero value of type for invalid inputs:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
vals = cfg.Section("").Key("STRINGS").Strings(",")
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
vals = cfg.Section("").Key("INTS").Ints(",")
vals = cfg.Section("").Key("INT64S").Int64s(",")
vals = cfg.Section("").Key("UINTS").Uints(",")
vals = cfg.Section("").Key("UINT64S").Uint64s(",")
vals = cfg.Section("").Key("TIMES").Times(",")
```
To exclude invalid values out of result slice:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> [2.2]
vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
vals = cfg.Section("").Key("INTS").ValidInts(",")
vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
vals = cfg.Section("").Key("UINTS").ValidUints(",")
vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
vals = cfg.Section("").Key("TIMES").ValidTimes(",")
```
Or to return nothing but error when have invalid inputs:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> error
vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
vals = cfg.Section("").Key("INTS").StrictInts(",")
vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
vals = cfg.Section("").Key("UINTS").StrictUints(",")
vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
vals = cfg.Section("").Key("TIMES").StrictTimes(",")
```
### Save your configuration
Finally, it's time to save your configuration to somewhere.
A typical way to save configuration is writing it to a file:
```go
// ...
err = cfg.SaveTo("my.ini")
err = cfg.SaveToIndent("my.ini", "\t")
```
Another way to save is writing to a `io.Writer` interface:
```go
// ...
cfg.WriteTo(writer)
cfg.WriteToIndent(writer, "\t")
```
By default, spaces are used to align "=" sign between key and values, to disable that:
```go
ini.PrettyFormat = false
```
## Advanced Usage
### Recursive Values
For all value of keys, there is a special syntax `%(<name>)s`, where `<name>` is the key name in same section or default section, and `%(<name>)s` will be replaced by corresponding value(empty string if key not found). You can use this syntax at most 99 level of recursions.
```ini
NAME = ini
[author]
NAME = Unknwon
GITHUB = https://github.com/%(NAME)s
[package]
FULL_NAME = github.com/go-ini/%(NAME)s
```
```go
cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon
cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini
```
### Parent-child Sections
You can use `.` in section name to indicate parent-child relationship between two or more sections. If the key not found in the child section, library will try again on its parent section until there is no parent section.
```ini
NAME = ini
VERSION = v1
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
[package]
CLONE_URL = https://%(IMPORT_PATH)s
[package.sub]
```
```go
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
```
#### Retrieve parent keys available to a child section
```go
cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
```
### Unparseable Sections
Sometimes, you have sections that do not contain key-value pairs but raw content, to handle such case, you can use `LoadOptions.UnparsableSections`:
```go
cfg, err := ini.LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
body := cfg.Section("COMMENTS").Body()
/* --- start ---
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
------ end --- */
```
### Auto-increment Key Names
If key name is `-` in data source, then it would be seen as special syntax for auto-increment key name start from 1, and every section is independent on counter.
```ini
[features]
-: Support read/write comments of keys and sections
-: Support auto-increment of key names
-: Support load multiple files to overwrite key values
```
```go
cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"}
```
### Map To Struct
Want more objective way to play with INI? Cool.
```ini
Name = Unknwon
age = 21
Male = true
Born = 1993-01-01T20:17:05Z
[Note]
Content = Hi is a good man!
Cities = HangZhou, Boston
```
```go
type Note struct {
Content string
Cities []string
}
type Person struct {
Name string
Age int `ini:"age"`
Male bool
Born time.Time
Note
Created time.Time `ini:"-"`
}
func main() {
cfg, err := ini.Load("path/to/ini")
// ...
p := new(Person)
err = cfg.MapTo(p)
// ...
// Things can be simpler.
err = ini.MapTo(p, "path/to/ini")
// ...
// Just map a section? Fine.
n := new(Note)
err = cfg.Section("Note").MapTo(n)
// ...
}
```
Can I have default value for field? Absolutely.
Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type.
```go
// ...
p := &Person{
Name: "Joe",
}
// ...
```
It's really cool, but what's the point if you can't give me my file back from struct?
### Reflect From Struct
Why not?
```go
type Embeded struct {
Dates []time.Time `delim:"|" comment:"Time data"`
Places []string `ini:"places,omitempty"`
None []int `ini:",omitempty"`
}
type Author struct {
Name string `ini:"NAME"`
Male bool
Age int `comment:"Author's age"`
GPA float64
NeverMind string `ini:"-"`
*Embeded `comment:"Embeded section"`
}
func main() {
a := &Author{"Unknwon", true, 21, 2.8, "",
&Embeded{
[]time.Time{time.Now(), time.Now()},
[]string{"HangZhou", "Boston"},
[]int{},
}}
cfg := ini.Empty()
err = ini.ReflectFrom(cfg, a)
// ...
}
```
So, what do I get?
```ini
NAME = Unknwon
Male = true
; Author's age
Age = 21
GPA = 2.8
; Embeded section
[Embeded]
; Time data
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
places = HangZhou,Boston
```
#### Name Mapper
To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual section and key name.
There are 2 built-in name mappers:
- `AllCapsUnderscore`: it converts to format `ALL_CAPS_UNDERSCORE` then match section or key.
- `TitleUnderscore`: it converts to format `title_underscore` then match section or key.
To use them:
```go
type Info struct {
PackageName string
}
func main() {
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
// ...
cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
// ...
info := new(Info)
cfg.NameMapper = ini.AllCapsUnderscore
err = cfg.MapTo(info)
// ...
}
```
Same rules of name mapper apply to `ini.ReflectFromWithMapper` function.
#### Value Mapper
To expand values (e.g. from environment variables), you can use the `ValueMapper` to transform values:
```go
type Env struct {
Foo string `ini:"foo"`
}
func main() {
cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
cfg.ValueMapper = os.ExpandEnv
// ...
env := &Env{}
err = cfg.Section("env").MapTo(env)
}
```
This would set the value of `env.Foo` to the value of the environment variable `MY_VAR`.
#### Other Notes On Map/Reflect
Any embedded struct is treated as a section by default, and there is no automatic parent-child relations in map/reflect feature:
```go
type Child struct {
Age string
}
type Parent struct {
Name string
Child
}
type Config struct {
City string
Parent
}
```
Example configuration:
```ini
City = Boston
[Parent]
Name = Unknwon
[Child]
Age = 21
```
What if, yes, I'm paranoid, I want embedded struct to be in the same section. Well, all roads lead to Rome.
```go
type Child struct {
Age string
}
type Parent struct {
Name string
Child `ini:"Parent"`
}
type Config struct {
City string
Parent
}
```
Example configuration:
```ini
City = Boston
[Parent]
Name = Unknwon
Age = 21
```
## Getting Help
- [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
- [File An Issue](https://github.com/go-ini/ini/issues/new)
## FAQs
### What does `BlockMode` field do?
By default, library lets you read and write values so we need a locker to make sure your data is safe. But in cases that you are very sure about only reading data through the library, you can set `cfg.BlockMode = false` to speed up read operations about **50-70%** faster.
### Why another INI library?
Many people are using my another INI library [goconfig](https://github.com/Unknwon/goconfig), so the reason for this one is I would like to make more Go style code. Also when you set `cfg.BlockMode = false`, this one is about **10-30%** faster.
To make those changes I have to confirm API broken, so it's safer to keep it in another place and start using `gopkg.in` to version my package at this time.(PS: shorter import path)
## License
-750
View File
@@ -1,750 +0,0 @@
本包提供了 Go 语言中读写 INI 文件的功能。
## 功能特性
- 支持覆盖加载多个数据源(`[]byte`、文件和 `io.ReadCloser`
- 支持递归读取键值
- 支持读取父子分区
- 支持读取自增键名
- 支持读取多行的键值
- 支持大量辅助方法
- 支持在读取时直接转换为 Go 语言类型
- 支持读取和 **写入** 分区和键的注释
- 轻松操作分区、键值和注释
- 在保存文件时分区和键值会保持原有的顺序
## 下载安装
使用一个特定版本:
go get gopkg.in/ini.v1
使用最新版:
go get github.com/go-ini/ini
如需更新请添加 `-u` 选项。
### 测试安装
如果您想要在自己的机器上运行测试,请使用 `-t` 标记:
go get -t gopkg.in/ini.v1
如需更新请添加 `-u` 选项。
## 开始使用
### 从数据源加载
一个 **数据源** 可以是 `[]byte` 类型的原始数据,`string` 类型的文件路径或 `io.ReadCloser`。您可以加载 **任意多个** 数据源。如果您传递其它类型的数据源,则会直接返回错误。
```go
cfg, err := ini.Load([]byte("raw data"), "filename", ioutil.NopCloser(bytes.NewReader([]byte("some other data"))))
```
或者从一个空白的文件开始:
```go
cfg := ini.Empty()
```
当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
```go
err := cfg.Append("other file", []byte("other raw data"))
```
当您想要加载一系列文件,但是不能够确定其中哪些文件是不存在的,可以通过调用函数 `LooseLoad` 来忽略它们(`Load` 会因为文件不存在而返回错误):
```go
cfg, err := ini.LooseLoad("filename", "filename_404")
```
更牛逼的是,当那些之前不存在的文件在重新调用 `Reload` 方法的时候突然出现了,那么它们会被正常加载。
#### 忽略键名的大小写
有时候分区和键的名称大小写混合非常烦人,这个时候就可以通过 `InsensitiveLoad` 将所有分区和键名在读取里强制转换为小写:
```go
cfg, err := ini.InsensitiveLoad("filename")
//...
// sec1 和 sec2 指向同一个分区对象
sec1, err := cfg.GetSection("Section")
sec2, err := cfg.GetSection("SecTIOn")
// key1 和 key2 指向同一个键对象
key1, err := sec1.GetKey("Key")
key2, err := sec2.GetKey("KeY")
```
#### 类似 MySQL 配置中的布尔值键
MySQL 的配置文件中会出现没有具体值的布尔类型的键:
```ini
[mysqld]
...
skip-host-cache
skip-name-resolve
```
默认情况下这被认为是缺失值而无法完成解析,但可以通过高级的加载选项对它们进行处理:
```go
cfg, err := ini.LoadSources(ini.LoadOptions{AllowBooleanKeys: true}, "my.cnf"))
```
这些键的值永远为 `true`,且在保存到文件时也只会输出键名。
如果您想要通过程序来生成此类键,则可以使用 `NewBooleanKey`
```go
key, err := sec.NewBooleanKey("skip-host-cache")
```
#### 关于注释
下述几种情况的内容将被视为注释:
1. 所有以 `#``;` 开头的行
2. 所有在 `#``;` 之后的内容
3. 分区标签后的文字 (即 `[分区名]` 之后的内容)
如果你希望使用包含 `#``;` 的值,请使用 ``` ` ``` 或 ``` """ ``` 进行包覆。
除此之外,您还可以通过 `LoadOptions` 完全忽略行内注释:
```go
cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, "app.ini"))
```
### 操作分区(Section
获取指定分区:
```go
section, err := cfg.GetSection("section name")
```
如果您想要获取默认分区,则可以用空字符串代替分区名:
```go
section, err := cfg.GetSection("")
```
当您非常确定某个分区是存在的,可以使用以下简便方法:
```go
section := cfg.Section("section name")
```
如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。
创建一个分区:
```go
err := cfg.NewSection("new section")
```
获取所有分区对象或名称:
```go
sections := cfg.Sections()
names := cfg.SectionStrings()
```
### 操作键(Key
获取某个分区下的键:
```go
key, err := cfg.Section("").GetKey("key name")
```
和分区一样,您也可以直接获取键而忽略错误处理:
```go
key := cfg.Section("").Key("key name")
```
判断某个键是否存在:
```go
yes := cfg.Section("").HasKey("key name")
```
创建一个新的键:
```go
err := cfg.Section("").NewKey("name", "value")
```
获取分区下的所有键或键名:
```go
keys := cfg.Section("").Keys()
names := cfg.Section("").KeyStrings()
```
获取分区下的所有键值对的克隆:
```go
hash := cfg.Section("").KeysHash()
```
### 操作键值(Value
获取一个类型为字符串(string)的值:
```go
val := cfg.Section("").Key("key name").String()
```
获取值的同时通过自定义函数进行处理验证:
```go
val := cfg.Section("").Key("key name").Validate(func(in string) string {
if len(in) == 0 {
return "default"
}
return in
})
```
如果您不需要任何对值的自动转变功能(例如递归读取),可以直接获取原值(这种方式性能最佳):
```go
val := cfg.Section("").Key("key name").Value()
```
判断某个原值是否存在:
```go
yes := cfg.Section("").HasValue("test value")
```
获取其它类型的值:
```go
// 布尔值的规则:
// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On
// false 当值为:0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off
v, err = cfg.Section("").Key("BOOL").Bool()
v, err = cfg.Section("").Key("FLOAT64").Float64()
v, err = cfg.Section("").Key("INT").Int()
v, err = cfg.Section("").Key("INT64").Int64()
v, err = cfg.Section("").Key("UINT").Uint()
v, err = cfg.Section("").Key("UINT64").Uint64()
v, err = cfg.Section("").Key("TIME").TimeFormat(time.RFC3339)
v, err = cfg.Section("").Key("TIME").Time() // RFC3339
v = cfg.Section("").Key("BOOL").MustBool()
v = cfg.Section("").Key("FLOAT64").MustFloat64()
v = cfg.Section("").Key("INT").MustInt()
v = cfg.Section("").Key("INT64").MustInt64()
v = cfg.Section("").Key("UINT").MustUint()
v = cfg.Section("").Key("UINT64").MustUint64()
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339)
v = cfg.Section("").Key("TIME").MustTime() // RFC3339
// 由 Must 开头的方法名允许接收一个相同类型的参数来作为默认值,
// 当键不存在或者转换失败时,则会直接返回该默认值。
// 但是,MustString 方法必须传递一个默认值。
v = cfg.Seciont("").Key("String").MustString("default")
v = cfg.Section("").Key("BOOL").MustBool(true)
v = cfg.Section("").Key("FLOAT64").MustFloat64(1.25)
v = cfg.Section("").Key("INT").MustInt(10)
v = cfg.Section("").Key("INT64").MustInt64(99)
v = cfg.Section("").Key("UINT").MustUint(3)
v = cfg.Section("").Key("UINT64").MustUint64(6)
v = cfg.Section("").Key("TIME").MustTimeFormat(time.RFC3339, time.Now())
v = cfg.Section("").Key("TIME").MustTime(time.Now()) // RFC3339
```
如果我的值有好多行怎么办?
```ini
[advance]
ADDRESS = """404 road,
NotFound, State, 5000
Earth"""
```
嗯哼?小 case
```go
cfg.Section("advance").Key("ADDRESS").String()
/* --- start ---
404 road,
NotFound, State, 5000
Earth
------ end --- */
```
赞爆了!那要是我属于一行的内容写不下想要写到第二行怎么办?
```ini
[advance]
two_lines = how about \
continuation lines?
lots_of_lines = 1 \
2 \
3 \
4
```
简直是小菜一碟!
```go
cfg.Section("advance").Key("two_lines").String() // how about continuation lines?
cfg.Section("advance").Key("lots_of_lines").String() // 1 2 3 4
```
可是我有时候觉得两行连在一起特别没劲,怎么才能不自动连接两行呢?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{
IgnoreContinuation: true,
}, "filename")
```
哇靠给力啊!
需要注意的是,值两侧的单引号会被自动剔除:
```ini
foo = "some value" // foo: some value
bar = 'some value' // bar: some value
```
有时您会获得像从 [Crowdin](https://crowdin.com/) 网站下载的文件那样具有特殊格式的值(值使用双引号括起来,内部的双引号被转义):
```ini
create_repo="创建了仓库 <a href=\"%s\">%s</a>"
```
那么,怎么自动地将这类值进行处理呢?
```go
cfg, err := ini.LoadSources(ini.LoadOptions{UnescapeValueDoubleQuotes: true}, "en-US.ini"))
cfg.Section("<name of your section>").Key("create_repo").String()
// You got: 创建了仓库 <a href="%s">%s</a>
```
这就是全部了?哈哈,当然不是。
#### 操作键值的辅助方法
获取键值时设定候选值:
```go
v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
v = cfg.Section("").Key("UINT").InUint(4, []int{3, 6, 9})
v = cfg.Section("").Key("UINT64").InUint64(8, []int64{3, 6, 9})
v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
```
如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
验证获取的值是否在指定范围内:
```go
vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
vals = cfg.Section("").Key("UINT").RangeUint(0, 3, 9)
vals = cfg.Section("").Key("UINT64").RangeUint64(0, 3, 9)
vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
```
##### 自动分割键值到切片(slice)
当存在无效输入时,使用零值代替:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> [0.0 2.2 0.0 0.0]
vals = cfg.Section("").Key("STRINGS").Strings(",")
vals = cfg.Section("").Key("FLOAT64S").Float64s(",")
vals = cfg.Section("").Key("INTS").Ints(",")
vals = cfg.Section("").Key("INT64S").Int64s(",")
vals = cfg.Section("").Key("UINTS").Uints(",")
vals = cfg.Section("").Key("UINT64S").Uint64s(",")
vals = cfg.Section("").Key("TIMES").Times(",")
```
从结果切片中剔除无效输入:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> [2.2]
vals = cfg.Section("").Key("FLOAT64S").ValidFloat64s(",")
vals = cfg.Section("").Key("INTS").ValidInts(",")
vals = cfg.Section("").Key("INT64S").ValidInt64s(",")
vals = cfg.Section("").Key("UINTS").ValidUints(",")
vals = cfg.Section("").Key("UINT64S").ValidUint64s(",")
vals = cfg.Section("").Key("TIMES").ValidTimes(",")
```
当存在无效输入时,直接返回错误:
```go
// Input: 1.1, 2.2, 3.3, 4.4 -> [1.1 2.2 3.3 4.4]
// Input: how, 2.2, are, you -> error
vals = cfg.Section("").Key("FLOAT64S").StrictFloat64s(",")
vals = cfg.Section("").Key("INTS").StrictInts(",")
vals = cfg.Section("").Key("INT64S").StrictInt64s(",")
vals = cfg.Section("").Key("UINTS").StrictUints(",")
vals = cfg.Section("").Key("UINT64S").StrictUint64s(",")
vals = cfg.Section("").Key("TIMES").StrictTimes(",")
```
### 保存配置
终于到了这个时刻,是时候保存一下配置了。
比较原始的做法是输出配置到某个文件:
```go
// ...
err = cfg.SaveTo("my.ini")
err = cfg.SaveToIndent("my.ini", "\t")
```
另一个比较高级的做法是写入到任何实现 `io.Writer` 接口的对象中:
```go
// ...
cfg.WriteTo(writer)
cfg.WriteToIndent(writer, "\t")
```
默认情况下,空格将被用于对齐键值之间的等号以美化输出结果,以下代码可以禁用该功能:
```go
ini.PrettyFormat = false
```
## 高级用法
### 递归读取键值
在获取所有键值的过程中,特殊语法 `%(<name>)s` 会被应用,其中 `<name>` 可以是相同分区或者默认分区下的键名。字符串 `%(<name>)s` 会被相应的键值所替代,如果指定的键不存在,则会用空字符串替代。您可以最多使用 99 层的递归嵌套。
```ini
NAME = ini
[author]
NAME = Unknwon
GITHUB = https://github.com/%(NAME)s
[package]
FULL_NAME = github.com/go-ini/%(NAME)s
```
```go
cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon
cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini
```
### 读取父子分区
您可以在分区名称中使用 `.` 来表示两个或多个分区之间的父子关系。如果某个键在子分区中不存在,则会去它的父分区中再次寻找,直到没有父分区为止。
```ini
NAME = ini
VERSION = v1
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
[package]
CLONE_URL = https://%(IMPORT_PATH)s
[package.sub]
```
```go
cfg.Section("package.sub").Key("CLONE_URL").String() // https://gopkg.in/ini.v1
```
#### 获取上级父分区下的所有键名
```go
cfg.Section("package.sub").ParentKeys() // ["CLONE_URL"]
```
### 无法解析的分区
如果遇到一些比较特殊的分区,它们不包含常见的键值对,而是没有固定格式的纯文本,则可以使用 `LoadOptions.UnparsableSections` 进行处理:
```go
cfg, err := LoadSources(ini.LoadOptions{UnparseableSections: []string{"COMMENTS"}}, `[COMMENTS]
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>`))
body := cfg.Section("COMMENTS").Body()
/* --- start ---
<1><L.Slide#2> This slide has the fuel listed in the wrong units <e.1>
------ end --- */
```
### 读取自增键名
如果数据源中的键名为 `-`,则认为该键使用了自增键名的特殊语法。计数器从 1 开始,并且分区之间是相互独立的。
```ini
[features]
-: Support read/write comments of keys and sections
-: Support auto-increment of key names
-: Support load multiple files to overwrite key values
```
```go
cfg.Section("features").KeyStrings() // []{"#1", "#2", "#3"}
```
### 映射到结构
想要使用更加面向对象的方式玩转 INI 吗?好主意。
```ini
Name = Unknwon
age = 21
Male = true
Born = 1993-01-01T20:17:05Z
[Note]
Content = Hi is a good man!
Cities = HangZhou, Boston
```
```go
type Note struct {
Content string
Cities []string
}
type Person struct {
Name string
Age int `ini:"age"`
Male bool
Born time.Time
Note
Created time.Time `ini:"-"`
}
func main() {
cfg, err := ini.Load("path/to/ini")
// ...
p := new(Person)
err = cfg.MapTo(p)
// ...
// 一切竟可以如此的简单。
err = ini.MapTo(p, "path/to/ini")
// ...
// 嗯哼?只需要映射一个分区吗?
n := new(Note)
err = cfg.Section("Note").MapTo(n)
// ...
}
```
结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。
```go
// ...
p := &Person{
Name: "Joe",
}
// ...
```
这样玩 INI 真的好酷啊!然而,如果不能还给我原来的配置文件,有什么卵用?
### 从结构反射
可是,我有说不能吗?
```go
type Embeded struct {
Dates []time.Time `delim:"|" comment:"Time data"`
Places []string `ini:"places,omitempty"`
None []int `ini:",omitempty"`
}
type Author struct {
Name string `ini:"NAME"`
Male bool
Age int `comment:"Author's age"`
GPA float64
NeverMind string `ini:"-"`
*Embeded `comment:"Embeded section"`
}
func main() {
a := &Author{"Unknwon", true, 21, 2.8, "",
&Embeded{
[]time.Time{time.Now(), time.Now()},
[]string{"HangZhou", "Boston"},
[]int{},
}}
cfg := ini.Empty()
err = ini.ReflectFrom(cfg, a)
// ...
}
```
瞧瞧,奇迹发生了。
```ini
NAME = Unknwon
Male = true
; Author's age
Age = 21
GPA = 2.8
; Embeded section
[Embeded]
; Time data
Dates = 2015-08-07T22:14:22+08:00|2015-08-07T22:14:22+08:00
places = HangZhou,Boston
```
#### 名称映射器(Name Mapper
为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
目前有 2 款内置的映射器:
- `AllCapsUnderscore`:该映射器将字段名转换至格式 `ALL_CAPS_UNDERSCORE` 后再去匹配分区名和键名。
- `TitleUnderscore`:该映射器将字段名转换至格式 `title_underscore` 后再去匹配分区名和键名。
使用方法:
```go
type Info struct{
PackageName string
}
func main() {
err = ini.MapToWithMapper(&Info{}, ini.TitleUnderscore, []byte("package_name=ini"))
// ...
cfg, err := ini.Load([]byte("PACKAGE_NAME=ini"))
// ...
info := new(Info)
cfg.NameMapper = ini.AllCapsUnderscore
err = cfg.MapTo(info)
// ...
}
```
使用函数 `ini.ReflectFromWithMapper` 时也可应用相同的规则。
#### 值映射器(Value Mapper
值映射器允许使用一个自定义函数自动展开值的具体内容,例如:运行时获取环境变量:
```go
type Env struct {
Foo string `ini:"foo"`
}
func main() {
cfg, err := ini.Load([]byte("[env]\nfoo = ${MY_VAR}\n")
cfg.ValueMapper = os.ExpandEnv
// ...
env := &Env{}
err = cfg.Section("env").MapTo(env)
}
```
本例中,`env.Foo` 将会是运行时所获取到环境变量 `MY_VAR` 的值。
#### 映射/反射的其它说明
任何嵌入的结构都会被默认认作一个不同的分区,并且不会自动产生所谓的父子分区关联:
```go
type Child struct {
Age string
}
type Parent struct {
Name string
Child
}
type Config struct {
City string
Parent
}
```
示例配置文件:
```ini
City = Boston
[Parent]
Name = Unknwon
[Child]
Age = 21
```
很好,但是,我就是要嵌入结构也在同一个分区。好吧,你爹是李刚!
```go
type Child struct {
Age string
}
type Parent struct {
Name string
Child `ini:"Parent"`
}
type Config struct {
City string
Parent
}
```
示例配置文件:
```ini
City = Boston
[Parent]
Name = Unknwon
Age = 21
```
## 获取帮助
- [API 文档](https://gowalker.org/gopkg.in/ini.v1)
- [创建工单](https://github.com/go-ini/ini/issues/new)
## 常见问题
### 字段 `BlockMode` 是什么?
默认情况下,本库会在您进行读写操作时采用锁机制来确保数据时间。但在某些情况下,您非常确定只进行读操作。此时,您可以通过设置 `cfg.BlockMode = false` 来将读操作提升大约 **50-70%** 的性能。
### 为什么要写另一个 INI 解析库?
许多人都在使用我的 [goconfig](https://github.com/Unknwon/goconfig) 来完成对 INI 文件的操作,但我希望使用更加 Go 风格的代码。并且当您设置 `cfg.BlockMode = false` 时,会有大约 **10-30%** 的性能提升。
为了做出这些改变,我必须对 API 进行破坏,所以新开一个仓库是最安全的做法。除此之外,本库直接使用 `gopkg.in` 来进行版本化发布。(其实真相是导入路径更短了)
+40 -20
View File
@@ -45,6 +45,9 @@ type File struct {
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
if len(opts.KeyValueDelimiters) == 0 {
opts.KeyValueDelimiters = "=:"
}
return &File{
BlockMode: true,
dataSources: dataSources,
@@ -140,9 +143,14 @@ func (f *File) Section(name string) *Section {
// Section returns list of Section.
func (f *File) Sections() []*Section {
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sections := make([]*Section, len(f.sectionList))
for i := range f.sectionList {
sections[i] = f.Section(f.sectionList[i])
for i, name := range f.sectionList {
sections[i] = f.sections[name]
}
return sections
}
@@ -222,8 +230,9 @@ func (f *File) Append(source interface{}, others ...interface{}) error {
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := "="
if PrettyFormat {
equalSign := DefaultFormatLeft + "=" + DefaultFormatRight
if PrettyFormat || PrettyEqual {
equalSign = " = "
}
@@ -232,13 +241,18 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
for i, sname := range f.sectionList {
sec := f.Section(sname)
if len(sec.Comment) > 0 {
if sec.Comment[0] != '#' && sec.Comment[0] != ';' {
sec.Comment = "; " + sec.Comment
} else {
sec.Comment = sec.Comment[:1] + " " + strings.TrimSpace(sec.Comment[1:])
}
if _, err := buf.WriteString(sec.Comment + LineBreak); err != nil {
return nil, err
// Support multiline comments
lines := strings.Split(sec.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + lines[i]
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
@@ -275,7 +289,7 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.ContainsAny(kname, "\"=:") {
if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
@@ -295,13 +309,19 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
if len(indent) > 0 && sname != DEFAULT_SECTION {
buf.WriteString(indent)
}
if key.Comment[0] != '#' && key.Comment[0] != ';' {
key.Comment = "; " + key.Comment
} else {
key.Comment = key.Comment[:1] + " " + strings.TrimSpace(key.Comment[1:])
}
if _, err := buf.WriteString(key.Comment + LineBreak); err != nil {
return nil, err
// Support multiline comments
lines := strings.Split(key.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + strings.TrimSpace(lines[i])
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
@@ -312,7 +332,7 @@ func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
switch {
case key.isAutoIncrement:
kname = "-"
case strings.ContainsAny(kname, "\"=:"):
case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
+31 -79
View File
@@ -16,6 +16,7 @@ package ini_test
import (
"bytes"
"io/ioutil"
"testing"
. "github.com/smartystreets/goconvey/convey"
@@ -253,93 +254,44 @@ func TestFile_WriteTo(t *testing.T) {
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, `; Package name
NAME = ini
; Package version
VERSION = v1
; Package import path
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
; Information about package author
# Bio can be written in multiple lines.
[author]
; This is author name
NAME = Unknwon
E-MAIL = u@gogs.io
GITHUB = https://github.com/%(NAME)s
# Succeeding comment
BIO = """Gopher.
Coding addict.
Good man.
"""
golden := "testdata/TestFile_WriteTo.golden"
if *update {
ioutil.WriteFile(golden, buf.Bytes(), 0644)
}
[package]
CLONE_URL = https://%(IMPORT_PATH)s
expected, err := ioutil.ReadFile(golden)
So(err, ShouldBeNil)
So(buf.String(), ShouldEqual, string(expected))
})
[package.sub]
UNUSED_KEY = should be deleted
Convey("Support multiline comments", t, func() {
f, err := ini.Load([]byte(`
#
# general.domain
#
# Domain name of XX system.
domain = mydomain.com
`))
So(err, ShouldBeNil)
[features]
- = Support read/write comments of keys and sections
- = Support auto-increment of key names
- = Support load multiple files to overwrite key values
f.Section("").Key("test").Comment = "Multiline\nComment"
[types]
STRING = str
BOOL = true
BOOL_FALSE = false
FLOAT64 = 1.25
INT = 10
TIME = 2015-01-01T20:17:05Z
DURATION = 2h45m
UINT = 3
var buf bytes.Buffer
_, err = f.WriteTo(&buf)
So(err, ShouldBeNil)
[array]
STRINGS = en, zh, de
FLOAT64S = 1.1, 2.2, 3.3
INTS = 1, 2, 3
UINTS = 1, 2, 3
TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
[note]
empty_lines = next line is empty
boolean_key
more = notes
; Comment before the section
; This is a comment for the section too
[comments]
; Comment before key
key = value
; This is a comment for key2
key2 = value2
key3 = "one", "two", "three"
[string escapes]
key1 = value1, value2, value3
key2 = value1\, value2
key3 = val\ue1, value2
key4 = value1\\, value\\\\2
key5 = value1\,, value2
key6 = aaa bbb\ and\ space ccc
[advance]
value with quotes = some value
value quote2 again = some value
includes comment sign = `+"`"+"my#password"+"`"+`
includes comment sign2 = `+"`"+"my;password"+"`"+`
true = 2+3=5
`+"`"+`1+1=2`+"`"+` = true
`+"`"+`6+1=7`+"`"+` = true
"""`+"`"+`5+5`+"`"+`""" = 10
`+"`"+`"6+6"`+"`"+` = 12
`+"`"+`7-2=4`+"`"+` = false
ADDRESS = """404 road,
NotFound, State, 50000"""
two_lines = how about continuation lines?
lots_of_lines = 1 2 3 4
So(buf.String(), ShouldEqual, `#
# general.domain
#
# Domain name of XX system.
domain = mydomain.com
; Multiline
; Comment
test =
`)
})
}
+28 -3
View File
@@ -1,3 +1,5 @@
// +build go1.6
// Copyright 2014 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
@@ -32,7 +34,7 @@ const (
// Maximum allowed depth when recursively substituing variable names.
_DEPTH_VALUES = 99
_VERSION = "1.32.0"
_VERSION = "1.42.0"
)
// Version returns current package version literal.
@@ -46,6 +48,10 @@ var (
// at package init time.
LineBreak = "\n"
// Place custom spaces when PrettyFormat and PrettyEqual are both disabled
DefaultFormatLeft = ""
DefaultFormatRight = ""
// Variable regexp pattern: %(variable)s
varPattern = regexp.MustCompile(`%\(([^\)]+)\)s`)
@@ -53,6 +59,9 @@ var (
// or reduce all possible spaces for compact format.
PrettyFormat = true
// Place spaces around "=" sign even when PrettyFormat is false
PrettyEqual = false
// Explicitly write DEFAULT section header
DefaultHeader = false
@@ -129,6 +138,8 @@ type LoadOptions struct {
IgnoreContinuation bool
// IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
IgnoreInlineComment bool
// SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs.
SkipUnrecognizableLines bool
// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
// This type of keys are mostly used in my.cnf.
AllowBooleanKeys bool
@@ -137,6 +148,16 @@ type LoadOptions struct {
// AllowNestedValues indicates whether to allow AWS-like nested values.
// Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
AllowNestedValues bool
// AllowPythonMultilineValues indicates whether to allow Python-like multi-line values.
// Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
// Relevant quote: Values can also span multiple lines, as long as they are indented deeper
// than the first line of the value.
AllowPythonMultilineValues bool
// SpaceBeforeInlineComment indicates whether to allow comment symbols (\# and \;) inside value.
// Docs: https://docs.python.org/2/library/configparser.html
// Quote: Comments may appear on their own in an otherwise empty line, or may be entered in lines holding values or section names.
// In the latter case, they need to be preceded by a whitespace character to be recognized as a comment.
SpaceBeforeInlineComment bool
// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
UnescapeValueDoubleQuotes bool
@@ -144,9 +165,13 @@ type LoadOptions struct {
// when value is NOT surrounded by any quotes.
// Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
UnescapeValueCommentSymbols bool
// Some INI formats allow group blocks that store a block of raw content that doesn't otherwise
// UnparseableSections stores a list of blocks that are allowed with raw content which do not otherwise
// conform to key/value pairs. Specify the names of those blocks here.
UnparseableSections []string
// KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:".
KeyValueDelimiters string
// PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes).
PreserveSurroundedQuote bool
}
func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
@@ -187,7 +212,7 @@ func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Insensitive: true}, source, others...)
}
// InsensitiveLoad has exactly same functionality as Load function
// ShadowLoad has exactly same functionality as Load function
// except it allows have shadow keys.
func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
+1097 -151
View File
File diff suppressed because it is too large Load Diff
+11 -10
View File
@@ -133,8 +133,7 @@ func (k *Key) transformValue(val string) string {
}
// Take off leading '%(' and trailing ')s'.
noption := strings.TrimLeft(vr, "%(")
noption = strings.TrimRight(noption, ")s")
noption := vr[2 : len(vr)-2]
// Search in the same section.
nk, err := k.s.GetKey(noption)
@@ -187,23 +186,24 @@ func (k *Key) Float64() (float64, error) {
// Int returns int type value.
func (k *Key) Int() (int, error) {
return strconv.Atoi(k.String())
v, err := strconv.ParseInt(k.String(), 0, 64)
return int(v), err
}
// Int64 returns int64 type value.
func (k *Key) Int64() (int64, error) {
return strconv.ParseInt(k.String(), 10, 64)
return strconv.ParseInt(k.String(), 0, 64)
}
// Uint returns uint type valued.
func (k *Key) Uint() (uint, error) {
u, e := strconv.ParseUint(k.String(), 10, 64)
u, e := strconv.ParseUint(k.String(), 0, 64)
return uint(u), e
}
// Uint64 returns uint64 type value.
func (k *Key) Uint64() (uint64, error) {
return strconv.ParseUint(k.String(), 10, 64)
return strconv.ParseUint(k.String(), 0, 64)
}
// Duration returns time.Duration type value.
@@ -668,7 +668,8 @@ func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]
func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
vals := make([]int, 0, len(strs))
for _, str := range strs {
val, err := strconv.Atoi(str)
valInt64, err := strconv.ParseInt(str, 0, 64)
val := int(valInt64)
if err != nil && returnOnInvalid {
return nil, err
}
@@ -683,7 +684,7 @@ func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int,
func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
vals := make([]int64, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseInt(str, 10, 64)
val, err := strconv.ParseInt(str, 0, 64)
if err != nil && returnOnInvalid {
return nil, err
}
@@ -698,7 +699,7 @@ func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]in
func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
vals := make([]uint, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseUint(str, 10, 0)
val, err := strconv.ParseUint(str, 0, 0)
if err != nil && returnOnInvalid {
return nil, err
}
@@ -713,7 +714,7 @@ func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uin
func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
vals := make([]uint64, 0, len(strs))
for _, str := range strs {
val, err := strconv.ParseUint(str, 10, 64)
val, err := strconv.ParseUint(str, 0, 64)
if err != nil && returnOnInvalid {
return nil, err
}
+11 -1
View File
@@ -216,6 +216,10 @@ func TestKey_Helpers(t *testing.T) {
So(err, ShouldBeNil)
So(v7.String(), ShouldEqual, t.String())
v8, err := sec.Key("HEX_NUMBER").Int()
So(err, ShouldBeNil)
So(v8, ShouldEqual, 0x3000)
Convey("Must get values with type", func() {
So(sec.Key("STRING").MustString("404"), ShouldEqual, "str")
So(sec.Key("BOOL").MustBool(), ShouldBeTrue)
@@ -225,6 +229,7 @@ func TestKey_Helpers(t *testing.T) {
So(sec.Key("UINT").MustUint(), ShouldEqual, 3)
So(sec.Key("UINT").MustUint64(), ShouldEqual, 3)
So(sec.Key("TIME").MustTime().String(), ShouldEqual, t.String())
So(sec.Key("HEX_NUMBER").MustInt(), ShouldEqual, 0x3000)
dur, err := time.ParseDuration("2h45m")
So(err, ShouldBeNil)
@@ -238,6 +243,7 @@ func TestKey_Helpers(t *testing.T) {
So(sec.Key("INT64_404").MustInt64(15), ShouldEqual, 15)
So(sec.Key("UINT_404").MustUint(6), ShouldEqual, 6)
So(sec.Key("UINT64_404").MustUint64(6), ShouldEqual, 6)
So(sec.Key("HEX_NUMBER_404").MustInt(0x3001), ShouldEqual, 0x3001)
t, err := time.Parse(time.RFC3339, "2014-01-01T20:17:05Z")
So(err, ShouldBeNil)
@@ -255,6 +261,7 @@ func TestKey_Helpers(t *testing.T) {
So(sec.Key("UINT64_404").String(), ShouldEqual, "6")
So(sec.Key("TIME_404").String(), ShouldEqual, "2014-01-01T20:17:05Z")
So(sec.Key("DURATION_404").String(), ShouldEqual, "2h45m0s")
So(sec.Key("HEX_NUMBER_404").String(), ShouldEqual, "12289")
})
})
})
@@ -514,10 +521,13 @@ func TestRecursiveValues(t *testing.T) {
Convey("Recursive values should not reflect on same key", t, func() {
f, err := ini.Load([]byte(`
NAME = ini
expires = yes
[package]
NAME = %(NAME)s`))
NAME = %(NAME)s
expires = %(expires)s`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("package").Key("NAME").String(), ShouldEqual, "ini")
So(f.Section("package").Key("expires").String(), ShouldEqual, "yes")
})
}
+114 -27
View File
@@ -19,11 +19,14 @@ import (
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)
var pythonMultiline = regexp.MustCompile("^(\\s+)([^\n]+)")
type tokenType int
const (
@@ -97,7 +100,7 @@ func cleanComment(in []byte) ([]byte, bool) {
return in[i:], true
}
func readKeyName(in []byte) (string, int, error) {
func readKeyName(delimiters string, in []byte) (string, int, error) {
line := string(in)
// Check if key name surrounded by quotes.
@@ -124,7 +127,7 @@ func readKeyName(in []byte) (string, int, error) {
pos += startIdx
// Find key-value delimiter
i := strings.IndexAny(line[pos+startIdx:], "=:")
i := strings.IndexAny(line[pos+startIdx:], delimiters)
if i < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
@@ -132,7 +135,7 @@ func readKeyName(in []byte) (string, int, error) {
return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
}
endIdx = strings.IndexAny(line, "=:")
endIdx = strings.IndexAny(line, delimiters)
if endIdx < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
@@ -194,7 +197,8 @@ func hasSurroundedQuote(in string, quote byte) bool {
}
func (p *parser) readValue(in []byte,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols bool) (string, error) {
parserBufferSize int,
ignoreContinuation, ignoreInlineComment, unescapeValueDoubleQuotes, unescapeValueCommentSymbols, allowPythonMultilines, spaceBeforeInlineComment, preserveSurroundedQuote bool) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
@@ -224,26 +228,39 @@ func (p *parser) readValue(in []byte,
return line[startIdx : pos+startIdx], nil
}
lastChar := line[len(line)-1]
// Won't be able to reach here if value only contains whitespace
line = strings.TrimSpace(line)
trimmedLastChar := line[len(line)-1]
// Check continuation lines when desired
if !ignoreContinuation && line[len(line)-1] == '\\' {
if !ignoreContinuation && trimmedLastChar == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
// Check if ignore inline comment
if !ignoreInlineComment {
i := strings.IndexAny(line, "#;")
var i int
if spaceBeforeInlineComment {
i = strings.Index(line, " #")
if i == -1 {
i = strings.Index(line, " ;")
}
} else {
i = strings.IndexAny(line, "#;")
}
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}
}
// Trim single and double quotes
if hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"') {
if (hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"')) && !preserveSurroundedQuote {
line = line[1 : len(line)-1]
} else if len(valQuote) == 0 && unescapeValueCommentSymbols {
if strings.Contains(line, `\;`) {
@@ -252,7 +269,42 @@ func (p *parser) readValue(in []byte,
if strings.Contains(line, `\#`) {
line = strings.Replace(line, `\#`, "#", -1)
}
} else if allowPythonMultilines && lastChar == '\n' {
parserBufferPeekResult, _ := p.buf.Peek(parserBufferSize)
peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
val := line
for {
peekData, peekErr := peekBuffer.ReadBytes('\n')
if peekErr != nil {
if peekErr == io.EOF {
return val, nil
}
return "", peekErr
}
peekMatches := pythonMultiline.FindStringSubmatch(string(peekData))
if len(peekMatches) != 3 {
return val, nil
}
// NOTE: Return if not a python-ini multi-line value.
currentIdentSize := len(peekMatches[1])
if currentIdentSize <= 0 {
return val, nil
}
// NOTE: Just advance the parser reader (buffer) in-sync with the peek buffer.
_, err := p.readUntil('\n')
if err != nil {
return "", err
}
val += fmt.Sprintf("\n%s", peekMatches[2])
}
}
return line, nil
}
@@ -276,6 +328,28 @@ func (f *File) parse(reader io.Reader) (err error) {
var line []byte
var inUnparseableSection bool
// NOTE: Iterate and increase `currentPeekSize` until
// the size of the parser buffer is found.
// TODO(unknwon): When Golang 1.10 is the lowest version supported, replace with `parserBufferSize := p.buf.Size()`.
parserBufferSize := 0
// NOTE: Peek 1kb at a time.
currentPeekSize := 1024
if f.options.AllowPythonMultilineValues {
for {
peekBytes, _ := p.buf.Peek(currentPeekSize)
peekBytesLength := len(peekBytes)
if parserBufferSize >= peekBytesLength {
break
}
currentPeekSize *= 2
parserBufferSize = peekBytesLength
}
}
for !p.isEOF {
line, err = p.readUntil('\n')
if err != nil {
@@ -307,8 +381,7 @@ func (f *File) parse(reader io.Reader) (err error) {
// Section
if line[0] == '[' {
// Read to the next ']' (TODO: support quoted strings)
// TODO(unknwon): use LastIndexByte when stop supporting Go1.4
closeIdx := bytes.LastIndex(line, []byte("]"))
closeIdx := bytes.LastIndexByte(line, ']')
if closeIdx == -1 {
return fmt.Errorf("unclosed section: %s", line)
}
@@ -347,25 +420,35 @@ func (f *File) parse(reader io.Reader) (err error) {
continue
}
kname, offset, err := readKeyName(line)
kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line)
if err != nil {
// Treat as boolean key when desired, and whole line is key name.
if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
kname, err := p.readValue(line,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
if err != nil {
return err
if IsErrDelimiterNotFound(err) {
switch {
case f.options.AllowBooleanKeys:
kname, err := p.readValue(line,
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues,
f.options.SpaceBeforeInlineComment,
f.options.PreserveSurroundedQuote)
if err != nil {
return err
}
key, err := section.NewBooleanKey(kname)
if err != nil {
return err
}
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
case f.options.SkipUnrecognizableLines:
continue
}
key, err := section.NewBooleanKey(kname)
if err != nil {
return err
}
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
}
return err
}
@@ -379,10 +462,14 @@ func (f *File) parse(reader io.Reader) (err error) {
}
value, err := p.readValue(line[offset:],
parserBufferSize,
f.options.IgnoreContinuation,
f.options.IgnoreInlineComment,
f.options.UnescapeValueDoubleQuotes,
f.options.UnescapeValueCommentSymbols)
f.options.UnescapeValueCommentSymbols,
f.options.AllowPythonMultilineValues,
f.options.SpaceBeforeInlineComment,
f.options.PreserveSurroundedQuote)
if err != nil {
return err
}
+1 -1
View File
@@ -28,7 +28,7 @@ func TestBOM(t *testing.T) {
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "u@gogs.io")
So(f.Section("author").Key("E-MAIL").String(), ShouldEqual, "example@email.com")
})
Convey("UTF-16-LE-BOM", func() {
+2
View File
@@ -82,6 +82,7 @@ func (s *Section) NewKey(name, val string) (*Key, error) {
}
} else {
s.keys[name].value = val
s.keysHash[name] = val
}
return s.keys[name], nil
}
@@ -237,6 +238,7 @@ func (s *Section) DeleteKey(name string) {
if k == name {
s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
delete(s.keys, name)
delete(s.keysHash, name)
return
}
}
+16 -14
View File
@@ -272,24 +272,26 @@ func TestSection_KeyStrings(t *testing.T) {
func TestSection_KeyHash(t *testing.T) {
Convey("Get clone of key hash", t, func() {
f := ini.Empty()
f, err := ini.Load([]byte(`
key = one
[log]
name = app
file = a.log
`), []byte(`
key = two
[log]
name = app2
file = b.log
`))
So(err, ShouldBeNil)
So(f, ShouldNotBeNil)
k, err := f.Section("").NewKey("NAME", "ini")
So(err, ShouldBeNil)
So(k, ShouldNotBeNil)
k, err = f.Section("").NewKey("VERSION", "v1")
So(err, ShouldBeNil)
So(k, ShouldNotBeNil)
k, err = f.Section("").NewKey("IMPORT_PATH", "gopkg.in/ini.v1")
So(err, ShouldBeNil)
So(k, ShouldNotBeNil)
So(f.Section("").Key("key").String(), ShouldEqual, "two")
hash := f.Section("").KeysHash()
hash := f.Section("log").KeysHash()
relation := map[string]string{
"NAME": "ini",
"VERSION": "v1",
"IMPORT_PATH": "gopkg.in/ini.v1",
"name": "app2",
"file": "b.log",
}
for k, v := range hash {
So(v, ShouldEqual, relation[k])
+1 -1
View File
@@ -180,7 +180,7 @@ func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim stri
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
durationVal, err := key.Duration()
// Skip zero value
if err == nil && int(durationVal) > 0 {
if err == nil && uint64(durationVal) > 0 {
field.Set(reflect.ValueOf(durationVal))
return nil
}
+5 -5
View File
@@ -37,7 +37,7 @@ type testNested struct {
Unused int `ini:"-"`
}
type testEmbeded struct {
type TestEmbeded struct {
GPA float64
}
@@ -49,7 +49,7 @@ type testStruct struct {
Born time.Time
Time time.Duration `ini:"Duration"`
Others testNested
*testEmbeded `ini:"grade"`
*TestEmbeded `ini:"grade"`
Unused int `ini:"-"`
Unsigned uint
Omitted bool `ini:"omitthis,omitempty"`
@@ -97,12 +97,12 @@ type unsupport2 struct {
}
}
type unsupport3 struct {
type Unsupport3 struct {
Cities byte
}
type unsupport4 struct {
*unsupport3 `ini:"Others"`
*Unsupport3 `ini:"Others"`
}
type defaultValue struct {
@@ -155,7 +155,7 @@ func Test_MapToStruct(t *testing.T) {
So(fmt.Sprint(ts.Others.Populations), ShouldEqual, "[12345678 98765432]")
So(fmt.Sprint(ts.Others.Coordinates), ShouldEqual, "[192.168 10.11]")
So(ts.Others.Note, ShouldEqual, "Hello world!")
So(ts.testEmbeded.GPA, ShouldEqual, 2.8)
So(ts.TestEmbeded.GPA, ShouldEqual, 2.8)
})
Convey("Map section to struct", func() {
+87
View File
@@ -0,0 +1,87 @@
; Package name
NAME = ini
; Package version
VERSION = v1
; Package import path
IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
; Information about package author
# Bio can be written in multiple lines.
[author]
; This is author name
NAME = Unknwon
E-MAIL = u@gogs.io
GITHUB = https://github.com/%(NAME)s
# Succeeding comment
BIO = """Gopher.
Coding addict.
Good man.
"""
[package]
CLONE_URL = https://%(IMPORT_PATH)s
[package.sub]
UNUSED_KEY = should be deleted
[features]
- = Support read/write comments of keys and sections
- = Support auto-increment of key names
- = Support load multiple files to overwrite key values
[types]
STRING = str
BOOL = true
BOOL_FALSE = false
FLOAT64 = 1.25
INT = 10
TIME = 2015-01-01T20:17:05Z
DURATION = 2h45m
UINT = 3
HEX_NUMBER = 0x3000
[array]
STRINGS = en, zh, de
FLOAT64S = 1.1, 2.2, 3.3
INTS = 1, 2, 3
UINTS = 1, 2, 3
TIMES = 2015-01-01T20:17:05Z,2015-01-01T20:17:05Z,2015-01-01T20:17:05Z
[note]
empty_lines = next line is empty
boolean_key
more = notes
; Comment before the section
; This is a comment for the section too
[comments]
; Comment before key
key = value
; This is a comment for key2
key2 = value2
key3 = "one", "two", "three"
[string escapes]
key1 = value1, value2, value3
key2 = value1\, value2
key3 = val\ue1, value2
key4 = value1\\, value\\\\2
key5 = value1\,, value2
key6 = aaa bbb\ and\ space ccc
[advance]
value with quotes = some value
value quote2 again = some value
includes comment sign = `my#password`
includes comment sign2 = `my;password`
true = 2+3=5
`1+1=2` = true
`6+1=7` = true
"""`5+5`""" = 10
`"6+6"` = 12
`7-2=4` = false
ADDRESS = """404 road,
NotFound, State, 50000"""
two_lines = how about continuation lines?
lots_of_lines = 1 2 3 4
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,2 +1,2 @@
[author]
E-MAIL = u@gogs.io
E-MAIL = example@email.com
+1
View File
@@ -36,6 +36,7 @@ INT = 10
TIME = 2015-01-01T20:17:05Z
DURATION = 2h45m
UINT = 3
HEX_NUMBER = 0x3000
[array]
STRINGS = en, zh, de
+8 -4
View File
@@ -1,10 +1,14 @@
language: go
go_import_path: github.com/pkg/errors
go:
- 1.4.3
- 1.5.4
- 1.6.2
- 1.7.1
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- 1.11.x
- tip
script:
+2 -2
View File
@@ -1,4 +1,4 @@
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
Package errors provides simple error handling primitives.
@@ -47,6 +47,6 @@ We welcome pull requests, bug fixes and issue reports. With that said, the bar f
Before proposing a change, please discuss your change by raising an issue.
## Licence
## License
BSD-2-Clause
+6 -2
View File
@@ -15,6 +15,7 @@ func noErrors(at, depth int) error {
}
return noErrors(at+1, depth)
}
func yesErrors(at, depth int) error {
if at >= depth {
return New("ye error")
@@ -22,8 +23,11 @@ func yesErrors(at, depth int) error {
return yesErrors(at+1, depth)
}
// GlobalE is an exported global to store the result of benchmark results,
// preventing the compiler from optimising the benchmark functions away.
var GlobalE error
func BenchmarkErrors(b *testing.B) {
var toperr error
type run struct {
stack int
std bool
@@ -53,7 +57,7 @@ func BenchmarkErrors(b *testing.B) {
err = f(0, r.stack)
}
b.StopTimer()
toperr = err
GlobalE = err
})
}
}
+28 -15
View File
@@ -6,7 +6,7 @@
// return err
// }
//
// which applied recursively up the call stack results in error reports
// which when applied recursively up the call stack results in error reports
// without context or debugging information. The errors package allows
// programmers to add context to the failure path in their code in a way
// that does not destroy the original value of the error.
@@ -15,16 +15,17 @@
//
// The errors.Wrap function returns a new error that adds context to the
// original error by recording a stack trace at the point Wrap is called,
// and the supplied message. For example
// together with the supplied message. For example
//
// _, err := ioutil.ReadAll(r)
// if err != nil {
// return errors.Wrap(err, "read failed")
// }
//
// If additional control is required the errors.WithStack and errors.WithMessage
// functions destructure errors.Wrap into its component operations of annotating
// an error with a stack trace and an a message, respectively.
// If additional control is required, the errors.WithStack and
// errors.WithMessage functions destructure errors.Wrap into its component
// operations: annotating an error with a stack trace and with a message,
// respectively.
//
// Retrieving the cause of an error
//
@@ -38,7 +39,7 @@
// }
//
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
// the topmost error which does not implement causer, which is assumed to be
// the topmost error that does not implement causer, which is assumed to be
// the original cause. For example:
//
// switch err := errors.Cause(err).(type) {
@@ -48,16 +49,16 @@
// // unknown error
// }
//
// causer interface is not exported by this package, but is considered a part
// of stable public API.
// Although the causer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// Formatted printing of errors
//
// All error values returned from this package implement fmt.Formatter and can
// be formatted by the fmt package. The following verbs are supported
// be formatted by the fmt package. The following verbs are supported:
//
// %s print the error. If the error has a Cause it will be
// printed recursively
// printed recursively.
// %v see %s
// %+v extended format. Each Frame of the error's StackTrace will
// be printed in detail.
@@ -65,13 +66,13 @@
// Retrieving the stack trace of an error or wrapper
//
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
// invoked. This information can be retrieved with the following interface.
// invoked. This information can be retrieved with the following interface:
//
// type stackTracer interface {
// StackTrace() errors.StackTrace
// }
//
// Where errors.StackTrace is defined as
// The returned errors.StackTrace type is defined as
//
// type StackTrace []Frame
//
@@ -85,8 +86,8 @@
// }
// }
//
// stackTracer interface is not exported by this package, but is considered a part
// of stable public API.
// Although the stackTracer interface is not exported by this package, it is
// considered a part of its stable public interface.
//
// See the documentation for Frame.Format for more details.
package errors
@@ -192,7 +193,7 @@ func Wrap(err error, message string) error {
}
// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is call, and the format specifier.
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
@@ -220,6 +221,18 @@ func WithMessage(err error, message string) error {
}
}
// WithMessagef annotates err with the format specifier.
// If err is nil, WithMessagef returns nil.
func WithMessagef(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
}
type withMessage struct {
cause error
msg string
+25
View File
@@ -196,7 +196,32 @@ func TestWithMessage(t *testing.T) {
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
}
}
}
func TestWithMessagefNil(t *testing.T) {
got := WithMessagef(nil, "no error")
if got != nil {
t.Errorf("WithMessage(nil, \"no error\"): got %#v, expected nil", got)
}
}
func TestWithMessagef(t *testing.T) {
tests := []struct {
err error
message string
want string
}{
{io.EOF, "read error", "read error: EOF"},
{WithMessagef(io.EOF, "read error without format specifier"), "client error", "client error: read error without format specifier: EOF"},
{WithMessagef(io.EOF, "read error with %d format specifier", 1), "client error", "client error: read error with 1 format specifier: EOF"},
}
for _, tt := range tests {
got := WithMessagef(tt.err, tt.message).Error()
if got != tt.want {
t.Errorf("WithMessage(%v, %q): got: %q, want %q", tt.err, tt.message, got, tt.want)
}
}
}
// errors.New, etc values are not expected to be compared by value
+1 -1
View File
@@ -491,7 +491,7 @@ type wrapper struct {
want []string
}
func prettyBlocks(blocks []string, prefix ...string) string {
func prettyBlocks(blocks []string) string {
var out []string
for _, b := range blocks {
+10 -41
View File
@@ -46,7 +46,8 @@ func (f Frame) line() int {
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+s path of source file relative to the compile time GOPATH
// %+s function name and path of source file relative to the compile time
// GOPATH separated by \n\t (<funcname>\n\t<path>)
// %+v equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
switch verb {
@@ -79,6 +80,14 @@ func (f Frame) Format(s fmt.State, verb rune) {
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
type StackTrace []Frame
// Format formats the stack of Frames according to the fmt.Formatter interface.
//
// %s lists source files for each Frame in the stack
// %v lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+v Prints filename, function, and line number for each Frame in the stack.
func (st StackTrace) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
@@ -136,43 +145,3 @@ func funcname(name string) string {
i = strings.Index(name, ".")
return name[i+1:]
}
func trimGOPATH(name, file string) string {
// Here we want to get the source file path relative to the compile time
// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
// GOPATH at runtime, but we can infer the number of path segments in the
// GOPATH. We note that fn.Name() returns the function name qualified by
// the import path, which does not include the GOPATH. Thus we can trim
// segments from the beginning of the file path until the number of path
// separators remaining is one more than the number of path separators in
// the function name. For example, given:
//
// GOPATH /home/user
// file /home/user/src/pkg/sub/file.go
// fn.Name() pkg/sub.Type.Method
//
// We want to produce:
//
// pkg/sub/file.go
//
// From this we can easily see that fn.Name() has one less path separator
// than our desired output. We count separators from the end of the file
// path until it finds two more than in the function name and then move
// one character forward to preserve the initial path segment without a
// leading separator.
const sep = "/"
goal := strings.Count(name, sep) + 2
i := len(file)
for n := 0; n < goal; n++ {
i = strings.LastIndex(file[:i], sep)
if i == -1 {
// not enough separators found, set i so that the slice expression
// below leaves file unmodified
i = -len(sep)
break
}
}
// get back to 0 or trim the leading separator
file = file[i+len(sep):]
return file
}
+12 -30
View File
@@ -146,24 +146,6 @@ func TestFuncname(t *testing.T) {
}
}
func TestTrimGOPATH(t *testing.T) {
var tests = []struct {
Frame
want string
}{{
Frame(initpc),
"github.com/pkg/errors/stack_test.go",
}}
for i, tt := range tests {
pc := tt.Frame.pc()
fn := runtime.FuncForPC(pc)
file, _ := fn.FileLine(pc)
got := trimGOPATH(fn.Name(), file)
testFormatRegexp(t, i, got, "%s", tt.want)
}
}
func TestStackTrace(t *testing.T) {
tests := []struct {
err error
@@ -171,24 +153,24 @@ func TestStackTrace(t *testing.T) {
}{{
New("ooh"), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:172",
"\t.+/github.com/pkg/errors/stack_test.go:154",
},
}, {
Wrap(New("ooh"), "ahh"), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:177", // this is the stack of Wrap, not New
"\t.+/github.com/pkg/errors/stack_test.go:159", // this is the stack of Wrap, not New
},
}, {
Cause(Wrap(New("ooh"), "ahh")), []string{
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:182", // this is the stack of New
"\t.+/github.com/pkg/errors/stack_test.go:164", // this is the stack of New
},
}, {
func() error { return New("ooh") }(), []string{
`github.com/pkg/errors.(func·009|TestStackTrace.func1)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New
"\n\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:187", // this is the stack of New's caller
"\t.+/github.com/pkg/errors/stack_test.go:169", // this is the stack of New's caller
},
}, {
Cause(func() error {
@@ -197,11 +179,11 @@ func TestStackTrace(t *testing.T) {
}()
}()), []string{
`github.com/pkg/errors.(func·010|TestStackTrace.func2.1)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:196", // this is the stack of Errorf
"\n\t.+/github.com/pkg/errors/stack_test.go:178", // this is the stack of Errorf
`github.com/pkg/errors.(func·011|TestStackTrace.func2)` +
"\n\t.+/github.com/pkg/errors/stack_test.go:197", // this is the stack of Errorf's caller
"\n\t.+/github.com/pkg/errors/stack_test.go:179", // this is the stack of Errorf's caller
"github.com/pkg/errors.TestStackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:198", // this is the stack of Errorf's caller's caller
"\t.+/github.com/pkg/errors/stack_test.go:180", // this is the stack of Errorf's caller's caller
},
}}
for i, tt := range tests {
@@ -271,19 +253,19 @@ func TestStackTraceFormat(t *testing.T) {
}, {
stackTrace()[:2],
"%v",
`\[stack_test.go:225 stack_test.go:272\]`,
`\[stack_test.go:207 stack_test.go:254\]`,
}, {
stackTrace()[:2],
"%+v",
"\n" +
"github.com/pkg/errors.stackTrace\n" +
"\t.+/github.com/pkg/errors/stack_test.go:225\n" +
"\t.+/github.com/pkg/errors/stack_test.go:207\n" +
"github.com/pkg/errors.TestStackTraceFormat\n" +
"\t.+/github.com/pkg/errors/stack_test.go:276",
"\t.+/github.com/pkg/errors/stack_test.go:258",
}, {
stackTrace()[:2],
"%#v",
`\[\]errors.Frame{stack_test.go:225, stack_test.go:284}`,
`\[\]errors.Frame{stack_test.go:207, stack_test.go:266}`,
}}
for i, tt := range tests {
+53
View File
@@ -0,0 +1,53 @@
version: 2
references:
workspace: &workspace
/go/src/github.com/spf13/cobra
run_tests: &run_tests
run:
name: "All Commands"
command: |
mkdir -p bin
curl -Lso bin/shellcheck https://github.com/caarlos0/shellcheck-docker/releases/download/v0.4.6/shellcheck
chmod +x bin/shellcheck
go get -t -v ./...
PATH=$PATH:$PWD/bin go test -v ./...
go build
if [ -z $NOVET ]; then
diff -u <(echo -n) <(go vet . 2>&1 | grep -vE 'ExampleCommand|bash_completions.*Fprint');
fi
jobs:
go-current:
docker:
- image: circleci/golang:1.12
working_directory: *workspace
steps:
- checkout
- *run_tests
- run:
name: "Check formatting"
command: diff -u <(echo -n) <(gofmt -d -s .)
go-previous:
docker:
- image: circleci/golang:1.11
working_directory: *workspace
steps:
- checkout
- *run_tests
go-latest:
docker:
- image: circleci/golang:latest
working_directory: *workspace
steps:
- checkout
- *run_tests
workflows:
version: 2
main:
jobs:
- go-current
- go-previous
- go-latest
+2
View File
@@ -34,3 +34,5 @@ tags
*.exe
cobra.test
.idea/*
+18 -8
View File
@@ -1,21 +1,31 @@
language: go
stages:
- diff
- test
go:
- 1.10.x
- 1.11.x
- 1.12.x
- tip
matrix:
include:
- go: 1.7.6
- go: 1.8.3
- go: tip
allow_failures:
- go: tip
include:
- stage: diff
go: 1.12.x
script: diff -u <(echo -n) <(gofmt -d -s .)
before_install:
- mkdir -p bin
- curl -Lso bin/shellcheck https://github.com/caarlos0/shellcheck-docker/releases/download/v0.4.3/shellcheck
- curl -Lso bin/shellcheck https://github.com/caarlos0/shellcheck-docker/releases/download/v0.6.0/shellcheck
- chmod +x bin/shellcheck
- go get -u github.com/kyoh86/richgo
script:
- PATH=$PATH:$PWD/bin go test -v ./...
- PATH=$PATH:$PWD/bin richgo test -v ./...
- go build
- diff -u <(echo -n) <(gofmt -d -s .)
- if [ -z $NOVET ]; then
diff -u <(echo -n) <(go tool vet . 2>&1 | grep -vE 'ExampleCommand|bash_completions.*Fprint');
diff -u <(echo -n) <(go vet . 2>&1 | grep -vE 'ExampleCommand|bash_completions.*Fprint');
fi
+77 -57
View File
@@ -2,24 +2,29 @@
Cobra is both a library for creating powerful modern CLI applications as well as a program to generate applications and command files.
Many of the most widely used Go projects are built using Cobra including:
* [Kubernetes](http://kubernetes.io/)
* [Hugo](http://gohugo.io)
* [rkt](https://github.com/coreos/rkt)
* [etcd](https://github.com/coreos/etcd)
* [Moby (former Docker)](https://github.com/moby/moby)
* [Docker (distribution)](https://github.com/docker/distribution)
* [OpenShift](https://www.openshift.com/)
* [Delve](https://github.com/derekparker/delve)
* [GopherJS](http://www.gopherjs.org/)
* [CockroachDB](http://www.cockroachlabs.com/)
* [Bleve](http://www.blevesearch.com/)
* [ProjectAtomic (enterprise)](http://www.projectatomic.io/)
* [GiantSwarm's swarm](https://github.com/giantswarm/cli)
* [Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack)
* [rclone](http://rclone.org/)
* [nehm](https://github.com/bogem/nehm)
Many of the most widely used Go projects are built using Cobra, such as:
[Kubernetes](http://kubernetes.io/),
[Hugo](http://gohugo.io),
[rkt](https://github.com/coreos/rkt),
[etcd](https://github.com/coreos/etcd),
[Moby (former Docker)](https://github.com/moby/moby),
[Docker (distribution)](https://github.com/docker/distribution),
[OpenShift](https://www.openshift.com/),
[Delve](https://github.com/derekparker/delve),
[GopherJS](http://www.gopherjs.org/),
[CockroachDB](http://www.cockroachlabs.com/),
[Bleve](http://www.blevesearch.com/),
[ProjectAtomic (enterprise)](http://www.projectatomic.io/),
[Giant Swarm's gsctl](https://github.com/giantswarm/gsctl),
[Nanobox](https://github.com/nanobox-io/nanobox)/[Nanopack](https://github.com/nanopack),
[rclone](http://rclone.org/),
[nehm](https://github.com/bogem/nehm),
[Pouch](https://github.com/alibaba/pouch),
[Istio](https://istio.io),
[Prototool](https://github.com/uber/prototool),
[mattermost-server](https://github.com/mattermost/mattermost-server),
[Gardener](https://github.com/gardener/gardenctl),
etc.
[![Build Status](https://travis-ci.org/spf13/cobra.svg "Travis CI status")](https://travis-ci.org/spf13/cobra)
[![CircleCI status](https://circleci.com/gh/spf13/cobra.png?circle-token=:circle-token "CircleCI status")](https://circleci.com/gh/spf13/cobra)
@@ -44,6 +49,7 @@ Many of the most widely used Go projects are built using Cobra including:
* [Suggestions when "unknown command" happens](#suggestions-when-unknown-command-happens)
* [Generating documentation for your command](#generating-documentation-for-your-command)
* [Generating bash completions](#generating-bash-completions)
* [Generating zsh completions](#generating-zsh-completions)
- [Contributing](#contributing)
- [License](#license)
@@ -151,17 +157,11 @@ In a Cobra app, typically the main.go file is very bare. It serves one purpose:
package main
import (
"fmt"
"os"
"{pathToYourApp}/cmd"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
cmd.Execute()
}
```
@@ -174,7 +174,7 @@ commands you want. It's the easiest way to incorporate Cobra into your applicati
## Using the Cobra Library
To manually implement Cobra you need to create a bare main.go file and a RootCmd file.
To manually implement Cobra you need to create a bare main.go file and a rootCmd file.
You will optionally provide additional commands as you see fit.
### Create rootCmd
@@ -184,7 +184,7 @@ Cobra doesn't require any special constructors. Simply create your commands.
Ideally you place this in app/cmd/root.go:
```go
var RootCmd = &cobra.Command{
var rootCmd = &cobra.Command{
Use: "hugo",
Short: "Hugo is a very fast static site generator",
Long: `A Fast and Flexible Static Site Generator built with
@@ -194,6 +194,13 @@ var RootCmd = &cobra.Command{
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
```
You will additionally define flags and handle configuration in your init() function.
@@ -212,22 +219,18 @@ import (
func init() {
cobra.OnInitialize(initConfig)
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
RootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
RootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
RootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
RootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
viper.BindPFlag("author", RootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("projectbase", RootCmd.PersistentFlags().Lookup("projectbase"))
viper.BindPFlag("useViper", RootCmd.PersistentFlags().Lookup("viper"))
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "Author name for copyright attribution")
rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "Name of license for the project (can provide `licensetext` in config)")
rootCmd.PersistentFlags().Bool("viper", true, "Use Viper for configuration")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
viper.BindPFlag("projectbase", rootCmd.PersistentFlags().Lookup("projectbase"))
viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
viper.SetDefault("license", "apache")
}
func Execute() {
RootCmd.Execute()
}
func initConfig() {
// Don't forget to read config either from cfgFile or from home directory!
if cfgFile != "" {
@@ -264,17 +267,11 @@ In a Cobra app, typically the main.go file is very bare. It serves, one purpose,
package main
import (
"fmt"
"os"
"{pathToYourApp}/cmd"
)
func main() {
if err := cmd.RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
cmd.Execute()
}
```
@@ -290,12 +287,13 @@ populate it with the following:
package cmd
import (
"github.com/spf13/cobra"
"fmt"
"github.com/spf13/cobra"
)
func init() {
RootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(versionCmd)
}
var versionCmd = &cobra.Command{
@@ -332,7 +330,7 @@ command it's assigned to as well as every command under that command. For
global flags, assign a flag as a persistent flag on the root.
```go
RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
```
### Local Flags
@@ -340,13 +338,13 @@ RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose out
A flag can also be assigned locally which will only apply to that specific command.
```go
RootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
```
### Local Flag on Parent Commands
By default Cobra only parses local flags on the target command, any local flags on
parent commands are ignored. By enabling `Command.TraverseChildren` Cobra will
By default Cobra only parses local flags on the target command, any local flags on
parent commands are ignored. By enabling `Command.TraverseChildren` Cobra will
parse local flags on each command before executing the target command.
```go
@@ -363,8 +361,8 @@ You can also bind your flags with [viper](https://github.com/spf13/viper):
var author string
func init() {
RootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", RootCmd.PersistentFlags().Lookup("author"))
rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}
```
@@ -374,6 +372,15 @@ when the `--author` flag is not provided by user.
More in [viper documentation](https://github.com/spf13/viper#working-with-flags).
### Required flags
Flags are optional by default. If instead you wish your command to report an error
when a flag has not been set, mark it as required:
```go
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
```
## Positional and Custom Arguments
Validation of positional arguments can be specified using the `Args` field
@@ -387,6 +394,7 @@ The following validators are built in:
- `MinimumNArgs(int)` - the command will report an error if there are not at least N positional args.
- `MaximumNArgs(int)` - the command will report an error if there are more than N positional args.
- `ExactArgs(int)` - the command will report an error if there are not exactly N positional args.
- `ExactValidArgs(int)` - the command will report an error if there are not exactly N positional args OR if there are any positional args that are not in the `ValidArgs` field of `Command`
- `RangeArgs(min, max)` - the command will report an error if the number of args is not between the minimum and maximum number of expected args.
An example of setting the custom validator:
@@ -396,7 +404,7 @@ var cmd = &cobra.Command{
Short: "hello",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("requires at least one arg")
return errors.New("requires a color argument")
}
if myapp.IsValidColor(args[0]) {
return nil
@@ -456,7 +464,7 @@ Echo works a lot like print, except it has a child command.`,
}
var cmdTimes = &cobra.Command{
Use: "times [# times] [string to echo]",
Use: "times [string to echo]",
Short: "Echo anything to the screen more times",
Long: `echo things multiple times back to the user by providing
a count and a string.`,
@@ -522,7 +530,7 @@ around it. In fact, you can provide your own if you want.
### Defining your own help
You can provide your own Help command or your own template for the default command to use
with followind functions:
with following functions:
```go
cmd.SetHelpCommand(cmd *Command)
@@ -569,6 +577,13 @@ cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)
```
## Version Flag
Cobra adds a top-level '--version' flag if the Version field is set on the root command.
Running an application with the '--version' flag will print the version to stdout using
the version template. The template can be customized using the
`cmd.SetVersionTemplate(s string)` function.
## PreRun and PostRun Hooks
It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. These functions are run in the following order:
@@ -706,6 +721,11 @@ Cobra can generate documentation based on subcommands, flags, etc. in the follow
Cobra can generate a bash-completion file. If you add more information to your command, these completions can be amazingly powerful and flexible. Read more about it in [Bash Completions](bash_completions.md).
## Generating zsh completions
Cobra can generate zsh-completion file. Read more about it in
[Zsh Completions](zsh_completions.md).
# Contributing
1. Fork it
+20 -17
View File
@@ -16,14 +16,14 @@ func legacyArgs(cmd *Command, args []string) error {
return nil
}
// root command with subcommands, do subcommand checking
// root command with subcommands, do subcommand checking.
if !cmd.HasParent() && len(args) > 0 {
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
}
return nil
}
// NoArgs returns an error if any args are included
// NoArgs returns an error if any args are included.
func NoArgs(cmd *Command, args []string) error {
if len(args) > 0 {
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
@@ -31,7 +31,7 @@ func NoArgs(cmd *Command, args []string) error {
return nil
}
// OnlyValidArgs returns an error if any args are not in the list of ValidArgs
// OnlyValidArgs returns an error if any args are not in the list of ValidArgs.
func OnlyValidArgs(cmd *Command, args []string) error {
if len(cmd.ValidArgs) > 0 {
for _, v := range args {
@@ -43,21 +43,12 @@ func OnlyValidArgs(cmd *Command, args []string) error {
return nil
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// ArbitraryArgs never returns an error
// ArbitraryArgs never returns an error.
func ArbitraryArgs(cmd *Command, args []string) error {
return nil
}
// MinimumNArgs returns an error if there is not at least N args
// MinimumNArgs returns an error if there is not at least N args.
func MinimumNArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) < n {
@@ -67,7 +58,7 @@ func MinimumNArgs(n int) PositionalArgs {
}
}
// MaximumNArgs returns an error if there are more than N args
// MaximumNArgs returns an error if there are more than N args.
func MaximumNArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) > n {
@@ -77,7 +68,7 @@ func MaximumNArgs(n int) PositionalArgs {
}
}
// ExactArgs returns an error if there are not exactly n args
// ExactArgs returns an error if there are not exactly n args.
func ExactArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) != n {
@@ -87,7 +78,19 @@ func ExactArgs(n int) PositionalArgs {
}
}
// RangeArgs returns an error if the number of args is not within the expected range
// ExactValidArgs returns an error if
// there are not exactly N positional args OR
// there are any positional args that are not in the `ValidArgs` field of `Command`
func ExactValidArgs(n int) PositionalArgs {
return func(cmd *Command, args []string) error {
if err := ExactArgs(n)(cmd, args); err != nil {
return err
}
return OnlyValidArgs(cmd, args)
}
}
// RangeArgs returns an error if the number of args is not within the expected range.
func RangeArgs(min int, max int) PositionalArgs {
return func(cmd *Command, args []string) error {
if len(args) < min || len(args) > max {
+287
View File
@@ -0,0 +1,287 @@
package cobra
import (
"strings"
"testing"
)
func TestNoArgs(t *testing.T) {
c := &Command{Use: "c", Args: NoArgs, Run: emptyRun}
output, err := executeCommand(c)
if output != "" {
t.Errorf("Unexpected string: %v", output)
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
func TestNoArgsWithArgs(t *testing.T) {
c := &Command{Use: "c", Args: NoArgs, Run: emptyRun}
_, err := executeCommand(c, "illegal")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `unknown command "illegal" for "c"`
if got != expected {
t.Errorf("Expected: %q, got: %q", expected, got)
}
}
func TestOnlyValidArgs(t *testing.T) {
c := &Command{
Use: "c",
Args: OnlyValidArgs,
ValidArgs: []string{"one", "two"},
Run: emptyRun,
}
output, err := executeCommand(c, "one", "two")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
func TestOnlyValidArgsWithInvalidArgs(t *testing.T) {
c := &Command{
Use: "c",
Args: OnlyValidArgs,
ValidArgs: []string{"one", "two"},
Run: emptyRun,
}
_, err := executeCommand(c, "three")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `invalid argument "three" for "c"`
if got != expected {
t.Errorf("Expected: %q, got: %q", expected, got)
}
}
func TestArbitraryArgs(t *testing.T) {
c := &Command{Use: "c", Args: ArbitraryArgs, Run: emptyRun}
output, err := executeCommand(c, "a", "b")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestMinimumNArgs(t *testing.T) {
c := &Command{Use: "c", Args: MinimumNArgs(2), Run: emptyRun}
output, err := executeCommand(c, "a", "b", "c")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestMinimumNArgsWithLessArgs(t *testing.T) {
c := &Command{Use: "c", Args: MinimumNArgs(2), Run: emptyRun}
_, err := executeCommand(c, "a")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := "requires at least 2 arg(s), only received 1"
if got != expected {
t.Fatalf("Expected %q, got %q", expected, got)
}
}
func TestMaximumNArgs(t *testing.T) {
c := &Command{Use: "c", Args: MaximumNArgs(3), Run: emptyRun}
output, err := executeCommand(c, "a", "b")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestMaximumNArgsWithMoreArgs(t *testing.T) {
c := &Command{Use: "c", Args: MaximumNArgs(2), Run: emptyRun}
_, err := executeCommand(c, "a", "b", "c")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := "accepts at most 2 arg(s), received 3"
if got != expected {
t.Fatalf("Expected %q, got %q", expected, got)
}
}
func TestExactArgs(t *testing.T) {
c := &Command{Use: "c", Args: ExactArgs(3), Run: emptyRun}
output, err := executeCommand(c, "a", "b", "c")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestExactArgsWithInvalidCount(t *testing.T) {
c := &Command{Use: "c", Args: ExactArgs(2), Run: emptyRun}
_, err := executeCommand(c, "a", "b", "c")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := "accepts 2 arg(s), received 3"
if got != expected {
t.Fatalf("Expected %q, got %q", expected, got)
}
}
func TestExactValidArgs(t *testing.T) {
c := &Command{Use: "c", Args: ExactValidArgs(3), ValidArgs: []string{"a", "b", "c"}, Run: emptyRun}
output, err := executeCommand(c, "a", "b", "c")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestExactValidArgsWithInvalidCount(t *testing.T) {
c := &Command{Use: "c", Args: ExactValidArgs(2), Run: emptyRun}
_, err := executeCommand(c, "a", "b", "c")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := "accepts 2 arg(s), received 3"
if got != expected {
t.Fatalf("Expected %q, got %q", expected, got)
}
}
func TestExactValidArgsWithInvalidArgs(t *testing.T) {
c := &Command{
Use: "c",
Args: ExactValidArgs(1),
ValidArgs: []string{"one", "two"},
Run: emptyRun,
}
_, err := executeCommand(c, "three")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `invalid argument "three" for "c"`
if got != expected {
t.Errorf("Expected: %q, got: %q", expected, got)
}
}
func TestRangeArgs(t *testing.T) {
c := &Command{Use: "c", Args: RangeArgs(2, 4), Run: emptyRun}
output, err := executeCommand(c, "a", "b", "c")
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestRangeArgsWithInvalidCount(t *testing.T) {
c := &Command{Use: "c", Args: RangeArgs(2, 4), Run: emptyRun}
_, err := executeCommand(c, "a")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := "accepts between 2 and 4 arg(s), received 1"
if got != expected {
t.Fatalf("Expected %q, got %q", expected, got)
}
}
func TestRootTakesNoArgs(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
childCmd := &Command{Use: "child", Run: emptyRun}
rootCmd.AddCommand(childCmd)
_, err := executeCommand(rootCmd, "illegal", "args")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `unknown command "illegal" for "root"`
if !strings.Contains(got, expected) {
t.Errorf("expected %q, got %q", expected, got)
}
}
func TestRootTakesArgs(t *testing.T) {
rootCmd := &Command{Use: "root", Args: ArbitraryArgs, Run: emptyRun}
childCmd := &Command{Use: "child", Run: emptyRun}
rootCmd.AddCommand(childCmd)
_, err := executeCommand(rootCmd, "legal", "args")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
func TestChildTakesNoArgs(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
childCmd := &Command{Use: "child", Args: NoArgs, Run: emptyRun}
rootCmd.AddCommand(childCmd)
_, err := executeCommand(rootCmd, "child", "illegal", "args")
if err == nil {
t.Fatal("Expected an error")
}
got := err.Error()
expected := `unknown command "illegal" for "root child"`
if !strings.Contains(got, expected) {
t.Errorf("expected %q, got %q", expected, got)
}
}
func TestChildTakesArgs(t *testing.T) {
rootCmd := &Command{Use: "root", Run: emptyRun}
childCmd := &Command{Use: "child", Args: ArbitraryArgs, Run: emptyRun}
rootCmd.AddCommand(childCmd)
_, err := executeCommand(rootCmd, "child", "legal", "args")
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
}
+113 -103
View File
@@ -21,8 +21,8 @@ const (
func writePreamble(buf *bytes.Buffer, name string) {
buf.WriteString(fmt.Sprintf("# bash completion for %-36s -*- shell-script -*-\n", name))
buf.WriteString(`
__debug()
buf.WriteString(fmt.Sprintf(`
__%[1]s_debug()
{
if [[ -n ${BASH_COMP_DEBUG_FILE} ]]; then
echo "$*" >> "${BASH_COMP_DEBUG_FILE}"
@@ -31,13 +31,13 @@ __debug()
# Homebrew on Macs have version 1.3 of bash-completion which doesn't include
# _init_completion. This is a very minimal version of that function.
__my_init_completion()
__%[1]s_init_completion()
{
COMPREPLY=()
_get_comp_words_by_ref "$@" cur prev words cword
}
__index_of_word()
__%[1]s_index_of_word()
{
local w word=$1
shift
@@ -49,7 +49,7 @@ __index_of_word()
index=-1
}
__contains_word()
__%[1]s_contains_word()
{
local w word=$1; shift
for w in "$@"; do
@@ -58,9 +58,9 @@ __contains_word()
return 1
}
__handle_reply()
__%[1]s_handle_reply()
{
__debug "${FUNCNAME[0]}"
__%[1]s_debug "${FUNCNAME[0]}"
case $cur in
-*)
if [[ $(type -t compopt) = "builtin" ]]; then
@@ -85,7 +85,7 @@ __handle_reply()
local index flag
flag="${cur%%=*}"
__index_of_word "${flag}" "${flags_with_completion[@]}"
__%[1]s_index_of_word "${flag}" "${flags_with_completion[@]}"
COMPREPLY=()
if [[ ${index} -ge 0 ]]; then
PREFIX=""
@@ -103,7 +103,7 @@ __handle_reply()
# check if we are handling a flag with special work handling
local index
__index_of_word "${prev}" "${flags_with_completion[@]}"
__%[1]s_index_of_word "${prev}" "${flags_with_completion[@]}"
if [[ ${index} -ge 0 ]]; then
${flags_completion[${index}]}
return
@@ -129,31 +129,43 @@ __handle_reply()
fi
if [[ ${#COMPREPLY[@]} -eq 0 ]]; then
declare -F __custom_func >/dev/null && __custom_func
if declare -F __%[1]s_custom_func >/dev/null; then
# try command name qualified custom func
__%[1]s_custom_func
else
# otherwise fall back to unqualified for compatibility
declare -F __custom_func >/dev/null && __custom_func
fi
fi
# available in bash-completion >= 2, not always present on macOS
if declare -F __ltrim_colon_completions >/dev/null; then
__ltrim_colon_completions "$cur"
fi
# If there is only 1 completion and it is a flag with an = it will be completed
# but we don't want a space after the =
if [[ "${#COMPREPLY[@]}" -eq "1" ]] && [[ $(type -t compopt) = "builtin" ]] && [[ "${COMPREPLY[0]}" == --*= ]]; then
compopt -o nospace
fi
}
# The arguments should be in the form "ext1|ext2|extn"
__handle_filename_extension_flag()
__%[1]s_handle_filename_extension_flag()
{
local ext="$1"
_filedir "@(${ext})"
}
__handle_subdirs_in_dir_flag()
__%[1]s_handle_subdirs_in_dir_flag()
{
local dir="$1"
pushd "${dir}" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1
}
__handle_flag()
__%[1]s_handle_flag()
{
__debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
# if a command required a flag, and we found it, unset must_have_one_flag()
local flagname=${words[c]}
@@ -164,27 +176,31 @@ __handle_flag()
flagname=${flagname%%=*} # strip everything after the =
flagname="${flagname}=" # but put the = back
fi
__debug "${FUNCNAME[0]}: looking for ${flagname}"
if __contains_word "${flagname}" "${must_have_one_flag[@]}"; then
__%[1]s_debug "${FUNCNAME[0]}: looking for ${flagname}"
if __%[1]s_contains_word "${flagname}" "${must_have_one_flag[@]}"; then
must_have_one_flag=()
fi
# if you set a flag which only applies to this command, don't show subcommands
if __contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
if __%[1]s_contains_word "${flagname}" "${local_nonpersistent_flags[@]}"; then
commands=()
fi
# keep flag value with flagname as flaghash
if [ -n "${flagvalue}" ] ; then
flaghash[${flagname}]=${flagvalue}
elif [ -n "${words[ $((c+1)) ]}" ] ; then
flaghash[${flagname}]=${words[ $((c+1)) ]}
else
flaghash[${flagname}]="true" # pad "true" for bool flag
# flaghash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then
if [ -n "${flagvalue}" ] ; then
flaghash[${flagname}]=${flagvalue}
elif [ -n "${words[ $((c+1)) ]}" ] ; then
flaghash[${flagname}]=${words[ $((c+1)) ]}
else
flaghash[${flagname}]="true" # pad "true" for bool flag
fi
fi
# skip the argument to a two word flag
if __contains_word "${words[c]}" "${two_word_flags[@]}"; then
if [[ ${words[c]} != *"="* ]] && __%[1]s_contains_word "${words[c]}" "${two_word_flags[@]}"; then
__%[1]s_debug "${FUNCNAME[0]}: found a flag ${words[c]}, skip the next argument"
c=$((c+1))
# if we are looking for a flags value, don't show commands
if [[ $c -eq $cword ]]; then
@@ -196,13 +212,13 @@ __handle_flag()
}
__handle_noun()
__%[1]s_handle_noun()
{
__debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if __contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
if __%[1]s_contains_word "${words[c]}" "${must_have_one_noun[@]}"; then
must_have_one_noun=()
elif __contains_word "${words[c]}" "${noun_aliases[@]}"; then
elif __%[1]s_contains_word "${words[c]}" "${noun_aliases[@]}"; then
must_have_one_noun=()
fi
@@ -210,45 +226,53 @@ __handle_noun()
c=$((c+1))
}
__handle_command()
__%[1]s_handle_command()
{
__debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local next_command
if [[ -n ${last_command} ]]; then
next_command="_${last_command}_${words[c]//:/__}"
else
if [[ $c -eq 0 ]]; then
next_command="_$(basename "${words[c]//:/__}")"
next_command="_%[1]s_root_command"
else
next_command="_${words[c]//:/__}"
fi
fi
c=$((c+1))
__debug "${FUNCNAME[0]}: looking for ${next_command}"
__%[1]s_debug "${FUNCNAME[0]}: looking for ${next_command}"
declare -F "$next_command" >/dev/null && $next_command
}
__handle_word()
__%[1]s_handle_word()
{
if [[ $c -ge $cword ]]; then
__handle_reply
__%[1]s_handle_reply
return
fi
__debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
__%[1]s_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
if [[ "${words[c]}" == -* ]]; then
__handle_flag
elif __contains_word "${words[c]}" "${commands[@]}"; then
__handle_command
elif [[ $c -eq 0 ]] && __contains_word "$(basename "${words[c]}")" "${commands[@]}"; then
__handle_command
__%[1]s_handle_flag
elif __%[1]s_contains_word "${words[c]}" "${commands[@]}"; then
__%[1]s_handle_command
elif [[ $c -eq 0 ]]; then
__%[1]s_handle_command
elif __%[1]s_contains_word "${words[c]}" "${command_aliases[@]}"; then
# aliashash variable is an associative array which is only supported in bash > 3.
if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then
words[c]=${aliashash[${words[c]}]}
__%[1]s_handle_command
else
__%[1]s_handle_noun
fi
else
__handle_noun
__%[1]s_handle_noun
fi
__handle_word
__%[1]s_handle_word
}
`)
`, name))
}
func writePostscript(buf *bytes.Buffer, name string) {
@@ -257,10 +281,11 @@ func writePostscript(buf *bytes.Buffer, name string) {
buf.WriteString(fmt.Sprintf(`{
local cur prev words cword
declare -A flaghash 2>/dev/null || :
declare -A aliashash 2>/dev/null || :
if declare -F _init_completion >/dev/null 2>&1; then
_init_completion -s || return
else
__my_init_completion -n "=" || return
__%[1]s_init_completion -n "=" || return
fi
local c=0
@@ -269,13 +294,13 @@ func writePostscript(buf *bytes.Buffer, name string) {
local local_nonpersistent_flags=()
local flags_with_completion=()
local flags_completion=()
local commands=("%s")
local commands=("%[1]s")
local must_have_one_flag=()
local must_have_one_noun=()
local last_command
local nouns=()
__handle_word
__%[1]s_handle_word
}
`, name))
@@ -296,11 +321,12 @@ func writeCommands(buf *bytes.Buffer, cmd *Command) {
continue
}
buf.WriteString(fmt.Sprintf(" commands+=(%q)\n", c.Name()))
writeCmdAliases(buf, c)
}
buf.WriteString("\n")
}
func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]string) {
func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]string, cmd *Command) {
for key, value := range annotations {
switch key {
case BashCompFilenameExt:
@@ -308,7 +334,7 @@ func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]s
var ext string
if len(value) > 0 {
ext = "__handle_filename_extension_flag " + strings.Join(value, "|")
ext = fmt.Sprintf("__%s_handle_filename_extension_flag ", cmd.Root().Name()) + strings.Join(value, "|")
} else {
ext = "_filedir"
}
@@ -326,7 +352,7 @@ func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]s
var ext string
if len(value) == 1 {
ext = "__handle_subdirs_in_dir_flag " + value[0]
ext = fmt.Sprintf("__%s_handle_subdirs_in_dir_flag ", cmd.Root().Name()) + value[0]
} else {
ext = "_filedir -d"
}
@@ -335,7 +361,7 @@ func writeFlagHandler(buf *bytes.Buffer, name string, annotations map[string][]s
}
}
func writeShortFlag(buf *bytes.Buffer, flag *pflag.Flag) {
func writeShortFlag(buf *bytes.Buffer, flag *pflag.Flag, cmd *Command) {
name := flag.Shorthand
format := " "
if len(flag.NoOptDefVal) == 0 {
@@ -343,10 +369,10 @@ func writeShortFlag(buf *bytes.Buffer, flag *pflag.Flag) {
}
format += "flags+=(\"-%s\")\n"
buf.WriteString(fmt.Sprintf(format, name))
writeFlagHandler(buf, "-"+name, flag.Annotations)
writeFlagHandler(buf, "-"+name, flag.Annotations, cmd)
}
func writeFlag(buf *bytes.Buffer, flag *pflag.Flag) {
func writeFlag(buf *bytes.Buffer, flag *pflag.Flag, cmd *Command) {
name := flag.Name
format := " flags+=(\"--%s"
if len(flag.NoOptDefVal) == 0 {
@@ -354,7 +380,11 @@ func writeFlag(buf *bytes.Buffer, flag *pflag.Flag) {
}
format += "\")\n"
buf.WriteString(fmt.Sprintf(format, name))
writeFlagHandler(buf, "--"+name, flag.Annotations)
if len(flag.NoOptDefVal) == 0 {
format = " two_word_flags+=(\"--%s\")\n"
buf.WriteString(fmt.Sprintf(format, name))
}
writeFlagHandler(buf, "--"+name, flag.Annotations, cmd)
}
func writeLocalNonPersistentFlag(buf *bytes.Buffer, flag *pflag.Flag) {
@@ -380,9 +410,9 @@ func writeFlags(buf *bytes.Buffer, cmd *Command) {
if nonCompletableFlag(flag) {
return
}
writeFlag(buf, flag)
writeFlag(buf, flag, cmd)
if len(flag.Shorthand) > 0 {
writeShortFlag(buf, flag)
writeShortFlag(buf, flag, cmd)
}
if localNonPersistentFlags.Lookup(flag.Name) != nil {
writeLocalNonPersistentFlag(buf, flag)
@@ -392,9 +422,9 @@ func writeFlags(buf *bytes.Buffer, cmd *Command) {
if nonCompletableFlag(flag) {
return
}
writeFlag(buf, flag)
writeFlag(buf, flag, cmd)
if len(flag.Shorthand) > 0 {
writeShortFlag(buf, flag)
writeShortFlag(buf, flag, cmd)
}
})
@@ -434,6 +464,21 @@ func writeRequiredNouns(buf *bytes.Buffer, cmd *Command) {
}
}
func writeCmdAliases(buf *bytes.Buffer, cmd *Command) {
if len(cmd.Aliases) == 0 {
return
}
sort.Sort(sort.StringSlice(cmd.Aliases))
buf.WriteString(fmt.Sprint(` if [[ -z "${BASH_VERSION}" || "${BASH_VERSINFO[0]}" -gt 3 ]]; then`, "\n"))
for _, value := range cmd.Aliases {
buf.WriteString(fmt.Sprintf(" command_aliases+=(%q)\n", value))
buf.WriteString(fmt.Sprintf(" aliashash[%q]=%q\n", value, cmd.Name()))
}
buf.WriteString(` fi`)
buf.WriteString("\n")
}
func writeArgAliases(buf *bytes.Buffer, cmd *Command) {
buf.WriteString(" noun_aliases=()\n")
sort.Sort(sort.StringSlice(cmd.ArgAliases))
@@ -452,8 +497,18 @@ func gen(buf *bytes.Buffer, cmd *Command) {
commandName := cmd.CommandPath()
commandName = strings.Replace(commandName, " ", "_", -1)
commandName = strings.Replace(commandName, ":", "__", -1)
buf.WriteString(fmt.Sprintf("_%s()\n{\n", commandName))
if cmd.Root() == cmd {
buf.WriteString(fmt.Sprintf("_%s_root_command()\n{\n", commandName))
} else {
buf.WriteString(fmt.Sprintf("_%s()\n{\n", commandName))
}
buf.WriteString(fmt.Sprintf(" last_command=%q\n", commandName))
buf.WriteString("\n")
buf.WriteString(" command_aliases=()\n")
buf.WriteString("\n")
writeCommands(buf, cmd)
writeFlags(buf, cmd)
writeRequiredFlag(buf, cmd)
@@ -490,48 +545,3 @@ func (c *Command) GenBashCompletionFile(filename string) error {
return c.GenBashCompletion(outFile)
}
// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag, if it exists.
func (c *Command) MarkFlagRequired(name string) error {
return MarkFlagRequired(c.Flags(), name)
}
// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag, if it exists.
func (c *Command) MarkPersistentFlagRequired(name string) error {
return MarkFlagRequired(c.PersistentFlags(), name)
}
// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag in the flag set, if it exists.
func MarkFlagRequired(flags *pflag.FlagSet, name string) error {
return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"})
}
// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists.
// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
func (c *Command) MarkFlagFilename(name string, extensions ...string) error {
return MarkFlagFilename(c.Flags(), name, extensions...)
}
// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists.
// Generated bash autocompletion will call the bash function f for the flag.
func (c *Command) MarkFlagCustom(name string, f string) error {
return MarkFlagCustom(c.Flags(), name, f)
}
// MarkPersistentFlagFilename adds the BashCompFilenameExt annotation to the named persistent flag, if it exists.
// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error {
return MarkFlagFilename(c.PersistentFlags(), name, extensions...)
}
// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag in the flag set, if it exists.
// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error {
return flags.SetAnnotation(name, BashCompFilenameExt, extensions)
}
// MarkFlagCustom adds the BashCompCustom annotation to the named flag in the flag set, if it exists.
// Generated bash autocompletion will call the bash function f for the flag.
func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error {
return flags.SetAnnotation(name, BashCompCustom, []string{f})
}
+60 -10
View File
@@ -1,20 +1,56 @@
# Generating Bash Completions For Your Own cobra.Command
If you are using the generator you can create a completion command by running
```bash
cobra add completion
```
Update the help text show how to install the bash_completion Linux show here [Kubectl docs show mac options](https://kubernetes.io/docs/tasks/tools/install-kubectl/#enabling-shell-autocompletion)
Writing the shell script to stdout allows the most flexible use.
```go
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion",
Short: "Generates bash completion scripts",
Long: `To load completion run
. <(bitbucket completion)
To configure your bash shell to load completions for each session add to your bashrc
# ~/.bashrc or ~/.profile
. <(bitbucket completion)
`,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout);
},
}
```
**Note:** The cobra generator may include messages printed to stdout for example if the config file is loaded, this will break the auto complete script
## Example from kubectl
Generating bash completions from a cobra command is incredibly easy. An actual program which does so for the kubernetes kubectl binary is as follows:
```go
package main
import (
"io/ioutil"
"os"
"io/ioutil"
"os"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd"
"k8s.io/kubernetes/pkg/kubectl/cmd"
"k8s.io/kubernetes/pkg/kubectl/cmd/util"
)
func main() {
kubectl := cmd.NewFactory(nil).NewKubectlCommand(os.Stdin, ioutil.Discard, ioutil.Discard)
kubectl.GenBashCompletionFile("out.sh")
kubectl := cmd.NewKubectlCommand(util.NewFactory(nil), os.Stdin, ioutil.Discard, ioutil.Discard)
kubectl.GenBashCompletionFile("out.sh")
}
```
@@ -46,7 +82,7 @@ __kubectl_get_resource()
fi
}
__custom_func() {
__kubectl_custom_func() {
case ${last_command} in
kubectl_get | kubectl_describe | kubectl_delete | kubectl_stop)
__kubectl_get_resource
@@ -73,7 +109,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
}
```
The `BashCompletionFunction` option is really only valid/useful on the root command. Doing the above will cause `__custom_func()` to be called when the built in processor was unable to find a solution. In the case of kubernetes a valid command might look something like `kubectl get pod [mypod]`. If you type `kubectl get pod [tab][tab]` the `__customc_func()` will run because the cobra.Command only understood "kubectl" and "get." `__custom_func()` will see that the cobra.Command is "kubectl_get" and will thus call another helper `__kubectl_get_resource()`. `__kubectl_get_resource` will look at the 'nouns' collected. In our example the only noun will be `pod`. So it will call `__kubectl_parse_get pod`. `__kubectl_parse_get` will actually call out to kubernetes and get any pods. It will then set `COMPREPLY` to valid pods!
The `BashCompletionFunction` option is really only valid/useful on the root command. Doing the above will cause `__kubectl_custom_func()` (`__<command-use>_custom_func()`) to be called when the built in processor was unable to find a solution. In the case of kubernetes a valid command might look something like `kubectl get pod [mypod]`. If you type `kubectl get pod [tab][tab]` the `__kubectl_customc_func()` will run because the cobra.Command only understood "kubectl" and "get." `__kubectl_custom_func()` will see that the cobra.Command is "kubectl_get" and will thus call another helper `__kubectl_get_resource()`. `__kubectl_get_resource` will look at the 'nouns' collected. In our example the only noun will be `pod`. So it will call `__kubectl_parse_get pod`. `__kubectl_parse_get` will actually call out to kubernetes and get any pods. It will then set `COMPREPLY` to valid pods!
## Have the completions code complete your 'nouns'
@@ -173,14 +209,14 @@ hello.yml test.json
So while there are many other files in the CWD it only shows me subdirs and those with valid extensions.
# Specifiy custom flag completion
# Specify custom flag completion
Similar to the filename completion and filtering using cobra.BashCompFilenameExt, you can specifiy
Similar to the filename completion and filtering using cobra.BashCompFilenameExt, you can specify
a custom flag completion function with cobra.BashCompCustom:
```go
annotation := make(map[string][]string)
annotation[cobra.BashCompFilenameExt] = []string{"__kubectl_get_namespaces"}
annotation[cobra.BashCompCustom] = []string{"__kubectl_get_namespaces"}
flag := &pflag.Flag{
Name: "namespace",
@@ -204,3 +240,17 @@ __kubectl_get_namespaces()
fi
}
```
# Using bash aliases for commands
You can also configure the `bash aliases` for the commands and they will also support completions.
```bash
alias aliasname=origcommand
complete -o default -F __start_origcommand aliasname
# and now when you run `aliasname` completion will make
# suggestions as it did for `origcommand`.
$) aliasname <tab><tab>
completion firstcommand secondcommand
```
+150 -107
View File
@@ -2,21 +2,40 @@ package cobra
import (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"testing"
)
func checkOmit(t *testing.T, found, unexpected string) {
if strings.Contains(found, unexpected) {
t.Errorf("Unexpected response.\nGot: %q\nBut should not have!\n", unexpected)
t.Errorf("Got: %q\nBut should not have!\n", unexpected)
}
}
func check(t *testing.T, found, expected string) {
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
t.Errorf("Expecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
}
func checkNumOccurrences(t *testing.T, found, expected string, expectedOccurrences int) {
numOccurrences := strings.Count(found, expected)
if numOccurrences != expectedOccurrences {
t.Errorf("Expecting to contain %d occurrences of: \n %q\nGot %d:\n %q\n", expectedOccurrences, expected, numOccurrences, found)
}
}
func checkRegex(t *testing.T, found, pattern string) {
matched, err := regexp.MatchString(pattern, found)
if err != nil {
t.Errorf("Error thrown performing MatchString: \n %s\n", err)
}
if !matched {
t.Errorf("Expecting to match: \n %q\nGot:\n %q\n", pattern, found)
}
}
@@ -33,162 +52,186 @@ func runShellCheck(s string) error {
return err
}
go func() {
defer stdin.Close()
stdin.Write([]byte(s))
stdin.Close()
}()
return cmd.Run()
}
// World worst custom function, just keep telling you to enter hello!
const (
bashCompletionFunc = `__custom_func() {
COMPREPLY=( "hello" )
const bashCompletionFunc = `__root_custom_func() {
COMPREPLY=( "hello" )
}
`
)
func TestBashCompletions(t *testing.T) {
c := initializeWithRootCmd()
cmdEcho.AddCommand(cmdTimes)
c.AddCommand(cmdEcho, cmdPrint, cmdDeprecated, cmdColon)
rootCmd := &Command{
Use: "root",
ArgAliases: []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"},
ValidArgs: []string{"pod", "node", "service", "replicationcontroller"},
BashCompletionFunction: bashCompletionFunc,
Run: emptyRun,
}
rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot")
rootCmd.MarkFlagRequired("introot")
// custom completion function
c.BashCompletionFunction = bashCompletionFunc
// Filename.
rootCmd.Flags().String("filename", "", "Enter a filename")
rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml")
// required flag
c.MarkFlagRequired("introot")
// Persistent filename.
rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename")
rootCmd.MarkPersistentFlagFilename("persistent-filename")
rootCmd.MarkPersistentFlagRequired("persistent-filename")
// valid nouns
validArgs := []string{"pod", "node", "service", "replicationcontroller"}
c.ValidArgs = validArgs
// Filename extensions.
rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)")
rootCmd.MarkFlagFilename("filename-ext")
rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)")
rootCmd.MarkFlagCustom("custom", "__complete_custom")
// noun aliases
argAliases := []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"}
c.ArgAliases = argAliases
// Subdirectories in a given directory.
rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)")
rootCmd.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"})
// filename
var flagval string
c.Flags().StringVar(&flagval, "filename", "", "Enter a filename")
c.MarkFlagFilename("filename", "json", "yaml", "yml")
// For two word flags check
rootCmd.Flags().StringP("two", "t", "", "this is two word flags")
rootCmd.Flags().BoolP("two-w-default", "T", false, "this is not two word flags")
// persistent filename
var flagvalPersistent string
c.PersistentFlags().StringVar(&flagvalPersistent, "persistent-filename", "", "Enter a filename")
c.MarkPersistentFlagFilename("persistent-filename")
c.MarkPersistentFlagRequired("persistent-filename")
echoCmd := &Command{
Use: "echo [string to echo]",
Aliases: []string{"say"},
Short: "Echo anything to the screen",
Long: "an utterly useless command for testing.",
Example: "Just run cobra-test echo",
Run: emptyRun,
}
// filename extensions
var flagvalExt string
c.Flags().StringVar(&flagvalExt, "filename-ext", "", "Enter a filename (extension limited)")
c.MarkFlagFilename("filename-ext")
echoCmd.Flags().String("filename", "", "Enter a filename")
echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml")
echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)")
echoCmd.Flags().SetAnnotation("config", BashCompSubdirsInDir, []string{"config"})
// filename extensions
var flagvalCustom string
c.Flags().StringVar(&flagvalCustom, "custom", "", "Enter a filename (extension limited)")
c.MarkFlagCustom("custom", "__complete_custom")
printCmd := &Command{
Use: "print [string to print]",
Args: MinimumNArgs(1),
Short: "Print anything to the screen",
Long: "an absolutely utterly useless command for testing.",
Run: emptyRun,
}
// subdirectories in a given directory
var flagvalTheme string
c.Flags().StringVar(&flagvalTheme, "theme", "", "theme to use (located in /themes/THEMENAME/)")
c.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"})
deprecatedCmd := &Command{
Use: "deprecated [can't do anything here]",
Args: NoArgs,
Short: "A command which is deprecated",
Long: "an absolutely utterly useless command for testing deprecation!.",
Deprecated: "Please use echo instead",
Run: emptyRun,
}
out := new(bytes.Buffer)
c.GenBashCompletion(out)
str := out.String()
colonCmd := &Command{
Use: "cmd:colon",
Run: emptyRun,
}
check(t, str, "_cobra-test")
check(t, str, "_cobra-test_echo")
check(t, str, "_cobra-test_echo_times")
check(t, str, "_cobra-test_print")
check(t, str, "_cobra-test_cmd__colon")
timesCmd := &Command{
Use: "times [# times] [string to echo]",
SuggestFor: []string{"counts"},
Args: OnlyValidArgs,
ValidArgs: []string{"one", "two", "three", "four"},
Short: "Echo anything to the screen more times",
Long: "a slightly useless command for testing.",
Run: emptyRun,
}
echoCmd.AddCommand(timesCmd)
rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd)
buf := new(bytes.Buffer)
rootCmd.GenBashCompletion(buf)
output := buf.String()
check(t, output, "_root")
check(t, output, "_root_echo")
check(t, output, "_root_echo_times")
check(t, output, "_root_print")
check(t, output, "_root_cmd__colon")
// check for required flags
check(t, str, `must_have_one_flag+=("--introot=")`)
check(t, str, `must_have_one_flag+=("--persistent-filename=")`)
// check for custom completion function
check(t, str, `COMPREPLY=( "hello" )`)
check(t, output, `must_have_one_flag+=("--introot=")`)
check(t, output, `must_have_one_flag+=("--persistent-filename=")`)
// check for custom completion function with both qualified and unqualified name
checkNumOccurrences(t, output, `__custom_func`, 2) // 1. check existence, 2. invoke
checkNumOccurrences(t, output, `__root_custom_func`, 3) // 1. check existence, 2. invoke, 3. actual definition
// check for custom completion function body
check(t, output, `COMPREPLY=( "hello" )`)
// check for required nouns
check(t, str, `must_have_one_noun+=("pod")`)
check(t, output, `must_have_one_noun+=("pod")`)
// check for noun aliases
check(t, str, `noun_aliases+=("pods")`)
check(t, str, `noun_aliases+=("rc")`)
checkOmit(t, str, `must_have_one_noun+=("pods")`)
check(t, output, `noun_aliases+=("pods")`)
check(t, output, `noun_aliases+=("rc")`)
checkOmit(t, output, `must_have_one_noun+=("pods")`)
// check for filename extension flags
check(t, str, `flags_completion+=("_filedir")`)
check(t, output, `flags_completion+=("_filedir")`)
// check for filename extension flags
check(t, str, `must_have_one_noun+=("three")`)
check(t, output, `must_have_one_noun+=("three")`)
// check for filename extension flags
check(t, str, `flags_completion+=("__handle_filename_extension_flag json|yaml|yml")`)
check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_filename_extension_flag json|yaml|yml")`, rootCmd.Name()))
// check for filename extension flags in a subcommand
checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_filename_extension_flag json\|yaml\|yml"\)`, rootCmd.Name()))
// check for custom flags
check(t, str, `flags_completion+=("__complete_custom")`)
check(t, output, `flags_completion+=("__complete_custom")`)
// check for subdirs_in_dir flags
check(t, str, `flags_completion+=("__handle_subdirs_in_dir_flag themes")`)
check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_subdirs_in_dir_flag themes")`, rootCmd.Name()))
// check for subdirs_in_dir flags in a subcommand
checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_subdirs_in_dir_flag config"\)`, rootCmd.Name()))
checkOmit(t, str, cmdDeprecated.Name())
// check two word flags
check(t, output, `two_word_flags+=("--two")`)
check(t, output, `two_word_flags+=("-t")`)
checkOmit(t, output, `two_word_flags+=("--two-w-default")`)
checkOmit(t, output, `two_word_flags+=("-T")`)
// if available, run shellcheck against the script
checkOmit(t, output, deprecatedCmd.Name())
// If available, run shellcheck against the script.
if err := exec.Command("which", "shellcheck").Run(); err != nil {
return
}
err := runShellCheck(str)
if err != nil {
if err := runShellCheck(output); err != nil {
t.Fatalf("shellcheck failed: %v", err)
}
}
func TestBashCompletionHiddenFlag(t *testing.T) {
var cmdTrue = &Command{
Use: "does nothing",
Run: func(cmd *Command, args []string) {},
}
c := &Command{Use: "c", Run: emptyRun}
const flagName = "hidden-foo-bar-baz"
const flagName = "hiddenFlag"
c.Flags().Bool(flagName, false, "")
c.Flags().MarkHidden(flagName)
var flagValue bool
cmdTrue.Flags().BoolVar(&flagValue, flagName, false, "hidden flag")
cmdTrue.Flags().MarkHidden(flagName)
buf := new(bytes.Buffer)
c.GenBashCompletion(buf)
output := buf.String()
out := new(bytes.Buffer)
cmdTrue.GenBashCompletion(out)
bashCompletion := out.String()
if strings.Contains(bashCompletion, flagName) {
t.Errorf("expected completion to not include %q flag: Got %v", flagName, bashCompletion)
if strings.Contains(output, flagName) {
t.Errorf("Expected completion to not include %q flag: Got %v", flagName, output)
}
}
func TestBashCompletionDeprecatedFlag(t *testing.T) {
var cmdTrue = &Command{
Use: "does nothing",
Run: func(cmd *Command, args []string) {},
}
c := &Command{Use: "c", Run: emptyRun}
const flagName = "deprecated-foo-bar-baz"
var flagValue bool
cmdTrue.Flags().BoolVar(&flagValue, flagName, false, "hidden flag")
cmdTrue.Flags().MarkDeprecated(flagName, "use --does-not-exist instead")
out := new(bytes.Buffer)
cmdTrue.GenBashCompletion(out)
bashCompletion := out.String()
if strings.Contains(bashCompletion, flagName) {
t.Errorf("expected completion to not include %q flag: Got %v", flagName, bashCompletion)
}
}
func BenchmarkBashCompletion(b *testing.B) {
c := initializeWithRootCmd()
cmdEcho.AddCommand(cmdTimes)
c.AddCommand(cmdEcho, cmdPrint, cmdDeprecated, cmdColon)
const flagName = "deprecated-flag"
c.Flags().Bool(flagName, false, "")
c.Flags().MarkDeprecated(flagName, "use --not-deprecated instead")
buf := new(bytes.Buffer)
c.GenBashCompletion(buf)
output := buf.String()
b.ResetTimer()
for i := 0; i < b.N; i++ {
buf.Reset()
if err := c.GenBashCompletion(buf); err != nil {
b.Fatal(err)
}
if strings.Contains(output, flagName) {
t.Errorf("expected completion to not include %q flag: Got %v", flagName, output)
}
}
+18 -1
View File
@@ -23,6 +23,7 @@ import (
"strconv"
"strings"
"text/template"
"time"
"unicode"
)
@@ -56,6 +57,12 @@ var MousetrapHelpText string = `This is a command line tool.
You need to open cmd.exe and run it from there.
`
// MousetrapDisplayDuration controls how long the MousetrapHelpText message is displayed on Windows
// if the CLI is started from explorer.exe. Set to 0 to wait for the return key to be pressed.
// To disable the mousetrap, just set MousetrapHelpText to blank string ("").
// Works only on Microsoft Windows.
var MousetrapDisplayDuration time.Duration = 5 * time.Second
// AddTemplateFunc adds a template function that's available to Usage and Help
// template generation.
func AddTemplateFunc(name string, tmplFunc interface{}) {
@@ -70,7 +77,8 @@ func AddTemplateFuncs(tmplFuncs template.FuncMap) {
}
}
// OnInitialize takes a series of func() arguments and appends them to a slice of func().
// OnInitialize sets the passed functions to be run when each command's
// Execute method is called.
func OnInitialize(y ...func()) {
initializers = append(initializers, y...)
}
@@ -188,3 +196,12 @@ func ld(s, t string, ignoreCase bool) int {
}
return d[len(s)][len(t)]
}
func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
+37 -87
View File
@@ -16,54 +16,63 @@ package cmd
import (
"fmt"
"os"
"path/filepath"
"unicode"
"github.com/spf13/cobra"
)
func init() {
addCmd.Flags().StringVarP(&packageName, "package", "t", "", "target package name (e.g. github.com/spf13/hugo)")
addCmd.Flags().StringVarP(&parentName, "parent", "p", "RootCmd", "variable name of parent command for this command")
}
var (
packageName string
parentName string
var packageName, parentName string
var addCmd = &cobra.Command{
Use: "add [command name]",
Aliases: []string{"command"},
Short: "Add a command to a Cobra Application",
Long: `Add (cobra add) will create a new command, with a license and
addCmd = &cobra.Command{
Use: "add [command name]",
Aliases: []string{"command"},
Short: "Add a command to a Cobra Application",
Long: `Add (cobra add) will create a new command, with a license and
the appropriate structure for a Cobra-based CLI application,
and register it to its parent (default RootCmd).
and register it to its parent (default rootCmd).
If you want your command to be public, pass in the command name
with an initial uppercase letter.
Example: cobra add server -> resulting in a new cmd/server.go`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
er("add needs a name for the command")
}
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
er("add needs a name for the command")
}
var project *Project
if packageName != "" {
project = NewProject(packageName)
} else {
wd, err := os.Getwd()
if err != nil {
er(err)
}
project = NewProjectFromPath(wd)
}
cmdName := validateCmdName(args[0])
cmdPath := filepath.Join(project.CmdPath(), cmdName+".go")
createCmdFile(project.License(), cmdPath, cmdName)
commandName := validateCmdName(args[0])
command := &Command{
CmdName: commandName,
CmdParent: parentName,
Project: &Project{
AbsolutePath: wd,
Legal: getLicense(),
Copyright: copyrightLine(),
},
}
fmt.Fprintln(cmd.OutOrStdout(), cmdName, "created at", cmdPath)
},
err = command.Create()
if err != nil {
er(err)
}
fmt.Printf("%s created at %s", command.CmdName, command.AbsolutePath)
},
}
)
func init() {
addCmd.Flags().StringVarP(&packageName, "package", "t", "", "target package name (e.g. github.com/spf13/hugo)")
addCmd.Flags().StringVarP(&parentName, "parent", "p", "rootCmd", "variable name of parent command for this command")
addCmd.Flags().MarkDeprecated("package", "this operation has been removed.")
}
// validateCmdName returns source without any dashes and underscore.
@@ -118,62 +127,3 @@ func validateCmdName(source string) string {
}
return output
}
func createCmdFile(license License, path, cmdName string) {
template := `{{comment .copyright}}
{{if .license}}{{comment .license}}{{end}}
package {{.cmdPackage}}
import (
"fmt"
"github.com/spf13/cobra"
)
// {{.cmdName}}Cmd represents the {{.cmdName}} command
var {{.cmdName}}Cmd = &cobra.Command{
Use: "{{.cmdName}}",
Short: "A brief description of your command",
Long: ` + "`" + `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.` + "`" + `,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("{{.cmdName}} called")
},
}
func init() {
{{.parentName}}.AddCommand({{.cmdName}}Cmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// {{.cmdName}}Cmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// {{.cmdName}}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
`
data := make(map[string]interface{})
data["copyright"] = copyrightLine()
data["license"] = license.Header
data["cmdPackage"] = filepath.Base(filepath.Dir(path)) // last dir of path
data["parentName"] = parentName
data["cmdName"] = cmdName
cmdScript, err := executeTemplate(template, data)
if err != nil {
er(err)
}
err = writeStringToFile(path, cmdScript)
if err != nil {
er(err)
}
}
+26 -66
View File
@@ -1,85 +1,45 @@
package cmd
import (
"errors"
"io/ioutil"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/spf13/viper"
)
// TestGoldenAddCmd initializes the project "github.com/spf13/testproject"
// in GOPATH, adds "test" command
// and compares the content of all files in cmd directory of testproject
// with appropriate golden files.
// Use -update to update existing golden files.
func TestGoldenAddCmd(t *testing.T) {
projectName := "github.com/spf13/testproject"
project := NewProject(projectName)
defer os.RemoveAll(project.AbsPath())
viper.Set("author", "NAME HERE <EMAIL ADDRESS>")
viper.Set("license", "apache")
viper.Set("year", 2017)
defer viper.Set("author", nil)
defer viper.Set("license", nil)
defer viper.Set("year", nil)
wd, _ := os.Getwd()
command := &Command{
CmdName: "test",
CmdParent: parentName,
Project: &Project{
AbsolutePath: fmt.Sprintf("%s/testproject", wd),
Legal: getLicense(),
Copyright: copyrightLine(),
// Initialize the project first.
initializeProject(project)
// required to init
AppName: "testproject",
PkgName: "github.com/spf13/testproject",
Viper: true,
},
}
// Then add the "test" command.
cmdName := "test"
cmdPath := filepath.Join(project.CmdPath(), cmdName+".go")
createCmdFile(project.License(), cmdPath, cmdName)
expectedFiles := []string{".", "root.go", "test.go"}
gotFiles := []string{}
// Check project file hierarchy and compare the content of every single file
// with appropriate golden file.
err := filepath.Walk(project.CmdPath(), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
// init project first
command.Project.Create()
defer func() {
if _, err := os.Stat(command.AbsolutePath); err == nil {
os.RemoveAll(command.AbsolutePath)
}
}()
// Make path relative to project.CmdPath().
// E.g. path = "/home/user/go/src/github.com/spf13/testproject/cmd/root.go"
// then it returns just "root.go".
relPath, err := filepath.Rel(project.CmdPath(), path)
if err != nil {
return err
}
relPath = filepath.ToSlash(relPath)
gotFiles = append(gotFiles, relPath)
goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden")
switch relPath {
// Known directories.
case ".":
return nil
// Known files.
case "root.go", "test.go":
if *update {
got, err := ioutil.ReadFile(path)
if err != nil {
return err
}
ioutil.WriteFile(goldenPath, got, 0644)
}
return compareFiles(path, goldenPath)
}
// Unknown file.
return errors.New("unknown file: " + path)
})
if err != nil {
if err := command.Create(); err != nil {
t.Fatal(err)
}
// Check if some files lack.
if err := checkLackFiles(expectedFiles, gotFiles); err != nil {
generatedFile := fmt.Sprintf("%s/cmd/%s.go", command.AbsolutePath, command.CmdName)
goldenFile := fmt.Sprintf("testdata/%s.go.golden", command.CmdName)
err := compareFiles(generatedFile, goldenFile)
if err != nil {
t.Fatal(err)
}
}
+6 -1
View File
@@ -17,6 +17,11 @@ func init() {
initCmd.SetOutput(new(bytes.Buffer))
}
// ensureLF converts any \r\n to \n
func ensureLF(content []byte) []byte {
return bytes.Replace(content, []byte("\r\n"), []byte("\n"), -1)
}
// compareFiles compares the content of files with pathA and pathB.
// If contents are equal, it returns nil.
// If not, it returns which files are not equal
@@ -30,7 +35,7 @@ func compareFiles(pathA, pathB string) error {
if err != nil {
return err
}
if !bytes.Equal(contentA, contentB) {
if !bytes.Equal(ensureLF(contentA), ensureLF(contentB)) {
output := new(bytes.Buffer)
output.WriteString(fmt.Sprintf("%q and %q are not equal!\n\n", pathA, pathB))
-3
View File
@@ -24,7 +24,6 @@ import (
"text/template"
)
var cmdDirs = [...]string{"cmd", "cmds", "command", "commands"}
var srcPaths []string
func init() {
@@ -128,8 +127,6 @@ func writeStringToFile(path string, s string) error {
// writeToFile writes r to file with path only
// if file/directory on given path doesn't exist.
// If file/directory exists on given path, then
// it terminates app and prints an appropriate error.
func writeToFile(path string, r io.Reader) error {
if exists(path) {
return fmt.Errorf("%v already exists", path)
+38 -195
View File
@@ -15,19 +15,20 @@ package cmd
import (
"fmt"
"os"
"path"
"path/filepath"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"path"
)
var initCmd = &cobra.Command{
Use: "init [name]",
Aliases: []string{"initialize", "initialise", "create"},
Short: "Initialize a Cobra Application",
Long: `Initialize (cobra init) will create a new application, with a license
var (
pkgName string
initCmd = &cobra.Command{
Use: "init [name]",
Aliases: []string{"initialize", "initialise", "create"},
Short: "Initialize a Cobra Application",
Long: `Initialize (cobra init) will create a new application, with a license
and the appropriate structure for a Cobra-based CLI application.
* If a name is provided, it will be created in the current directory;
@@ -39,196 +40,38 @@ and the appropriate structure for a Cobra-based CLI application.
Init will not use an existing directory with contents.`,
Run: func(cmd *cobra.Command, args []string) {
wd, err := os.Getwd()
if err != nil {
er(err)
}
Run: func(cmd *cobra.Command, args []string) {
var project *Project
if len(args) == 0 {
project = NewProjectFromPath(wd)
} else if len(args) == 1 {
arg := args[0]
if arg[0] == '.' {
arg = filepath.Join(wd, arg)
wd, err := os.Getwd()
if err != nil {
er(err)
}
if filepath.IsAbs(arg) {
project = NewProjectFromPath(arg)
} else {
project = NewProject(arg)
if len(args) > 0 {
if args[0] != "." {
wd = fmt.Sprintf("%s/%s", wd, args[0])
}
}
} else {
er("please provide only one argument")
}
initializeProject(project)
project := &Project{
AbsolutePath: wd,
PkgName: pkgName,
Legal: getLicense(),
Copyright: copyrightLine(),
Viper: viper.GetBool("useViper"),
AppName: path.Base(pkgName),
}
fmt.Fprintln(cmd.OutOrStdout(), `Your Cobra application is ready at
`+project.AbsPath()+`.
Give it a try by going there and running `+"`go run main.go`."+`
Add commands to it by running `+"`cobra add [cmdname]`.")
},
}
func initializeProject(project *Project) {
if !exists(project.AbsPath()) { // If path doesn't yet exist, create it
err := os.MkdirAll(project.AbsPath(), os.ModePerm)
if err != nil {
er(err)
}
} else if !isEmpty(project.AbsPath()) { // If path exists and is not empty don't use it
er("Cobra will not create a new project in a non empty directory: " + project.AbsPath())
}
// We have a directory and it's empty. Time to initialize it.
createLicenseFile(project.License(), project.AbsPath())
createMainFile(project)
createRootCmdFile(project)
}
func createLicenseFile(license License, path string) {
data := make(map[string]interface{})
data["copyright"] = copyrightLine()
// Generate license template from text and data.
text, err := executeTemplate(license.Text, data)
if err != nil {
er(err)
}
// Write license text to LICENSE file.
err = writeStringToFile(filepath.Join(path, "LICENSE"), text)
if err != nil {
er(err)
}
}
func createMainFile(project *Project) {
mainTemplate := `{{ comment .copyright }}
{{if .license}}{{ comment .license }}{{end}}
package main
import "{{ .importpath }}"
func main() {
cmd.Execute()
}
`
data := make(map[string]interface{})
data["copyright"] = copyrightLine()
data["license"] = project.License().Header
data["importpath"] = path.Join(project.Name(), filepath.Base(project.CmdPath()))
mainScript, err := executeTemplate(mainTemplate, data)
if err != nil {
er(err)
}
err = writeStringToFile(filepath.Join(project.AbsPath(), "main.go"), mainScript)
if err != nil {
er(err)
}
}
func createRootCmdFile(project *Project) {
template := `{{comment .copyright}}
{{if .license}}{{comment .license}}{{end}}
package cmd
import (
"fmt"
"os"
{{if .viper}}
homedir "github.com/mitchellh/go-homedir"{{end}}
"github.com/spf13/cobra"{{if .viper}}
"github.com/spf13/viper"{{end}}
){{if .viper}}
var cfgFile string{{end}}
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "{{.appName}}",
Short: "A brief description of your application",
Long: ` + "`" + `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.` + "`" + `,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() { {{if .viper}}
cobra.OnInitialize(initConfig)
{{end}}
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.{{ if .viper }}
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .appName }}.yaml)"){{ else }}
// RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .appName }}.yaml)"){{ end }}
// Cobra also supports local flags, which will only run
// when this action is called directly.
RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}{{ if .viper }}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".{{ .appName }}" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".{{ .appName }}")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}{{ end }}
`
data := make(map[string]interface{})
data["copyright"] = copyrightLine()
data["viper"] = viper.GetBool("useViper")
data["license"] = project.License().Header
data["appName"] = path.Base(project.Name())
rootCmdScript, err := executeTemplate(template, data)
if err != nil {
er(err)
}
err = writeStringToFile(filepath.Join(project.CmdPath(), "root.go"), rootCmdScript)
if err != nil {
er(err)
}
if err := project.Create(); err != nil {
er(err)
}
fmt.Printf("Your Cobra applicaton is ready at\n%s\n", project.AbsolutePath)
},
}
)
func init() {
initCmd.Flags().StringVar(&pkgName, "pkg-name", "", "fully qualified pkg name")
initCmd.MarkFlagRequired("pkg-name")
}
+24 -65
View File
@@ -1,83 +1,42 @@
package cmd
import (
"errors"
"io/ioutil"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/spf13/viper"
)
// TestGoldenInitCmd initializes the project "github.com/spf13/testproject"
// in GOPATH and compares the content of files in initialized project with
// appropriate golden files ("testdata/*.golden").
// Use -update to update existing golden files.
func TestGoldenInitCmd(t *testing.T) {
projectName := "github.com/spf13/testproject"
project := NewProject(projectName)
defer os.RemoveAll(project.AbsPath())
viper.Set("author", "NAME HERE <EMAIL ADDRESS>")
viper.Set("license", "apache")
viper.Set("year", 2017)
defer viper.Set("author", nil)
defer viper.Set("license", nil)
defer viper.Set("year", nil)
os.Args = []string{"cobra", "init", projectName}
if err := rootCmd.Execute(); err != nil {
t.Fatal("Error by execution:", err)
wd, _ := os.Getwd()
project := &Project{
AbsolutePath: fmt.Sprintf("%s/testproject", wd),
PkgName: "github.com/spf13/testproject",
Legal: getLicense(),
Copyright: copyrightLine(),
Viper: true,
AppName: "testproject",
}
expectedFiles := []string{".", "cmd", "LICENSE", "main.go", "cmd/root.go"}
gotFiles := []string{}
// Check project file hierarchy and compare the content of every single file
// with appropriate golden file.
err := filepath.Walk(project.AbsPath(), func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Make path relative to project.AbsPath().
// E.g. path = "/home/user/go/src/github.com/spf13/testproject/cmd/root.go"
// then it returns just "cmd/root.go".
relPath, err := filepath.Rel(project.AbsPath(), path)
if err != nil {
return err
}
relPath = filepath.ToSlash(relPath)
gotFiles = append(gotFiles, relPath)
goldenPath := filepath.Join("testdata", filepath.Base(path)+".golden")
switch relPath {
// Known directories.
case ".", "cmd":
return nil
// Known files.
case "LICENSE", "main.go", "cmd/root.go":
if *update {
got, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if err := ioutil.WriteFile(goldenPath, got, 0644); err != nil {
t.Fatal("Error while updating file:", err)
}
}
return compareFiles(path, goldenPath)
}
// Unknown file.
return errors.New("unknown file: " + path)
})
err := project.Create()
if err != nil {
t.Fatal(err)
}
// Check if some files lack.
if err := checkLackFiles(expectedFiles, gotFiles); err != nil {
t.Fatal(err)
defer func() {
if _, err := os.Stat(project.AbsolutePath); err == nil {
os.RemoveAll(project.AbsolutePath)
}
}()
expectedFiles := []string{"LICENSE", "main.go", "cmd/root.go"}
for _, f := range expectedFiles {
generatedFile := fmt.Sprintf("%s/%s", project.AbsolutePath, f)
goldenFile := fmt.Sprintf("testdata/%s.golden", filepath.Base(f))
err := compareFiles(generatedFile, goldenFile)
if err != nil {
t.Fatal(err)
}
}
}
+69 -168
View File
@@ -1,195 +1,96 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra/cobra/tpl"
"os"
"path/filepath"
"runtime"
"strings"
"text/template"
)
// Project contains name, license and paths to projects.
type Project struct {
absPath string
cmdPath string
srcPath string
license License
name string
// v2
PkgName string
Copyright string
AbsolutePath string
Legal License
Viper bool
AppName string
}
// NewProject returns Project with specified project name.
// If projectName is blank string, it returns nil.
func NewProject(projectName string) *Project {
if projectName == "" {
return nil
}
type Command struct {
CmdName string
CmdParent string
*Project
}
p := new(Project)
p.name = projectName
func (p *Project) Create() error {
// 1. Find already created protect.
p.absPath = findPackage(projectName)
// 2. If there are no created project with this path, and user is in GOPATH,
// then use GOPATH/src/projectName.
if p.absPath == "" {
wd, err := os.Getwd()
if err != nil {
er(err)
}
for _, srcPath := range srcPaths {
goPath := filepath.Dir(srcPath)
if filepathHasPrefix(wd, goPath) {
p.absPath = filepath.Join(srcPath, projectName)
break
}
// check if AbsolutePath exists
if _, err := os.Stat(p.AbsolutePath); os.IsNotExist(err) {
// create directory
if err := os.Mkdir(p.AbsolutePath, 0754); err != nil {
return err
}
}
// 3. If user is not in GOPATH, then use (first GOPATH)/src/projectName.
if p.absPath == "" {
p.absPath = filepath.Join(srcPaths[0], projectName)
}
return p
}
// findPackage returns full path to existing go package in GOPATHs.
// findPackage returns "", if it can't find path.
// If packageName is "", findPackage returns "".
func findPackage(packageName string) string {
if packageName == "" {
return ""
}
for _, srcPath := range srcPaths {
packagePath := filepath.Join(srcPath, packageName)
if exists(packagePath) {
return packagePath
}
}
return ""
}
// NewProjectFromPath returns Project with specified absolute path to
// package.
// If absPath is blank string or if absPath is not actually absolute,
// it returns nil.
func NewProjectFromPath(absPath string) *Project {
if absPath == "" || !filepath.IsAbs(absPath) {
return nil
}
p := new(Project)
p.absPath = absPath
p.absPath = strings.TrimSuffix(p.absPath, findCmdDir(p.absPath))
p.name = filepath.ToSlash(trimSrcPath(p.absPath, p.SrcPath()))
return p
}
// trimSrcPath trims at the beginning of absPath the srcPath.
func trimSrcPath(absPath, srcPath string) string {
relPath, err := filepath.Rel(srcPath, absPath)
// create main.go
mainFile, err := os.Create(fmt.Sprintf("%s/main.go", p.AbsolutePath))
if err != nil {
er("Cobra supports project only within $GOPATH: " + err.Error())
return err
}
return relPath
defer mainFile.Close()
mainTemplate := template.Must(template.New("main").Parse(string(tpl.MainTemplate())))
err = mainTemplate.Execute(mainFile, p)
if err != nil {
return err
}
// create cmd/root.go
if _, err = os.Stat(fmt.Sprintf("%s/cmd", p.AbsolutePath)); os.IsNotExist(err) {
os.Mkdir(fmt.Sprintf("%s/cmd", p.AbsolutePath), 0751)
}
rootFile, err := os.Create(fmt.Sprintf("%s/cmd/root.go", p.AbsolutePath))
if err != nil {
return err
}
defer rootFile.Close()
rootTemplate := template.Must(template.New("root").Parse(string(tpl.RootTemplate())))
err = rootTemplate.Execute(rootFile, p)
if err != nil {
return err
}
// create license
return p.createLicenseFile()
}
// License returns the License object of project.
func (p *Project) License() License {
if p.license.Text == "" && p.license.Name != "None" {
p.license = getLicense()
func (p *Project) createLicenseFile() error {
data := map[string]interface{}{
"copyright": copyrightLine(),
}
licenseFile, err := os.Create(fmt.Sprintf("%s/LICENSE", p.AbsolutePath))
if err != nil {
return err
}
return p.license
licenseTemplate := template.Must(template.New("license").Parse(p.Legal.Text))
return licenseTemplate.Execute(licenseFile, data)
}
// Name returns the name of project, e.g. "github.com/spf13/cobra"
func (p Project) Name() string {
return p.name
}
// CmdPath returns absolute path to directory, where all commands are located.
//
// CmdPath returns blank string, only if p.AbsPath() is a blank string.
func (p *Project) CmdPath() string {
if p.absPath == "" {
return ""
}
if p.cmdPath == "" {
p.cmdPath = filepath.Join(p.absPath, findCmdDir(p.absPath))
}
return p.cmdPath
}
// findCmdDir checks if base of absPath is cmd dir and returns it or
// looks for existing cmd dir in absPath.
// If the cmd dir doesn't exist, empty, or cannot be found,
// it returns "cmd".
func findCmdDir(absPath string) string {
if !exists(absPath) || isEmpty(absPath) {
return "cmd"
}
if isCmdDir(absPath) {
return filepath.Base(absPath)
}
files, _ := filepath.Glob(filepath.Join(absPath, "c*"))
for _, file := range files {
if isCmdDir(file) {
return filepath.Base(file)
}
}
return "cmd"
}
// isCmdDir checks if base of name is one of cmdDir.
func isCmdDir(name string) bool {
name = filepath.Base(name)
for _, cmdDir := range cmdDirs {
if name == cmdDir {
return true
}
}
return false
}
// AbsPath returns absolute path of project.
func (p Project) AbsPath() string {
return p.absPath
}
// SrcPath returns absolute path to $GOPATH/src where project is located.
func (p *Project) SrcPath() string {
if p.srcPath != "" {
return p.srcPath
}
if p.absPath == "" {
p.srcPath = srcPaths[0]
return p.srcPath
func (c *Command) Create() error {
cmdFile, err := os.Create(fmt.Sprintf("%s/cmd/%s.go", c.AbsolutePath, c.CmdName))
if err != nil {
return err
}
defer cmdFile.Close()
for _, srcPath := range srcPaths {
if filepathHasPrefix(p.absPath, srcPath) {
p.srcPath = srcPath
break
}
commandTemplate := template.Must(template.New("sub").Parse(string(tpl.AddCommandTemplate())))
err = commandTemplate.Execute(cmdFile, c)
if err != nil {
return err
}
return p.srcPath
}
func filepathHasPrefix(path string, prefix string) bool {
if len(path) <= len(prefix) {
return false
}
if runtime.GOOS == "windows" {
// Paths in windows are case-insensitive.
return strings.EqualFold(path[0:len(prefix)], prefix)
}
return path[0:len(prefix)] == prefix
return nil
}
+1 -22
View File
@@ -1,24 +1,3 @@
package cmd
import (
"testing"
)
func TestFindExistingPackage(t *testing.T) {
path := findPackage("github.com/spf13/cobra")
if path == "" {
t.Fatal("findPackage didn't find the existing package")
}
if !hasGoPathPrefix(path) {
t.Fatalf("%q is not in GOPATH, but must be", path)
}
}
func hasGoPathPrefix(path string) bool {
for _, srcPath := range srcPaths {
if filepathHasPrefix(path, srcPath) {
return true
}
}
return false
}
/* todo: write tests */
+2 -1
View File
@@ -23,7 +23,8 @@ import (
var (
// Used for flags.
cfgFile, userLicense string
cfgFile string
userLicense string
rootCmd = &cobra.Command{
Use: "cobra",
+15 -14
View File
@@ -1,21 +1,22 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Copyright © 2019 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "github.com/spf13/testproject/cmd"
func main() {
cmd.Execute()
cmd.Execute()
}
+66 -58
View File
@@ -1,89 +1,97 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Copyright © 2019 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"fmt"
"os"
"github.com/spf13/cobra"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "testproject",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "testproject",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.testproject.yaml)")
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// Cobra also supports local flags, which will only run
// when this action is called directly.
RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.testproject.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".testproject" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".testproject")
}
// Search config in home directory with name ".testproject" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".testproject")
}
viper.AutomaticEnv() // read in environment variables that match
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
+15 -14
View File
@@ -1,17 +1,18 @@
// Copyright © 2017 NAME HERE <EMAIL ADDRESS>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Copyright © 2019 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
@@ -36,7 +37,7 @@ to quickly create a Cobra application.`,
}
func init() {
RootCmd.AddCommand(testCmd)
rootCmd.AddCommand(testCmd)
// Here you will define your flags and configuration settings.
+153
View File
@@ -0,0 +1,153 @@
package tpl
func MainTemplate() []byte {
return []byte(`/*
{{ .Copyright }}
{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }}
*/
package main
import "{{ .PkgName }}/cmd"
func main() {
cmd.Execute()
}
`)
}
func RootTemplate() []byte {
return []byte(`/*
{{ .Copyright }}
{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }}
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
{{ if .Viper }}
homedir "github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
{{ end }}
)
{{ if .Viper }}
var cfgFile string
{{ end }}
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "{{ .AppName }}",
Short: "A brief description of your application",
Long: ` + "`" + `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.` + "`" + `,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
{{- if .Viper }}
cobra.OnInitialize(initConfig)
{{ end }}
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
{{ if .Viper }}
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)")
{{ else }}
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.{{ .AppName }}.yaml)")
{{ end }}
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
{{ if .Viper }}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".{{ .AppName }}" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".{{ .AppName }}")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
}
{{ end }}
`)
}
func AddCommandTemplate() []byte {
return []byte(`/*
{{ .Project.Copyright }}
{{ if .Legal.Header }}{{ .Legal.Header }}{{ end }}
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// {{ .CmdName }}Cmd represents the {{ .CmdName }} command
var {{ .CmdName }}Cmd = &cobra.Command{
Use: "{{ .CmdName }}",
Short: "A brief description of your command",
Long: ` + "`" + `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.` + "`" + `,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("{{ .CmdName }} called")
},
}
func init() {
{{ .CmdParent }}.AddCommand({{ .CmdName }}Cmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// {{ .CmdName }}Cmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// {{ .CmdName }}Cmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
`)
}
+3 -1295
View File
File diff suppressed because it is too large Load Diff
+209 -24
View File
@@ -27,6 +27,9 @@ import (
flag "github.com/spf13/pflag"
)
// FParseErrWhitelist configures Flag parse errors to be ignored
type FParseErrWhitelist flag.ParseErrorsWhitelist
// Command is just that, a command for your application.
// E.g. 'go run ...' - 'run' is the command. Cobra requires
// you to define the usage and description as part of your command
@@ -75,6 +78,11 @@ type Command struct {
// group commands.
Annotations map[string]string
// Version defines the version for this command. If this value is non-empty and the command does not
// define a "version" flag, a "version" boolean flag will be added to the command and, if specified,
// will print content of the "Version" variable.
Version string
// The *Run functions are executed in the following order:
// * PersistentPreRun()
// * PreRun()
@@ -118,6 +126,10 @@ type Command struct {
// will be printed by generating docs for this command.
DisableAutoGenTag bool
// DisableFlagsInUseLine will disable the addition of [flags] to the usage
// line of a command when printing help or generating docs
DisableFlagsInUseLine bool
// DisableSuggestions disables the suggestions based on Levenshtein distance
// that go along with 'unknown command' messages.
DisableSuggestions bool
@@ -128,6 +140,9 @@ type Command struct {
// TraverseChildren parses flags on all parents before executing child command.
TraverseChildren bool
//FParseErrWhitelist flag parse errors to be ignored
FParseErrWhitelist FParseErrWhitelist
// commands is the list of commands supported by this program.
commands []*Command
// parent is a parent command for this command.
@@ -138,6 +153,11 @@ type Command struct {
commandsMaxNameLen int
// commandsAreSorted defines, if command slice are sorted or not.
commandsAreSorted bool
// commandCalledAs is the name or alias value used to call this command.
commandCalledAs struct {
name string
called bool
}
// args is actual args parsed from flags.
args []string
@@ -157,8 +177,6 @@ type Command struct {
// that we can use on every pflag set and children commands
globNormFunc func(f *flag.FlagSet, name string) flag.NormalizedName
// output is an output writer defined by user.
output io.Writer
// usageFunc is usage func defined by user.
usageFunc func(*Command) error
// usageTemplate is usage template defined by user.
@@ -173,6 +191,15 @@ type Command struct {
// helpCommand is command with usage 'help'. If it's not defined by user,
// cobra uses default help command.
helpCommand *Command
// versionTemplate is the version template defined by user.
versionTemplate string
// inReader is a reader defined by the user that replaces stdin
inReader io.Reader
// outWriter is a writer defined by the user that replaces stdout
outWriter io.Writer
// errWriter is a writer defined by the user that replaces stderr
errWriter io.Writer
}
// SetArgs sets arguments for the command. It is set to os.Args[1:] by default, if desired, can be overridden
@@ -183,8 +210,28 @@ func (c *Command) SetArgs(a []string) {
// SetOutput sets the destination for usage and error messages.
// If output is nil, os.Stderr is used.
// Deprecated: Use SetOut and/or SetErr instead
func (c *Command) SetOutput(output io.Writer) {
c.output = output
c.outWriter = output
c.errWriter = output
}
// SetOut sets the destination for usage messages.
// If newOut is nil, os.Stdout is used.
func (c *Command) SetOut(newOut io.Writer) {
c.outWriter = newOut
}
// SetErr sets the destination for error messages.
// If newErr is nil, os.Stderr is used.
func (c *Command) SetErr(newErr io.Writer) {
c.errWriter = newErr
}
// SetOut sets the source for input data
// If newIn is nil, os.Stdin is used.
func (c *Command) SetIn(newIn io.Reader) {
c.inReader = newIn
}
// SetUsageFunc sets usage function. Usage can be defined by application.
@@ -218,6 +265,11 @@ func (c *Command) SetHelpTemplate(s string) {
c.helpTemplate = s
}
// SetVersionTemplate sets version template to be used. Application can use it to set custom template.
func (c *Command) SetVersionTemplate(s string) {
c.versionTemplate = s
}
// SetGlobalNormalizationFunc sets a normalization function to all flag sets and also to child commands.
// The user should not have a cyclic dependency on commands.
func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) {
@@ -240,9 +292,19 @@ func (c *Command) OutOrStderr() io.Writer {
return c.getOut(os.Stderr)
}
// ErrOrStderr returns output to stderr
func (c *Command) ErrOrStderr() io.Writer {
return c.getErr(os.Stderr)
}
// ErrOrStderr returns output to stderr
func (c *Command) InOrStdin() io.Reader {
return c.getIn(os.Stdin)
}
func (c *Command) getOut(def io.Writer) io.Writer {
if c.output != nil {
return c.output
if c.outWriter != nil {
return c.outWriter
}
if c.HasParent() {
return c.parent.getOut(def)
@@ -250,6 +312,26 @@ func (c *Command) getOut(def io.Writer) io.Writer {
return def
}
func (c *Command) getErr(def io.Writer) io.Writer {
if c.errWriter != nil {
return c.errWriter
}
if c.HasParent() {
return c.parent.getErr(def)
}
return def
}
func (c *Command) getIn(def io.Reader) io.Reader {
if c.inReader != nil {
return c.inReader
}
if c.HasParent() {
return c.parent.getIn(def)
}
return def
}
// UsageFunc returns either the function set by SetUsageFunc for this command
// or a parent, or it returns a default usage function.
func (c *Command) UsageFunc() (f func(*Command) error) {
@@ -302,13 +384,22 @@ func (c *Command) Help() error {
return nil
}
// UsageString return usage string.
// UsageString returns usage string.
func (c *Command) UsageString() string {
tmpOutput := c.output
// Storing normal writers
tmpOutput := c.outWriter
tmpErr := c.errWriter
bb := new(bytes.Buffer)
c.SetOutput(bb)
c.outWriter = bb
c.errWriter = bb
c.Usage()
c.output = tmpOutput
// Setting things back to normal
c.outWriter = tmpOutput
c.errWriter = tmpErr
return bb.String()
}
@@ -407,6 +498,19 @@ func (c *Command) HelpTemplate() string {
{{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
}
// VersionTemplate return version template for the command.
func (c *Command) VersionTemplate() string {
if c.versionTemplate != "" {
return c.versionTemplate
}
if c.HasParent() {
return c.parent.VersionTemplate()
}
return `{{with .Name}}{{printf "%s " .}}{{end}}{{printf "version %s" .Version}}
`
}
func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.Lookup(name)
if flag == nil {
@@ -441,6 +545,9 @@ Loop:
s := args[0]
args = args[1:]
switch {
case s == "--":
// "--" terminates the flags
break Loop
case strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags):
// If '--flag arg' then
// delete arg from args.
@@ -528,6 +635,7 @@ func (c *Command) findNext(next string) *Command {
matches := make([]*Command, 0)
for _, cmd := range c.commands {
if cmd.Name() == next || cmd.HasAlias(next) {
cmd.commandCalledAs.name = next
return cmd
}
if EnablePrefixMatching && cmd.hasNameOrAliasPrefix(next) {
@@ -538,6 +646,7 @@ func (c *Command) findNext(next string) *Command {
if len(matches) == 1 {
return matches[0]
}
return nil
}
@@ -621,10 +730,8 @@ func (c *Command) Root() *Command {
return c
}
// ArgsLenAtDash will return the length of f.Args at the moment when a -- was
// found during arg parsing. This allows your program to know which args were
// before the -- and which came after. (Description from
// https://godoc.org/github.com/spf13/pflag#FlagSet.ArgsLenAtDash).
// ArgsLenAtDash will return the length of c.Flags().Args at the moment
// when a -- was found during args parsing.
func (c *Command) ArgsLenAtDash() int {
return c.Flags().ArgsLenAtDash()
}
@@ -638,9 +745,10 @@ func (c *Command) execute(a []string) (err error) {
c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated)
}
// initialize help flag as the last point possible to allow for user
// initialize help and version flag at the last point possible to allow for user
// overriding
c.InitDefaultHelpFlag()
c.InitDefaultVersionFlag()
err = c.ParseFlags(a)
if err != nil {
@@ -657,7 +765,27 @@ func (c *Command) execute(a []string) (err error) {
return err
}
if helpVal || !c.Runnable() {
if helpVal {
return flag.ErrHelp
}
// for back-compat, only add version flag behavior if version is defined
if c.Version != "" {
versionVal, err := c.Flags().GetBool("version")
if err != nil {
c.Println("\"version\" flag declared as non-bool. Please correct your code")
return err
}
if versionVal {
err := tmpl(c.OutOrStdout(), c.VersionTemplate(), c)
if err != nil {
c.Println(err)
}
return err
}
}
if !c.Runnable() {
return flag.ErrHelp
}
@@ -753,13 +881,11 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
// overriding
c.InitDefaultHelpCmd()
var args []string
args := c.args
// Workaround FAIL with "go test -v" or "cobra.test -test.v", see #155
if c.args == nil && filepath.Base(os.Args[0]) != "cobra.test" {
args = os.Args[1:]
} else {
args = c.args
}
var flags []string
@@ -780,6 +906,11 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
return c, err
}
cmd.commandCalledAs.called = true
if cmd.commandCalledAs.name == "" {
cmd.commandCalledAs.name = cmd.Name()
}
err = cmd.execute(flags)
if err != nil {
// Always show help if requested, even if SilenceErrors is in
@@ -825,7 +956,7 @@ func (c *Command) validateRequiredFlags() error {
})
if len(missingFlagNames) > 0 {
return fmt.Errorf(`Required flag(s) "%s" have/has not been set`, strings.Join(missingFlagNames, `", "`))
return fmt.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlagNames, `", "`))
}
return nil
}
@@ -846,6 +977,27 @@ func (c *Command) InitDefaultHelpFlag() {
}
}
// InitDefaultVersionFlag adds default version flag to c.
// It is called automatically by executing the c.
// If c already has a version flag, it will do nothing.
// If c.Version is empty, it will do nothing.
func (c *Command) InitDefaultVersionFlag() {
if c.Version == "" {
return
}
c.mergePersistentFlags()
if c.Flags().Lookup("version") == nil {
usage := "version for "
if c.Name() == "" {
usage += "this command"
} else {
usage += c.Name()
}
c.Flags().Bool("version", false, usage)
}
}
// InitDefaultHelpCmd adds default help command to c.
// It is called automatically by executing the c or by calling help and usage.
// If c already has help command or c has no subcommands, it will do nothing.
@@ -877,7 +1029,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`,
c.AddCommand(c.helpCommand)
}
// ResetCommands used for testing.
// ResetCommands delete parent, subcommand and help command from c.
func (c *Command) ResetCommands() {
c.parent = nil
c.commands = nil
@@ -980,6 +1132,21 @@ func (c *Command) Printf(format string, i ...interface{}) {
c.Print(fmt.Sprintf(format, i...))
}
// PrintErr is a convenience method to Print to the defined Err output, fallback to Stderr if not set.
func (c *Command) PrintErr(i ...interface{}) {
fmt.Fprint(c.ErrOrStderr(), i...)
}
// PrintErrln is a convenience method to Println to the defined Err output, fallback to Stderr if not set.
func (c *Command) PrintErrln(i ...interface{}) {
c.Print(fmt.Sprintln(i...))
}
// PrintErrf is a convenience method to Printf to the defined Err output, fallback to Stderr if not set.
func (c *Command) PrintErrf(format string, i ...interface{}) {
c.Print(fmt.Sprintf(format, i...))
}
// CommandPath returns the full path to this command.
func (c *Command) CommandPath() string {
if c.HasParent() {
@@ -996,6 +1163,9 @@ func (c *Command) UseLine() string {
} else {
useline = c.Use
}
if c.DisableFlagsInUseLine {
return useline
}
if c.HasAvailableFlags() && !strings.Contains(useline, "[flags]") {
useline += " [flags]"
}
@@ -1063,14 +1233,25 @@ func (c *Command) HasAlias(s string) bool {
return false
}
// CalledAs returns the command name or alias that was used to invoke
// this command or an empty string if the command has not been called.
func (c *Command) CalledAs() string {
if c.commandCalledAs.called {
return c.commandCalledAs.name
}
return ""
}
// hasNameOrAliasPrefix returns true if the Name or any of aliases start
// with prefix
func (c *Command) hasNameOrAliasPrefix(prefix string) bool {
if strings.HasPrefix(c.Name(), prefix) {
c.commandCalledAs.name = c.Name()
return true
}
for _, alias := range c.Aliases {
if strings.HasPrefix(alias, prefix) {
c.commandCalledAs.name = alias
return true
}
}
@@ -1163,7 +1344,7 @@ func (c *Command) HasAvailableSubCommands() bool {
}
}
// the command either has no sub comamnds, or no available (non deprecated/help/hidden)
// the command either has no sub commands, or no available (non deprecated/help/hidden)
// sub commands
return false
}
@@ -1173,7 +1354,7 @@ func (c *Command) HasParent() bool {
return c.parent != nil
}
// GlobalNormalizationFunc returns the global normalization function or nil if doesn't exists.
// GlobalNormalizationFunc returns the global normalization function or nil if it doesn't exist.
func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) flag.NormalizedName {
return c.globNormFunc
}
@@ -1231,7 +1412,7 @@ func (c *Command) LocalFlags() *flag.FlagSet {
return c.lflags
}
// InheritedFlags returns all flags which were inherited from parents commands.
// InheritedFlags returns all flags which were inherited from parent commands.
func (c *Command) InheritedFlags() *flag.FlagSet {
c.mergePersistentFlags()
@@ -1273,7 +1454,7 @@ func (c *Command) PersistentFlags() *flag.FlagSet {
return c.pflags
}
// ResetFlags is used in testing.
// ResetFlags deletes all flags from command.
func (c *Command) ResetFlags() {
c.flagErrorBuf = new(bytes.Buffer)
c.flagErrorBuf.Reset()
@@ -1365,6 +1546,10 @@ func (c *Command) ParseFlags(args []string) error {
}
beforeErrorBufLen := c.flagErrorBuf.Len()
c.mergePersistentFlags()
//do it here after merging all flags and just before parse
c.Flags().ParseErrorsWhitelist = flag.ParseErrorsWhitelist(c.FParseErrWhitelist)
err := c.Flags().Parse(args)
// Print warnings if they occurred (e.g. deprecated flag messages).
if c.flagErrorBuf.Len()-beforeErrorBufLen > 0 && err == nil {
+1532 -283
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -3,6 +3,7 @@
package cobra
import (
"fmt"
"os"
"time"
@@ -14,7 +15,12 @@ var preExecHookFn = preExecHook
func preExecHook(c *Command) {
if MousetrapHelpText != "" && mousetrap.StartedByExplorer() {
c.Print(MousetrapHelpText)
time.Sleep(5 * time.Second)
if MousetrapDisplayDuration > 0 {
time.Sleep(MousetrapDisplayDuration)
} else {
c.Println("Press return to continue...")
fmt.Scanln()
}
os.Exit(1)
}
}
+49 -108
View File
@@ -1,145 +1,86 @@
package doc
import (
"bytes"
"fmt"
"runtime"
"strings"
"testing"
"github.com/spf13/cobra"
)
var flagb1, flagb2, flagb3, flagbr, flagbp bool
var flags1, flags2a, flags2b, flags3 string
var flagi1, flagi2, flagi3, flagir int
func emptyRun(*cobra.Command, []string) {}
const strtwoParentHelp = "help message for parent flag strtwo"
const strtwoChildHelp = "help message for child flag strtwo"
func init() {
rootCmd.PersistentFlags().StringP("rootflag", "r", "two", "")
rootCmd.PersistentFlags().StringP("strtwo", "t", "two", "help message for parent flag strtwo")
var cmdEcho = &cobra.Command{
echoCmd.PersistentFlags().StringP("strone", "s", "one", "help message for flag strone")
echoCmd.PersistentFlags().BoolP("persistentbool", "p", false, "help message for flag persistentbool")
echoCmd.Flags().IntP("intone", "i", 123, "help message for flag intone")
echoCmd.Flags().BoolP("boolone", "b", true, "help message for flag boolone")
timesCmd.PersistentFlags().StringP("strtwo", "t", "2", "help message for child flag strtwo")
timesCmd.Flags().IntP("inttwo", "j", 234, "help message for flag inttwo")
timesCmd.Flags().BoolP("booltwo", "c", false, "help message for flag booltwo")
printCmd.PersistentFlags().StringP("strthree", "s", "three", "help message for flag strthree")
printCmd.Flags().IntP("intthree", "i", 345, "help message for flag intthree")
printCmd.Flags().BoolP("boolthree", "b", true, "help message for flag boolthree")
echoCmd.AddCommand(timesCmd, echoSubCmd, deprecatedCmd)
rootCmd.AddCommand(printCmd, echoCmd)
}
var rootCmd = &cobra.Command{
Use: "root",
Short: "Root short description",
Long: "Root long description",
Run: emptyRun,
}
var echoCmd = &cobra.Command{
Use: "echo [string to echo]",
Aliases: []string{"say"},
Short: "Echo anything to the screen",
Long: `an utterly useless command for testing.`,
Long: "an utterly useless command for testing",
Example: "Just run cobra-test echo",
}
var cmdEchoSub = &cobra.Command{
var echoSubCmd = &cobra.Command{
Use: "echosub [string to print]",
Short: "second sub command for echo",
Long: `an absolutely utterly useless command for testing gendocs!.`,
Run: func(cmd *cobra.Command, args []string) {},
Long: "an absolutely utterly useless command for testing gendocs!.",
Run: emptyRun,
}
var cmdDeprecated = &cobra.Command{
var timesCmd = &cobra.Command{
Use: "times [# times] [string to echo]",
SuggestFor: []string{"counts"},
Short: "Echo anything to the screen more times",
Long: `a slightly useless command for testing.`,
Run: emptyRun,
}
var deprecatedCmd = &cobra.Command{
Use: "deprecated [can't do anything here]",
Short: "A command which is deprecated",
Long: `an absolutely utterly useless command for testing deprecation!.`,
Deprecated: "Please use echo instead",
}
var cmdTimes = &cobra.Command{
Use: "times [# times] [string to echo]",
SuggestFor: []string{"counts"},
Short: "Echo anything to the screen more times",
Long: `a slightly useless command for testing.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
Run: func(cmd *cobra.Command, args []string) {},
}
var cmdPrint = &cobra.Command{
var printCmd = &cobra.Command{
Use: "print [string to print]",
Short: "Print anything to the screen",
Long: `an absolutely utterly useless command for testing.`,
}
var cmdRootNoRun = &cobra.Command{
Use: "cobra-test",
Short: "The root can run its own function",
Long: "The root description for help",
}
var cmdRootSameName = &cobra.Command{
Use: "print",
Short: "Root with the same name as a subcommand",
Long: "The root description for help",
}
var cmdRootWithRun = &cobra.Command{
Use: "cobra-test",
Short: "The root can run its own function",
Long: "The root description for help",
}
var cmdSubNoRun = &cobra.Command{
Use: "subnorun",
Short: "A subcommand without a Run function",
Long: "A long output about a subcommand without a Run function",
}
var cmdVersion1 = &cobra.Command{
Use: "version",
Short: "Print the version number",
Long: `First version of the version command`,
}
var cmdVersion2 = &cobra.Command{
Use: "version",
Short: "Print the version number",
Long: `Second version of the version command`,
}
func flagInit() {
cmdEcho.ResetFlags()
cmdPrint.ResetFlags()
cmdTimes.ResetFlags()
cmdRootNoRun.ResetFlags()
cmdRootSameName.ResetFlags()
cmdRootWithRun.ResetFlags()
cmdSubNoRun.ResetFlags()
cmdRootNoRun.PersistentFlags().StringVarP(&flags2a, "strtwo", "t", "two", strtwoParentHelp)
cmdEcho.Flags().IntVarP(&flagi1, "intone", "i", 123, "help message for flag intone")
cmdTimes.Flags().IntVarP(&flagi2, "inttwo", "j", 234, "help message for flag inttwo")
cmdPrint.Flags().IntVarP(&flagi3, "intthree", "i", 345, "help message for flag intthree")
cmdEcho.PersistentFlags().StringVarP(&flags1, "strone", "s", "one", "help message for flag strone")
cmdEcho.PersistentFlags().BoolVarP(&flagbp, "persistentbool", "p", false, "help message for flag persistentbool")
cmdTimes.PersistentFlags().StringVarP(&flags2b, "strtwo", "t", "2", strtwoChildHelp)
cmdPrint.PersistentFlags().StringVarP(&flags3, "strthree", "s", "three", "help message for flag strthree")
cmdEcho.Flags().BoolVarP(&flagb1, "boolone", "b", true, "help message for flag boolone")
cmdTimes.Flags().BoolVarP(&flagb2, "booltwo", "c", false, "help message for flag booltwo")
cmdPrint.Flags().BoolVarP(&flagb3, "boolthree", "b", true, "help message for flag boolthree")
cmdVersion1.ResetFlags()
cmdVersion2.ResetFlags()
}
func initializeWithRootCmd() *cobra.Command {
cmdRootWithRun.ResetCommands()
flagInit()
cmdRootWithRun.Flags().BoolVarP(&flagbr, "boolroot", "b", false, "help message for flag boolroot")
cmdRootWithRun.Flags().IntVarP(&flagir, "introot", "i", 321, "help message for flag introot")
return cmdRootWithRun
}
func checkStringContains(t *testing.T, found, expected string) {
if !strings.Contains(found, expected) {
logErr(t, found, expected)
func checkStringContains(t *testing.T, got, expected string) {
if !strings.Contains(got, expected) {
t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got)
}
}
func checkStringOmits(t *testing.T, found, expected string) {
if strings.Contains(found, expected) {
logErr(t, found, expected)
func checkStringOmits(t *testing.T, got, expected string) {
if strings.Contains(got, expected) {
t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got)
}
}
func logErr(t *testing.T, found, expected string) {
out := new(bytes.Buffer)
_, _, line, ok := runtime.Caller(2)
if ok {
fmt.Fprintf(out, "Line: %d ", line)
}
fmt.Fprintf(out, "Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
t.Errorf(out.String())
}
+16 -5
View File
@@ -20,6 +20,7 @@ import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@@ -87,7 +88,7 @@ type GenManTreeOptions struct {
// GenManHeader is a lot like the .TH header at the start of man pages. These
// include the title, section, date, source, and manual. We will use the
// current time if Date if unset and will use "Auto generated by spf13/cobra"
// current time if Date is unset and will use "Auto generated by spf13/cobra"
// if the Source is unset.
type GenManHeader struct {
Title string
@@ -104,14 +105,16 @@ func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
if header == nil {
header = &GenManHeader{}
}
fillHeader(header, cmd.CommandPath())
if err := fillHeader(header, cmd.CommandPath()); err != nil {
return err
}
b := genMan(cmd, header)
_, err := w.Write(md2man.Render(b))
return err
}
func fillHeader(header *GenManHeader, name string) {
func fillHeader(header *GenManHeader, name string) error {
if header.Title == "" {
header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
}
@@ -120,12 +123,20 @@ func fillHeader(header *GenManHeader, name string) {
}
if header.Date == nil {
now := time.Now()
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
if err != nil {
return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
}
now = time.Unix(unixEpoch, 0)
}
header.Date = &now
}
header.date = (*header.Date).Format("Jan 2006")
if header.Source == "" {
header.Source = "Auto generated by spf13/cobra"
}
return nil
}
func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
@@ -176,13 +187,13 @@ func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
flags := command.NonInheritedFlags()
if flags.HasFlags() {
if flags.HasAvailableFlags() {
buf.WriteString("# OPTIONS\n")
manPrintFlags(buf, flags)
buf.WriteString("\n")
}
flags = command.InheritedFlags()
if flags.HasFlags() {
if flags.HasAvailableFlags() {
buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
manPrintFlags(buf, flags)
buf.WriteString("\n")
+85 -88
View File
@@ -18,135 +18,133 @@ func translate(in string) string {
}
func TestGenManDoc(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
header := &GenManHeader{
Title: "Project",
Section: "2",
}
// We generate on a subcommand so we have both subcommands and parents
if err := GenMan(cmdEcho, header, out); err != nil {
buf := new(bytes.Buffer)
if err := GenMan(echoCmd, header, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
// Make sure parent has - in CommandPath() in SEE ALSO:
parentPath := cmdEcho.Parent().CommandPath()
parentPath := echoCmd.Parent().CommandPath()
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
expected := translate(dashParentPath)
expected = expected + "(" + header.Section + ")"
checkStringContains(t, found, expected)
checkStringContains(t, output, expected)
// Our description
expected = translate(cmdEcho.Name())
checkStringContains(t, found, expected)
checkStringContains(t, output, translate(echoCmd.Name()))
checkStringContains(t, output, translate(echoCmd.Name()))
checkStringContains(t, output, "boolone")
checkStringContains(t, output, "rootflag")
checkStringContains(t, output, translate(rootCmd.Name()))
checkStringContains(t, output, translate(echoSubCmd.Name()))
checkStringOmits(t, output, translate(deprecatedCmd.Name()))
checkStringContains(t, output, translate("Auto generated"))
}
// Better have our example
expected = translate(cmdEcho.Name())
checkStringContains(t, found, expected)
func TestGenManNoHiddenParents(t *testing.T) {
header := &GenManHeader{
Title: "Project",
Section: "2",
}
// A local flag
expected = "boolone"
checkStringContains(t, found, expected)
// We generate on a subcommand so we have both subcommands and parents
for _, name := range []string{"rootflag", "strtwo"} {
f := rootCmd.PersistentFlags().Lookup(name)
f.Hidden = true
defer func() { f.Hidden = false }()
}
buf := new(bytes.Buffer)
if err := GenMan(echoCmd, header, buf); err != nil {
t.Fatal(err)
}
output := buf.String()
// persistent flag on parent
expected = "rootflag"
checkStringContains(t, found, expected)
// Make sure parent has - in CommandPath() in SEE ALSO:
parentPath := echoCmd.Parent().CommandPath()
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
expected := translate(dashParentPath)
expected = expected + "(" + header.Section + ")"
checkStringContains(t, output, expected)
// We better output info about our parent
expected = translate(cmdRootWithRun.Name())
checkStringContains(t, found, expected)
// And about subcommands
expected = translate(cmdEchoSub.Name())
checkStringContains(t, found, expected)
unexpected := translate(cmdDeprecated.Name())
checkStringOmits(t, found, unexpected)
// auto generated
expected = translate("Auto generated")
checkStringContains(t, found, expected)
checkStringContains(t, output, translate(echoCmd.Name()))
checkStringContains(t, output, translate(echoCmd.Name()))
checkStringContains(t, output, "boolone")
checkStringOmits(t, output, "rootflag")
checkStringContains(t, output, translate(rootCmd.Name()))
checkStringContains(t, output, translate(echoSubCmd.Name()))
checkStringOmits(t, output, translate(deprecatedCmd.Name()))
checkStringContains(t, output, translate("Auto generated"))
checkStringOmits(t, output, "OPTIONS INHERITED FROM PARENT COMMANDS")
}
func TestGenManNoGenTag(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
cmdEcho.DisableAutoGenTag = true
out := new(bytes.Buffer)
echoCmd.DisableAutoGenTag = true
defer func() { echoCmd.DisableAutoGenTag = false }()
header := &GenManHeader{
Title: "Project",
Section: "2",
}
// We generate on a subcommand so we have both subcommands and parents
if err := GenMan(cmdEcho, header, out); err != nil {
buf := new(bytes.Buffer)
if err := GenMan(echoCmd, header, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
unexpected := translate("#HISTORY")
checkStringOmits(t, found, unexpected)
checkStringOmits(t, output, unexpected)
}
func TestGenManSeeAlso(t *testing.T) {
noop := func(cmd *cobra.Command, args []string) {}
rootCmd := &cobra.Command{Use: "root", Run: emptyRun}
aCmd := &cobra.Command{Use: "aaa", Run: emptyRun, Hidden: true} // #229
bCmd := &cobra.Command{Use: "bbb", Run: emptyRun}
cCmd := &cobra.Command{Use: "ccc", Run: emptyRun}
rootCmd.AddCommand(aCmd, bCmd, cCmd)
top := &cobra.Command{Use: "top", Run: noop}
aaa := &cobra.Command{Use: "aaa", Run: noop, Hidden: true} // #229
bbb := &cobra.Command{Use: "bbb", Run: noop}
ccc := &cobra.Command{Use: "ccc", Run: noop}
top.AddCommand(aaa, bbb, ccc)
out := new(bytes.Buffer)
buf := new(bytes.Buffer)
header := &GenManHeader{}
if err := GenMan(top, header, out); err != nil {
if err := GenMan(rootCmd, header, buf); err != nil {
t.Fatal(err)
}
scanner := bufio.NewScanner(buf)
scanner := bufio.NewScanner(out)
if err := AssertLineFound(scanner, ".SH SEE ALSO"); err != nil {
t.Fatal(fmt.Errorf("Couldn't find SEE ALSO section header: %s", err.Error()))
if err := assertLineFound(scanner, ".SH SEE ALSO"); err != nil {
t.Fatalf("Couldn't find SEE ALSO section header: %v", err)
}
if err := AssertNextLineEquals(scanner, ".PP"); err != nil {
t.Fatal(fmt.Errorf("First line after SEE ALSO wasn't break-indent: %s", err.Error()))
if err := assertNextLineEquals(scanner, ".PP"); err != nil {
t.Fatalf("First line after SEE ALSO wasn't break-indent: %v", err)
}
if err := AssertNextLineEquals(scanner, `\fBtop\-bbb(1)\fP, \fBtop\-ccc(1)\fP`); err != nil {
t.Fatal(fmt.Errorf("Second line after SEE ALSO wasn't correct: %s", err.Error()))
if err := assertNextLineEquals(scanner, `\fBroot\-bbb(1)\fP, \fBroot\-ccc(1)\fP`); err != nil {
t.Fatalf("Second line after SEE ALSO wasn't correct: %v", err)
}
}
func TestManPrintFlagsHidesShortDeperecated(t *testing.T) {
cmd := &cobra.Command{}
flags := cmd.Flags()
flags.StringP("foo", "f", "default", "Foo flag")
flags.MarkShorthandDeprecated("foo", "don't use it no more")
c := &cobra.Command{}
c.Flags().StringP("foo", "f", "default", "Foo flag")
c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")
out := new(bytes.Buffer)
manPrintFlags(out, flags)
buf := new(bytes.Buffer)
manPrintFlags(buf, c.Flags())
got := buf.String()
expected := "**--foo**=\"default\"\n\tFoo flag\n\n"
if out.String() != expected {
t.Fatalf("Expected %s, but got %s", expected, out.String())
if got != expected {
t.Errorf("Expected %v, got %v", expected, got)
}
}
func TestGenManTree(t *testing.T) {
cmd := &cobra.Command{
Use: "do [OPTIONS] arg1 arg2",
}
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
header := &GenManHeader{Section: "2"}
tmpdir, err := ioutil.TempDir("", "test-gen-man-tree")
if err != nil {
@@ -154,7 +152,7 @@ func TestGenManTree(t *testing.T) {
}
defer os.RemoveAll(tmpdir)
if err := GenManTree(cmd, header, tmpdir); err != nil {
if err := GenManTree(c, header, tmpdir); err != nil {
t.Fatalf("GenManTree failed: %s", err.Error())
}
@@ -167,7 +165,7 @@ func TestGenManTree(t *testing.T) {
}
}
func AssertLineFound(scanner *bufio.Scanner, expectedLine string) error {
func assertLineFound(scanner *bufio.Scanner, expectedLine string) error {
for scanner.Scan() {
line := scanner.Text()
if line == expectedLine {
@@ -176,30 +174,29 @@ func AssertLineFound(scanner *bufio.Scanner, expectedLine string) error {
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("AssertLineFound: scan failed: %s", err.Error())
return fmt.Errorf("scan failed: %s", err)
}
return fmt.Errorf("AssertLineFound: hit EOF before finding %#v", expectedLine)
return fmt.Errorf("hit EOF before finding %v", expectedLine)
}
func AssertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error {
func assertNextLineEquals(scanner *bufio.Scanner, expectedLine string) error {
if scanner.Scan() {
line := scanner.Text()
if line == expectedLine {
return nil
}
return fmt.Errorf("AssertNextLineEquals: got %#v, not %#v", line, expectedLine)
return fmt.Errorf("got %v, not %v", line, expectedLine)
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("AssertNextLineEquals: scan failed: %s", err.Error())
return fmt.Errorf("scan failed: %v", err)
}
return fmt.Errorf("AssertNextLineEquals: hit EOF before finding %#v", expectedLine)
return fmt.Errorf("hit EOF before finding %v", expectedLine)
}
func BenchmarkGenManToFile(b *testing.B) {
c := initializeWithRootCmd()
file, err := ioutil.TempFile("", "")
if err != nil {
b.Fatal(err)
@@ -209,7 +206,7 @@ func BenchmarkGenManToFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := GenMan(c, nil, file); err != nil {
if err := GenMan(rootCmd, nil, file); err != nil {
b.Fatal(err)
}
}
+4 -4
View File
@@ -29,7 +29,7 @@ import (
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasFlags() {
if flags.HasAvailableFlags() {
buf.WriteString("### Options\n\n```\n")
flags.PrintDefaults()
buf.WriteString("```\n\n")
@@ -37,7 +37,7 @@ func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasFlags() {
if parentFlags.HasAvailableFlags() {
buf.WriteString("### Options inherited from parent commands\n\n```\n")
parentFlags.PrintDefaults()
buf.WriteString("```\n\n")
@@ -67,7 +67,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
buf.WriteString("## " + name + "\n\n")
buf.WriteString(short + "\n\n")
buf.WriteString("### Synopsis\n\n")
buf.WriteString("\n" + long + "\n\n")
buf.WriteString(long + "\n\n")
if cmd.Runnable() {
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
@@ -82,7 +82,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
return err
}
if hasSeeAlso(cmd) {
buf.WriteString("### SEE ALSO\n")
buf.WriteString("### SEE ALSO\n\n")
if cmd.HasParent() {
parent := cmd.Parent()
pname := parent.CommandPath()
+43 -69
View File
@@ -5,100 +5,75 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestGenMdDoc(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
// We generate on s subcommand so we have both subcommands and parents
if err := GenMarkdown(cmdEcho, out); err != nil {
// We generate on subcommand so we have both subcommands and parents.
buf := new(bytes.Buffer)
if err := GenMarkdown(echoCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
// Our description
expected := cmdEcho.Long
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
checkStringContains(t, output, echoCmd.Long)
checkStringContains(t, output, echoCmd.Example)
checkStringContains(t, output, "boolone")
checkStringContains(t, output, "rootflag")
checkStringContains(t, output, rootCmd.Short)
checkStringContains(t, output, echoSubCmd.Short)
checkStringOmits(t, output, deprecatedCmd.Short)
checkStringContains(t, output, "Options inherited from parent commands")
}
// Better have our example
expected = cmdEcho.Example
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
func TestGenMdNoHiddenParents(t *testing.T) {
// We generate on subcommand so we have both subcommands and parents.
for _, name := range []string{"rootflag", "strtwo"} {
f := rootCmd.PersistentFlags().Lookup(name)
f.Hidden = true
defer func() { f.Hidden = false }()
}
buf := new(bytes.Buffer)
if err := GenMarkdown(echoCmd, buf); err != nil {
t.Fatal(err)
}
output := buf.String()
// A local flag
expected = "boolone"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// persistent flag on parent
expected = "rootflag"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// We better output info about our parent
expected = cmdRootWithRun.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// And about subcommands
expected = cmdEchoSub.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
unexpected := cmdDeprecated.Short
if strings.Contains(found, unexpected) {
t.Errorf("Unexpected response.\nFound: %v\nBut should not have!!\n", unexpected)
}
checkStringContains(t, output, echoCmd.Long)
checkStringContains(t, output, echoCmd.Example)
checkStringContains(t, output, "boolone")
checkStringOmits(t, output, "rootflag")
checkStringContains(t, output, rootCmd.Short)
checkStringContains(t, output, echoSubCmd.Short)
checkStringOmits(t, output, deprecatedCmd.Short)
checkStringOmits(t, output, "Options inherited from parent commands")
}
func TestGenMdNoTag(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
c.DisableAutoGenTag = true
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
rootCmd.DisableAutoGenTag = true
defer func() { rootCmd.DisableAutoGenTag = false }()
if err := GenMarkdown(c, out); err != nil {
buf := new(bytes.Buffer)
if err := GenMarkdown(rootCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
unexpected := "Auto generated"
checkStringOmits(t, found, unexpected)
output := buf.String()
checkStringOmits(t, output, "Auto generated")
}
func TestGenMdTree(t *testing.T) {
cmd := &cobra.Command{
Use: "do [OPTIONS] arg1 arg2",
}
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
tmpdir, err := ioutil.TempDir("", "test-gen-md-tree")
if err != nil {
t.Fatalf("Failed to create tmpdir: %s", err.Error())
t.Fatalf("Failed to create tmpdir: %v", err)
}
defer os.RemoveAll(tmpdir)
if err := GenMarkdownTree(cmd, tmpdir); err != nil {
t.Fatalf("GenMarkdownTree failed: %s", err.Error())
if err := GenMarkdownTree(c, tmpdir); err != nil {
t.Fatalf("GenMarkdownTree failed: %v", err)
}
if _, err := os.Stat(filepath.Join(tmpdir, "do.md")); err != nil {
@@ -107,7 +82,6 @@ func TestGenMdTree(t *testing.T) {
}
func BenchmarkGenMarkdownToFile(b *testing.B) {
c := initializeWithRootCmd()
file, err := ioutil.TempFile("", "")
if err != nil {
b.Fatal(err)
@@ -117,7 +91,7 @@ func BenchmarkGenMarkdownToFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := GenMarkdown(c, file); err != nil {
if err := GenMarkdown(rootCmd, file); err != nil {
b.Fatal(err)
}
}
+2 -2
View File
@@ -29,7 +29,7 @@ import (
func printOptionsReST(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
flags := cmd.NonInheritedFlags()
flags.SetOutput(buf)
if flags.HasFlags() {
if flags.HasAvailableFlags() {
buf.WriteString("Options\n")
buf.WriteString("~~~~~~~\n\n::\n\n")
flags.PrintDefaults()
@@ -38,7 +38,7 @@ func printOptionsReST(buf *bytes.Buffer, cmd *cobra.Command, name string) error
parentFlags := cmd.InheritedFlags()
parentFlags.SetOutput(buf)
if parentFlags.HasFlags() {
if parentFlags.HasAvailableFlags() {
buf.WriteString("Options inherited from parent commands\n")
buf.WriteString("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n::\n\n")
parentFlags.PrintDefaults()
+41 -66
View File
@@ -5,99 +5,75 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestGenRSTDoc(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
// We generate on s subcommand so we have both subcommands and parents
if err := GenReST(cmdEcho, out); err != nil {
// We generate on a subcommand so we have both subcommands and parents
buf := new(bytes.Buffer)
if err := GenReST(echoCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
// Our description
expected := cmdEcho.Long
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
checkStringContains(t, output, echoCmd.Long)
checkStringContains(t, output, echoCmd.Example)
checkStringContains(t, output, "boolone")
checkStringContains(t, output, "rootflag")
checkStringContains(t, output, rootCmd.Short)
checkStringContains(t, output, echoSubCmd.Short)
checkStringOmits(t, output, deprecatedCmd.Short)
}
// Better have our example
expected = cmdEcho.Example
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
func TestGenRSTNoHiddenParents(t *testing.T) {
// We generate on a subcommand so we have both subcommands and parents
for _, name := range []string{"rootflag", "strtwo"} {
f := rootCmd.PersistentFlags().Lookup(name)
f.Hidden = true
defer func() { f.Hidden = false }()
}
buf := new(bytes.Buffer)
if err := GenReST(echoCmd, buf); err != nil {
t.Fatal(err)
}
output := buf.String()
// A local flag
expected = "boolone"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// persistent flag on parent
expected = "rootflag"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// We better output info about our parent
expected = cmdRootWithRun.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// And about subcommands
expected = cmdEchoSub.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
unexpected := cmdDeprecated.Short
if strings.Contains(found, unexpected) {
t.Errorf("Unexpected response.\nFound: %v\nBut should not have!!\n", unexpected)
}
checkStringContains(t, output, echoCmd.Long)
checkStringContains(t, output, echoCmd.Example)
checkStringContains(t, output, "boolone")
checkStringOmits(t, output, "rootflag")
checkStringContains(t, output, rootCmd.Short)
checkStringContains(t, output, echoSubCmd.Short)
checkStringOmits(t, output, deprecatedCmd.Short)
checkStringOmits(t, output, "Options inherited from parent commands")
}
func TestGenRSTNoTag(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
c.DisableAutoGenTag = true
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
rootCmd.DisableAutoGenTag = true
defer func() { rootCmd.DisableAutoGenTag = false }()
if err := GenReST(c, out); err != nil {
buf := new(bytes.Buffer)
if err := GenReST(rootCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
unexpected := "Auto generated"
checkStringOmits(t, found, unexpected)
checkStringOmits(t, output, unexpected)
}
func TestGenRSTTree(t *testing.T) {
cmd := &cobra.Command{
Use: "do [OPTIONS] arg1 arg2",
}
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
tmpdir, err := ioutil.TempDir("", "test-gen-rst-tree")
if err != nil {
t.Fatalf("Failed to create tmpdir: %s", err.Error())
}
defer os.RemoveAll(tmpdir)
if err := GenReSTTree(cmd, tmpdir); err != nil {
if err := GenReSTTree(c, tmpdir); err != nil {
t.Fatalf("GenReSTTree failed: %s", err.Error())
}
@@ -107,7 +83,6 @@ func TestGenRSTTree(t *testing.T) {
}
func BenchmarkGenReSTToFile(b *testing.B) {
c := initializeWithRootCmd()
file, err := ioutil.TempFile("", "")
if err != nil {
b.Fatal(err)
@@ -117,7 +92,7 @@ func BenchmarkGenReSTToFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := GenReST(c, file); err != nil {
if err := GenReST(rootCmd, file); err != nil {
b.Fatal(err)
}
}
+18 -69
View File
@@ -5,92 +5,42 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
)
func TestGenYamlDoc(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
// We generate on s subcommand so we have both subcommands and parents
if err := GenYaml(cmdEcho, out); err != nil {
buf := new(bytes.Buffer)
if err := GenYaml(echoCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
output := buf.String()
// Our description
expected := cmdEcho.Long
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// Better have our example
expected = cmdEcho.Example
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// A local flag
expected = "boolone"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// persistent flag on parent
expected = "rootflag"
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// We better output info about our parent
expected = cmdRootWithRun.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
// And about subcommands
expected = cmdEchoSub.Short
if !strings.Contains(found, expected) {
t.Errorf("Unexpected response.\nExpecting to contain: \n %q\nGot:\n %q\n", expected, found)
}
unexpected := cmdDeprecated.Short
if strings.Contains(found, unexpected) {
t.Errorf("Unexpected response.\nFound: %v\nBut should not have!!\n", unexpected)
}
checkStringContains(t, output, echoCmd.Long)
checkStringContains(t, output, echoCmd.Example)
checkStringContains(t, output, "boolone")
checkStringContains(t, output, "rootflag")
checkStringContains(t, output, rootCmd.Short)
checkStringContains(t, output, echoSubCmd.Short)
}
func TestGenYamlNoTag(t *testing.T) {
c := initializeWithRootCmd()
// Need two commands to run the command alphabetical sort
cmdEcho.AddCommand(cmdTimes, cmdEchoSub, cmdDeprecated)
c.AddCommand(cmdPrint, cmdEcho)
c.DisableAutoGenTag = true
cmdRootWithRun.PersistentFlags().StringVarP(&flags2a, "rootflag", "r", "two", strtwoParentHelp)
out := new(bytes.Buffer)
rootCmd.DisableAutoGenTag = true
defer func() { rootCmd.DisableAutoGenTag = false }()
if err := GenYaml(c, out); err != nil {
buf := new(bytes.Buffer)
if err := GenYaml(rootCmd, buf); err != nil {
t.Fatal(err)
}
found := out.String()
unexpected := "Auto generated"
checkStringOmits(t, found, unexpected)
output := buf.String()
checkStringOmits(t, output, "Auto generated")
}
func TestGenYamlTree(t *testing.T) {
cmd := &cobra.Command{
Use: "do [OPTIONS] arg1 arg2",
}
c := &cobra.Command{Use: "do [OPTIONS] arg1 arg2"}
tmpdir, err := ioutil.TempDir("", "test-gen-yaml-tree")
if err != nil {
@@ -98,7 +48,7 @@ func TestGenYamlTree(t *testing.T) {
}
defer os.RemoveAll(tmpdir)
if err := GenYamlTree(cmd, tmpdir); err != nil {
if err := GenYamlTree(c, tmpdir); err != nil {
t.Fatalf("GenYamlTree failed: %s", err.Error())
}
@@ -108,7 +58,6 @@ func TestGenYamlTree(t *testing.T) {
}
func BenchmarkGenYamlToFile(b *testing.B) {
c := initializeWithRootCmd()
file, err := ioutil.TempFile("", "")
if err != nil {
b.Fatal(err)
@@ -118,7 +67,7 @@ func BenchmarkGenYamlToFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := GenYaml(c, file); err != nil {
if err := GenYaml(rootCmd, file); err != nil {
b.Fatal(err)
}
}
+13
View File
@@ -0,0 +1,13 @@
module github.com/spf13/cobra
go 1.12
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/cpuguy83/go-md2man v1.0.10
github.com/inconshreveable/mousetrap v1.0.0
github.com/mitchellh/go-homedir v1.1.0
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.3.2
gopkg.in/yaml.v2 v2.2.2
)
+51
View File
@@ -0,0 +1,51 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+100
View File
@@ -0,0 +1,100 @@
// PowerShell completions are based on the amazing work from clap:
// https://github.com/clap-rs/clap/blob/3294d18efe5f264d12c9035f404c7d189d4824e1/src/completions/powershell.rs
//
// The generated scripts require PowerShell v5.0+ (which comes Windows 10, but
// can be downloaded separately for windows 7 or 8.1).
package cobra
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/pflag"
)
var powerShellCompletionTemplate = `using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName '%s' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'%s'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-')) {
break
}
$element.Value
}
) -join ';'
$completions = @(switch ($command) {%s
})
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}`
func generatePowerShellSubcommandCases(out io.Writer, cmd *Command, previousCommandName string) {
var cmdName string
if previousCommandName == "" {
cmdName = cmd.Name()
} else {
cmdName = fmt.Sprintf("%s;%s", previousCommandName, cmd.Name())
}
fmt.Fprintf(out, "\n '%s' {", cmdName)
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
if nonCompletableFlag(flag) {
return
}
usage := escapeStringForPowerShell(flag.Usage)
if len(flag.Shorthand) > 0 {
fmt.Fprintf(out, "\n [CompletionResult]::new('-%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Shorthand, flag.Shorthand, usage)
}
fmt.Fprintf(out, "\n [CompletionResult]::new('--%s', '%s', [CompletionResultType]::ParameterName, '%s')", flag.Name, flag.Name, usage)
})
for _, subCmd := range cmd.Commands() {
usage := escapeStringForPowerShell(subCmd.Short)
fmt.Fprintf(out, "\n [CompletionResult]::new('%s', '%s', [CompletionResultType]::ParameterValue, '%s')", subCmd.Name(), subCmd.Name(), usage)
}
fmt.Fprint(out, "\n break\n }")
for _, subCmd := range cmd.Commands() {
generatePowerShellSubcommandCases(out, subCmd, cmdName)
}
}
func escapeStringForPowerShell(s string) string {
return strings.Replace(s, "'", "''", -1)
}
// GenPowerShellCompletion generates PowerShell completion file and writes to the passed writer.
func (c *Command) GenPowerShellCompletion(w io.Writer) error {
buf := new(bytes.Buffer)
var subCommandCases bytes.Buffer
generatePowerShellSubcommandCases(&subCommandCases, c, "")
fmt.Fprintf(buf, powerShellCompletionTemplate, c.Name(), c.Name(), subCommandCases.String())
_, err := buf.WriteTo(w)
return err
}
// GenPowerShellCompletionFile generates PowerShell completion file.
func (c *Command) GenPowerShellCompletionFile(filename string) error {
outFile, err := os.Create(filename)
if err != nil {
return err
}
defer outFile.Close()
return c.GenPowerShellCompletion(outFile)
}
+14
View File
@@ -0,0 +1,14 @@
# Generating PowerShell Completions For Your Own cobra.Command
Cobra can generate PowerShell completion scripts. Users need PowerShell version 5.0 or above, which comes with Windows 10 and can be downloaded separately for Windows 7 or 8.1. They can then write the completions to a file and source this file from their PowerShell profile, which is referenced by the `$Profile` environment variable. See `Get-Help about_Profiles` for more info about PowerShell profiles.
# What's supported
- Completion for subcommands using their `.Short` description
- Completion for non-hidden flags using their `.Name` and `.Shorthand`
# What's not yet supported
- Command aliases
- Required, filename or custom flags (they will work like normal flags)
- Custom completion scripts
+122
View File
@@ -0,0 +1,122 @@
package cobra
import (
"bytes"
"strings"
"testing"
)
func TestPowerShellCompletion(t *testing.T) {
tcs := []struct {
name string
root *Command
expectedExpressions []string
}{
{
name: "trivial",
root: &Command{Use: "trivialapp"},
expectedExpressions: []string{
"Register-ArgumentCompleter -Native -CommandName 'trivialapp' -ScriptBlock",
"$command = @(\n 'trivialapp'\n",
},
},
{
name: "tree",
root: func() *Command {
r := &Command{Use: "tree"}
sub1 := &Command{Use: "sub1"}
r.AddCommand(sub1)
sub11 := &Command{Use: "sub11"}
sub12 := &Command{Use: "sub12"}
sub1.AddCommand(sub11)
sub1.AddCommand(sub12)
sub2 := &Command{Use: "sub2"}
r.AddCommand(sub2)
sub21 := &Command{Use: "sub21"}
sub22 := &Command{Use: "sub22"}
sub2.AddCommand(sub21)
sub2.AddCommand(sub22)
return r
}(),
expectedExpressions: []string{
"'tree'",
"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')",
"[CompletionResult]::new('sub2', 'sub2', [CompletionResultType]::ParameterValue, '')",
"'tree;sub1'",
"[CompletionResult]::new('sub11', 'sub11', [CompletionResultType]::ParameterValue, '')",
"[CompletionResult]::new('sub12', 'sub12', [CompletionResultType]::ParameterValue, '')",
"'tree;sub1;sub11'",
"'tree;sub1;sub12'",
"'tree;sub2'",
"[CompletionResult]::new('sub21', 'sub21', [CompletionResultType]::ParameterValue, '')",
"[CompletionResult]::new('sub22', 'sub22', [CompletionResultType]::ParameterValue, '')",
"'tree;sub2;sub21'",
"'tree;sub2;sub22'",
},
},
{
name: "flags",
root: func() *Command {
r := &Command{Use: "flags"}
r.Flags().StringP("flag1", "a", "", "")
r.Flags().String("flag2", "", "")
sub1 := &Command{Use: "sub1"}
sub1.Flags().StringP("flag3", "c", "", "")
r.AddCommand(sub1)
return r
}(),
expectedExpressions: []string{
"'flags'",
"[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, '')",
"[CompletionResult]::new('--flag1', 'flag1', [CompletionResultType]::ParameterName, '')",
"[CompletionResult]::new('--flag2', 'flag2', [CompletionResultType]::ParameterName, '')",
"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, '')",
"'flags;sub1'",
"[CompletionResult]::new('-c', 'c', [CompletionResultType]::ParameterName, '')",
"[CompletionResult]::new('--flag3', 'flag3', [CompletionResultType]::ParameterName, '')",
},
},
{
name: "usage",
root: func() *Command {
r := &Command{Use: "usage"}
r.Flags().String("flag", "", "this describes the usage of the 'flag' flag")
sub1 := &Command{
Use: "sub1",
Short: "short describes 'sub1'",
}
r.AddCommand(sub1)
return r
}(),
expectedExpressions: []string{
"[CompletionResult]::new('--flag', 'flag', [CompletionResultType]::ParameterName, 'this describes the usage of the ''flag'' flag')",
"[CompletionResult]::new('sub1', 'sub1', [CompletionResultType]::ParameterValue, 'short describes ''sub1''')",
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
buf := new(bytes.Buffer)
tc.root.GenPowerShellCompletion(buf)
output := buf.String()
for _, expectedExpression := range tc.expectedExpressions {
if !strings.Contains(output, expectedExpression) {
t.Errorf("Expected completion to contain %q somewhere; got %q", expectedExpression, output)
}
}
})
}
}
+85
View File
@@ -0,0 +1,85 @@
package cobra
import (
"github.com/spf13/pflag"
)
// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists,
// and causes your command to report an error if invoked without the flag.
func (c *Command) MarkFlagRequired(name string) error {
return MarkFlagRequired(c.Flags(), name)
}
// MarkPersistentFlagRequired adds the BashCompOneRequiredFlag annotation to the named persistent flag if it exists,
// and causes your command to report an error if invoked without the flag.
func (c *Command) MarkPersistentFlagRequired(name string) error {
return MarkFlagRequired(c.PersistentFlags(), name)
}
// MarkFlagRequired adds the BashCompOneRequiredFlag annotation to the named flag if it exists,
// and causes your command to report an error if invoked without the flag.
func MarkFlagRequired(flags *pflag.FlagSet, name string) error {
return flags.SetAnnotation(name, BashCompOneRequiredFlag, []string{"true"})
}
// MarkFlagFilename adds the BashCompFilenameExt annotation to the named flag, if it exists.
// Generated bash autocompletion will select filenames for the flag, limiting to named extensions if provided.
func (c *Command) MarkFlagFilename(name string, extensions ...string) error {
return MarkFlagFilename(c.Flags(), name, extensions...)
}
// MarkFlagCustom adds the BashCompCustom annotation to the named flag, if it exists.
// Generated bash autocompletion will call the bash function f for the flag.
func (c *Command) MarkFlagCustom(name string, f string) error {
return MarkFlagCustom(c.Flags(), name, f)
}
// MarkPersistentFlagFilename instructs the various shell completion
// implementations to limit completions for this persistent flag to the
// specified extensions (patterns).
//
// Shell Completion compatibility matrix: bash, zsh
func (c *Command) MarkPersistentFlagFilename(name string, extensions ...string) error {
return MarkFlagFilename(c.PersistentFlags(), name, extensions...)
}
// MarkFlagFilename instructs the various shell completion implementations to
// limit completions for this flag to the specified extensions (patterns).
//
// Shell Completion compatibility matrix: bash, zsh
func MarkFlagFilename(flags *pflag.FlagSet, name string, extensions ...string) error {
return flags.SetAnnotation(name, BashCompFilenameExt, extensions)
}
// MarkFlagCustom instructs the various shell completion implementations to
// limit completions for this flag to the specified extensions (patterns).
//
// Shell Completion compatibility matrix: bash, zsh
func MarkFlagCustom(flags *pflag.FlagSet, name string, f string) error {
return flags.SetAnnotation(name, BashCompCustom, []string{f})
}
// MarkFlagDirname instructs the various shell completion implementations to
// complete only directories with this named flag.
//
// Shell Completion compatibility matrix: zsh
func (c *Command) MarkFlagDirname(name string) error {
return MarkFlagDirname(c.Flags(), name)
}
// MarkPersistentFlagDirname instructs the various shell completion
// implementations to complete only directories with this persistent named flag.
//
// Shell Completion compatibility matrix: zsh
func (c *Command) MarkPersistentFlagDirname(name string) error {
return MarkFlagDirname(c.PersistentFlags(), name)
}
// MarkFlagDirname instructs the various shell completion implementations to
// complete only directories with this specified flag.
//
// Shell Completion compatibility matrix: zsh
func MarkFlagDirname(flags *pflag.FlagSet, name string) error {
zshPattern := "-(/)"
return flags.SetAnnotation(name, zshCompDirname, []string{zshPattern})
}
+304 -94
View File
@@ -1,13 +1,102 @@
package cobra
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"text/template"
"github.com/spf13/pflag"
)
const (
zshCompArgumentAnnotation = "cobra_annotations_zsh_completion_argument_annotation"
zshCompArgumentFilenameComp = "cobra_annotations_zsh_completion_argument_file_completion"
zshCompArgumentWordComp = "cobra_annotations_zsh_completion_argument_word_completion"
zshCompDirname = "cobra_annotations_zsh_dirname"
)
var (
zshCompFuncMap = template.FuncMap{
"genZshFuncName": zshCompGenFuncName,
"extractFlags": zshCompExtractFlag,
"genFlagEntryForZshArguments": zshCompGenFlagEntryForArguments,
"extractArgsCompletions": zshCompExtractArgumentCompletionHintsForRendering,
}
zshCompletionText = `
{{/* should accept Command (that contains subcommands) as parameter */}}
{{define "argumentsC" -}}
{{ $cmdPath := genZshFuncName .}}
function {{$cmdPath}} {
local -a commands
_arguments -C \{{- range extractFlags .}}
{{genFlagEntryForZshArguments .}} \{{- end}}
"1: :->cmnds" \
"*::arg:->args"
case $state in
cmnds)
commands=({{range .Commands}}{{if not .Hidden}}
"{{.Name}}:{{.Short}}"{{end}}{{end}}
)
_describe "command" commands
;;
esac
case "$words[1]" in {{- range .Commands}}{{if not .Hidden}}
{{.Name}})
{{$cmdPath}}_{{.Name}}
;;{{end}}{{end}}
esac
}
{{range .Commands}}{{if not .Hidden}}
{{template "selectCmdTemplate" .}}
{{- end}}{{end}}
{{- end}}
{{/* should accept Command without subcommands as parameter */}}
{{define "arguments" -}}
function {{genZshFuncName .}} {
{{" _arguments"}}{{range extractFlags .}} \
{{genFlagEntryForZshArguments . -}}
{{end}}{{range extractArgsCompletions .}} \
{{.}}{{end}}
}
{{end}}
{{/* dispatcher for commands with or without subcommands */}}
{{define "selectCmdTemplate" -}}
{{if .Hidden}}{{/* ignore hidden*/}}{{else -}}
{{if .Commands}}{{template "argumentsC" .}}{{else}}{{template "arguments" .}}{{end}}
{{- end}}
{{- end}}
{{/* template entry point */}}
{{define "Main" -}}
#compdef _{{.Name}} {{.Name}}
{{template "selectCmdTemplate" .}}
{{end}}
`
)
// zshCompArgsAnnotation is used to encode/decode zsh completion for
// arguments to/from Command.Annotations.
type zshCompArgsAnnotation map[int]zshCompArgHint
type zshCompArgHint struct {
// Indicates the type of the completion to use. One of:
// zshCompArgumentFilenameComp or zshCompArgumentWordComp
Tipe string `json:"type"`
// A value for the type above (globs for file completion or words)
Options []string `json:"options"`
}
// GenZshCompletionFile generates zsh completion file.
func (c *Command) GenZshCompletionFile(filename string) error {
outFile, err := os.Create(filename)
@@ -19,108 +108,229 @@ func (c *Command) GenZshCompletionFile(filename string) error {
return c.GenZshCompletion(outFile)
}
// GenZshCompletion generates a zsh completion file and writes to the passed writer.
// GenZshCompletion generates a zsh completion file and writes to the passed
// writer. The completion always run on the root command regardless of the
// command it was called from.
func (c *Command) GenZshCompletion(w io.Writer) error {
buf := new(bytes.Buffer)
writeHeader(buf, c)
maxDepth := maxDepth(c)
writeLevelMapping(buf, maxDepth)
writeLevelCases(buf, maxDepth, c)
_, err := buf.WriteTo(w)
return err
}
func writeHeader(w io.Writer, cmd *Command) {
fmt.Fprintf(w, "#compdef %s\n\n", cmd.Name())
}
func maxDepth(c *Command) int {
if len(c.Commands()) == 0 {
return 0
tmpl, err := template.New("Main").Funcs(zshCompFuncMap).Parse(zshCompletionText)
if err != nil {
return fmt.Errorf("error creating zsh completion template: %v", err)
}
maxDepthSub := 0
for _, s := range c.Commands() {
subDepth := maxDepth(s)
if subDepth > maxDepthSub {
maxDepthSub = subDepth
return tmpl.Execute(w, c.Root())
}
// MarkZshCompPositionalArgumentFile marks the specified argument (first
// argument is 1) as completed by file selection. patterns (e.g. "*.txt") are
// optional - if not provided the completion will search for all files.
func (c *Command) MarkZshCompPositionalArgumentFile(argPosition int, patterns ...string) error {
if argPosition < 1 {
return fmt.Errorf("Invalid argument position (%d)", argPosition)
}
annotation, err := c.zshCompGetArgsAnnotations()
if err != nil {
return err
}
if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) {
return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition)
}
annotation[argPosition] = zshCompArgHint{
Tipe: zshCompArgumentFilenameComp,
Options: patterns,
}
return c.zshCompSetArgsAnnotations(annotation)
}
// MarkZshCompPositionalArgumentWords marks the specified positional argument
// (first argument is 1) as completed by the provided words. At east one word
// must be provided, spaces within words will be offered completion with
// "word\ word".
func (c *Command) MarkZshCompPositionalArgumentWords(argPosition int, words ...string) error {
if argPosition < 1 {
return fmt.Errorf("Invalid argument position (%d)", argPosition)
}
if len(words) == 0 {
return fmt.Errorf("Trying to set empty word list for positional argument %d", argPosition)
}
annotation, err := c.zshCompGetArgsAnnotations()
if err != nil {
return err
}
if c.zshcompArgsAnnotationnIsDuplicatePosition(annotation, argPosition) {
return fmt.Errorf("Duplicate annotation for positional argument at index %d", argPosition)
}
annotation[argPosition] = zshCompArgHint{
Tipe: zshCompArgumentWordComp,
Options: words,
}
return c.zshCompSetArgsAnnotations(annotation)
}
func zshCompExtractArgumentCompletionHintsForRendering(c *Command) ([]string, error) {
var result []string
annotation, err := c.zshCompGetArgsAnnotations()
if err != nil {
return nil, err
}
for k, v := range annotation {
s, err := zshCompRenderZshCompArgHint(k, v)
if err != nil {
return nil, err
}
result = append(result, s)
}
if len(c.ValidArgs) > 0 {
if _, positionOneExists := annotation[1]; !positionOneExists {
s, err := zshCompRenderZshCompArgHint(1, zshCompArgHint{
Tipe: zshCompArgumentWordComp,
Options: c.ValidArgs,
})
if err != nil {
return nil, err
}
result = append(result, s)
}
}
return 1 + maxDepthSub
sort.Strings(result)
return result, nil
}
func writeLevelMapping(w io.Writer, numLevels int) {
fmt.Fprintln(w, `_arguments \`)
for i := 1; i <= numLevels; i++ {
fmt.Fprintf(w, ` '%d: :->level%d' \`, i, i)
fmt.Fprintln(w)
}
fmt.Fprintf(w, ` '%d: :%s'`, numLevels+1, "_files")
fmt.Fprintln(w)
}
func writeLevelCases(w io.Writer, maxDepth int, root *Command) {
fmt.Fprintln(w, "case $state in")
defer fmt.Fprintln(w, "esac")
for i := 1; i <= maxDepth; i++ {
fmt.Fprintf(w, " level%d)\n", i)
writeLevel(w, root, i)
fmt.Fprintln(w, " ;;")
}
fmt.Fprintln(w, " *)")
fmt.Fprintln(w, " _arguments '*: :_files'")
fmt.Fprintln(w, " ;;")
}
func writeLevel(w io.Writer, root *Command, i int) {
fmt.Fprintf(w, " case $words[%d] in\n", i)
defer fmt.Fprintln(w, " esac")
commands := filterByLevel(root, i)
byParent := groupByParent(commands)
for p, c := range byParent {
names := names(c)
fmt.Fprintf(w, " %s)\n", p)
fmt.Fprintf(w, " _arguments '%d: :(%s)'\n", i, strings.Join(names, " "))
fmt.Fprintln(w, " ;;")
}
fmt.Fprintln(w, " *)")
fmt.Fprintln(w, " _arguments '*: :_files'")
fmt.Fprintln(w, " ;;")
}
func filterByLevel(c *Command, l int) []*Command {
cs := make([]*Command, 0)
if l == 0 {
cs = append(cs, c)
return cs
}
for _, s := range c.Commands() {
cs = append(cs, filterByLevel(s, l-1)...)
}
return cs
}
func groupByParent(commands []*Command) map[string][]*Command {
m := make(map[string][]*Command)
for _, c := range commands {
parent := c.Parent()
if parent == nil {
continue
func zshCompRenderZshCompArgHint(i int, z zshCompArgHint) (string, error) {
switch t := z.Tipe; t {
case zshCompArgumentFilenameComp:
var globs []string
for _, g := range z.Options {
globs = append(globs, fmt.Sprintf(`-g "%s"`, g))
}
m[parent.Name()] = append(m[parent.Name()], c)
return fmt.Sprintf(`'%d: :_files %s'`, i, strings.Join(globs, " ")), nil
case zshCompArgumentWordComp:
var words []string
for _, w := range z.Options {
words = append(words, fmt.Sprintf("%q", w))
}
return fmt.Sprintf(`'%d: :(%s)'`, i, strings.Join(words, " ")), nil
default:
return "", fmt.Errorf("Invalid zsh argument completion annotation: %s", t)
}
return m
}
func names(commands []*Command) []string {
ns := make([]string, len(commands))
for i, c := range commands {
ns[i] = c.Name()
func (c *Command) zshcompArgsAnnotationnIsDuplicatePosition(annotation zshCompArgsAnnotation, position int) bool {
_, dup := annotation[position]
return dup
}
func (c *Command) zshCompGetArgsAnnotations() (zshCompArgsAnnotation, error) {
annotation := make(zshCompArgsAnnotation)
annotationString, ok := c.Annotations[zshCompArgumentAnnotation]
if !ok {
return annotation, nil
}
return ns
err := json.Unmarshal([]byte(annotationString), &annotation)
if err != nil {
return annotation, fmt.Errorf("Error unmarshaling zsh argument annotation: %v", err)
}
return annotation, nil
}
func (c *Command) zshCompSetArgsAnnotations(annotation zshCompArgsAnnotation) error {
jsn, err := json.Marshal(annotation)
if err != nil {
return fmt.Errorf("Error marshaling zsh argument annotation: %v", err)
}
if c.Annotations == nil {
c.Annotations = make(map[string]string)
}
c.Annotations[zshCompArgumentAnnotation] = string(jsn)
return nil
}
func zshCompGenFuncName(c *Command) string {
if c.HasParent() {
return zshCompGenFuncName(c.Parent()) + "_" + c.Name()
}
return "_" + c.Name()
}
func zshCompExtractFlag(c *Command) []*pflag.Flag {
var flags []*pflag.Flag
c.LocalFlags().VisitAll(func(f *pflag.Flag) {
if !f.Hidden {
flags = append(flags, f)
}
})
c.InheritedFlags().VisitAll(func(f *pflag.Flag) {
if !f.Hidden {
flags = append(flags, f)
}
})
return flags
}
// zshCompGenFlagEntryForArguments returns an entry that matches _arguments
// zsh-completion parameters. It's too complicated to generate in a template.
func zshCompGenFlagEntryForArguments(f *pflag.Flag) string {
if f.Name == "" || f.Shorthand == "" {
return zshCompGenFlagEntryForSingleOptionFlag(f)
}
return zshCompGenFlagEntryForMultiOptionFlag(f)
}
func zshCompGenFlagEntryForSingleOptionFlag(f *pflag.Flag) string {
var option, multiMark, extras string
if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) {
multiMark = "*"
}
option = "--" + f.Name
if option == "--" {
option = "-" + f.Shorthand
}
extras = zshCompGenFlagEntryExtras(f)
return fmt.Sprintf(`'%s%s[%s]%s'`, multiMark, option, zshCompQuoteFlagDescription(f.Usage), extras)
}
func zshCompGenFlagEntryForMultiOptionFlag(f *pflag.Flag) string {
var options, parenMultiMark, curlyMultiMark, extras string
if zshCompFlagCouldBeSpecifiedMoreThenOnce(f) {
parenMultiMark = "*"
curlyMultiMark = "\\*"
}
options = fmt.Sprintf(`'(%s-%s %s--%s)'{%s-%s,%s--%s}`,
parenMultiMark, f.Shorthand, parenMultiMark, f.Name, curlyMultiMark, f.Shorthand, curlyMultiMark, f.Name)
extras = zshCompGenFlagEntryExtras(f)
return fmt.Sprintf(`%s'[%s]%s'`, options, zshCompQuoteFlagDescription(f.Usage), extras)
}
func zshCompGenFlagEntryExtras(f *pflag.Flag) string {
if f.NoOptDefVal != "" {
return ""
}
extras := ":" // allow options for flag (even without assistance)
for key, values := range f.Annotations {
switch key {
case zshCompDirname:
extras = fmt.Sprintf(":filename:_files -g %q", values[0])
case BashCompFilenameExt:
extras = ":filename:_files"
for _, pattern := range values {
extras = extras + fmt.Sprintf(` -g "%s"`, pattern)
}
}
}
return extras
}
func zshCompFlagCouldBeSpecifiedMoreThenOnce(f *pflag.Flag) bool {
return strings.Contains(f.Value.Type(), "Slice") ||
strings.Contains(f.Value.Type(), "Array")
}
func zshCompQuoteFlagDescription(s string) string {
return strings.Replace(s, "'", `'\''`, -1)
}
+39
View File
@@ -0,0 +1,39 @@
## Generating Zsh Completion for your cobra.Command
Cobra supports native Zsh completion generated from the root `cobra.Command`.
The generated completion script should be put somewhere in your `$fpath` named
`_<YOUR COMMAND>`.
### What's Supported
* Completion for all non-hidden subcommands using their `.Short` description.
* Completion for all non-hidden flags using the following rules:
* Filename completion works by marking the flag with `cmd.MarkFlagFilename...`
family of commands.
* The requirement for argument to the flag is decided by the `.NoOptDefVal`
flag value - if it's empty then completion will expect an argument.
* Flags of one of the various `*Array` and `*Slice` types supports multiple
specifications (with or without argument depending on the specific type).
* Completion of positional arguments using the following rules:
* Argument position for all options below starts at `1`. If argument position
`0` is requested it will raise an error.
* Use `command.MarkZshCompPositionalArgumentFile` to complete filenames. Glob
patterns (e.g. `"*.log"`) are optional - if not specified it will offer to
complete all file types.
* Use `command.MarkZshCompPositionalArgumentWords` to offer specific words for
completion. At least one word is required.
* It's possible to specify completion for some arguments and leave some
unspecified (e.g. offer words for second argument but nothing for first
argument). This will cause no completion for first argument but words
completion for second argument.
* If no argument completion was specified for 1st argument (but optionally was
specified for 2nd) and the command has `ValidArgs` it will be used as
completion options for 1st argument.
* Argument completions only offered for commands with no subcommands.
### What's not yet Supported
* Custom completion scripts are not supported yet (We should probably create zsh
specific one, doesn't make sense to re-use the bash one as the functions will
be different).
* Whatever other feature you're looking for and doesn't exist :)
+446 -59
View File
@@ -2,87 +2,474 @@ package cobra
import (
"bytes"
"regexp"
"strings"
"testing"
)
func TestZshCompletion(t *testing.T) {
func TestGenZshCompletion(t *testing.T) {
var debug bool
var option string
tcs := []struct {
name string
root *Command
expectedExpressions []string
invocationArgs []string
skip string
}{
{
name: "simple command",
root: func() *Command {
r := &Command{
Use: "mycommand",
Long: "My Command long description",
Run: emptyRun,
}
r.Flags().BoolVar(&debug, "debug", debug, "description")
return r
}(),
expectedExpressions: []string{
`(?s)function _mycommand {\s+_arguments \\\s+'--debug\[description\]'.*--help.*}`,
"#compdef _mycommand mycommand",
},
},
{
name: "flags with both long and short flags",
root: func() *Command {
r := &Command{
Use: "testcmd",
Long: "long description",
Run: emptyRun,
}
r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description")
return r
}(),
expectedExpressions: []string{
`'\(-d --debug\)'{-d,--debug}'\[debug description\]'`,
},
},
{
name: "command with subcommands and flags with values",
root: func() *Command {
r := &Command{
Use: "rootcmd",
Long: "Long rootcmd description",
}
d := &Command{
Use: "subcmd1",
Short: "Subcmd1 short description",
Run: emptyRun,
}
e := &Command{
Use: "subcmd2",
Long: "Subcmd2 short description",
Run: emptyRun,
}
r.PersistentFlags().BoolVar(&debug, "debug", debug, "description")
d.Flags().StringVarP(&option, "option", "o", option, "option description")
r.AddCommand(d, e)
return r
}(),
expectedExpressions: []string{
`commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`,
`_arguments \\\n.*'--debug\[description]'`,
`_arguments -C \\\n.*'--debug\[description]'`,
`function _rootcmd_subcmd1 {`,
`function _rootcmd_subcmd1 {`,
`_arguments \\\n.*'\(-o --option\)'{-o,--option}'\[option description]:' \\\n`,
},
},
{
name: "filename completion with and without globs",
root: func() *Command {
var file string
r := &Command{
Use: "mycmd",
Short: "my command short description",
Run: emptyRun,
}
r.Flags().StringVarP(&file, "config", "c", file, "config file")
r.MarkFlagFilename("config")
r.Flags().String("output", "", "output file")
r.MarkFlagFilename("output", "*.log", "*.txt")
return r
}(),
expectedExpressions: []string{
`\n +'\(-c --config\)'{-c,--config}'\[config file]:filename:_files'`,
`:_files -g "\*.log" -g "\*.txt"`,
},
},
{
name: "repeated variables both with and without value",
root: func() *Command {
r := genTestCommand("mycmd", true)
_ = r.Flags().BoolSliceP("debug", "d", []bool{}, "debug usage")
_ = r.Flags().StringArray("option", []string{}, "options")
return r
}(),
expectedExpressions: []string{
`'\*--option\[options]`,
`'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`,
},
},
{
name: "generated flags --help and --version should be created even when not executing root cmd",
root: func() *Command {
r := &Command{
Use: "mycmd",
Short: "mycmd short description",
Version: "myversion",
}
s := genTestCommand("sub1", true)
r.AddCommand(s)
return s
}(),
expectedExpressions: []string{
"--version",
"--help",
},
invocationArgs: []string{
"sub1",
},
skip: "--version and --help are currently not generated when not running on root command",
},
{
name: "zsh generation should run on root command",
root: func() *Command {
r := genTestCommand("root", false)
s := genTestCommand("sub1", true)
r.AddCommand(s)
return s
}(),
expectedExpressions: []string{
"function _root {",
},
},
{
name: "flag description with single quote (') shouldn't break quotes in completion file",
root: func() *Command {
r := genTestCommand("root", true)
r.Flags().Bool("private", false, "Don't show public info")
return r
}(),
expectedExpressions: []string{
`--private\[Don'\\''t show public info]`,
},
},
{
name: "argument completion for file with and without patterns",
root: func() *Command {
r := genTestCommand("root", true)
r.MarkZshCompPositionalArgumentFile(1, "*.log")
r.MarkZshCompPositionalArgumentFile(2)
return r
}(),
expectedExpressions: []string{
`'1: :_files -g "\*.log"' \\\n\s+'2: :_files`,
},
},
{
name: "argument zsh completion for words",
root: func() *Command {
r := genTestCommand("root", true)
r.MarkZshCompPositionalArgumentWords(1, "word1", "word2")
return r
}(),
expectedExpressions: []string{
`'1: :\("word1" "word2"\)`,
},
},
{
name: "argument completion for words with spaces",
root: func() *Command {
r := genTestCommand("root", true)
r.MarkZshCompPositionalArgumentWords(1, "single", "multiple words")
return r
}(),
expectedExpressions: []string{
`'1: :\("single" "multiple words"\)'`,
},
},
{
name: "argument completion when command has ValidArgs and no annotation for argument completion",
root: func() *Command {
r := genTestCommand("root", true)
r.ValidArgs = []string{"word1", "word2"}
return r
}(),
expectedExpressions: []string{
`'1: :\("word1" "word2"\)'`,
},
},
{
name: "argument completion when command has ValidArgs and no annotation for argument at argPosition 1",
root: func() *Command {
r := genTestCommand("root", true)
r.ValidArgs = []string{"word1", "word2"}
r.MarkZshCompPositionalArgumentFile(2)
return r
}(),
expectedExpressions: []string{
`'1: :\("word1" "word2"\)' \\`,
},
},
{
name: "directory completion for flag",
root: func() *Command {
r := genTestCommand("root", true)
r.Flags().String("test", "", "test")
r.PersistentFlags().String("ptest", "", "ptest")
r.MarkFlagDirname("test")
r.MarkPersistentFlagDirname("ptest")
return r
}(),
expectedExpressions: []string{
`--test\[test]:filename:_files -g "-\(/\)"`,
`--ptest\[ptest]:filename:_files -g "-\(/\)"`,
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
tc.root.Root().SetArgs(tc.invocationArgs)
tc.root.Execute()
buf := new(bytes.Buffer)
if err := tc.root.GenZshCompletion(buf); err != nil {
t.Error(err)
}
output := buf.Bytes()
for _, expr := range tc.expectedExpressions {
rgx, err := regexp.Compile(expr)
if err != nil {
t.Errorf("error compiling expression (%s): %v", expr, err)
}
if !rgx.Match(output) {
t.Errorf("expected completion (%s) to match '%s'", buf.String(), expr)
}
}
})
}
}
func TestGenZshCompletionHidden(t *testing.T) {
tcs := []struct {
name string
root *Command
expectedExpressions []string
}{
{
name: "trivial",
root: &Command{Use: "trivialapp"},
expectedExpressions: []string{"#compdef trivial"},
},
{
name: "linear",
name: "hidden commands",
root: func() *Command {
r := &Command{Use: "linear"}
sub1 := &Command{Use: "sub1"}
r.AddCommand(sub1)
sub2 := &Command{Use: "sub2"}
sub1.AddCommand(sub2)
sub3 := &Command{Use: "sub3"}
sub2.AddCommand(sub3)
return r
}(),
expectedExpressions: []string{"sub1", "sub2", "sub3"},
},
{
name: "flat",
root: func() *Command {
r := &Command{Use: "flat"}
r.AddCommand(&Command{Use: "c1"})
r.AddCommand(&Command{Use: "c2"})
return r
}(),
expectedExpressions: []string{"(c1 c2)"},
},
{
name: "tree",
root: func() *Command {
r := &Command{Use: "tree"}
sub1 := &Command{Use: "sub1"}
r.AddCommand(sub1)
sub11 := &Command{Use: "sub11"}
sub12 := &Command{Use: "sub12"}
sub1.AddCommand(sub11)
sub1.AddCommand(sub12)
sub2 := &Command{Use: "sub2"}
r.AddCommand(sub2)
sub21 := &Command{Use: "sub21"}
sub22 := &Command{Use: "sub22"}
sub2.AddCommand(sub21)
sub2.AddCommand(sub22)
r := &Command{
Use: "main",
Short: "main short description",
}
s1 := &Command{
Use: "sub1",
Hidden: true,
Run: emptyRun,
}
s2 := &Command{
Use: "sub2",
Short: "short sub2 description",
Run: emptyRun,
}
r.AddCommand(s1, s2)
return r
}(),
expectedExpressions: []string{"(sub11 sub12)", "(sub21 sub22)"},
expectedExpressions: []string{
"sub1",
},
},
{
name: "hidden flags",
root: func() *Command {
var hidden string
r := &Command{
Use: "root",
Short: "root short description",
Run: emptyRun,
}
r.Flags().StringVarP(&hidden, "hidden", "H", hidden, "hidden usage")
if err := r.Flags().MarkHidden("hidden"); err != nil {
t.Errorf("Error setting flag hidden: %v\n", err)
}
return r
}(),
expectedExpressions: []string{
"--hidden",
},
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
tc.root.Execute()
buf := new(bytes.Buffer)
tc.root.GenZshCompletion(buf)
completion := buf.String()
for _, expectedExpression := range tc.expectedExpressions {
if !strings.Contains(completion, expectedExpression) {
t.Errorf("expected completion to contain '%v' somewhere; got '%v'", expectedExpression, completion)
if err := tc.root.GenZshCompletion(buf); err != nil {
t.Error(err)
}
output := buf.String()
for _, expr := range tc.expectedExpressions {
if strings.Contains(output, expr) {
t.Errorf("Expected completion (%s) not to contain '%s' but it does", output, expr)
}
}
})
}
}
func TestMarkZshCompPositionalArgumentFile(t *testing.T) {
t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
c := &Command{}
if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
t.Errorf("Received error when we shouldn't have: %v\n", err)
}
if err := c.MarkZshCompPositionalArgumentFile(1); err == nil {
t.Error("Didn't receive an error when trying to overwrite argument position")
}
})
t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
c := &Command{}
err := c.MarkZshCompPositionalArgumentFile(0, "*")
if err == nil {
t.Fatal("Error was not thrown when indicating argument position 0")
}
if !strings.Contains(err.Error(), "position") {
t.Errorf("expected error message '%s' to contain 'position'", err.Error())
}
})
}
func TestMarkZshCompPositionalArgumentWords(t *testing.T) {
t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
c := &Command{}
if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
t.Errorf("Received error when we shouldn't have: %v\n", err)
}
if err := c.MarkZshCompPositionalArgumentWords(1, "hello"); err == nil {
t.Error("Didn't receive an error when trying to overwrite argument position")
}
})
t.Run("Doesn't allow calling without words", func(t *testing.T) {
c := &Command{}
if err := c.MarkZshCompPositionalArgumentWords(0); err == nil {
t.Error("Should not allow saving empty word list for annotation")
}
})
t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
c := &Command{}
err := c.MarkZshCompPositionalArgumentWords(0, "word")
if err == nil {
t.Fatal("Should not allow setting argument position less then 1")
}
if !strings.Contains(err.Error(), "position") {
t.Errorf("Expected error '%s' to contain 'position' but didn't", err.Error())
}
})
}
func BenchmarkMediumSizeConstruct(b *testing.B) {
root := constructLargeCommandHierarchy()
// if err := root.GenZshCompletionFile("_mycmd"); err != nil {
// b.Error(err)
// }
for i := 0; i < b.N; i++ {
buf := new(bytes.Buffer)
err := root.GenZshCompletion(buf)
if err != nil {
b.Error(err)
}
}
}
func TestExtractFlags(t *testing.T) {
var debug, cmdc, cmdd bool
c := &Command{
Use: "cmdC",
Long: "Command C",
}
c.PersistentFlags().BoolVarP(&debug, "debug", "d", debug, "debug mode")
c.Flags().BoolVar(&cmdc, "cmd-c", cmdc, "Command C")
d := &Command{
Use: "CmdD",
Long: "Command D",
}
d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D")
c.AddCommand(d)
resC := zshCompExtractFlag(c)
resD := zshCompExtractFlag(d)
if len(resC) != 2 {
t.Errorf("expected Command C to return 2 flags, got %d", len(resC))
}
if len(resD) != 2 {
t.Errorf("expected Command D to return 2 flags, got %d", len(resD))
}
}
func constructLargeCommandHierarchy() *Command {
var config, st1, st2 string
var long, debug bool
var in1, in2 int
var verbose []bool
r := genTestCommand("mycmd", false)
r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage")
if err := r.MarkPersistentFlagFilename("config", "*"); err != nil {
panic(err)
}
s1 := genTestCommand("sub1", true)
s1.Flags().BoolVar(&long, "long", long, "long description")
s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description")
s1.Flags().StringArray("option", []string{}, "various options")
s2 := genTestCommand("sub2", true)
s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description")
s3 := genTestCommand("sub3", true)
s3.Hidden = true
s1_1 := genTestCommand("sub1sub1", true)
s1_1.Flags().StringVar(&st1, "st1", st1, "st1 description")
s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description")
s1_2 := genTestCommand("sub1sub2", true)
s1_3 := genTestCommand("sub1sub3", true)
s1_3.Flags().IntVar(&in1, "int1", in1, "int1 description")
s1_3.Flags().IntVar(&in2, "int2", in2, "int2 description")
s1_3.Flags().StringArrayP("option", "O", []string{}, "more options")
s2_1 := genTestCommand("sub2sub1", true)
s2_2 := genTestCommand("sub2sub2", true)
s2_3 := genTestCommand("sub2sub3", true)
s2_4 := genTestCommand("sub2sub4", true)
s2_5 := genTestCommand("sub2sub5", true)
s1.AddCommand(s1_1, s1_2, s1_3)
s2.AddCommand(s2_1, s2_2, s2_3, s2_4, s2_5)
r.AddCommand(s1, s2, s3)
r.Execute()
return r
}
func genTestCommand(name string, withRun bool) *Command {
r := &Command{
Use: name,
Short: name + " short description",
Long: "Long description for " + name,
}
if withRun {
r.Run = emptyRun
}
return r
}
+209
View File
@@ -0,0 +1,209 @@
package pflag
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
// BytesHex adapts []byte for use as a flag. Value of flag is HEX encoded
type bytesHexValue []byte
// String implements pflag.Value.String.
func (bytesHex bytesHexValue) String() string {
return fmt.Sprintf("%X", []byte(bytesHex))
}
// Set implements pflag.Value.Set.
func (bytesHex *bytesHexValue) Set(value string) error {
bin, err := hex.DecodeString(strings.TrimSpace(value))
if err != nil {
return err
}
*bytesHex = bin
return nil
}
// Type implements pflag.Value.Type.
func (*bytesHexValue) Type() string {
return "bytesHex"
}
func newBytesHexValue(val []byte, p *[]byte) *bytesHexValue {
*p = val
return (*bytesHexValue)(p)
}
func bytesHexConv(sval string) (interface{}, error) {
bin, err := hex.DecodeString(sval)
if err == nil {
return bin, nil
}
return nil, fmt.Errorf("invalid string being converted to Bytes: %s %s", sval, err)
}
// GetBytesHex return the []byte value of a flag with the given name
func (f *FlagSet) GetBytesHex(name string) ([]byte, error) {
val, err := f.getFlagType(name, "bytesHex", bytesHexConv)
if err != nil {
return []byte{}, err
}
return val.([]byte), nil
}
// BytesHexVar defines an []byte flag with specified name, default value, and usage string.
// The argument p points to an []byte variable in which to store the value of the flag.
func (f *FlagSet) BytesHexVar(p *[]byte, name string, value []byte, usage string) {
f.VarP(newBytesHexValue(value, p), name, "", usage)
}
// BytesHexVarP is like BytesHexVar, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) BytesHexVarP(p *[]byte, name, shorthand string, value []byte, usage string) {
f.VarP(newBytesHexValue(value, p), name, shorthand, usage)
}
// BytesHexVar defines an []byte flag with specified name, default value, and usage string.
// The argument p points to an []byte variable in which to store the value of the flag.
func BytesHexVar(p *[]byte, name string, value []byte, usage string) {
CommandLine.VarP(newBytesHexValue(value, p), name, "", usage)
}
// BytesHexVarP is like BytesHexVar, but accepts a shorthand letter that can be used after a single dash.
func BytesHexVarP(p *[]byte, name, shorthand string, value []byte, usage string) {
CommandLine.VarP(newBytesHexValue(value, p), name, shorthand, usage)
}
// BytesHex defines an []byte flag with specified name, default value, and usage string.
// The return value is the address of an []byte variable that stores the value of the flag.
func (f *FlagSet) BytesHex(name string, value []byte, usage string) *[]byte {
p := new([]byte)
f.BytesHexVarP(p, name, "", value, usage)
return p
}
// BytesHexP is like BytesHex, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) BytesHexP(name, shorthand string, value []byte, usage string) *[]byte {
p := new([]byte)
f.BytesHexVarP(p, name, shorthand, value, usage)
return p
}
// BytesHex defines an []byte flag with specified name, default value, and usage string.
// The return value is the address of an []byte variable that stores the value of the flag.
func BytesHex(name string, value []byte, usage string) *[]byte {
return CommandLine.BytesHexP(name, "", value, usage)
}
// BytesHexP is like BytesHex, but accepts a shorthand letter that can be used after a single dash.
func BytesHexP(name, shorthand string, value []byte, usage string) *[]byte {
return CommandLine.BytesHexP(name, shorthand, value, usage)
}
// BytesBase64 adapts []byte for use as a flag. Value of flag is Base64 encoded
type bytesBase64Value []byte
// String implements pflag.Value.String.
func (bytesBase64 bytesBase64Value) String() string {
return base64.StdEncoding.EncodeToString([]byte(bytesBase64))
}
// Set implements pflag.Value.Set.
func (bytesBase64 *bytesBase64Value) Set(value string) error {
bin, err := base64.StdEncoding.DecodeString(strings.TrimSpace(value))
if err != nil {
return err
}
*bytesBase64 = bin
return nil
}
// Type implements pflag.Value.Type.
func (*bytesBase64Value) Type() string {
return "bytesBase64"
}
func newBytesBase64Value(val []byte, p *[]byte) *bytesBase64Value {
*p = val
return (*bytesBase64Value)(p)
}
func bytesBase64ValueConv(sval string) (interface{}, error) {
bin, err := base64.StdEncoding.DecodeString(sval)
if err == nil {
return bin, nil
}
return nil, fmt.Errorf("invalid string being converted to Bytes: %s %s", sval, err)
}
// GetBytesBase64 return the []byte value of a flag with the given name
func (f *FlagSet) GetBytesBase64(name string) ([]byte, error) {
val, err := f.getFlagType(name, "bytesBase64", bytesBase64ValueConv)
if err != nil {
return []byte{}, err
}
return val.([]byte), nil
}
// BytesBase64Var defines an []byte flag with specified name, default value, and usage string.
// The argument p points to an []byte variable in which to store the value of the flag.
func (f *FlagSet) BytesBase64Var(p *[]byte, name string, value []byte, usage string) {
f.VarP(newBytesBase64Value(value, p), name, "", usage)
}
// BytesBase64VarP is like BytesBase64Var, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) BytesBase64VarP(p *[]byte, name, shorthand string, value []byte, usage string) {
f.VarP(newBytesBase64Value(value, p), name, shorthand, usage)
}
// BytesBase64Var defines an []byte flag with specified name, default value, and usage string.
// The argument p points to an []byte variable in which to store the value of the flag.
func BytesBase64Var(p *[]byte, name string, value []byte, usage string) {
CommandLine.VarP(newBytesBase64Value(value, p), name, "", usage)
}
// BytesBase64VarP is like BytesBase64Var, but accepts a shorthand letter that can be used after a single dash.
func BytesBase64VarP(p *[]byte, name, shorthand string, value []byte, usage string) {
CommandLine.VarP(newBytesBase64Value(value, p), name, shorthand, usage)
}
// BytesBase64 defines an []byte flag with specified name, default value, and usage string.
// The return value is the address of an []byte variable that stores the value of the flag.
func (f *FlagSet) BytesBase64(name string, value []byte, usage string) *[]byte {
p := new([]byte)
f.BytesBase64VarP(p, name, "", value, usage)
return p
}
// BytesBase64P is like BytesBase64, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) BytesBase64P(name, shorthand string, value []byte, usage string) *[]byte {
p := new([]byte)
f.BytesBase64VarP(p, name, shorthand, value, usage)
return p
}
// BytesBase64 defines an []byte flag with specified name, default value, and usage string.
// The return value is the address of an []byte variable that stores the value of the flag.
func BytesBase64(name string, value []byte, usage string) *[]byte {
return CommandLine.BytesBase64P(name, "", value, usage)
}
// BytesBase64P is like BytesBase64, but accepts a shorthand letter that can be used after a single dash.
func BytesBase64P(name, shorthand string, value []byte, usage string) *[]byte {
return CommandLine.BytesBase64P(name, shorthand, value, usage)
}
+134
View File
@@ -0,0 +1,134 @@
package pflag
import (
"encoding/base64"
"fmt"
"os"
"testing"
)
func setUpBytesHex(bytesHex *[]byte) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.BytesHexVar(bytesHex, "bytes", []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, "Some bytes in HEX")
f.BytesHexVarP(bytesHex, "bytes2", "B", []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, "Some bytes in HEX")
return f
}
func TestBytesHex(t *testing.T) {
testCases := []struct {
input string
success bool
expected string
}{
/// Positive cases
{"", true, ""}, // Is empty string OK ?
{"01", true, "01"},
{"0101", true, "0101"},
{"1234567890abcdef", true, "1234567890ABCDEF"},
{"1234567890ABCDEF", true, "1234567890ABCDEF"},
// Negative cases
{"0", false, ""}, // Short string
{"000", false, ""}, /// Odd-length string
{"qq", false, ""}, /// non-hex character
}
devnull, _ := os.Open(os.DevNull)
os.Stderr = devnull
for i := range testCases {
var bytesHex []byte
f := setUpBytesHex(&bytesHex)
tc := &testCases[i]
// --bytes
args := []string{
fmt.Sprintf("--bytes=%s", tc.input),
fmt.Sprintf("-B %s", tc.input),
fmt.Sprintf("--bytes2=%s", tc.input),
}
for _, arg := range args {
err := f.Parse([]string{arg})
if err != nil && tc.success == true {
t.Errorf("expected success, got %q", err)
continue
} else if err == nil && tc.success == false {
// bytesHex, err := f.GetBytesHex("bytes")
t.Errorf("expected failure while processing %q", tc.input)
continue
} else if tc.success {
bytesHex, err := f.GetBytesHex("bytes")
if err != nil {
t.Errorf("Got error trying to fetch the 'bytes' flag: %v", err)
}
if fmt.Sprintf("%X", bytesHex) != tc.expected {
t.Errorf("expected %q, got '%X'", tc.expected, bytesHex)
}
}
}
}
}
func setUpBytesBase64(bytesBase64 *[]byte) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.BytesBase64Var(bytesBase64, "bytes", []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, "Some bytes in Base64")
f.BytesBase64VarP(bytesBase64, "bytes2", "B", []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}, "Some bytes in Base64")
return f
}
func TestBytesBase64(t *testing.T) {
testCases := []struct {
input string
success bool
expected string
}{
/// Positive cases
{"", true, ""}, // Is empty string OK ?
{"AQ==", true, "AQ=="},
// Negative cases
{"AQ", false, ""}, // Padding removed
{"ï", false, ""}, // non-base64 characters
}
devnull, _ := os.Open(os.DevNull)
os.Stderr = devnull
for i := range testCases {
var bytesBase64 []byte
f := setUpBytesBase64(&bytesBase64)
tc := &testCases[i]
// --bytes
args := []string{
fmt.Sprintf("--bytes=%s", tc.input),
fmt.Sprintf("-B %s", tc.input),
fmt.Sprintf("--bytes2=%s", tc.input),
}
for _, arg := range args {
err := f.Parse([]string{arg})
if err != nil && tc.success == true {
t.Errorf("expected success, got %q", err)
continue
} else if err == nil && tc.success == false {
// bytesBase64, err := f.GetBytesBase64("bytes")
t.Errorf("expected failure while processing %q", tc.input)
continue
} else if tc.success {
bytesBase64, err := f.GetBytesBase64("bytes")
if err != nil {
t.Errorf("Got error trying to fetch the 'bytes' flag: %v", err)
}
if base64.StdEncoding.EncodeToString(bytesBase64) != tc.expected {
t.Errorf("expected %q, got '%X'", tc.expected, bytesBase64)
}
}
}
}
}
+6 -6
View File
@@ -11,13 +11,13 @@ func newCountValue(val int, p *int) *countValue {
}
func (i *countValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 64)
// -1 means that no specific value was passed, so increment
if v == -1 {
// "+1" means that no specific value was passed, so increment
if s == "+1" {
*i = countValue(*i + 1)
} else {
*i = countValue(v)
return nil
}
v, err := strconv.ParseInt(s, 0, 0)
*i = countValue(v)
return err
}
@@ -54,7 +54,7 @@ func (f *FlagSet) CountVar(p *int, name string, usage string) {
// CountVarP is like CountVar only take a shorthand for the flag name.
func (f *FlagSet) CountVarP(p *int, name, shorthand string, usage string) {
flag := f.VarPF(newCountValue(0, p), name, shorthand, usage)
flag.NoOptDefVal = "-1"
flag.NoOptDefVal = "+1"
}
// CountVar like CountVar only the flag is placed on the CommandLine instead of a given flag set
+5 -1
View File
@@ -17,10 +17,14 @@ func TestCount(t *testing.T) {
success bool
expected int
}{
{[]string{}, true, 0},
{[]string{"-v"}, true, 1},
{[]string{"-vvv"}, true, 3},
{[]string{"-v", "-v", "-v"}, true, 3},
{[]string{"-v", "--verbose", "-v"}, true, 3},
{[]string{"-v=3", "-v"}, true, 4},
{[]string{"--verbose=0"}, true, 0},
{[]string{"-v=0"}, true, 0},
{[]string{"-v=a"}, false, 0},
}
@@ -45,7 +49,7 @@ func TestCount(t *testing.T) {
t.Errorf("Got error trying to fetch the counter flag")
}
if c != tc.expected {
t.Errorf("expected %q, got %q", tc.expected, c)
t.Errorf("expected %d, got %d", tc.expected, c)
}
}
}
+128
View File
@@ -0,0 +1,128 @@
package pflag
import (
"fmt"
"strings"
"time"
)
// -- durationSlice Value
type durationSliceValue struct {
value *[]time.Duration
changed bool
}
func newDurationSliceValue(val []time.Duration, p *[]time.Duration) *durationSliceValue {
dsv := new(durationSliceValue)
dsv.value = p
*dsv.value = val
return dsv
}
func (s *durationSliceValue) Set(val string) error {
ss := strings.Split(val, ",")
out := make([]time.Duration, len(ss))
for i, d := range ss {
var err error
out[i], err = time.ParseDuration(d)
if err != nil {
return err
}
}
if !s.changed {
*s.value = out
} else {
*s.value = append(*s.value, out...)
}
s.changed = true
return nil
}
func (s *durationSliceValue) Type() string {
return "durationSlice"
}
func (s *durationSliceValue) String() string {
out := make([]string, len(*s.value))
for i, d := range *s.value {
out[i] = fmt.Sprintf("%s", d)
}
return "[" + strings.Join(out, ",") + "]"
}
func durationSliceConv(val string) (interface{}, error) {
val = strings.Trim(val, "[]")
// Empty string would cause a slice with one (empty) entry
if len(val) == 0 {
return []time.Duration{}, nil
}
ss := strings.Split(val, ",")
out := make([]time.Duration, len(ss))
for i, d := range ss {
var err error
out[i], err = time.ParseDuration(d)
if err != nil {
return nil, err
}
}
return out, nil
}
// GetDurationSlice returns the []time.Duration value of a flag with the given name
func (f *FlagSet) GetDurationSlice(name string) ([]time.Duration, error) {
val, err := f.getFlagType(name, "durationSlice", durationSliceConv)
if err != nil {
return []time.Duration{}, err
}
return val.([]time.Duration), nil
}
// DurationSliceVar defines a durationSlice flag with specified name, default value, and usage string.
// The argument p points to a []time.Duration variable in which to store the value of the flag.
func (f *FlagSet) DurationSliceVar(p *[]time.Duration, name string, value []time.Duration, usage string) {
f.VarP(newDurationSliceValue(value, p), name, "", usage)
}
// DurationSliceVarP is like DurationSliceVar, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) DurationSliceVarP(p *[]time.Duration, name, shorthand string, value []time.Duration, usage string) {
f.VarP(newDurationSliceValue(value, p), name, shorthand, usage)
}
// DurationSliceVar defines a duration[] flag with specified name, default value, and usage string.
// The argument p points to a duration[] variable in which to store the value of the flag.
func DurationSliceVar(p *[]time.Duration, name string, value []time.Duration, usage string) {
CommandLine.VarP(newDurationSliceValue(value, p), name, "", usage)
}
// DurationSliceVarP is like DurationSliceVar, but accepts a shorthand letter that can be used after a single dash.
func DurationSliceVarP(p *[]time.Duration, name, shorthand string, value []time.Duration, usage string) {
CommandLine.VarP(newDurationSliceValue(value, p), name, shorthand, usage)
}
// DurationSlice defines a []time.Duration flag with specified name, default value, and usage string.
// The return value is the address of a []time.Duration variable that stores the value of the flag.
func (f *FlagSet) DurationSlice(name string, value []time.Duration, usage string) *[]time.Duration {
p := []time.Duration{}
f.DurationSliceVarP(&p, name, "", value, usage)
return &p
}
// DurationSliceP is like DurationSlice, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) DurationSliceP(name, shorthand string, value []time.Duration, usage string) *[]time.Duration {
p := []time.Duration{}
f.DurationSliceVarP(&p, name, shorthand, value, usage)
return &p
}
// DurationSlice defines a []time.Duration flag with specified name, default value, and usage string.
// The return value is the address of a []time.Duration variable that stores the value of the flag.
func DurationSlice(name string, value []time.Duration, usage string) *[]time.Duration {
return CommandLine.DurationSliceP(name, "", value, usage)
}
// DurationSliceP is like DurationSlice, but accepts a shorthand letter that can be used after a single dash.
func DurationSliceP(name, shorthand string, value []time.Duration, usage string) *[]time.Duration {
return CommandLine.DurationSliceP(name, shorthand, value, usage)
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code ds governed by a BSD-style
// license that can be found in the LICENSE file.
package pflag
import (
"fmt"
"strings"
"testing"
"time"
)
func setUpDSFlagSet(dsp *[]time.Duration) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.DurationSliceVar(dsp, "ds", []time.Duration{}, "Command separated list!")
return f
}
func setUpDSFlagSetWithDefault(dsp *[]time.Duration) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.DurationSliceVar(dsp, "ds", []time.Duration{0, 1}, "Command separated list!")
return f
}
func TestEmptyDS(t *testing.T) {
var ds []time.Duration
f := setUpDSFlagSet(&ds)
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
getDS, err := f.GetDurationSlice("ds")
if err != nil {
t.Fatal("got an error from GetDurationSlice():", err)
}
if len(getDS) != 0 {
t.Fatalf("got ds %v with len=%d but expected length=0", getDS, len(getDS))
}
}
func TestDS(t *testing.T) {
var ds []time.Duration
f := setUpDSFlagSet(&ds)
vals := []string{"1ns", "2ms", "3m", "4h"}
arg := fmt.Sprintf("--ds=%s", strings.Join(vals, ","))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for i, v := range ds {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatalf("got error: %v", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %s but got: %d", i, vals[i], v)
}
}
getDS, err := f.GetDurationSlice("ds")
if err != nil {
t.Fatalf("got error: %v", err)
}
for i, v := range getDS {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatalf("got error: %v", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %s but got: %d from GetDurationSlice", i, vals[i], v)
}
}
}
func TestDSDefault(t *testing.T) {
var ds []time.Duration
f := setUpDSFlagSetWithDefault(&ds)
vals := []string{"0s", "1ns"}
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
for i, v := range ds {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatalf("got error: %v", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %d but got: %d", i, d, v)
}
}
getDS, err := f.GetDurationSlice("ds")
if err != nil {
t.Fatal("got an error from GetDurationSlice():", err)
}
for i, v := range getDS {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatal("got an error from GetDurationSlice():", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %d from GetDurationSlice but got: %d", i, d, v)
}
}
}
func TestDSWithDefault(t *testing.T) {
var ds []time.Duration
f := setUpDSFlagSetWithDefault(&ds)
vals := []string{"1ns", "2ns"}
arg := fmt.Sprintf("--ds=%s", strings.Join(vals, ","))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for i, v := range ds {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatalf("got error: %v", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %d but got: %d", i, d, v)
}
}
getDS, err := f.GetDurationSlice("ds")
if err != nil {
t.Fatal("got an error from GetDurationSlice():", err)
}
for i, v := range getDS {
d, err := time.ParseDuration(vals[i])
if err != nil {
t.Fatalf("got error: %v", err)
}
if d != v {
t.Fatalf("expected ds[%d] to be %d from GetDurationSlice but got: %d", i, d, v)
}
}
}
func TestDSCalledTwice(t *testing.T) {
var ds []time.Duration
f := setUpDSFlagSet(&ds)
in := []string{"1ns,2ns", "3ns"}
expected := []time.Duration{1, 2, 3}
argfmt := "--ds=%s"
arg1 := fmt.Sprintf(argfmt, in[0])
arg2 := fmt.Sprintf(argfmt, in[1])
err := f.Parse([]string{arg1, arg2})
if err != nil {
t.Fatal("expected no error; got", err)
}
for i, v := range ds {
if expected[i] != v {
t.Fatalf("expected ds[%d] to be %d but got: %d", i, expected[i], v)
}
}
}
+130 -31
View File
@@ -101,6 +101,7 @@ package pflag
import (
"bytes"
"errors"
goflag "flag"
"fmt"
"io"
"os"
@@ -123,6 +124,12 @@ const (
PanicOnError
)
// ParseErrorsWhitelist defines the parsing errors that can be ignored
type ParseErrorsWhitelist struct {
// UnknownFlags will ignore unknown flags errors and continue parsing rest of the flags
UnknownFlags bool
}
// NormalizedName is a flag name that has been normalized according to rules
// for the FlagSet (e.g. making '-' and '_' equivalent).
type NormalizedName string
@@ -138,6 +145,9 @@ type FlagSet struct {
// help/usage messages.
SortFlags bool
// ParseErrorsWhitelist is used to configure a whitelist of errors
ParseErrorsWhitelist ParseErrorsWhitelist
name string
parsed bool
actual map[NormalizedName]*Flag
@@ -153,6 +163,8 @@ type FlagSet struct {
output io.Writer // nil means stderr; use out() accessor
interspersed bool // allow interspersed option/non-option args
normalizeNameFunc func(f *FlagSet, name string) NormalizedName
addedGoFlagSets []*goflag.FlagSet
}
// A Flag represents the state of a flag.
@@ -202,12 +214,18 @@ func sortFlags(flags map[NormalizedName]*Flag) []*Flag {
func (f *FlagSet) SetNormalizeFunc(n func(f *FlagSet, name string) NormalizedName) {
f.normalizeNameFunc = n
f.sortedFormal = f.sortedFormal[:0]
for k, v := range f.orderedFormal {
delete(f.formal, NormalizedName(v.Name))
nname := f.normalizeFlagName(v.Name)
v.Name = string(nname)
f.formal[nname] = v
f.orderedFormal[k] = v
for fname, flag := range f.formal {
nname := f.normalizeFlagName(flag.Name)
if fname == nname {
continue
}
flag.Name = string(nname)
delete(f.formal, fname)
f.formal[nname] = flag
if _, set := f.actual[fname]; set {
delete(f.actual, fname)
f.actual[nname] = flag
}
}
}
@@ -261,16 +279,16 @@ func (f *FlagSet) VisitAll(fn func(*Flag)) {
}
}
// HasFlags returns a bool to indicate if the FlagSet has any flags definied.
// HasFlags returns a bool to indicate if the FlagSet has any flags defined.
func (f *FlagSet) HasFlags() bool {
return len(f.formal) > 0
}
// HasAvailableFlags returns a bool to indicate if the FlagSet has any flags
// definied that are not hidden or deprecated.
// that are not hidden.
func (f *FlagSet) HasAvailableFlags() bool {
for _, flag := range f.formal {
if !flag.Hidden && len(flag.Deprecated) == 0 {
if !flag.Hidden {
return true
}
}
@@ -380,6 +398,7 @@ func (f *FlagSet) MarkDeprecated(name string, usageMessage string) error {
return fmt.Errorf("deprecated message for flag %q must be set", name)
}
flag.Deprecated = usageMessage
flag.Hidden = true
return nil
}
@@ -440,13 +459,15 @@ func (f *FlagSet) Set(name, value string) error {
return fmt.Errorf("invalid argument %q for %q flag: %v", value, flagName, err)
}
if f.actual == nil {
f.actual = make(map[NormalizedName]*Flag)
}
f.actual[normalName] = flag
f.orderedActual = append(f.orderedActual, flag)
if !flag.Changed {
if f.actual == nil {
f.actual = make(map[NormalizedName]*Flag)
}
f.actual[normalName] = flag
f.orderedActual = append(f.orderedActual, flag)
flag.Changed = true
flag.Changed = true
}
if flag.Deprecated != "" {
fmt.Fprintf(f.out(), "Flag --%s has been deprecated, %s\n", flag.Name, flag.Deprecated)
@@ -556,6 +577,14 @@ func UnquoteUsage(flag *Flag) (name string, usage string) {
name = "int"
case "uint64":
name = "uint"
case "stringSlice":
name = "strings"
case "intSlice":
name = "ints"
case "uintSlice":
name = "uints"
case "boolSlice":
name = "bools"
}
return
@@ -570,11 +599,14 @@ func wrapN(i, slop int, s string) (string, string) {
return s, ""
}
w := strings.LastIndexAny(s[:i], " \t")
w := strings.LastIndexAny(s[:i], " \t\n")
if w <= 0 {
return s, ""
}
nlPos := strings.LastIndex(s[:i], "\n")
if nlPos > 0 && nlPos < w {
return s[:nlPos], s[nlPos+1:]
}
return s[:w], s[w+1:]
}
@@ -583,7 +615,7 @@ func wrapN(i, slop int, s string) (string, string) {
// caller). Pass `w` == 0 to do no wrapping
func wrap(i, w int, s string) string {
if w == 0 {
return s
return strings.Replace(s, "\n", "\n"+strings.Repeat(" ", i), -1)
}
// space between indent i and end of line width w into which
@@ -601,7 +633,7 @@ func wrap(i, w int, s string) string {
}
// If still not enough space then don't even try to wrap.
if wrap < 24 {
return s
return strings.Replace(s, "\n", r, -1)
}
// Try to avoid short orphan words on the final line, by
@@ -613,14 +645,14 @@ func wrap(i, w int, s string) string {
// Handle first line, which is indented by the caller (or the
// special case above)
l, s = wrapN(wrap, slop, s)
r = r + l
r = r + strings.Replace(l, "\n", "\n"+strings.Repeat(" ", i), -1)
// Now wrap the rest
for s != "" {
var t string
t, s = wrapN(wrap, slop, s)
r = r + "\n" + strings.Repeat(" ", i) + t
r = r + "\n" + strings.Repeat(" ", i) + strings.Replace(t, "\n", "\n"+strings.Repeat(" ", i), -1)
}
return r
@@ -637,7 +669,7 @@ func (f *FlagSet) FlagUsagesWrapped(cols int) string {
maxlen := 0
f.VisitAll(func(flag *Flag) {
if flag.Deprecated != "" || flag.Hidden {
if flag.Hidden {
return
}
@@ -660,6 +692,10 @@ func (f *FlagSet) FlagUsagesWrapped(cols int) string {
if flag.NoOptDefVal != "true" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
case "count":
if flag.NoOptDefVal != "+1" {
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
default:
line += fmt.Sprintf("[=%s]", flag.NoOptDefVal)
}
@@ -680,6 +716,9 @@ func (f *FlagSet) FlagUsagesWrapped(cols int) string {
line += fmt.Sprintf(" (default %s)", flag.DefValue)
}
}
if len(flag.Deprecated) != 0 {
line += fmt.Sprintf(" (DEPRECATED: %s)", flag.Deprecated)
}
lines = append(lines, line)
})
@@ -857,8 +896,10 @@ func VarP(value Value, name, shorthand, usage string) {
// returns the error.
func (f *FlagSet) failf(format string, a ...interface{}) error {
err := fmt.Errorf(format, a...)
fmt.Fprintln(f.out(), err)
f.usage()
if f.errorHandling != ContinueOnError {
fmt.Fprintln(f.out(), err)
f.usage()
}
return err
}
@@ -874,6 +915,28 @@ func (f *FlagSet) usage() {
}
}
//--unknown (args will be empty)
//--unknown --next-flag ... (args will be --next-flag ...)
//--unknown arg ... (args will be arg ...)
func stripUnknownFlagValue(args []string) []string {
if len(args) == 0 {
//--unknown
return args
}
first := args[0]
if len(first) > 0 && first[0] == '-' {
//--unknown --next-flag ...
return args
}
//--unknown arg ... (args will be arg ...)
if len(args) > 1 {
return args[1:]
}
return nil
}
func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []string, err error) {
a = args
name := s[2:]
@@ -885,13 +948,24 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
split := strings.SplitN(name, "=", 2)
name = split[0]
flag, exists := f.formal[f.normalizeFlagName(name)]
if !exists {
if name == "help" { // special case for nice help message.
switch {
case name == "help":
f.usage()
return a, ErrHelp
case f.ParseErrorsWhitelist.UnknownFlags:
// --unknown=unknownval arg ...
// we do not want to lose arg in this case
if len(split) >= 2 {
return a, nil
}
return stripUnknownFlagValue(a), nil
default:
err = f.failf("unknown flag: --%s", name)
return
}
err = f.failf("unknown flag: --%s", name)
return
}
var value string
@@ -912,27 +986,43 @@ func (f *FlagSet) parseLongArg(s string, args []string, fn parseFunc) (a []strin
}
err = fn(flag, value)
if err != nil {
f.failf(err.Error())
}
return
}
func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parseFunc) (outShorts string, outArgs []string, err error) {
outArgs = args
if strings.HasPrefix(shorthands, "test.") {
return
}
outArgs = args
outShorts = shorthands[1:]
c := shorthands[0]
flag, exists := f.shorthands[c]
if !exists {
if c == 'h' { // special case for nice help message.
switch {
case c == 'h':
f.usage()
err = ErrHelp
return
case f.ParseErrorsWhitelist.UnknownFlags:
// '-f=arg arg ...'
// we do not want to lose arg in this case
if len(shorthands) > 2 && shorthands[1] == '=' {
outShorts = ""
return
}
outArgs = stripUnknownFlagValue(outArgs)
return
default:
err = f.failf("unknown shorthand flag: %q in -%s", c, shorthands)
return
}
err = f.failf("unknown shorthand flag: %q in -%s", c, shorthands)
return
}
var value string
@@ -962,6 +1052,9 @@ func (f *FlagSet) parseSingleShortArg(shorthands string, args []string, fn parse
}
err = fn(flag, value)
if err != nil {
f.failf(err.Error())
}
return
}
@@ -1016,6 +1109,11 @@ func (f *FlagSet) parseArgs(args []string, fn parseFunc) (err error) {
// are defined and before flags are accessed by the program.
// The return value will be ErrHelp if -help was set but not defined.
func (f *FlagSet) Parse(arguments []string) error {
if f.addedGoFlagSets != nil {
for _, goFlagSet := range f.addedGoFlagSets {
goFlagSet.Parse(nil)
}
}
f.parsed = true
if len(arguments) < 0 {
@@ -1034,6 +1132,7 @@ func (f *FlagSet) Parse(arguments []string) error {
case ContinueOnError:
return err
case ExitOnError:
fmt.Println(err)
os.Exit(2)
case PanicOnError:
panic(err)
+186 -7
View File
@@ -106,8 +106,8 @@ func TestUsage(t *testing.T) {
if GetCommandLine().Parse([]string{"--x"}) == nil {
t.Error("parse did not fail for unknown flag")
}
if !called {
t.Error("did not call Usage for unknown flag")
if called {
t.Error("did call Usage while using ContinueOnError")
}
}
@@ -168,6 +168,7 @@ func testParse(f *FlagSet, t *testing.T) {
bool3Flag := f.Bool("bool3", false, "bool3 value")
intFlag := f.Int("int", 0, "int value")
int8Flag := f.Int8("int8", 0, "int value")
int16Flag := f.Int16("int16", 0, "int value")
int32Flag := f.Int32("int32", 0, "int value")
int64Flag := f.Int64("int64", 0, "int64 value")
uintFlag := f.Uint("uint", 0, "uint value")
@@ -192,6 +193,7 @@ func testParse(f *FlagSet, t *testing.T) {
"--bool3=false",
"--int=22",
"--int8=-8",
"--int16=-16",
"--int32=-32",
"--int64=0x23",
"--uint", "24",
@@ -236,9 +238,15 @@ func testParse(f *FlagSet, t *testing.T) {
if *int8Flag != -8 {
t.Error("int8 flag should be 0x23, is ", *int8Flag)
}
if *int16Flag != -16 {
t.Error("int16 flag should be -16, is ", *int16Flag)
}
if v, err := f.GetInt8("int8"); err != nil || v != *int8Flag {
t.Error("GetInt8 does not work.")
}
if v, err := f.GetInt16("int16"); err != nil || v != *int16Flag {
t.Error("GetInt16 does not work.")
}
if *int32Flag != -32 {
t.Error("int32 flag should be 0x23, is ", *int32Flag)
}
@@ -381,7 +389,83 @@ func testParseAll(f *FlagSet, t *testing.T) {
}
if !reflect.DeepEqual(got, want) {
t.Errorf("f.ParseAll() fail to restore the args")
t.Errorf("Got: %v", got)
t.Errorf("Got: %v", got)
t.Errorf("Want: %v", want)
}
}
func testParseWithUnknownFlags(f *FlagSet, t *testing.T) {
if f.Parsed() {
t.Error("f.Parse() = true before Parse")
}
f.ParseErrorsWhitelist.UnknownFlags = true
f.BoolP("boola", "a", false, "bool value")
f.BoolP("boolb", "b", false, "bool2 value")
f.BoolP("boolc", "c", false, "bool3 value")
f.BoolP("boold", "d", false, "bool4 value")
f.BoolP("boole", "e", false, "bool4 value")
f.StringP("stringa", "s", "0", "string value")
f.StringP("stringz", "z", "0", "string value")
f.StringP("stringx", "x", "0", "string value")
f.StringP("stringy", "y", "0", "string value")
f.StringP("stringo", "o", "0", "string value")
f.Lookup("stringx").NoOptDefVal = "1"
args := []string{
"-ab",
"-cs=xx",
"--stringz=something",
"--unknown1",
"unknown1Value",
"-d=true",
"-x",
"--unknown2=unknown2Value",
"-u=unknown3Value",
"-p",
"unknown4Value",
"-q", //another unknown with bool value
"-y",
"ee",
"--unknown7=unknown7value",
"--stringo=ovalue",
"--unknown8=unknown8value",
"--boole",
"--unknown6",
"",
"-uuuuu",
"",
"--unknown10",
"--unknown11",
}
want := []string{
"boola", "true",
"boolb", "true",
"boolc", "true",
"stringa", "xx",
"stringz", "something",
"boold", "true",
"stringx", "1",
"stringy", "ee",
"stringo", "ovalue",
"boole", "true",
}
got := []string{}
store := func(flag *Flag, value string) error {
got = append(got, flag.Name)
if len(value) > 0 {
got = append(got, value)
}
return nil
}
if err := f.ParseAll(args, store); err != nil {
t.Errorf("expected no error, got %s", err)
}
if !f.Parsed() {
t.Errorf("f.Parse() = false after Parse")
}
if !reflect.DeepEqual(got, want) {
t.Errorf("f.ParseAll() fail to restore the args")
t.Errorf("Got: %v", got)
t.Errorf("Want: %v", want)
}
}
@@ -492,6 +576,11 @@ func TestParseAll(t *testing.T) {
testParseAll(GetCommandLine(), t)
}
func TestIgnoreUnknownFlags(t *testing.T) {
ResetForTesting(func() { t.Error("bad parse") })
testParseWithUnknownFlags(GetCommandLine(), t)
}
func TestFlagSetParse(t *testing.T) {
testParse(NewFlagSet("test", ContinueOnError), t)
}
@@ -604,7 +693,6 @@ func aliasAndWordSepFlagNames(f *FlagSet, name string) NormalizedName {
switch name {
case oldName:
name = newName
break
}
return NormalizedName(name)
@@ -658,6 +746,70 @@ func TestNormalizationFuncShouldChangeFlagName(t *testing.T) {
}
}
// Related to https://github.com/spf13/cobra/issues/521.
func TestNormalizationSharedFlags(t *testing.T) {
f := NewFlagSet("set f", ContinueOnError)
g := NewFlagSet("set g", ContinueOnError)
nfunc := wordSepNormalizeFunc
testName := "valid_flag"
normName := nfunc(nil, testName)
if testName == string(normName) {
t.Error("TestNormalizationSharedFlags meaningless: the original and normalized flag names are identical:", testName)
}
f.Bool(testName, false, "bool value")
g.AddFlagSet(f)
f.SetNormalizeFunc(nfunc)
g.SetNormalizeFunc(nfunc)
if len(f.formal) != 1 {
t.Error("Normalizing flags should not result in duplications in the flag set:", f.formal)
}
if f.orderedFormal[0].Name != string(normName) {
t.Error("Flag name not normalized")
}
for k := range f.formal {
if k != "valid.flag" {
t.Errorf("The key in the flag map should have been normalized: wanted \"%s\", got \"%s\" instead", normName, k)
}
}
if !reflect.DeepEqual(f.formal, g.formal) || !reflect.DeepEqual(f.orderedFormal, g.orderedFormal) {
t.Error("Two flag sets sharing the same flags should stay consistent after being normalized. Original set:", f.formal, "Duplicate set:", g.formal)
}
}
func TestNormalizationSetFlags(t *testing.T) {
f := NewFlagSet("normalized", ContinueOnError)
nfunc := wordSepNormalizeFunc
testName := "valid_flag"
normName := nfunc(nil, testName)
if testName == string(normName) {
t.Error("TestNormalizationSetFlags meaningless: the original and normalized flag names are identical:", testName)
}
f.Bool(testName, false, "bool value")
f.Set(testName, "true")
f.SetNormalizeFunc(nfunc)
if len(f.formal) != 1 {
t.Error("Normalizing flags should not result in duplications in the flag set:", f.formal)
}
if f.orderedFormal[0].Name != string(normName) {
t.Error("Flag name not normalized")
}
for k := range f.formal {
if k != "valid.flag" {
t.Errorf("The key in the flag map should have been normalized: wanted \"%s\", got \"%s\" instead", normName, k)
}
}
if !reflect.DeepEqual(f.formal, f.actual) {
t.Error("The map of set flags should get normalized. Formal:", f.formal, "Actual:", f.actual)
}
}
// Declare a user-defined flag type.
type flagVar []string
@@ -819,10 +971,14 @@ func TestTermination(t *testing.T) {
}
}
func TestDeprecatedFlagInDocs(t *testing.T) {
func getDeprecatedFlagSet() *FlagSet {
f := NewFlagSet("bob", ContinueOnError)
f.Bool("badflag", true, "always true")
f.MarkDeprecated("badflag", "use --good-flag instead")
return f
}
func TestDeprecatedFlagInDocs(t *testing.T) {
f := getDeprecatedFlagSet()
out := new(bytes.Buffer)
f.SetOutput(out)
@@ -833,6 +989,27 @@ func TestDeprecatedFlagInDocs(t *testing.T) {
}
}
func TestUnHiddenDeprecatedFlagInDocs(t *testing.T) {
f := getDeprecatedFlagSet()
flg := f.Lookup("badflag")
if flg == nil {
t.Fatalf("Unable to lookup 'bob' in TestUnHiddenDeprecatedFlagInDocs")
}
flg.Hidden = false
out := new(bytes.Buffer)
f.SetOutput(out)
f.PrintDefaults()
defaults := out.String()
if !strings.Contains(defaults, "badflag") {
t.Errorf("Did not find deprecated flag in usage!")
}
if !strings.Contains(defaults, "use --good-flag instead") {
t.Errorf("Did not find 'use --good-flag instead' in defaults")
}
}
func TestDeprecatedFlagShorthandInDocs(t *testing.T) {
f := NewFlagSet("bob", ContinueOnError)
name := "noshorthandflag"
@@ -978,16 +1155,17 @@ const defaultOutput = ` --A for bootstrapping, allo
--IP ip IP address with no default
--IPMask ipMask Netmask address with no default
--IPNet ipNet IP network with no default
--Ints intSlice int slice with zero default
--Ints ints int slice with zero default
--N int a non-zero int (default 27)
--ND1 string[="bar"] a string with NoOptDefVal (default "foo")
--ND2 num[=4321] a num with NoOptDefVal (default 1234)
--StringArray stringArray string array with zero default
--StringSlice stringSlice string slice with zero default
--StringSlice strings string slice with zero default
--Z int an int that defaults to zero
--custom custom custom Value implementation
--customP custom a VarP with default (default 10)
--maxT timeout set timeout for dial
-v, --verbose count verbosity
`
// Custom value that satisfies the Value interface.
@@ -1028,6 +1206,7 @@ func TestPrintDefaults(t *testing.T) {
fs.ShorthandLookup("E").NoOptDefVal = "1234"
fs.StringSlice("StringSlice", []string{}, "string slice with zero default")
fs.StringArray("StringArray", []string{}, "string array with zero default")
fs.CountP("verbose", "v", "verbosity")
var cv customValue
fs.Var(&cv, "custom", "custom Value implementation")
+4
View File
@@ -98,4 +98,8 @@ func (f *FlagSet) AddGoFlagSet(newSet *goflag.FlagSet) {
newSet.VisitAll(func(goflag *goflag.Flag) {
f.AddGoFlag(goflag)
})
if f.addedGoFlagSets == nil {
f.addedGoFlagSets = make([]*goflag.FlagSet, 0)
}
f.addedGoFlagSets = append(f.addedGoFlagSets, newSet)
}
+8
View File
@@ -36,4 +36,12 @@ func TestGoflags(t *testing.T) {
if getBool != true {
t.Fatalf("expected getBool=true but got getBool=%v", getBool)
}
if !f.Parsed() {
t.Fatal("f.Parsed() return false after f.Parse() called")
}
// in fact it is useless. because `go test` called flag.Parse()
if !goflag.CommandLine.Parsed() {
t.Fatal("goflag.CommandLine.Parsed() return false after f.Parse() called")
}
}
+88
View File
@@ -0,0 +1,88 @@
package pflag
import "strconv"
// -- int16 Value
type int16Value int16
func newInt16Value(val int16, p *int16) *int16Value {
*p = val
return (*int16Value)(p)
}
func (i *int16Value) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 16)
*i = int16Value(v)
return err
}
func (i *int16Value) Type() string {
return "int16"
}
func (i *int16Value) String() string { return strconv.FormatInt(int64(*i), 10) }
func int16Conv(sval string) (interface{}, error) {
v, err := strconv.ParseInt(sval, 0, 16)
if err != nil {
return 0, err
}
return int16(v), nil
}
// GetInt16 returns the int16 value of a flag with the given name
func (f *FlagSet) GetInt16(name string) (int16, error) {
val, err := f.getFlagType(name, "int16", int16Conv)
if err != nil {
return 0, err
}
return val.(int16), nil
}
// Int16Var defines an int16 flag with specified name, default value, and usage string.
// The argument p points to an int16 variable in which to store the value of the flag.
func (f *FlagSet) Int16Var(p *int16, name string, value int16, usage string) {
f.VarP(newInt16Value(value, p), name, "", usage)
}
// Int16VarP is like Int16Var, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) Int16VarP(p *int16, name, shorthand string, value int16, usage string) {
f.VarP(newInt16Value(value, p), name, shorthand, usage)
}
// Int16Var defines an int16 flag with specified name, default value, and usage string.
// The argument p points to an int16 variable in which to store the value of the flag.
func Int16Var(p *int16, name string, value int16, usage string) {
CommandLine.VarP(newInt16Value(value, p), name, "", usage)
}
// Int16VarP is like Int16Var, but accepts a shorthand letter that can be used after a single dash.
func Int16VarP(p *int16, name, shorthand string, value int16, usage string) {
CommandLine.VarP(newInt16Value(value, p), name, shorthand, usage)
}
// Int16 defines an int16 flag with specified name, default value, and usage string.
// The return value is the address of an int16 variable that stores the value of the flag.
func (f *FlagSet) Int16(name string, value int16, usage string) *int16 {
p := new(int16)
f.Int16VarP(p, name, "", value, usage)
return p
}
// Int16P is like Int16, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) Int16P(name, shorthand string, value int16, usage string) *int16 {
p := new(int16)
f.Int16VarP(p, name, shorthand, value, usage)
return p
}
// Int16 defines an int16 flag with specified name, default value, and usage string.
// The return value is the address of an int16 variable that stores the value of the flag.
func Int16(name string, value int16, usage string) *int16 {
return CommandLine.Int16P(name, "", value, usage)
}
// Int16P is like Int16, but accepts a shorthand letter that can be used after a single dash.
func Int16P(name, shorthand string, value int16, usage string) *int16 {
return CommandLine.Int16P(name, shorthand, value, usage)
}
+74
View File
@@ -0,0 +1,74 @@
package pflag
import (
"bytes"
"io"
"testing"
)
const expectedOutput = ` --long-form Some description
--long-form2 Some description
with multiline
-s, --long-name Some description
-t, --long-name2 Some description with
multiline
`
func setUpPFlagSet(buf io.Writer) *FlagSet {
f := NewFlagSet("test", ExitOnError)
f.Bool("long-form", false, "Some description")
f.Bool("long-form2", false, "Some description\n with multiline")
f.BoolP("long-name", "s", false, "Some description")
f.BoolP("long-name2", "t", false, "Some description with\n multiline")
f.SetOutput(buf)
return f
}
func TestPrintUsage(t *testing.T) {
buf := bytes.Buffer{}
f := setUpPFlagSet(&buf)
f.PrintDefaults()
res := buf.String()
if res != expectedOutput {
t.Errorf("Expected \n%s \nActual \n%s", expectedOutput, res)
}
}
func setUpPFlagSet2(buf io.Writer) *FlagSet {
f := NewFlagSet("test", ExitOnError)
f.Bool("long-form", false, "Some description")
f.Bool("long-form2", false, "Some description\n with multiline")
f.BoolP("long-name", "s", false, "Some description")
f.BoolP("long-name2", "t", false, "Some description with\n multiline")
f.StringP("some-very-long-arg", "l", "test", "Some very long description having break the limit")
f.StringP("other-very-long-arg", "o", "long-default-value", "Some very long description having break the limit")
f.String("some-very-long-arg2", "very long default value", "Some very long description\nwith line break\nmultiple")
f.SetOutput(buf)
return f
}
const expectedOutput2 = ` --long-form Some description
--long-form2 Some description
with multiline
-s, --long-name Some description
-t, --long-name2 Some description with
multiline
-o, --other-very-long-arg string Some very long description having
break the limit (default
"long-default-value")
-l, --some-very-long-arg string Some very long description having
break the limit (default "test")
--some-very-long-arg2 string Some very long description
with line break
multiple (default "very long default
value")
`
func TestPrintUsage_2(t *testing.T) {
buf := bytes.Buffer{}
f := setUpPFlagSet2(&buf)
res := f.FlagUsagesWrapped(80)
if res != expectedOutput2 {
t.Errorf("Expected \n%q \nActual \n%q", expectedOutput2, res)
}
}
+4 -4
View File
@@ -52,7 +52,7 @@ func (f *FlagSet) GetStringArray(name string) ([]string, error) {
// StringArrayVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a []string variable in which to store the values of the multiple flags.
// The value of each argument will not try to be separated by comma
// The value of each argument will not try to be separated by comma. Use a StringSlice for that.
func (f *FlagSet) StringArrayVar(p *[]string, name string, value []string, usage string) {
f.VarP(newStringArrayValue(value, p), name, "", usage)
}
@@ -64,7 +64,7 @@ func (f *FlagSet) StringArrayVarP(p *[]string, name, shorthand string, value []s
// StringArrayVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a []string variable in which to store the value of the flag.
// The value of each argument will not try to be separated by comma
// The value of each argument will not try to be separated by comma. Use a StringSlice for that.
func StringArrayVar(p *[]string, name string, value []string, usage string) {
CommandLine.VarP(newStringArrayValue(value, p), name, "", usage)
}
@@ -76,7 +76,7 @@ func StringArrayVarP(p *[]string, name, shorthand string, value []string, usage
// StringArray defines a string flag with specified name, default value, and usage string.
// The return value is the address of a []string variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
// The value of each argument will not try to be separated by comma. Use a StringSlice for that.
func (f *FlagSet) StringArray(name string, value []string, usage string) *[]string {
p := []string{}
f.StringArrayVarP(&p, name, "", value, usage)
@@ -92,7 +92,7 @@ func (f *FlagSet) StringArrayP(name, shorthand string, value []string, usage str
// StringArray defines a string flag with specified name, default value, and usage string.
// The return value is the address of a []string variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
// The value of each argument will not try to be separated by comma. Use a StringSlice for that.
func StringArray(name string, value []string, usage string) *[]string {
return CommandLine.StringArrayP(name, "", value, usage)
}
+20
View File
@@ -82,6 +82,11 @@ func (f *FlagSet) GetStringSlice(name string) ([]string, error) {
// StringSliceVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a []string variable in which to store the value of the flag.
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly.
// For example:
// --ss="v1,v2" -ss="v3"
// will result in
// []string{"v1", "v2", "v3"}
func (f *FlagSet) StringSliceVar(p *[]string, name string, value []string, usage string) {
f.VarP(newStringSliceValue(value, p), name, "", usage)
}
@@ -93,6 +98,11 @@ func (f *FlagSet) StringSliceVarP(p *[]string, name, shorthand string, value []s
// StringSliceVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a []string variable in which to store the value of the flag.
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly.
// For example:
// --ss="v1,v2" -ss="v3"
// will result in
// []string{"v1", "v2", "v3"}
func StringSliceVar(p *[]string, name string, value []string, usage string) {
CommandLine.VarP(newStringSliceValue(value, p), name, "", usage)
}
@@ -104,6 +114,11 @@ func StringSliceVarP(p *[]string, name, shorthand string, value []string, usage
// StringSlice defines a string flag with specified name, default value, and usage string.
// The return value is the address of a []string variable that stores the value of the flag.
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly.
// For example:
// --ss="v1,v2" -ss="v3"
// will result in
// []string{"v1", "v2", "v3"}
func (f *FlagSet) StringSlice(name string, value []string, usage string) *[]string {
p := []string{}
f.StringSliceVarP(&p, name, "", value, usage)
@@ -119,6 +134,11 @@ func (f *FlagSet) StringSliceP(name, shorthand string, value []string, usage str
// StringSlice defines a string flag with specified name, default value, and usage string.
// The return value is the address of a []string variable that stores the value of the flag.
// Compared to StringArray flags, StringSlice flags take comma-separated value as arguments and split them accordingly.
// For example:
// --ss="v1,v2" -ss="v3"
// will result in
// []string{"v1", "v2", "v3"}
func StringSlice(name string, value []string, usage string) *[]string {
return CommandLine.StringSliceP(name, "", value, usage)
}
+149
View File
@@ -0,0 +1,149 @@
package pflag
import (
"bytes"
"fmt"
"strconv"
"strings"
)
// -- stringToInt Value
type stringToIntValue struct {
value *map[string]int
changed bool
}
func newStringToIntValue(val map[string]int, p *map[string]int) *stringToIntValue {
ssv := new(stringToIntValue)
ssv.value = p
*ssv.value = val
return ssv
}
// Format: a=1,b=2
func (s *stringToIntValue) Set(val string) error {
ss := strings.Split(val, ",")
out := make(map[string]int, len(ss))
for _, pair := range ss {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return fmt.Errorf("%s must be formatted as key=value", pair)
}
var err error
out[kv[0]], err = strconv.Atoi(kv[1])
if err != nil {
return err
}
}
if !s.changed {
*s.value = out
} else {
for k, v := range out {
(*s.value)[k] = v
}
}
s.changed = true
return nil
}
func (s *stringToIntValue) Type() string {
return "stringToInt"
}
func (s *stringToIntValue) String() string {
var buf bytes.Buffer
i := 0
for k, v := range *s.value {
if i > 0 {
buf.WriteRune(',')
}
buf.WriteString(k)
buf.WriteRune('=')
buf.WriteString(strconv.Itoa(v))
i++
}
return "[" + buf.String() + "]"
}
func stringToIntConv(val string) (interface{}, error) {
val = strings.Trim(val, "[]")
// An empty string would cause an empty map
if len(val) == 0 {
return map[string]int{}, nil
}
ss := strings.Split(val, ",")
out := make(map[string]int, len(ss))
for _, pair := range ss {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("%s must be formatted as key=value", pair)
}
var err error
out[kv[0]], err = strconv.Atoi(kv[1])
if err != nil {
return nil, err
}
}
return out, nil
}
// GetStringToInt return the map[string]int value of a flag with the given name
func (f *FlagSet) GetStringToInt(name string) (map[string]int, error) {
val, err := f.getFlagType(name, "stringToInt", stringToIntConv)
if err != nil {
return map[string]int{}, err
}
return val.(map[string]int), nil
}
// StringToIntVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a map[string]int variable in which to store the values of the multiple flags.
// The value of each argument will not try to be separated by comma
func (f *FlagSet) StringToIntVar(p *map[string]int, name string, value map[string]int, usage string) {
f.VarP(newStringToIntValue(value, p), name, "", usage)
}
// StringToIntVarP is like StringToIntVar, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) StringToIntVarP(p *map[string]int, name, shorthand string, value map[string]int, usage string) {
f.VarP(newStringToIntValue(value, p), name, shorthand, usage)
}
// StringToIntVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a map[string]int variable in which to store the value of the flag.
// The value of each argument will not try to be separated by comma
func StringToIntVar(p *map[string]int, name string, value map[string]int, usage string) {
CommandLine.VarP(newStringToIntValue(value, p), name, "", usage)
}
// StringToIntVarP is like StringToIntVar, but accepts a shorthand letter that can be used after a single dash.
func StringToIntVarP(p *map[string]int, name, shorthand string, value map[string]int, usage string) {
CommandLine.VarP(newStringToIntValue(value, p), name, shorthand, usage)
}
// StringToInt defines a string flag with specified name, default value, and usage string.
// The return value is the address of a map[string]int variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
func (f *FlagSet) StringToInt(name string, value map[string]int, usage string) *map[string]int {
p := map[string]int{}
f.StringToIntVarP(&p, name, "", value, usage)
return &p
}
// StringToIntP is like StringToInt, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) StringToIntP(name, shorthand string, value map[string]int, usage string) *map[string]int {
p := map[string]int{}
f.StringToIntVarP(&p, name, shorthand, value, usage)
return &p
}
// StringToInt defines a string flag with specified name, default value, and usage string.
// The return value is the address of a map[string]int variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
func StringToInt(name string, value map[string]int, usage string) *map[string]int {
return CommandLine.StringToIntP(name, "", value, usage)
}
// StringToIntP is like StringToInt, but accepts a shorthand letter that can be used after a single dash.
func StringToIntP(name, shorthand string, value map[string]int, usage string) *map[string]int {
return CommandLine.StringToIntP(name, shorthand, value, usage)
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of ths2i source code s2i governed by a BSD-style
// license that can be found in the LICENSE file.
package pflag
import (
"bytes"
"fmt"
"strconv"
"testing"
)
func setUpS2IFlagSet(s2ip *map[string]int) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.StringToIntVar(s2ip, "s2i", map[string]int{}, "Command separated ls2it!")
return f
}
func setUpS2IFlagSetWithDefault(s2ip *map[string]int) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.StringToIntVar(s2ip, "s2i", map[string]int{"a": 1, "b": 2}, "Command separated ls2it!")
return f
}
func createS2IFlag(vals map[string]int) string {
var buf bytes.Buffer
i := 0
for k, v := range vals {
if i > 0 {
buf.WriteRune(',')
}
buf.WriteString(k)
buf.WriteRune('=')
buf.WriteString(strconv.Itoa(v))
i++
}
return buf.String()
}
func TestEmptyS2I(t *testing.T) {
var s2i map[string]int
f := setUpS2IFlagSet(&s2i)
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
getS2I, err := f.GetStringToInt("s2i")
if err != nil {
t.Fatal("got an error from GetStringToInt():", err)
}
if len(getS2I) != 0 {
t.Fatalf("got s2i %v with len=%d but expected length=0", getS2I, len(getS2I))
}
}
func TestS2I(t *testing.T) {
var s2i map[string]int
f := setUpS2IFlagSet(&s2i)
vals := map[string]int{"a": 1, "b": 2, "d": 4, "c": 3}
arg := fmt.Sprintf("--s2i=%s", createS2IFlag(vals))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2i {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d but got: %d", k, vals[k], v)
}
}
getS2I, err := f.GetStringToInt("s2i")
if err != nil {
t.Fatalf("got error: %v", err)
}
for k, v := range getS2I {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d but got: %d from GetStringToInt", k, vals[k], v)
}
}
}
func TestS2IDefault(t *testing.T) {
var s2i map[string]int
f := setUpS2IFlagSetWithDefault(&s2i)
vals := map[string]int{"a": 1, "b": 2}
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2i {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d but got: %d", k, vals[k], v)
}
}
getS2I, err := f.GetStringToInt("s2i")
if err != nil {
t.Fatal("got an error from GetStringToInt():", err)
}
for k, v := range getS2I {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d from GetStringToInt but got: %d", k, vals[k], v)
}
}
}
func TestS2IWithDefault(t *testing.T) {
var s2i map[string]int
f := setUpS2IFlagSetWithDefault(&s2i)
vals := map[string]int{"a": 1, "b": 2}
arg := fmt.Sprintf("--s2i=%s", createS2IFlag(vals))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2i {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d but got: %d", k, vals[k], v)
}
}
getS2I, err := f.GetStringToInt("s2i")
if err != nil {
t.Fatal("got an error from GetStringToInt():", err)
}
for k, v := range getS2I {
if vals[k] != v {
t.Fatalf("expected s2i[%s] to be %d from GetStringToInt but got: %d", k, vals[k], v)
}
}
}
func TestS2ICalledTwice(t *testing.T) {
var s2i map[string]int
f := setUpS2IFlagSet(&s2i)
in := []string{"a=1,b=2", "b=3"}
expected := map[string]int{"a": 1, "b": 3}
argfmt := "--s2i=%s"
arg1 := fmt.Sprintf(argfmt, in[0])
arg2 := fmt.Sprintf(argfmt, in[1])
err := f.Parse([]string{arg1, arg2})
if err != nil {
t.Fatal("expected no error; got", err)
}
for i, v := range s2i {
if expected[i] != v {
t.Fatalf("expected s2i[%s] to be %d but got: %d", i, expected[i], v)
}
}
}
+160
View File
@@ -0,0 +1,160 @@
package pflag
import (
"bytes"
"encoding/csv"
"fmt"
"strings"
)
// -- stringToString Value
type stringToStringValue struct {
value *map[string]string
changed bool
}
func newStringToStringValue(val map[string]string, p *map[string]string) *stringToStringValue {
ssv := new(stringToStringValue)
ssv.value = p
*ssv.value = val
return ssv
}
// Format: a=1,b=2
func (s *stringToStringValue) Set(val string) error {
var ss []string
n := strings.Count(val, "=")
switch n {
case 0:
return fmt.Errorf("%s must be formatted as key=value", val)
case 1:
ss = append(ss, strings.Trim(val, `"`))
default:
r := csv.NewReader(strings.NewReader(val))
var err error
ss, err = r.Read()
if err != nil {
return err
}
}
out := make(map[string]string, len(ss))
for _, pair := range ss {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return fmt.Errorf("%s must be formatted as key=value", pair)
}
out[kv[0]] = kv[1]
}
if !s.changed {
*s.value = out
} else {
for k, v := range out {
(*s.value)[k] = v
}
}
s.changed = true
return nil
}
func (s *stringToStringValue) Type() string {
return "stringToString"
}
func (s *stringToStringValue) String() string {
records := make([]string, 0, len(*s.value)>>1)
for k, v := range *s.value {
records = append(records, k+"="+v)
}
var buf bytes.Buffer
w := csv.NewWriter(&buf)
if err := w.Write(records); err != nil {
panic(err)
}
w.Flush()
return "[" + strings.TrimSpace(buf.String()) + "]"
}
func stringToStringConv(val string) (interface{}, error) {
val = strings.Trim(val, "[]")
// An empty string would cause an empty map
if len(val) == 0 {
return map[string]string{}, nil
}
r := csv.NewReader(strings.NewReader(val))
ss, err := r.Read()
if err != nil {
return nil, err
}
out := make(map[string]string, len(ss))
for _, pair := range ss {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("%s must be formatted as key=value", pair)
}
out[kv[0]] = kv[1]
}
return out, nil
}
// GetStringToString return the map[string]string value of a flag with the given name
func (f *FlagSet) GetStringToString(name string) (map[string]string, error) {
val, err := f.getFlagType(name, "stringToString", stringToStringConv)
if err != nil {
return map[string]string{}, err
}
return val.(map[string]string), nil
}
// StringToStringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a map[string]string variable in which to store the values of the multiple flags.
// The value of each argument will not try to be separated by comma
func (f *FlagSet) StringToStringVar(p *map[string]string, name string, value map[string]string, usage string) {
f.VarP(newStringToStringValue(value, p), name, "", usage)
}
// StringToStringVarP is like StringToStringVar, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) StringToStringVarP(p *map[string]string, name, shorthand string, value map[string]string, usage string) {
f.VarP(newStringToStringValue(value, p), name, shorthand, usage)
}
// StringToStringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a map[string]string variable in which to store the value of the flag.
// The value of each argument will not try to be separated by comma
func StringToStringVar(p *map[string]string, name string, value map[string]string, usage string) {
CommandLine.VarP(newStringToStringValue(value, p), name, "", usage)
}
// StringToStringVarP is like StringToStringVar, but accepts a shorthand letter that can be used after a single dash.
func StringToStringVarP(p *map[string]string, name, shorthand string, value map[string]string, usage string) {
CommandLine.VarP(newStringToStringValue(value, p), name, shorthand, usage)
}
// StringToString defines a string flag with specified name, default value, and usage string.
// The return value is the address of a map[string]string variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
func (f *FlagSet) StringToString(name string, value map[string]string, usage string) *map[string]string {
p := map[string]string{}
f.StringToStringVarP(&p, name, "", value, usage)
return &p
}
// StringToStringP is like StringToString, but accepts a shorthand letter that can be used after a single dash.
func (f *FlagSet) StringToStringP(name, shorthand string, value map[string]string, usage string) *map[string]string {
p := map[string]string{}
f.StringToStringVarP(&p, name, shorthand, value, usage)
return &p
}
// StringToString defines a string flag with specified name, default value, and usage string.
// The return value is the address of a map[string]string variable that stores the value of the flag.
// The value of each argument will not try to be separated by comma
func StringToString(name string, value map[string]string, usage string) *map[string]string {
return CommandLine.StringToStringP(name, "", value, usage)
}
// StringToStringP is like StringToString, but accepts a shorthand letter that can be used after a single dash.
func StringToStringP(name, shorthand string, value map[string]string, usage string) *map[string]string {
return CommandLine.StringToStringP(name, shorthand, value, usage)
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of ths2s source code s2s governed by a BSD-style
// license that can be found in the LICENSE file.
package pflag
import (
"bytes"
"encoding/csv"
"fmt"
"strings"
"testing"
)
func setUpS2SFlagSet(s2sp *map[string]string) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.StringToStringVar(s2sp, "s2s", map[string]string{}, "Command separated ls2st!")
return f
}
func setUpS2SFlagSetWithDefault(s2sp *map[string]string) *FlagSet {
f := NewFlagSet("test", ContinueOnError)
f.StringToStringVar(s2sp, "s2s", map[string]string{"da": "1", "db": "2", "de": "5,6"}, "Command separated ls2st!")
return f
}
func createS2SFlag(vals map[string]string) string {
records := make([]string, 0, len(vals)>>1)
for k, v := range vals {
records = append(records, k+"="+v)
}
var buf bytes.Buffer
w := csv.NewWriter(&buf)
if err := w.Write(records); err != nil {
panic(err)
}
w.Flush()
return strings.TrimSpace(buf.String())
}
func TestEmptyS2S(t *testing.T) {
var s2s map[string]string
f := setUpS2SFlagSet(&s2s)
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
getS2S, err := f.GetStringToString("s2s")
if err != nil {
t.Fatal("got an error from GetStringToString():", err)
}
if len(getS2S) != 0 {
t.Fatalf("got s2s %v with len=%d but expected length=0", getS2S, len(getS2S))
}
}
func TestS2S(t *testing.T) {
var s2s map[string]string
f := setUpS2SFlagSet(&s2s)
vals := map[string]string{"a": "1", "b": "2", "d": "4", "c": "3", "e": "5,6"}
arg := fmt.Sprintf("--s2s=%s", createS2SFlag(vals))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2s {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s but got: %s", k, vals[k], v)
}
}
getS2S, err := f.GetStringToString("s2s")
if err != nil {
t.Fatalf("got error: %v", err)
}
for k, v := range getS2S {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s but got: %s from GetStringToString", k, vals[k], v)
}
}
}
func TestS2SDefault(t *testing.T) {
var s2s map[string]string
f := setUpS2SFlagSetWithDefault(&s2s)
vals := map[string]string{"da": "1", "db": "2", "de": "5,6"}
err := f.Parse([]string{})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2s {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s but got: %s", k, vals[k], v)
}
}
getS2S, err := f.GetStringToString("s2s")
if err != nil {
t.Fatal("got an error from GetStringToString():", err)
}
for k, v := range getS2S {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s from GetStringToString but got: %s", k, vals[k], v)
}
}
}
func TestS2SWithDefault(t *testing.T) {
var s2s map[string]string
f := setUpS2SFlagSetWithDefault(&s2s)
vals := map[string]string{"a": "1", "b": "2", "e": "5,6"}
arg := fmt.Sprintf("--s2s=%s", createS2SFlag(vals))
err := f.Parse([]string{arg})
if err != nil {
t.Fatal("expected no error; got", err)
}
for k, v := range s2s {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s but got: %s", k, vals[k], v)
}
}
getS2S, err := f.GetStringToString("s2s")
if err != nil {
t.Fatal("got an error from GetStringToString():", err)
}
for k, v := range getS2S {
if vals[k] != v {
t.Fatalf("expected s2s[%s] to be %s from GetStringToString but got: %s", k, vals[k], v)
}
}
}
func TestS2SCalledTwice(t *testing.T) {
var s2s map[string]string
f := setUpS2SFlagSet(&s2s)
in := []string{"a=1,b=2", "b=3", `"e=5,6"`, `f=7,8`}
expected := map[string]string{"a": "1", "b": "3", "e": "5,6", "f": "7,8"}
argfmt := "--s2s=%s"
arg0 := fmt.Sprintf(argfmt, in[0])
arg1 := fmt.Sprintf(argfmt, in[1])
arg2 := fmt.Sprintf(argfmt, in[2])
arg3 := fmt.Sprintf(argfmt, in[3])
err := f.Parse([]string{arg0, arg1, arg2, arg3})
if err != nil {
t.Fatal("expected no error; got", err)
}
if len(s2s) != len(expected) {
t.Fatalf("expected %d flags; got %d flags", len(expected), len(s2s))
}
for i, v := range s2s {
if expected[i] != v {
t.Fatalf("expected s2s[%s] to be %s but got: %s", i, expected[i], v)
}
}
}