Skip to content

Commit 4ee1c14

Browse files
ncwyuval-cloudinary
authored andcommitted
fs: fix setting stringArray config values from environment variables
After the config re-organisation, the setting of stringArray config values (eg `--exclude` set with `RCLONE_EXCLUDE`) was broken and gave a message like this for `RCLONE_EXCLUDE=*.jpg`: Failed to load "filter" default values: failed to initialise "filter" options: couldn't parse config item "exclude" = "*.jpg" as []string: parsing "*.jpg" as []string failed: invalid character '/' looking for beginning of value This was caused by the parser trying to parse the input string as a JSON value. When the config was re-organised it was thought that the internal representation of stringArray values was not important as it was never visible externally, however this turned out not to be true. A defined representation was chosen - a comma separated string and this was documented and tests were introduced in this patch. This potentially introduces a very small backwards incompatibility. In rclone v1.67.0 RCLONE_EXCLUDE=a,b Would be interpreted as --exclude "a,b" Whereas this new code will interpret it as --exclude "a" --exclude "b" The benefit of being able to set multiple values with an environment variable was deemed to outweigh the very small backwards compatibility risk. If a value with a `,` is needed, then use CSV escaping, eg RCLONE_EXCLUDE="a,b" (Note this needs to have the quotes in so at the unix shell that would be RCLONE_EXCLUDE='"a,b"' Fixes rclone#8063
1 parent d62e37b commit 4ee1c14

File tree

6 files changed

+101
-24
lines changed

6 files changed

+101
-24
lines changed

cmdtest/environment_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package cmdtest
66

