Skip to content

Commit 5fe469e

Browse files
committed
feat(file-ignore): add Strings option type
- first step toward implementing file ignore feat(file-ignore): implement file ignore - add flags for providing a gitignore and/or list of files to ignore - construct a filter to be passed to a SerialFile feat(file-ignore): use go-ipfs-files fork; use renamed constructor feat(file-ignore): test case w. ignore rules; refactor parseArgs feat(file-ignore): ignore-rule opt is open-ended - make ignore-rules options accept a path to a file - add test case for ignore rules file feat(file-ignore): help-text for variardic opts - show StringsOption as variardic option in help-text - add test case for variardic option feat(file-ignore): rename rulesfile opt; fix typos feat(file-ignore): temp replace of go-ipfs-files - temporarily using go-ipfs-files fork until go-ipfs-files#26 is merged - trying to get ci/cd builds working - will revert before merging feat(file-ignore): refactor cli/parse.go#setOpts feat(file-ignore): cleanup parse.go; add test-case feat(file-ignore): update ignore option copytext feat(file-ignore): check opt against `optDef.Type` - revert exclusion of options with `Strings` type, so those option values are typechecked - add command test cases feat(file-ignore): add test-case w. hidden file feat(file-ignore): build against go-ipfs-files@latest feat(file-ignore): update go.mod/go.sum
1 parent 0c2a21b commit 5fe469e

File tree

10 files changed

+586
-19
lines changed

10 files changed

+586
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ cover.html
66
examples/adder/local/local
77
examples/adder/remote/client/client
88
examples/adder/remote/server/server
9+
coverage.out

