Skip to content

Commit 3476fbc

Browse files
authored
Variables: Allow to escape "$" with "$$" (#216)
* Variables: Allow to escape "$" with "$$" * Variables: Added $var style to documentation Also added missing env expansion on known path config values
1 parent b0767ad commit 3476fbc

File tree

10 files changed

+141
-15
lines changed

10 files changed

+141
-15
lines changed

config/config_mixins.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ func (m *mixin) translate(source, variables map[string]interface{}) map[string]i
5757

5858
func (m *mixin) expandVariables(value string, variables map[string]interface{}) string {
5959
return os.Expand(value, func(name string) string {
60-
lookup := strings.ToUpper(name)
60+
if name == "$" {
61+
return "$" // allow to escape "$" as "$$"
62+
}
6163

64+
lookup := strings.ToUpper(name)
6265
replacement := variables[lookup]
6366
if replacement == nil {
6467
replacement = m.DefaultVariables[lookup]

config/config_mixins_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ func TestMixin(t *testing.T) {
3939
"int", 321,
4040
"bool", true,
4141
"list", list(1, "2", 3),
42-
"list_with_vars", list("${var-1}", "$VAR_2", "[$MyVar]", "-${myvar}-", "-${MYVAR}-"),
43-
"string_with_vars", "${var-1} $Var_2 [$MyVar] -${myvar}- -${MYVAR}-",
42+
"list_with_vars", list("${var-1}", "$VAR_2", "[$MyVar]", "-${myvar}-", "-${MYVAR}-", "-$${escaped}-"),
43+
"string_with_vars", "${var-1} $Var_2 [$MyVar] -${myvar}- -${MYVAR}- $$escaped",
4444
"nested_with_vars", list(
4545
mm(
4646
"list_with_vars", list("${var-1}"),
@@ -58,8 +58,8 @@ func TestMixin(t *testing.T) {
5858
"int", 321,
5959
"bool", true,
6060
"list", list(1, "2", 3),
61-
"list_with_vars", list("${var-1}", "${VAR_2}", "[MyDefault]", "-MyDefault-", "-MyDefault-"),
62-
"string_with_vars", "${var-1} ${Var_2} [MyDefault] -MyDefault- -MyDefault-",
61+
"list_with_vars", list("${var-1}", "${VAR_2}", "[MyDefault]", "-MyDefault-", "-MyDefault-", "-${escaped}-"),
62+
"string_with_vars", "${var-1} ${Var_2} [MyDefault] -MyDefault- -MyDefault- $escaped",
6363
"nested_with_vars", list(
6464
mm(
6565
"list_with_vars", list("${var-1}"),
@@ -77,8 +77,8 @@ func TestMixin(t *testing.T) {
7777
"int", 321,
7878
"bool", true,
7979
"list", list(1, "2", 3),
80-
"list_with_vars", list("${var-1}", "${VAR_2}", "[MySpecific]", "-MySpecific-", "-MySpecific-"),
81-
"string_with_vars", "${var-1} ${Var_2} [MySpecific] -MySpecific- -MySpecific-",
80+
"list_with_vars", list("${var-1}", "${VAR_2}", "[MySpecific]", "-MySpecific-", "-MySpecific-", "-${escaped}-"),
81+
"string_with_vars", "${var-1} ${Var_2} [MySpecific] -MySpecific- -MySpecific- $escaped",
8282
"nested_with_vars", list(
8383
mm(
8484
"list_with_vars", list("${var-1}"),
@@ -87,7 +87,7 @@ func TestMixin(t *testing.T) {
8787
),
8888
"${var_2}",
8989
),
90-
), tpl.Resolve(keysToUpper(mm("myvar", "MySpecific"))))
90+
), tpl.Resolve(keysToUpper(mm("myvar", "MySpecific", "escaped", "-"))))
9191
})
9292

9393
t.Run("all-resolved", func(t *testing.T) {
@@ -96,8 +96,8 @@ func TestMixin(t *testing.T) {
9696
"int", 321,
9797
"bool", true,
9898
"list", list(1, "2", 3),
99-
"list_with_vars", list("val1", "val2", "[MySpecific]", "-MySpecific-", "-MySpecific-"),
100-
"string_with_vars", "val1 val2 [MySpecific] -MySpecific- -MySpecific-",
99+
"list_with_vars", list("val1", "val2", "[MySpecific]", "-MySpecific-", "-MySpecific-", "-${escaped}-"),
100+
"string_with_vars", "val1 val2 [MySpecific] -MySpecific- -MySpecific- $escaped",
101101
"nested_with_vars", list(
102102
mm(
103103
"list_with_vars", list("val1"),
@@ -111,6 +111,7 @@ func TestMixin(t *testing.T) {
111111
"myvar", "MySpecific",
112112
"var-1", "val1",
113113
"var_2", "val2",
114+
"escaped", "-",
114115
)),
115116
))
116117
})

config/global.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ func NewGlobal() *Global {
4848
}
4949

5050
func (p *Global) SetRootPath(rootPath string) {
51+
p.ShellBinary = fixPaths(p.ShellBinary, expandEnv)
52+
p.ResticBinary = fixPath(p.ResticBinary, expandEnv)
53+
5154
p.SystemdUnitTemplate = fixPath(p.SystemdUnitTemplate, expandEnv, absolutePrefix(rootPath))
5255
p.SystemdTimerTemplate = fixPath(p.SystemdTimerTemplate, expandEnv, absolutePrefix(rootPath))
5356

config/path.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ func fixPaths(sources []string, callbacks ...pathFix) (fixed []string) {
3434

3535
func expandEnv(value string) string {
3636
if strings.Contains(value, "$") || strings.Contains(value, "%") {
37-
value = os.ExpandEnv(value)
37+
value = os.Expand(value, func(name string) string {
38+
if name == "$" {
39+
return "$" // allow to escape "$" as "$$"
40+
}
41+
return os.Getenv(name)
42+
})
3843
}
3944
return value
4045
}

config/path_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,13 @@ func TestFixWindowsPaths(t *testing.T) {
8686
assert.Equalf(t, testPath.expected, fixed, "source was '%s'", testPath.source)
8787
}
8888
}
89+
90+
func TestExpandEnv(t *testing.T) {
91+
path := os.Getenv("PATH")
92+
assert.Equal(t, path, expandEnv("$PATH"))
93+
assert.Equal(t, path, expandEnv("${PATH}"))
94+
assert.Equal(t, "%PATH%", expandEnv("%PATH%"))
95+
assert.Equal(t, "$PATH", expandEnv("$$PATH"))
96+
assert.Equal(t, "", expandEnv("${__UNDEFINED_ENV_VAR__}"))
97+
assert.Equal(t, "", expandEnv("$__UNDEFINED_ENV_VAR__"))
98+
}

config/profile.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func (b *BackupSection) resolve(p *Profile) {
184184
}
185185

186186
func (s *BackupSection) setRootPath(p *Profile, rootPath string) {
187-
s.SendMonitoringSections.setRootPath(p, rootPath)
187+
s.SectionWithScheduleAndMonitoring.setRootPath(p, rootPath)
188188

189189
s.ExcludeFile = fixPaths(s.ExcludeFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
190190
s.FilesFrom = fixPaths(s.FilesFrom, expandEnv, expandUserHome, absolutePrefix(rootPath))
@@ -229,6 +229,11 @@ type SectionWithScheduleAndMonitoring struct {
229229
OtherFlagsSection `mapstructure:",squash"`
230230
}
231231

232+
func (s *SectionWithScheduleAndMonitoring) setRootPath(p *Profile, rootPath string) {
233+
s.SendMonitoringSections.setRootPath(p, rootPath)
234+
s.ScheduleBaseSection.setRootPath(p, rootPath)
235+
}
236+
232237
func (s *SectionWithScheduleAndMonitoring) IsEmpty() bool { return s == nil }
233238

234239
// ScheduleBaseSection contains the parameters for scheduling a command (backup, check, forget, etc.)
@@ -241,6 +246,10 @@ type ScheduleBaseSection struct {
241246
ScheduleLockWait time.Duration `mapstructure:"schedule-lock-wait" show:"noshow" examples:"150s;15m;30m;45m;1h;2h30m" description:"Set the maximum time to wait for acquiring locks when running on schedule"`
242247
}
243248

249+
func (s *ScheduleBaseSection) setRootPath(_ *Profile, _ string) {
250+
s.ScheduleLog = fixPath(s.ScheduleLog, expandEnv, expandUserHome)
251+
}
252+
244253
func (s *ScheduleBaseSection) GetSchedule() *ScheduleBaseSection { return s }
245254

246255
// CopySection contains the destination parameters for a copy command
@@ -259,7 +268,7 @@ type CopySection struct {
259268
func (s *CopySection) IsEmpty() bool { return s == nil }
260269

261270
func (c *CopySection) setRootPath(p *Profile, rootPath string) {
262-
c.SendMonitoringSections.setRootPath(p, rootPath)
271+
c.SectionWithScheduleAndMonitoring.setRootPath(p, rootPath)
263272

264273
c.PasswordFile = fixPath(c.PasswordFile, expandEnv, expandUserHome, absolutePrefix(rootPath))
265274
c.RepositoryFile = fixPath(c.RepositoryFile, expandEnv, expandUserHome, absolutePrefix(rootPath))

docs/content/configuration/variables/index.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,87 @@ default {
471471

472472
{{% /tab %}}
473473
{{% /tabs %}}
474+
475+
476+
## Variable expansion in configuration **values**
477+
478+
Variable expansion as described in the previous section using the `{{ .Var }}` syntax refers to [template variables]({{< ref "/configuration/templates" >}}) that are expanded prior to parsing the configuration file.
479+
This means they must be used carefully to create correct config markup, but they are also very flexible.
480+
481+
There is also unix style variable expansion using the `${variable}` or `$variable` syntax on configuration **values** that expand after the config file was parsed. Values that take a file path or path expression and a few others support this expansion.
482+
483+
If not specified differently, these variables resolve to the corresponding environment variable or to an empty value if no such environment variable exists. Exceptions are [mixins]({{< ref "/configuration/inheritance#mixins" >}}) where `$variable` style is used for parametrisation and the profile [config flag]({{< ref "configuration/reference#section-profile" >}}) `prometheus-push-job`.
484+
485+
### Example
486+
487+
Backup current dir (`$PWD`) but prevent backup of `$HOME` where the repository is located:
488+
489+
{{< tabs groupId="config-with-json" >}}
490+
{{% tab name="toml" %}}
491+
492+
```toml
493+
version = "1"
494+
495+
[default]
496+
repository = "local:${HOME}/backup"
497+
password-file = "${HOME}/backup.key"
498+
499+
[default.backup]
500+
source = "$PWD"
501+
exclude = ["$HOME/**", ".*", "~*"]
502+
503+
```
504+
505+
{{% /tab %}}
506+
{{% tab name="yaml" %}}
507+
508+
```yaml
509+
version: "1"
510+
511+
default:
512+
repository: 'local:${HOME}/backup'
513+
password-file: '${HOME}/backup.key'
514+
515+
backup:
516+
source: '$PWD'
517+
exclude: ['$HOME/**', '.*', '~*']
518+
519+
```
520+
521+
{{% /tab %}}
522+
{{% tab name="hcl" %}}
523+
524+
```hcl
525+
default {
526+
repository = "local:${HOME}/backup"
527+
password-file = "${HOME}/backup.key"
528+
529+
backup {
530+
source = [ "$PWD" ]
531+
exclude = [ "$HOME/**", ".*", "~*" ]
532+
}
533+
}
534+
```
535+
536+
{{% /tab %}}
537+
{{% tab name="json" %}}
538+
539+
```json
540+
{
541+
"default": {
542+
"repository": "local:${HOME}/backup",
543+
"password-file": "${HOME}/backup.key",
544+
"backup": {
545+
"source": [ "$PWD" ],
546+
"exclude": [ "$HOME/**", ".*", "~*" ]
547+
}
548+
}
549+
}
550+
```
551+
552+
{{% /tab %}}
553+
{{% /tabs %}}
554+
555+
{{% notice style="hint" %}}
556+
Use `$$` to escape a single `$` in configuration values that support variable expansion. E.g. on Windows you might want to exclude `$RECYCLE.BIN`. Specify it as: `exclude = ["$$RECYCLE.BIN"]`.
557+
{{% /notice %}}

monitor/hook/sender.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ func resolve(body string, ctx Context) string {
212212
case constants.EnvErrorStderr:
213213
return ctx.Error.Stderr
214214

215+
case "$":
216+
return "$" // allow to escape "$" as "$$"
217+
215218
default:
216219
return os.Getenv(s)
217220
}

monitor/hook/sender_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package hook
33
import (
44
"bytes"
55
"encoding/pem"
6+
"fmt"
67
"io"
78
"net/http"
89
"net/http/httptest"
@@ -37,6 +38,10 @@ func TestSend(t *testing.T) {
3738
Method: http.MethodPost,
3839
Body: "test body\n",
3940
}, 1},
41+
{config.SendMonitoringSection{
42+
Method: http.MethodPost,
43+
Body: "test $$escaped\n",
44+
}, 1},
4045
{config.SendMonitoringSection{
4146
Method: http.MethodPost,
4247
Body: "$PROFILE_NAME\n$PROFILE_COMMAND",
@@ -47,8 +52,8 @@ func TestSend(t *testing.T) {
4752
}, 1},
4853
}
4954

50-
for _, testCase := range testCases {
51-
t.Run("", func(t *testing.T) {
55+
for i, testCase := range testCases {
56+
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
5257
calls := 0
5358
if testCase.cfg.URL.Value() == "" {
5459
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -67,6 +72,7 @@ func TestSend(t *testing.T) {
6772
body = strings.ReplaceAll(body, "$ERROR_EXIT_CODE", "test_exit_code")
6873
body = strings.ReplaceAll(body, "$ERROR_STDERR", "test_stderr")
6974
body = strings.ReplaceAll(body, "$ERROR", "test_error_message")
75+
body = strings.ReplaceAll(body, "$$", "$")
7076
assert.Equal(t, body, buffer.String())
7177
calls++
7278
})

monitor/prom/progress.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ func (p *Progress) Summary(command string, summary monitor.Summary, stderr strin
6666
jobName = os.Expand(jobName, func(name string) string {
6767
if strings.EqualFold(name, "command") {
6868
return command
69+
} else if name == "$" {
70+
return "$" // allow to escape "$" as "$$"
6971
}
7072
return ""
7173
})

0 commit comments

Comments
 (0)