77
import (
88
"os"
9+
"regexp"
910
"runtime"
11+
"strings"
1012
"testing"
1113

1214
"github.com/stretchr/testify/assert"
@@ -344,4 +346,42 @@ func TestEnvironmentVariables(t *testing.T) {
344346
env = ""
345347
out, err = rcloneEnv(env, "version", "-vv", "--use-json-log")
346348
jsonLogOK()
349+
350+
// Find all the File filter lines in out and return them
351+
parseFileFilters := func(out string) (extensions []string) {
352+
// Match: - (^|/)[^/]*\.jpg$
353+
find := regexp.MustCompile(`^- \(\^\|\/\)\[\^\/\]\*\\\.(.*?)\$$`)
354+
for _, line := range strings.Split(out, "\n") {
355+
if m := find.FindStringSubmatch(line); m != nil {
356+
extensions = append(extensions, m[1])
357+
}
358+
}
359+
return extensions
360+
}
361+
362+
// Make sure that multiple valued (stringArray) environment variables are handled properly
363+
env = ``
364+
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif", "--exclude", "*.tif")
365+
require.NoError(t, err)
366+
assert.Equal(t, []string{"gif", "tif"}, parseFileFilters(out))
367+
368+
env = `RCLONE_EXCLUDE=*.jpg`
369+
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif")
370+
require.NoError(t, err)
371+
assert.Equal(t, []string{"jpg", "gif"}, parseFileFilters(out))
372+
373+
env = `RCLONE_EXCLUDE=*.jpg,*.png`
374+
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters", "--exclude", "*.gif", "--exclude", "*.tif")
375+
require.NoError(t, err)
376+
assert.Equal(t, []string{"jpg", "png", "gif", "tif"}, parseFileFilters(out))
377+
378+
env = `RCLONE_EXCLUDE="*.jpg","*.png"`
379+
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters")
380+
require.NoError(t, err)
381+
assert.Equal(t, []string{"jpg", "png"}, parseFileFilters(out))
382+
383+
env = `RCLONE_EXCLUDE="*.,,,","*.png"`
384+
out, err = rcloneEnv(env, "version", "-vv", "--dump", "filters")
385+
require.NoError(t, err)
386+
assert.Equal(t, []string{",,,", "png"}, parseFileFilters(out))
347387
}

docs/content/docs.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2912,6 +2912,22 @@ so they take exactly the same form.
29122912

29132913
The options set by environment variables can be seen with the `-vv` flag, e.g. `rclone version -vv`.
29142914

2915+
Options that can appear multiple times (type `stringArray`) are
2916+
treated slighly differently as environment variables can only be
2917+
defined once. In order to allow a simple mechanism for adding one or
2918+
many items, the input is treated as a [CSV encoded](https://godoc.org/encoding/csv)
2919+
string. For example
2920+
2921+
| Environment Variable | Equivalent options |
2922+
|----------------------|--------------------|
2923+
| `RCLONE_EXCLUDE="*.jpg"` | `--exclude "*.jpg"` |
2924+
| `RCLONE_EXCLUDE="*.jpg,*.png"` | `--exclude "*.jpg"` `--exclude "*.png"` |
2925+
| `RCLONE_EXCLUDE='"*.jpg","*.png"'` | `--exclude "*.jpg"` `--exclude "*.png"` |
2926+
| `RCLONE_EXCLUDE='"/directory with comma , in it /**"'` | `--exclude "/directory with comma , in it /**" |
2927+
2928+
If `stringArray` options are defined as environment variables **and**
2929+
options on the command line then all the values will be used.
2930+
29152931
### Config file ###
29162932

29172933
You can set defaults for values in the config file on an individual

fs/config/configstruct/configstruct.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
package configstruct
33

44
import (
5-
"encoding/json"
5+
"encoding/csv"
66
"errors"
77
"fmt"
88
"reflect"
@@ -31,7 +31,7 @@ func camelToSnake(in string) string {
3131
//
3232
// Builtin types are expected to be encoding as their natural
3333
// stringificatons as produced by fmt.Sprint except for []string which
34-
// is expected to be encoded as JSON with empty array encoded as "".
34+
// is expected to be encoded a a CSV with empty array encoded as "".
3535
//
3636
// Any other types are expected to be encoded by their String()
3737
// methods and decoded by their `Set(s string) error` methods.
@@ -58,14 +58,18 @@ func StringToInterface(def interface{}, in string) (newValue interface{}, err er
5858
case time.Duration:
5959
newValue, err = time.ParseDuration(in)
6060
case []string:
61-
// JSON decode arrays of strings
62-
if in != "" {
63-
var out []string
64-
err = json.Unmarshal([]byte(in), &out)
65-
newValue = out
66-
} else {
67-
// Empty string we will treat as empty array
61+
// CSV decode arrays of strings - ideally we would use
62+
// fs.CommaSepList here but we can't as it would cause
63+
// a circular import.
64+
if len(in) == 0 {
6865
newValue = []string{}
66+
} else {
67+
r := csv.NewReader(strings.NewReader(in))
68+
newValue, err = r.Read()
69+
switch _err := err.(type) {
70+
case *csv.ParseError:
71+
err = _err.Err // remove line numbers from the error message
72+
}
6973
}
7074
default:
7175
// Try using a Set method

fs/config/configstruct/configstruct_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,11 @@ func TestStringToInterface(t *testing.T) {
204204
{"1m1s", fs.Duration(0), fs.Duration(61 * time.Second), ""},
205205
{"1potato", fs.Duration(0), nil, `parsing "1potato" as fs.Duration failed: parsing time "1potato" as "2006-01-02": cannot parse "1potato" as "2006"`},
206206
{``, []string{}, []string{}, ""},
207-
{`[]`, []string(nil), []string{}, ""},
208-
{`["hello"]`, []string{}, []string{"hello"}, ""},
209-
{`["hello","world!"]`, []string(nil), []string{"hello", "world!"}, ""},
207+
{`""`, []string(nil), []string{""}, ""},
208+
{`hello`, []string{}, []string{"hello"}, ""},
209+
{`"hello"`, []string{}, []string{"hello"}, ""},
210+
{`hello,world!`, []string(nil), []string{"hello", "world!"}, ""},
211+
{`"hello","world!"`, []string(nil), []string{"hello", "world!"}, ""},
210212
{"1s", time.Duration(0), time.Second, ""},
211213
{"1m1s", time.Duration(0), 61 * time.Second, ""},
212214
{"1potato", time.Duration(0), nil, `parsing "1potato" as time.Duration failed: time: unknown unit "potato" in duration "1potato"`},

fs/config/flags/flags.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,32 @@ func installFlag(flags *pflag.FlagSet, name string, groupsString string) {
143143
// Read default from environment if possible
144144
envKey := fs.OptionToEnv(name)
145145
if envValue, envFound := os.LookupEnv(envKey); envFound {
146-
err := flags.Set(name, envValue)
147-
if err != nil {
148-
fs.Fatalf(nil, "Invalid value when setting --%s from environment variable %s=%q: %v", name, envKey, envValue, err)
146+
isStringArray := false
147+
opt, isOption := flag.Value.(*fs.Option)
148+
if isOption {
149+
_, isStringArray = opt.Default.([]string)
150+
}
151+
if isStringArray {
152+
// Treat stringArray differently, treating the environment variable as a CSV array
153+
var list fs.CommaSepList
154+
err := list.Set(envValue)
155+
if err != nil {
156+
fs.Fatalf(nil, "Invalid value when setting stringArray --%s from environment variable %s=%q: %v", name, envKey, envValue, err)
157+
}
158+
// Set both the Value (so items on the command line get added) and DefValue so the help is correct
159+
opt.Value = ([]string)(list)
160+
flag.DefValue = list.String()
161+
for _, v := range list {
162+
fs.Debugf(nil, "Setting --%s %q from environment variable %s=%q", name, v, envKey, envValue)
163+
}
164+
} else {
165+
err := flags.Set(name, envValue)
166+
if err != nil {
167+
fs.Fatalf(nil, "Invalid value when setting --%s from environment variable %s=%q: %v", name, envKey, envValue, err)
168+
}
169+
fs.Debugf(nil, "Setting --%s %q from environment variable %s=%q", name, flag.Value, envKey, envValue)
170+
flag.DefValue = envValue
149171
}
150-
fs.Debugf(nil, "Setting --%s %q from environment variable %s=%q", name, flag.Value, envKey, envValue)
151-
flag.DefValue = envValue
152172
}
153173

154174
// Add flag to Group if it is a global flag

fs/registry.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,9 @@ func (o *Option) String() string {
264264
if len(stringArray) == 0 {
265265
return ""
266266
}
267-
// Encode string arrays as JSON
267+
// Encode string arrays as CSV
268268
// The default Go encoding can't be decoded uniquely
269-
buf, err := json.Marshal(stringArray)
270-
if err != nil {
271-
Errorf(nil, "Can't encode default value for %q key - ignoring: %v", o.Name, err)
272-
return "[]"
273-
}
274-
return string(buf)
269+
return CommaSepList(stringArray).String()
275270
}
276271
return fmt.Sprint(v)
277272
}

0 commit comments

Comments
 (0)