cli/helptext.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,12 @@ func generateSynopsis(width int, cmd *cmds.Command, path string) string {
301301
}
302302
}
303303
}
304-
appendText("[" + sopt + "]")
304+
305+
if opt.Type() == cmds.Strings {
306+
appendText("[" + sopt + "]...")
307+
} else {
308+
appendText("[" + sopt + "]")
309+
}
305310
}
306311
if len(cmd.Arguments) > 0 {
307312
appendText("[--]")

cli/helptext_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func TestSynopsisGenerator(t *testing.T) {
1515
},
1616
Options: []cmds.Option{
1717
cmds.StringOption("opt", "o", "Option"),
18+
cmds.StringsOption("var-opt", "Variadic Option"),
1819
},
1920
Helptext: cmds.HelpText{
2021
SynopsisOptionsValues: map[string]string{
@@ -31,6 +32,9 @@ func TestSynopsisGenerator(t *testing.T) {
3132
if !strings.Contains(syn, "[--opt=<OPTION> | -o]") {
3233
t.Fatal("Synopsis should contain option descriptor")
3334
}
35+
if !strings.Contains(syn, "[--var-opt=<var-opt>]...") {
36+
t.Fatal("Synopsis should contain option descriptor")
37+
}
3438
if !strings.Contains(syn, "<required>") {
3539
t.Fatal("Synopsis should contain required argument")
3640
}

cli/parse.go

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"path"
1010
"path/filepath"
11+
"reflect"
1112
"strings"
1213

1314
osh "github.com/Kubuxu/go-os-helper"
@@ -67,6 +68,16 @@ func isRecursive(req *cmds.Request) bool {
6768
return rec && ok
6869
}
6970

71+
func getIgnoreRulesFile(req *cmds.Request) string {
72+
rulesFile, _ := req.Options[cmds.IgnoreRules].(string)
73+
return rulesFile
74+
}
75+
76+
func getIgnoreRules(req *cmds.Request) []string {
77+
rules, _ := req.Options[cmds.Ignore].([]string)
78+
return rules
79+
}
80+
7081
func stdinName(req *cmds.Request) string {
7182
name, _ := req.Options[cmds.StdinName].(string)
7283
return name
@@ -85,6 +96,19 @@ func (st *parseState) peek() string {
8596
return st.cmdline[st.i]
8697
}
8798

99+
func setOpts(kv kv, kvType reflect.Kind, opts cmds.OptMap) error {
100+
101+
if kvType == cmds.Strings {
102+
res, _ := opts[kv.Key].([]string)
103+
opts[kv.Key] = append(res, kv.Value.(string))
104+
} else if _, exists := opts[kv.Key]; !exists {
105+
opts[kv.Key] = kv.Value
106+
} else {
107+
return fmt.Errorf("multiple values for option %q", kv.Key)
108+
}
109+
return nil
110+
}
111+
88112
func parse(req *cmds.Request, cmdline []string, root *cmds.Command) (err error) {
89113
var (
90114
path = make([]string, 0, len(cmdline))
@@ -116,13 +140,13 @@ L:
116140
if err != nil {
117141
return err
118142
}
119-
120-
if _, exists := opts[k]; exists {
121-
return fmt.Errorf("multiple values for option %q", k)
143+
kvType, err := getOptType(k, optDefs)
144+
if err != nil {
145+
return err // shouldn't happen b/c k,v was parsed from optsDef
146+
}
147+
if err := setOpts(kv{Key: k, Value: v}, kvType, opts); err != nil {
148+
return err
122149
}
123-
124-
k = optDefs[k].Name()
125-
opts[k] = v
126150

127151
case strings.HasPrefix(param, "-") && param != "-":
128152
// short options
@@ -134,11 +158,13 @@ L:
134158
for _, kv := range kvs {
135159
kv.Key = optDefs[kv.Key].Names()[0]
136160

137-
if _, exists := opts[kv.Key]; exists {
138-
return fmt.Errorf("multiple values for option %q", kv.Key)
161+
kvType, err := getOptType(kv.Key, optDefs)
162+
if err != nil {
163+
return err // shouldn't happen b/c kvs was parsed from optsDef
164+
}
165+
if err := setOpts(kv, kvType, opts); err != nil {
166+
return err
139167
}
140-
141-
opts[kv.Key] = kv.Value
142168
}
143169
default:
144170
arg := param
@@ -294,8 +320,13 @@ func parseArgs(req *cmds.Request, root *cmds.Command, stdin *os.File) error {
294320
return err
295321
}
296322
}
297-
298-
nf, err := appendFile(fpath, argDef, isRecursive(req), isHidden(req))
323+
rulesFile := getIgnoreRulesFile(req)
324+
ignoreRules := getIgnoreRules(req)
325+
filter, err := files.NewFilter(rulesFile, ignoreRules, isHidden(req))
326+
if err != nil {
327+
return err
328+
}
329+
nf, err := appendFile(fpath, argDef, isRecursive(req), filter)
299330
if err != nil {
300331
return err
301332
}
@@ -497,7 +528,7 @@ func getArgDef(i int, argDefs []cmds.Argument) *cmds.Argument {
497528
const notRecursiveFmtStr = "'%s' is a directory, use the '-%s' flag to specify directories"
498529
const dirNotSupportedFmtStr = "invalid path '%s', argument '%s' does not support directories"
499530

500-
func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (files.Node, error) {
531+
func appendFile(fpath string, argDef *cmds.Argument, recursive bool, filter *files.Filter) (files.Node, error) {
501532
stat, err := os.Lstat(fpath)
502533
if err != nil {
503534
return nil, err
@@ -521,8 +552,7 @@ func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (fi
521552

522553
return files.NewReaderFile(file), nil
523554
}
524-
525-
return files.NewSerialFile(fpath, hidden, stat)
555+
return files.NewSerialFileWithFilter(fpath, filter, stat)
526556
}
527557

528558
// Inform the user if a file is waiting on input
@@ -574,3 +604,10 @@ func (r *messageReader) Read(b []byte) (int, error) {
574604
func (r *messageReader) Close() error {
575605
return r.r.Close()
576606
}
607+
608+
func getOptType(k string, optDefs map[string]cmds.Option) (reflect.Kind, error) {
609+
if opt, ok := optDefs[k]; ok {
610+
return opt.Type(), nil
611+
}
612+
return reflect.Invalid, fmt.Errorf("unknown option %q", k)
613+
}

cli/parse_test.go

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package cli
33
import (
44
"context"
55
"fmt"
6+
"github.com/ipfs/go-ipfs-files"
67
"io"
78
"io/ioutil"
89
"net/url"
910
"os"
11+
"path"
1012
"strings"
1113
"testing"
1214

@@ -33,7 +35,14 @@ func sameKVs(a kvs, b kvs) bool {
3335
return false
3436
}
3537
for k, v := range a {
36-
if v != b[k] {
38+
if ks, ok := v.([]string); ok {
39+
bks, _ := b[k].([]string)
40+
for i := 0; i < len(ks); i++ {
41+
if ks[i] != bks[i] {
42+
return false
43+
}
44+
}
45+
} else if v != b[k] {
3746
return false
3847
}
3948
}
@@ -72,6 +81,7 @@ func TestOptionParsing(t *testing.T) {
7281
Options: []cmds.Option{
7382
cmds.StringOption("string", "s", "a string"),
7483
cmds.BoolOption("bool", "b", "a bool"),
84+
cmds.StringsOption("strings", "r", "strings array"),
7585
},
7686
Subcommands: map[string]*cmds.Command{
7787
"test": &cmds.Command{},
@@ -141,6 +151,7 @@ func TestOptionParsing(t *testing.T) {
141151
test("-b test false", kvs{"bool": true}, words{"false"})
142152
test("-b --string foo test bar", kvs{"bool": true, "string": "foo"}, words{"bar"})
143153
test("-b=false --string bar", kvs{"bool": false, "string": "bar"}, words{})
154+
test("--strings a --strings b", kvs{"strings": []string{"a", "b"}}, words{})
144155
testFail("foo test")
145156
test("defaults", kvs{"opt": "def"}, words{})
146157
test("defaults -o foo", kvs{"opt": "foo"}, words{})
@@ -552,3 +563,149 @@ func Test_urlBase(t *testing.T) {
552563
}
553564
}
554565
}
566+
567+
func TestFileArgs(t *testing.T) {
568+
rootCmd := &cmds.Command{
569+
Subcommands: map[string]*cmds.Command{
570+
"fileOp": {
571+
Arguments: []cmds.Argument{
572+
cmds.FileArg("path", true, true, "The path to the file to be operated upon.").EnableRecursive().EnableStdin(),
573+
},
574+
Options: []cmds.Option{
575+
cmds.OptionRecursivePath, // a builtin option that allows recursive paths (-r, --recursive)
576+
cmds.OptionHidden,
577+
cmds.OptionIgnoreRules,
578+
cmds.OptionIgnore,
579+
},
580+
},
581+
},
582+
}
583+
mkTempFile := func(t *testing.T, dir, pattern, content string) *os.File {
584+
pat := "test_tmpFile_"
585+
if pattern != "" {
586+
pat = pattern
587+
}
588+
tmpFile, err := ioutil.TempFile(dir, pat)
589+
if err != nil {
590+
t.Fatal(err)
591+
}
592+
593+
if _, err := io.WriteString(tmpFile, content); err != nil {
594+
t.Fatal(err)
595+
}
596+
return tmpFile
597+
}
598+
tmpDir1, err := ioutil.TempDir("", "parsetest_fileargs_tmpdir_")
599+
if err != nil {
600+
t.Fatal(err)
601+
}
602+
tmpDir2, err := ioutil.TempDir("", "parsetest_utildir_")
603+
if err != nil {
604+
t.Fatal(err)
605+
}
606+
tmpFile1 := mkTempFile(t, "", "", "test1")
607+
tmpFile2 := mkTempFile(t, tmpDir1, "", "toBeIgnored")
608+
tmpFile3 := mkTempFile(t, tmpDir1, "", "test3")
609+
ignoreFile := mkTempFile(t, tmpDir2, "", path.Base(tmpFile2.Name()))
610+
tmpHiddenFile := mkTempFile(t, tmpDir1, ".test_hidden_file_*", "test")
611+
defer func() {
612+
for _, f := range []string{
613+
tmpDir1,
614+
tmpFile1.Name(),
615+
tmpFile2.Name(),
616+
tmpHiddenFile.Name(),
617+
tmpFile3.Name(),
618+
ignoreFile.Name(),
619+
tmpDir2,
620+
} {
621+
os.Remove(f)
622+
}
623+
}()
624+
var testCases = []struct {
625+
cmd words
626+
f *os.File
627+
args words
628+
parseErr error
629+
}{
630+
{
631+
cmd: words{"fileOp"},
632+
args: nil,
633+
parseErr: fmt.Errorf("argument %q is required", "path"),
634+
},
635+
{
636+
cmd: words{"fileOp", "--ignore", path.Base(tmpFile2.Name()), tmpDir1, tmpFile1.Name()}, f: nil,
637+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name()},
638+
parseErr: fmt.Errorf(notRecursiveFmtStr, tmpDir1, "r"),
639+
}, {
640+
cmd: words{"fileOp", tmpFile1.Name(), "--ignore", path.Base(tmpFile2.Name()), "--ignore"}, f: nil,
641+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name()},
642+
parseErr: fmt.Errorf("missing argument for option %q", "ignore"),
643+
},
644+
{
645+
cmd: words{"fileOp", "-r", "--ignore", path.Base(tmpFile2.Name()), tmpDir1, tmpFile1.Name()}, f: nil,
646+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name()},
647+
parseErr: nil,
648+
},
649+
{
650+
cmd: words{"fileOp", "--hidden", "-r", "--ignore", path.Base(tmpFile2.Name()), tmpDir1, tmpFile1.Name()}, f: nil,
651+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name(), tmpHiddenFile.Name()},
652+
parseErr: nil,
653+
},
654+
{
655+
cmd: words{"fileOp", "-r", "--ignore", path.Base(tmpFile2.Name()), tmpDir1, tmpFile1.Name(), "--ignore", "anotherRule"}, f: nil,
656+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name()},
657+
parseErr: nil,
658+
},
659+
{
660+
cmd: words{"fileOp", "-r", "--ignore-rules-path", ignoreFile.Name(), tmpDir1, tmpFile1.Name()}, f: nil,
661+
args: words{tmpDir1, tmpFile1.Name(), tmpFile3.Name()},
662+
parseErr: nil,
663+
},
664+
}
665+
666+
for _, tc := range testCases {
667+
req, err := Parse(context.Background(), tc.cmd, tc.f, rootCmd)
668+
if err == nil {
669+
err = req.Command.CheckArguments(req)
670+
}
671+
if !errEq(err, tc.parseErr) {
672+
t.Fatalf("parsing request for cmd %q: expected error %q, got %q", tc.cmd, tc.parseErr, err)
673+
}
674+
if err != nil {
675+
continue
676+
}
677+
678+
if len(tc.args) == 0 {
679+
continue
680+
}
681+
expectedFileMap := make(map[string]bool)
682+
for _, arg := range tc.args {
683+
expectedFileMap[path.Base(arg)] = false
684+
}
685+
it := req.Files.Entries()
686+
for it.Next() {
687+
name := it.Name()
688+
if _, ok := expectedFileMap[name]; ok {
689+
expectedFileMap[name] = true
690+
} else {
691+
t.Errorf("found unexpected file %q in request %v", name, req)
692+
}
693+
file := it.Node()
694+
files.Walk(file, func(fpath string, nd files.Node) error {
695+
if fpath != "" {
696+
if _, ok := expectedFileMap[fpath]; ok {
697+
expectedFileMap[fpath] = true
698+
} else {
699+
t.Errorf("found unexpected file %q in request file arguments", fpath)
700+
}
701+
}
702+
return nil
703+
})
704+
}
705+
for p, found := range expectedFileMap {
706+
if !found {
707+
t.Errorf("failed to find expected path %q in req %v", p, req)
708+
}
709+
}
710+
}
711+
}

command_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ func TestOptionValidation(t *testing.T) {
1717
Options: []Option{
1818
IntOption("b", "beep", "enables beeper"),
1919
StringOption("B", "boop", "password for booper"),
20+
StringsOption("S", "shoop", "what to shoop"),
2021
},
2122
Run: noop,
2223
}
@@ -57,6 +58,10 @@ func TestOptionValidation(t *testing.T) {
5758
{opts: map[string]interface{}{"foo": 5}},
5859
{opts: map[string]interface{}{EncLong: "json"}},
5960
{opts: map[string]interface{}{"beep": "100"}},
61+
{opts: map[string]interface{}{"S": [2]string{"a", "b"}}},
62+
{
63+
opts: map[string]interface{}{"S": true},
64+
NewRequestError: `Option "S" should be type "array", but got type "bool"`},
6065
{
6166
opts: map[string]interface{}{"beep": ":)"},
6267
NewRequestError: `Could not convert value ":)" to type "int" (for option "-beep")`,

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ go 1.14
44

55
require (
66
github.com/Kubuxu/go-os-helper v0.0.1
7-
github.com/ipfs/go-ipfs-files v0.0.6
7+
github.com/ipfs/go-ipfs-files v0.0.7
88
github.com/ipfs/go-log v0.0.1
99
github.com/rs/cors v1.7.0
1010
github.com/texttheater/golang-levenshtein v0.0.0-20180516184445-d188e65d659e
11-
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f
11+
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d
1212
)

0 commit comments

Comments
 (0)