From 9b7584207fddd1a9b90f76c2ce678249ae411b18 Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Sat, 17 Dec 2016 22:19:56 -0800 Subject: [PATCH] structured/sectioned .driverc configuration Fixes https://github.com/odeke-em/drive/issues/778. Now allows you to restructure your .driverc separating commands into sections and having different clauses. Please note: * You can put multiple commands in the same section by using separator "/". See the rules at https://github.com/odeke-em/namespace/blob/0ab79ba44f1328b1ec75ea985ad5c338ba3d56a6/ns.go#L3-L14 * Overwriting of command rules is command_section > global and commandX_higher_line > commandX_lower_line where lines are natural numbers that are ascending * Exhibits: This is a sample of a .driverc that I've just defined that has flexible behavior in different sections. ``` id=true # Will be applied globally unless overwritten in a command [global] depth=10 # The depth to use if not defined [pull/list] depth=2 # The depth to be used for pull or list [push] verbose=true # verbose is only true for push [list] long=true # Allow long output for list ``` And see the tests in src/rc_test.go that lock in the promised behavior. --- README.md | 17 +++- cmd/drive/main.go | 24 +++-- src/misc.go | 2 + src/rc.go | 107 +++++++++++++++------ src/rc_test.go | 142 ++++++++++++++++++++++++++++ src/testdata/non-sectioned/.driverc | 5 + src/testdata/structured/.driverc | 17 ++++ 7 files changed, 278 insertions(+), 36 deletions(-) create mode 100644 src/rc_test.go create mode 100644 src/testdata/non-sectioned/.driverc create mode 100644 src/testdata/structured/.driverc diff --git a/README.md b/README.md index f1db00c0..610a00a9 100644 --- a/README.md +++ b/README.md @@ -1154,7 +1154,8 @@ drive -h drive push -h ``` -and the value is the argument that you'd ordinarily supply on the commandline +and the value is the argument that you'd ordinarily supply on the commandline. +.driverc configurations can be optionally grouped in sections. See https://github.com/odeke-em/drive/issues/778. For example: @@ -1164,6 +1165,20 @@ cat << ! >> ~/.driverc > exports=doc,pdf > depth=100 > no-prompt=true +> +> # For pushes +> [list] +> depth=2 +> long=true +> +> [push] +> verbose=false +> +> [stat] +> depth=3 +> +> [pull/push] +> no-clobber=true > ! cat << ! >> ~/emm.odeke-drive/.driverc diff --git a/cmd/drive/main.go b/cmd/drive/main.go index 070d4a49..5d12dd70 100644 --- a/cmd/drive/main.go +++ b/cmd/drive/main.go @@ -59,6 +59,7 @@ func translateKeyChecks(definedFlags map[string]*flag.Flag) map[string]bool { } type defaultsFiller struct { + command string from, to interface{} rcSourcePath string definedFlags map[string]*flag.Flag @@ -66,7 +67,7 @@ type defaultsFiller struct { func fillWithDefaults(df defaultsFiller) error { alreadyDefined := translateKeyChecks(df.definedFlags) - jsonStringified, err := drive.JSONStringifySiftedCLITags(df.from, df.rcSourcePath, alreadyDefined) + jsonStringified, err := drive.JSONStringifySiftedCLITags(df.from, df.rcSourcePath, alreadyDefined, df.command) if err != nil { return err @@ -352,7 +353,8 @@ func (lCmd *listCmd) _run(args []string, definedFlags map[string]*flag.Flag, dis sources, context, path := preprocessArgsByToggle(args, (*lCmd.ById || *lCmd.Matches)) cmd := listCmd{} df := defaultsFiller{ - from: *lCmd, to: &cmd, + command: "list", + from: *lCmd, to: &cmd, rcSourcePath: context.AbsPathOf(path), definedFlags: definedFlags, } @@ -690,7 +692,8 @@ func (pCmd *pullCmd) Run(args []string, definedFlags map[string]*flag.Flag) { sources, context, path := preprocessArgsByToggle(args, (*pCmd.ById || *pCmd.Matches || *pCmd.Starred)) cmd := pullCmd{} df := defaultsFiller{ - from: *pCmd, to: &cmd, + command: "pull", + from: *pCmd, to: &cmd, rcSourcePath: context.AbsPathOf(path), definedFlags: definedFlags, } @@ -889,7 +892,8 @@ func (qCmd *qrLinkCmd) Run(args []string, definedFlags map[string]*flag.Flag) { cmd := qrLinkCmd{} df := defaultsFiller{ - from: *qCmd, to: &cmd, + command: "qr", + from: *qCmd, to: &cmd, rcSourcePath: path, definedFlags: definedFlags, } @@ -986,7 +990,8 @@ func exitIfIllogicalFileAndFolder(mask int) { func (pCmd *pushCmd) createPushOptions(absEntryPath string, definedFlags map[string]*flag.Flag) (*drive.Options, error) { cmd := pushCmd{} df := defaultsFiller{ - from: *pCmd, to: &cmd, + command: "push", + from: *pCmd, to: &cmd, rcSourcePath: absEntryPath, definedFlags: definedFlags, } @@ -1226,7 +1231,8 @@ func (uCmd *unpublishCmd) Run(args []string, definedFlags map[string]*flag.Flag) cmd := unpublishCmd{} df := defaultsFiller{ - from: *uCmd, to: &cmd, + command: "unpublish", + from: *uCmd, to: &cmd, rcSourcePath: context.AbsPathOf(path), definedFlags: definedFlags, } @@ -1700,7 +1706,8 @@ func (icmd *idCmd) Run(args []string, definedFlags map[string]*flag.Flag) { sources, context, path := preprocessArgs(args) cmd := idCmd{} df := defaultsFiller{ - from: *icmd, to: &cmd, + command: "id", + from: *icmd, to: &cmd, rcSourcePath: context.AbsPathOf(path), definedFlags: definedFlags, } @@ -1741,7 +1748,8 @@ func (ccmd *clashesCmd) Run(args []string, definedFlags map[string]*flag.Flag) { sources, context, path := preprocessArgsByToggle(args, *ccmd.ById) cmd := clashesCmd{} df := defaultsFiller{ - from: *ccmd, to: &cmd, + command: "clashes", + from: *ccmd, to: &cmd, rcSourcePath: context.AbsPathOf(path), definedFlags: definedFlags, } diff --git a/src/misc.go b/src/misc.go index b730b1fb..577ae926 100644 --- a/src/misc.go +++ b/src/misc.go @@ -911,6 +911,8 @@ func SiftCliTags(cs *CliSifter) string { switch elem.Kind() { case reflect.String: stringified = fmt.Sprintf("%q", elem) + case reflect.Invalid: + continue default: stringified = fmt.Sprintf("%v", elem.Interface()) } diff --git a/src/rc.go b/src/rc.go index efa3044e..c874342c 100644 --- a/src/rc.go +++ b/src/rc.go @@ -20,6 +20,8 @@ import ( "os" "path" "strings" + + "github.com/odeke-em/namespace" ) var ( @@ -36,47 +38,70 @@ var ( FsHomeDir = os.Getenv(HomeEnvKey) ) -func kvifyCommentedFile(p, comment string) (kvMap map[string]string, err error) { - var clauses []string - kvMap = make(map[string]string) - clauses, err = readCommentedFile(p, comment) +func kvifyCommentedFile(p, comment string) (map[string]map[string]string, error) { + clauses, err := readCommentedFile(p, comment) if err != nil { - return + return nil, err } - for i, clause := range clauses { - kvf, kvErr := splitAndStrip(clause, true) - if kvErr != nil { - err = kvErr - return + linesChan := make(chan string) + go func() { + defer close(linesChan) + for _, clause := range clauses { + linesChan <- clause } + }() - value, ok := kvf.value.(string) - if !ok { - err = fmt.Errorf("clause %d: expected a string instead got %v", i, kvf.value) - return - } + ns, err := namespace.ParseCh(linesChan) + if err != nil { + return nil, err + } + + gkvMap := make(map[string]map[string]string) + for key, clauses := range ns { + kvMap := make(map[string]string) + for i, clause := range clauses { + kvf, kvErr := splitAndStrip(clause, true) + if kvErr != nil { + return nil, kvErr + } - kvMap[kvf.key] = value + value, ok := kvf.value.(string) + if !ok { + err = fmt.Errorf("clause %d: expected a string instead got %v", i, kvf.value) + return nil, err + } + + kvMap[kvf.key] = value + } + gkvMap[key] = kvMap } - return + return gkvMap, nil } -func ResourceMappings(rcPath string) (parsed map[string]interface{}, err error) { +func ResourceMappings(rcPath string) (map[string]map[string]interface{}, error) { beginOpts := Options{Path: rcPath} rcPath, rcErr := beginOpts.rcPath() if rcErr != nil { - err = rcErr - return + return nil, rcErr } - rcMap, rErr := kvifyCommentedFile(rcPath, CommentStr) + nsRCMap, rErr := kvifyCommentedFile(rcPath, CommentStr) if rErr != nil { - err = rErr - return + return nil, rErr } - return parseRCValues(rcMap) + + grouped := make(map[string]map[string]interface{}) + for key, ns := range nsRCMap { + parsed, err := parseRCValues(ns) + if err != nil { + return nil, err + } + grouped[key] = parsed + } + + return grouped, nil } func parseRCValues(rcMap map[string]string) (valueMappings map[string]interface{}, err error) { @@ -105,7 +130,7 @@ func parseRCValues(rcMap map[string]string) (valueMappings map[string]interface{ CLIOptionUnified, CLIOptionDiffBaseLocal, ExportsKey, ExcludeOpsKey, CLIOptionUnifiedShortKey, CLIOptionNotOwner, ExportsDirKey, CLIOptionExactTitle, AddressKey, - CLIEncryptionPassword, CLIDecryptionPassword, + CLIEncryptionPassword, CLIDecryptionPassword, SortKey, }, }, { @@ -197,7 +222,7 @@ func splitAndStrip(line string, resolveFromEnv bool) (kv keyValue, err error) { return } -func JSONStringifySiftedCLITags(from interface{}, rcSourcePath string, defined map[string]bool) (string, error) { +func JSONStringifySiftedCLITags(from interface{}, rcSourcePath string, defined map[string]bool, relevantNamespaces ...string) (string, error) { rcMappings, err := ResourceMappings(rcSourcePath) if err != nil && !NotExist(err) { return "", err @@ -205,9 +230,37 @@ func JSONStringifySiftedCLITags(from interface{}, rcSourcePath string, defined m cs := CliSifter{ From: from, - Defaults: rcMappings, + Defaults: mergeNamespaces(rcMappings, relevantNamespaces...), AlreadyDefined: defined, } return SiftCliTags(&cs), nil } + +func copyAndOverWriteNs(from, to map[string]interface{}) { + for fK, fV := range from { + to[fK] = fV + } +} + +func mergeNamespaces(ns map[string]map[string]interface{}, relevantKeys ...string) map[string]interface{} { + // Start with the global namespace and proceed overwriting from specific keys + var combinedNs map[string]interface{} = ns[namespace.GlobalNamespaceKey] + if combinedNs == nil { + combinedNs = make(map[string]interface{}) + } + + for _, key := range relevantKeys { + kNs := ns[key] + if len(kNs) < 1 { + // TODO: Decide if not setting anything in the namespace + // means exclude it entirely hence make combinedNs nil? + continue + } + + // Now merge them: from -> to + copyAndOverWriteNs(kNs, combinedNs) + } + + return combinedNs +} diff --git a/src/rc_test.go b/src/rc_test.go new file mode 100644 index 00000000..58055e74 --- /dev/null +++ b/src/rc_test.go @@ -0,0 +1,142 @@ +package drive_test + +import ( + "bytes" + "encoding/json" + "testing" + + drive "github.com/odeke-em/drive/src" +) + +func TestStructuredRC(t *testing.T) { + tests := [...]struct { + rcDir string + want map[string]map[string]interface{} + wantErr bool + }{ + 0: { + rcDir: "./testdata/structured", + want: map[string]map[string]interface{}{ + "global": {"depth": 10, "verbose": false}, + "list": {"long": true}, + "push": {"no-prompt": true}, + "pull": {"depth": 3, "no-prompt": false, "verbose": true}, + }, + }, + } + + for i, tt := range tests { + rcMap, err := drive.ResourceMappings(tt.rcDir) + + if tt.wantErr { + if err == nil { + t.Errorf("#%d: err=nil", i) + } + continue + } + + if err != nil { + t.Errorf("#%d: err=%v", i, err) + continue + } + + // Not going to use reflect.DeepEqual because + // we've have trouble comparing any []string + gotBlob, _ := json.MarshalIndent(rcMap, "", " ") + wantBlob, _ := json.MarshalIndent(tt.want, "", " ") + + if !bytes.Equal(gotBlob, wantBlob) { + t.Errorf("#%d:\n\thave: %s\n\twant: %s", i, gotBlob, wantBlob) + } + } +} + +type cliDefinition struct { + Depth *int `json:"depth"` + NoPrompt *bool `json:"no-prompt"` + Verbose *bool `json:"verbose"` + Long *bool `json:"long"` + Sort *string `json:"sort"` +} + +func TestStructuredRCJSONSifting(t *testing.T) { + tests := [...]struct { + rcDir string + wantErr bool + relevantKeys []string + want map[string]interface{} + }{ + 0: { + rcDir: "./testdata/structured", + relevantKeys: []string{"push"}, + want: map[string]interface{}{ + "depth": 10, "verbose": false, "no-prompt": true, + }, + }, + + 1: { + rcDir: "./testdata/structured", + relevantKeys: []string{"pull"}, + want: map[string]interface{}{ + "depth": 3, "verbose": true, "no-prompt": false, + }, + }, + + // Since we aren't passing in any keys to get from, + // make sure we can still read from the global scope + 2: { + rcDir: "./testdata/structured", + relevantKeys: nil, + want: map[string]interface{}{ + "depth": 10, + "verbose": false, + }, + }, + + // Ensure that we can read from the + // old style, non-sectioned .driverc file. + 3: { + rcDir: "./testdata/non-sectioned", + relevantKeys: nil, + want: map[string]interface{}{ + "depth": 10, + "verbose": true, + "long": true, + "no-prompt": false, + "sort": "name,date_r", + }, + }, + } + + for i, tt := range tests { + filler := cliDefinition{} + jsonStr, err := drive.JSONStringifySiftedCLITags(filler, tt.rcDir, nil, tt.relevantKeys...) + + if tt.wantErr { + if err == nil { + t.Errorf("#%d: err=nil", i) + } + continue + } + + if err != nil { + t.Errorf("#%d: err=%v", i, err) + continue + } + + saveMap := make(map[string]interface{}) + if err := json.Unmarshal([]byte(jsonStr), &saveMap); err != nil { + t.Errorf("#%d: err=%v", i, err) + continue + } + + // Not going to use reflect.DeepEqual because + // we've have trouble comparing any []string + gotBlob, _ := json.MarshalIndent(saveMap, "", " ") + wantBlob, _ := json.MarshalIndent(tt.want, "", " ") + + if !bytes.Equal(gotBlob, wantBlob) { + t.Errorf("#%d:\n\thave: %s\n\twant: %s", i, gotBlob, wantBlob) + } + } +} diff --git a/src/testdata/non-sectioned/.driverc b/src/testdata/non-sectioned/.driverc new file mode 100644 index 00000000..8cdb4c87 --- /dev/null +++ b/src/testdata/non-sectioned/.driverc @@ -0,0 +1,5 @@ +depth=10 +verbose=true +long=true +no-prompt=false +sort=name,date_r diff --git a/src/testdata/structured/.driverc b/src/testdata/structured/.driverc new file mode 100644 index 00000000..de4712cb --- /dev/null +++ b/src/testdata/structured/.driverc @@ -0,0 +1,17 @@ +[global] +depth=10 +verbose=false + +[push/pull] +no-prompt=false + +[pull] +depth=3 +verbose=true + +[push] +# Last seen value overwrites in the same namespace +no-prompt=true + +[list] +long=true