Skip to content

Commit

Permalink
Merge pull request #392 from codegangsta/string-slice-overwrite-on-set
Browse files Browse the repository at this point in the history
Overwrite slice flag defaults when set
  • Loading branch information
jszwedko committed May 5, 2016
2 parents 75b97d0 + a4590ca commit eb8680b
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 66 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

**ATTN**: This project uses [semantic versioning](http://semver.org/).

## [Unreleased]
## 2.0.0 - (unreleased 2.x series)
### Added
- `NewStringSlice` and `NewIntSlice` for creating their related types

### Removed
- the ability to specify `&StringSlice{...string}` or `&IntSlice{...int}`.
To migrate to the new API, you may choose to run [this helper
(python) script](./cli-migrate-slice-types).

## [Unreleased] - (1.x series)
### Added
- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc`

Expand Down
4 changes: 2 additions & 2 deletions altsrc/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (f *StringSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputS
return err
}
if value != nil {
var sliceValue cli.StringSlice = value
var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...))
eachName(f.Name, func(name string) {
underlyingFlag := f.set.Lookup(f.Name)
if underlyingFlag != nil {
Expand Down Expand Up @@ -163,7 +163,7 @@ func (f *IntSliceFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour
return err
}
if value != nil {
var sliceValue cli.IntSlice = value
var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...))
eachName(f.Name, func(name string) {
underlyingFlag := f.set.Lookup(f.Name)
if underlyingFlag != nil {
Expand Down
4 changes: 2 additions & 2 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ func TestApp_ParseSliceFlags(t *testing.T) {
command := Command{
Name: "cmd",
Flags: []Flag{
IntSliceFlag{Name: "p", Value: &IntSlice{}, Usage: "set one or more ip addr"},
StringSliceFlag{Name: "ip", Value: &StringSlice{}, Usage: "set one or more ports to open"},
IntSliceFlag{Name: "p", Value: NewIntSlice(), Usage: "set one or more ip addr"},
StringSliceFlag{Name: "ip", Value: NewStringSlice(), Usage: "set one or more ports to open"},
},
Action: func(c *Context) error {
parsedIntSlice = c.IntSlice("p")
Expand Down
75 changes: 75 additions & 0 deletions cli-migrate-slice-types
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python
from __future__ import print_function, unicode_literals

import argparse
import os
import re
import sys


DESCRIPTION = """\
Migrate arbitrary `.go` sources from the pre-v2.0.0 API for StringSlice and
IntSlice types, optionally writing the changes back to file.
"""
SLICE_TYPE_RE = re.compile(
'&cli\\.(?P<type>IntSlice|StringSlice){(?P<args>[^}]*)}',
flags=re.DOTALL
)


def main(sysargs=sys.argv[:]):
parser = argparse.ArgumentParser(
description=DESCRIPTION,
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('basedir', nargs='?', metavar='BASEDIR',
type=os.path.abspath, default=os.getcwd())
parser.add_argument('-w', '--write', help='write changes back to file',
action='store_true', default=False)

args = parser.parse_args(sysargs[1:])

for filepath in _find_candidate_files(args.basedir):
updated_source = _update_source(filepath)
if args.write:
print('Updating {!r}'.format(filepath))

with open(filepath, 'w') as outfile:
outfile.write(updated_source)
else:
print('// -> Updated {!r}'.format(filepath))
print(updated_source)

return 0


def _update_source(filepath):
with open(filepath) as infile:
source = infile.read()
return SLICE_TYPE_RE.sub(_slice_type_repl, source)


def _slice_type_repl(match):
return 'cli.New{}({})'.format(
match.groupdict()['type'], match.groupdict()['args']
)


def _find_candidate_files(basedir):
for curdir, dirs, files in os.walk(basedir):
for i, dirname in enumerate(dirs[:]):
if dirname.startswith('.'):
dirs.pop(i)

for filename in files:
if not filename.endswith('.go'):
continue

filepath = os.path.join(curdir, filename)
if not os.access(filepath, os.R_OK | os.W_OK):
continue

yield filepath


if __name__ == '__main__':
sys.exit(main())
3 changes: 2 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ func lookupBoolT(name string, set *flag.FlagSet) bool {

func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) {
switch ff.Value.(type) {
case *StringSlice:
case Serializeder:
set.Set(name, ff.Value.(Serializeder).Serialized())
default:
set.Set(name, ff.Value.String())
}
Expand Down
116 changes: 94 additions & 22 deletions flag.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cli

import (
"encoding/json"
"flag"
"fmt"
"os"
Expand All @@ -13,6 +14,8 @@ import (

const defaultPlaceholder = "value"

var slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano())

// This flag enables bash-completion for all commands and subcommands
var BashCompletionFlag = BoolFlag{
Name: "generate-bash-completion",
Expand All @@ -35,6 +38,11 @@ var HelpFlag = BoolFlag{

var FlagStringer FlagStringFunc = stringifyFlag

// Serializeder is used to circumvent the limitations of flag.FlagSet.Set
type Serializeder interface {
Serialized() string
}

// Flag is a common interface related to parsing flags in cli.
// For more advanced flag parsing techniques, it is recommended that
// this interface be implemented.
Expand Down Expand Up @@ -107,26 +115,52 @@ func (f GenericFlag) GetName() string {
return f.Name
}

// StringSlice is an opaque type for []string to satisfy flag.Value
type StringSlice []string
// StringSlice wraps a []string to satisfy flag.Value
type StringSlice struct {
slice []string
hasBeenSet bool
}

// NewStringSlice creates a *StringSlice with default values
func NewStringSlice(defaults ...string) *StringSlice {
return &StringSlice{slice: append([]string{}, defaults...)}
}

// Set appends the string value to the list of values
func (f *StringSlice) Set(value string) error {
*f = append(*f, value)
if !f.hasBeenSet {
f.slice = []string{}
f.hasBeenSet = true
}

if strings.HasPrefix(value, slPfx) {
// Deserializing assumes overwrite
_ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice)
f.hasBeenSet = true
return nil
}

f.slice = append(f.slice, value)
return nil
}

// String returns a readable representation of this value (for usage defaults)
func (f *StringSlice) String() string {
return fmt.Sprintf("%s", *f)
return fmt.Sprintf("%s", f.slice)
}

// Serialized allows StringSlice to fulfill Serializeder
func (f *StringSlice) Serialized() string {
jsonBytes, _ := json.Marshal(f.slice)
return fmt.Sprintf("%s%s", slPfx, string(jsonBytes))
}

// Value returns the slice of strings set by this flag
func (f *StringSlice) Value() []string {
return *f
return f.slice
}

// StringSlice is a string flag that can be specified multiple times on the
// StringSliceFlag is a string flag that can be specified multiple times on the
// command-line
type StringSliceFlag struct {
Name string
Expand All @@ -147,7 +181,7 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &StringSlice{}
newVal := NewStringSlice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
newVal.Set(s)
Expand All @@ -158,10 +192,11 @@ func (f StringSliceFlag) Apply(set *flag.FlagSet) {
}
}

if f.Value == nil {
f.Value = NewStringSlice()
}

eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &StringSlice{}
}
set.Var(f.Value, name, f.Usage)
})
}
Expand All @@ -170,28 +205,64 @@ func (f StringSliceFlag) GetName() string {
return f.Name
}

// StringSlice is an opaque type for []int to satisfy flag.Value
type IntSlice []int
// IntSlice wraps an []int to satisfy flag.Value
type IntSlice struct {
slice []int
hasBeenSet bool
}

// NewIntSlice makes an *IntSlice with default values
func NewIntSlice(defaults ...int) *IntSlice {
return &IntSlice{slice: append([]int{}, defaults...)}
}

// SetInt directly adds an integer to the list of values
func (i *IntSlice) SetInt(value int) {
if !i.hasBeenSet {
i.slice = []int{}
i.hasBeenSet = true
}

i.slice = append(i.slice, value)
}

// Set parses the value into an integer and appends it to the list of values
func (f *IntSlice) Set(value string) error {
func (i *IntSlice) Set(value string) error {
if !i.hasBeenSet {
i.slice = []int{}
i.hasBeenSet = true
}

if strings.HasPrefix(value, slPfx) {
// Deserializing assumes overwrite
_ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice)
i.hasBeenSet = true
return nil
}

tmp, err := strconv.Atoi(value)
if err != nil {
return err
} else {
*f = append(*f, tmp)
i.slice = append(i.slice, tmp)
}
return nil
}

// String returns a readable representation of this value (for usage defaults)
func (f *IntSlice) String() string {
return fmt.Sprintf("%d", *f)
func (i *IntSlice) String() string {
return fmt.Sprintf("%v", i.slice)
}

// Serialized allows IntSlice to fulfill Serializeder
func (i *IntSlice) Serialized() string {
jsonBytes, _ := json.Marshal(i.slice)
return fmt.Sprintf("%s%s", slPfx, string(jsonBytes))
}

// Value returns the slice of ints set by this flag
func (f *IntSlice) Value() []int {
return *f
func (i *IntSlice) Value() []int {
return i.slice
}

// IntSliceFlag is an int flag that can be specified multiple times on the
Expand All @@ -215,7 +286,7 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) {
for _, envVar := range strings.Split(f.EnvVar, ",") {
envVar = strings.TrimSpace(envVar)
if envVal := os.Getenv(envVar); envVal != "" {
newVal := &IntSlice{}
newVal := NewIntSlice()
for _, s := range strings.Split(envVal, ",") {
s = strings.TrimSpace(s)
err := newVal.Set(s)
Expand All @@ -229,10 +300,11 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) {
}
}

if f.Value == nil {
f.Value = NewIntSlice()
}

eachName(f.Name, func(name string) {
if f.Value == nil {
f.Value = &IntSlice{}
}
set.Var(f.Value, name, f.Usage)
})
}
Expand Down
Loading

0 comments on commit eb8680b

Please sign in to comment.