Repository: creativeprojects/resticprofile (language: python)

Issue:
Question: How to calculate a value in `before-run` which is available afterwards for the restic commands
I am having a hard time, getting my head around how the environment is available during resticprofile runs and how I can manipulate it to inject values to variables which I need to be present during restic execution (or at parsing time)

Ultimately the Problem to solve is that I want to have a `check` profile which does a `read-data-subset: n/m` where `m` is some value that can be static but `n` should count upwards based on the day of year and be reset to `1` when it reaches `m`.

I first built some complicated things with multiple profiles that make use of `{{ (.Now.AddDate 0 0 -(m-1)).YearDay }}` scheduling it with multiple `OnCalendar`-directives (Which kinda worked), but that requires a lot of code duplication which I am apparently not able (given my current knowledge) to deduplicate using the template language and ends up in for example at least 7 profile entries if `m` happens to be `50`. Also to deal with the leap year is really complicated and makes everything even messier.

So I can easily calculate the value of `n` externally using a script or binary. I only need to have that value present for accessing it in the `read-data-subset:` call.

I am not able to even simulate it by hardcoding an `env:` variable. At the actual check command line, it is always empty. 

I am a bit lost at this point. Any Ideas how to proceed?

P.S.:

This was the complicated approach which kinda works, I described above:

```
read-data-daily-1:
  inherit: dummy
  run-before: "curl -m 10 --retry 5 {{ $healthchecks_ping_url }}/slug/start?create=1"
  run-finally: "if [ -z $ERROR_EXIT_CODE ];then ERROR_EXIT_CODE=\"0\";fi;curl -m 10 --retry 5 \"{{ $healthchecks_ping_url }}/slug/$ERROR_EXIT_CODE\""
  check:
    read-data-subset: {{ .Now.YearDay }}/50
    # first 50 days of the year
    schedule: 
      - '*-1-* 02:00:00'
      - '*-2-1..19 02:00:00'
    schedule-permission: user
    schedule-priority: background
    schedule-lock-mode: default
    schedule-lock-wait: 1h
```

And this was an approach where I experimented with a hardcoded `env:` config, which did not expand the variable on the actual check execution:

```
read-data-daily:
  inherit: read-data-daily-1
  env:
    EXAMPLE_PORTION: "100"
  run-before:
    # Is correctly expanded at runtime
    - "echo \"Portion: $EXAMPLE_PORTION\""
  check:
    # is empty
    read-data-subset: ${EXAMPLE_PORTION}/255
    schedule: 
      - 'daily' 
```

 


Discussion / hints:
After more research, I doubt that things in the configuration can be dynamically calculated at all if not possible via the built-in mechanisms that the templates provide.

So I came up with a workaround. I use `run-before` to collect the parameters for read-data-subset and execute the restic command manually there. That way scheduling and using resticprofile as the wrapper still works. However, the `check` command without arguments is executed nonetheless after the `run-before-section` has passed. But since this is a less time consuming option the advantages outweigh the disadvantages imho. To speed that up even more, I explicitly use `with-cache: true` for this.

In case anyone else should be interested, Here is how I implemented the workaround:

```
# profile to perform a full read-data consistency check on <repo>. Data is read in subsets. We split the data so that n/m of the whole set is read each day.
# That means after m passed days, we visited most of the repo during that interval (of course, changes might be re-checked only every other m days when data has been added)
# Since there seems to be no way to natively use a dynamic value for the read-data-subset parameter as of now (2024-01-16), we have to do a hack where we run the entire restic 
# command manually in `run-before` with dynamically calculated values. Resticprofile will then additionally call a regular check afterwards. This is unavoidable when we want to use
# the resticprofile api as is. We can try to keep the execution time of that quick check as low as possible by using the `--with-cache` option for this. We should switch to a more 
# native approach as soon as a possibility to add dynamic values for the `read-data-subset` parameter will be available. See https://github.com/creativeprojects/resticprofile/issues/299
read-data-daily:
  inherit: dummy
  env:
    MAX_DATA_PORTIONS: 30
    BIN_DIR: {{ .CurrentDir }}/../../../bin
    RESTIC_HOST_DIR: {{ .CurrentDir }}/../../../hosts/<host>
  run-before: 
    - curl -m 10 --retry 5 {{ $healthchecks_ping_url }}/slug/start?create=1
    - echo "Starting manual restic check";
      echo "";
      source $RESTIC_HOST_DIR/resticenv.sh;
      PORTION_OF_THE_DAY=$($BIN_DIR/calculate-portion.sh $MAX_DATA_PORTIONS);
      restic check --read-data-subset=$PORTION_OF_THE_DAY/$MAX_DATA_PORTIONS;
      unset RESTIC_REPOSITORY;
      unset RESTIC_PASSWORD;
      echo "proceeding with resticprofile check command";
      echo ""
  run-finally: "if [ -z $ERROR_EXIT_CODE ];then ERROR_EXIT_CODE=\"0\";fi;curl -m 10 --retry 5 \"{{ $healthchecks_ping_url }}/slug/$ERROR_EXIT_CODE\""
  check:
    with-cache: true
    schedule: '*-*-* 01:00:00'
    schedule-permission: user
    schedule-priority: background
    schedule-lock-mode: default
    schedule-lock-wait: 1h
```

And the script `calculate-portion.sh` which calculates n for a given m is here:

```
#!/bin/bash

print_usage_and_exit(){
    echo "Usage: $0 <m> [<o>] [<YYYY-MM-DD>]"
    echo "This script calculates a specific value <n> on each different day it is called."
    echo "<n> will always be a positive integer in the range [1,<m>]."
    echo "<m> is passed as a mandatory argument. It has to be a positive integer."
    echo "<o> is an optional, non-positional argument which is an offset in days to the day the calculation is done on."
    echo "<YYYY-MM-DD> is an optional, non positional date argument for which <n> should be calculated. If not given, the script assumes the current date for the calculation."
	echo "For each call of the script on subsequent <YYYY-MM-DD>, as long as the offset and the value of <m> are not changed,the resulting value of <n> is"
	echo "incremented by one. So that <n> cycles from 1 to <m> on each call on subsequent days. It will start over with 1 the day after the day on which it equals <m>"
    exit $1
}

# echo "Your input for <o> was \"$1\". <o> must be an integral value!"
validate_offset(){
	if ! [[ $1 =~ ^-?[0-9]+$ ]];then
		return 1
	fi
	return 0
}

validate_date(){
	if [[ ! "$1" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
		return 1
	fi
	if ! date -d "$1" >/dev/null 2>&1; then
			return 1
	fi
	return 0
}

# Check if at least one argument <m> is provided
if [ $# -lt 1 ]; then
	echo "At least one argument for <m> has to be given"
	echo ""
    print_usage_and_exit 1
fi

# Check if at most 3 arguments are provided
if [[ $# -gt 3 ]]; then
	echo "Too many arguments given. Expected 3 at most, got $#"
	echo ""
	print_usage_and_exit 1
fi

# Extract the upper limit "m" from the arguments and test it for being a positive integer
m=$1
if ! [[ $m =~ ^[0-9]+$ ]]; then
	echo "Your input for <m> was \"$m\". <m> must be a positive integral value!"
	echo ""
	print_usage_and_exit 1
fi

# Defaults
offset_days=0
input_date=$(date +%y-%m-%d) 

# if a second argument is provided, check whether it is an offset or a year
if [ $# -ge 2 ]; then
	if validate_offset $2; then
		offset_days=$2
	elif validate_date $2; then
		input_date=$2
	else
		echo "Could neither get a valid offset value nor a valid date from argument \"$2\". Offsets have to be positive and dates have to be passed in the form of \"YYYY-MM-DD\""
		echo ""
		print_usage_and_exit 1
	fi
fi

# If a third argument is provided, check whether it is an offset or a year
if [ $# -eq 3 ]; then
	if (! validate_offset $3) && (! validate_date $3) then
		echo "Could neither get a valid offset value nor a valid date from argument \"$3\". Offsets have to be positive and dates have to be passed in the form of \"YYYY-MM-DD\""
		echo ""
		print_usage_and_exit 1
	fi
	if validate_offset $3 && validate_date $2; then
		offset_days=$3
	elif validate_date $3 && validate_offset $2; then
		input_date=$3
	else
		echo "It looks like you either provided two date arguments or two offset arguments. Please provide either type once. Offsets have to be positive and dates have to be passed in the form of \"YYYY-MM-DD\""
		echo ""
		print_usage_and_exit 1
	fi
fi

# Calculate days since the Unix epoch
input_timestamp=$(date -d "$input_date" +%s)
seconds_since_epoch=$(( (input_timestamp - $(date -d "1970-01-01" +"%s")) ))
days_since_epoch=$(( (seconds_since_epoch / 86400)  ))
days_since_epoch_with_offset=$(( (days_since_epoch + offset_days) ))

# Only positive results are allowed for the calculated days since the epoch when the offset is applied
if [[ $days_since_epoch_with_offset -lt 0 ]]; then
	echo "Invalid date or offset given. Use dates after and including 1970-01-01 + days offset value: $offset_days"
	print_usage_and_exit 1
fi

# Calculate the cyclic value between 1 and m
cyclic_value=$(( (1 + (days_since_epoch_with_offset % m)) ))

#echo "Days since the epoch for $input_date and an offset of $offset_days days: $days_since_epoch_with_offset"
#echo "Cyclic value (1 to $m) since the Unix epoch for $input_date: $cyclic_value"
echo "$cyclic_value"
```

I do like this idea of using an environment variable inside any configuration value:

```yaml
read-data-daily:
  inherit: read-data-daily-1
  env:
    EXAMPLE_PORTION: "100"
  run-before:
    # Is correctly expanded at runtime
    - "echo \"Portion: $EXAMPLE_PORTION\""
  check:
    # introducing new feature to make it work
    read-data-subset: ${EXAMPLE_PORTION}/255
    schedule: 
      - 'daily' 
```

that would simplify a lot of the pain of using template variables, which can only be referenced during the compilation of the configuration anyway.

Now we need to extract the environment variables that were set during a command.
I had an idea about that:

What if we wrap up any command inside a tiny shell script, like:

```sh
[command goes here, any `run-before`, `run-after` or calling `restic`]

# grab the exit code
exitCode = $?

# save environment variables
env > output_env.tmp

# return the exit code from the command
exit $exitCode
```

This way we can save the environment variables that have been set by the command.
It should be possible to use `set` for cmd.exe and `Get-ChildItem ENV:` on powershell

What do you think @jkellerer ?

@creativeprojects I tried the new version but to no avail. I cannot access those variables later. I have a feeling that this is not yet solved. 

Or, I am doing something entirely wrong. 
I am still using my approach from above, which works good by bypassing the actual check command and executing it inside `before-run`.
I now tried to echo the variables value to the env file and use them later on, however, the values are not accessible. The `show` command reads "no value" when I use the vars with `read-data-subset`. The env file is correctly populated with the values.

Here is my example code:

```
target-read-data-daily:
  inherit: target
  env:
    MAX_DATA_PORTIONS: 50
    BIN_DIR: "{{ .ConfigDir }}/../../../bin"
    RESTIC_HOST_DIR: "{{ .ConfigDir }}/../../../hosts/hostname"
    HEALTHCHECKS_SLUG: "hostname-backup-check-target"
  env-file: ".env"
  run-before:
    - $CURL_COMMAND $HEALTCHECKS_PING_URL/$HEALTHCHECKS_SLUG/start?create=1
    - set -e;
      echo "Calculating portion of the day";
      echo "";
      PORTION_OF_THE_DAY=$($BIN_DIR/calculate-portion.sh $MAX_DATA_PORTIONS);
      echo "PORTION_OF_THE_DAY=$PORTION_OF_THE_DAY" > ".env";
      echo "MAX_DATA_PORTIONS=$MAX_DATA_PORTIONS" >> ".env";
      echo "proceeding with resticprofile check command";
      echo ""
  run-finally: 'if [ -z $ERROR_EXIT_CODE ];then ERROR_EXIT_CODE="0";fi;$CURL_COMMAND "$HEALTCHECKS_PING_URL/$HEALTHCHECKS_SLUG/$ERROR_EXIT_CODE"'
  check:
    with-cache: false
    read-data-subset: "$PORTION_OF_THE_DAY/$MAX_DATA_PORTIONS"
    schedule: "*-*-* 06:30:00"
```

Am I doing it wrong?
Hey!

I think we need to use the internal `env` file as referenced with `{{ env }}`.

I haven't tried this configuration but it should work like that:

```yaml
target-read-data-daily:
  inherit: target
  env:
    MAX_DATA_PORTIONS: 50
    BIN_DIR: "{{ .ConfigDir }}/../../../bin"
    RESTIC_HOST_DIR: "{{ .ConfigDir }}/../../../hosts/hostname"
    HEALTHCHECKS_SLUG: "hostname-backup-check-target"
  env-file: ".env"
  run-before:
    - $CURL_COMMAND $HEALTCHECKS_PING_URL/$HEALTHCHECKS_SLUG/start?create=1
    - set -e;
      echo "Calculating portion of the day";
      echo "";
      PORTION_OF_THE_DAY=$($BIN_DIR/calculate-portion.sh $MAX_DATA_PORTIONS);
      echo "PORTION_OF_THE_DAY=$PORTION_OF_THE_DAY" > {{ env }};
      echo "MAX_DATA_PORTIONS=$MAX_DATA_PORTIONS" >> {{ env }};
      echo "proceeding with resticprofile check command";
      echo ""
  run-finally: 'if [ -z $ERROR_EXIT_CODE ];then ERROR_EXIT_CODE="0";fi;$CURL_COMMAND "$HEALTCHECKS_PING_URL/$HEALTHCHECKS_SLUG/$ERROR_EXIT_CODE"'
  check:
    with-cache: false
    read-data-subset: "$PORTION_OF_THE_DAY/$MAX_DATA_PORTIONS"
    schedule: "*-*-* 06:30:00"
```




That was the first I tried yesterday, but that also did not work.
Oh I get it. Sorry I missed that part of the configuration.

As it turns out you cannot use an environment variable inside any flag (like `read-data-subset`).

We need to do a big refactoring to allow that.
It's on our list of things to do; but it's a lot more complicated than it looks 😞 

In a nutshell, the configuration is fully resolved before starting the profile. In your case we need to re-scan the configuration and update the values during the execution of the profile.


Fix patch:
diff --git a/config/confidential.go b/config/confidential.go
index 6f96d8ae..97169679 100644
--- a/config/confidential.go
+++ b/config/confidential.go
@@ -15,6 +15,10 @@ type ConfidentialValue struct {
 	public, confidential string
 }
 
+func (c ConfidentialValue) HasValue() bool {
+	return c.confidential != ""
+}
+
 // Value returns the unmasked original value
 func (c ConfidentialValue) Value() string {
 	return c.confidential
@@ -206,19 +210,8 @@ func GetNonConfidentialArgs(profile *Profile, args *shell.Args) *shell.Args {
 		return nil
 	}
 
-	target := args.Clone()
-	confidentials := getAllConfidentialValues(profile)
+	modifier := shell.NewConfidentialArgModifier()
+	newArgs := args.Modify(modifier)
 
-	target.Walk(func(name string, arg *shell.Arg) *shell.Arg {
-		if arg.HasValue() {
-			value := convertToNonConfidential(confidentials, arg.Value())
-			if value != arg.Value() {
-				a := shell.NewArg(value, arg.Type())
-				return &a
-			}
-		}
-		return arg
-	})
-
-	return target
+	return newArgs
 }
diff --git a/config/flag.go b/config/flag.go
index 39d058ab..1beea445 100644
--- a/config/flag.go
+++ b/config/flag.go
@@ -28,7 +28,7 @@ var allowedEmptyValueArgs = []string{
 func tryAddEmptyArg(args *shell.Args, name string, value any) bool {
 	sv, ok := value.(string)
 	if ok && sv == "" && slices.Contains(allowedEmptyValueArgs, name) {
-		args.AddFlag(name, shell.EmptyArgValue(), shell.NewEmptyValueArg().Type())
+		args.AddFlag(name, shell.NewEmptyValueArg())
 		return true
 	}
 	return false
@@ -49,13 +49,13 @@ func addArgsFromStruct(args *shell.Args, section any) {
 			if argument != "" {
 				convert, ok := stringifyConfidentialValue(valueOf.Field(i))
 				if ok {
-					argType := shell.ArgConfigEscape
-					// check if the argument type was specified
-					rawType, ok := field.Tag.Lookup("argument-type")
-					if ok && rawType == "no-glob" {
-						argType = shell.ArgConfigKeepGlobQuote
+					// FIXME quick hack for now
+					isConfidential := valueOf.Field(i).Type() == reflect.TypeOf(ConfidentialValue{})
+					flags := make([]shell.Arg, 0, len(convert))
+					for _, v := range convert {
+						flags = append(flags, shell.NewArg(v, getArgType(field), shell.NewConfidentialArgOption(isConfidential)))
 					}
-					args.AddFlags(argument, convert, argType)
+					args.AddFlags(argument, flags)
 				} else if len(convert) == 1 {
 					_ = tryAddEmptyArg(args, argument, convert[0])
 				}
@@ -89,13 +89,23 @@ func addArgsFromMap(args *shell.Args, argAliases map[string]string, argsMap map[
 			name = targetName
 		}
 		if convert, ok := stringifyValueOf(value); ok {
-			args.AddFlags(name, convert, shell.ArgConfigEscape)
+			args.AddFlags(name, shell.NewArgsSlice(convert, shell.ArgConfigEscape))
 		} else {
 			_ = tryAddEmptyArg(args, name, value)
 		}
 	}
 }
 
+func getArgType(field reflect.StructField) shell.ArgType {
+	argType := shell.ArgConfigEscape
+	// check if the argument type was specified
+	rawType, ok := field.Tag.Lookup("argument-type")
+	if ok && rawType == "no-glob" {
+		argType = shell.ArgConfigKeepGlobQuote
+	}
+	return argType
+}
+
 // stringifyValueOf returns a string representation of the value, and if it has any value at all
 func stringifyValueOf(value interface{}) ([]string, bool) {
 	return stringifyAnyValueOf(value, true)
diff --git a/config/profile.go b/config/profile.go
index e4e7dbc1..0e34ca51 100644
--- a/config/profile.go
+++ b/config/profile.go
@@ -68,7 +68,6 @@ type Profile struct {
 	RunShellCommandsSection `mapstructure:",squash"`
 	OtherFlagsSection       `mapstructure:",squash"`
 	config                  *Config
-	legacyArg               bool
 	resticVersion           *semver.Version
 	Name                    string
 	Description             string                            `mapstructure:"description" description:"Describes the profile"`
@@ -420,7 +419,7 @@ func (s *CopySection) getCommandFlags(profile *Profile) (flags *shell.Args) {
 		// restic < 0.14: repo2, password-file2, etc. is the destination, repo, password-file, etc. the source
 		for name, value := range repositoryArgs {
 			if len(value) > 0 {
-				flags.AddFlag(fmt.Sprintf("%s2", name), value, shell.ArgConfigEscape)
+				flags.AddFlag(fmt.Sprintf("%s2", name), shell.NewArg(value, shell.ArgConfigEscape))
 			}
 		}
 	} else {
@@ -430,7 +429,7 @@ func (s *CopySection) getCommandFlags(profile *Profile) (flags *shell.Args) {
 		}
 		for name, value := range repositoryArgs {
 			if len(value) > 0 {
-				flags.AddFlag(name, value, shell.ArgConfigEscape)
+				flags.AddFlag(name, shell.NewArg(value, shell.ArgConfigEscape))
 			}
 		}
 	}
@@ -604,11 +603,6 @@ func (p *Profile) ResolveConfiguration() {
 	}
 }
 
-// SetLegacyArg is used to activate the legacy (broken) mode of sending arguments on the restic command line
-func (p *Profile) SetLegacyArg(legacy bool) {
-	p.legacyArg = legacy
-}
-
 // SetResticVersion sets the effective restic version for validation and to determine how to format flags.
 // Note that flags filtering happens later inside resticWrapper and is not necessary inside the profile.
 func (p *Profile) SetResticVersion(resticVersion string) (err error) {
@@ -819,7 +813,7 @@ func (p *Profile) GetCommonFlags() (flags *shell.Args) {
 	defer restore()
 
 	// Flags from the profile fields
-	flags = shell.NewArgs().SetLegacyArg(p.legacyArg)
+	flags = shell.NewArgs()
 	addArgsFromStruct(flags, p)
 	addArgsFromOtherFlags(flags, p, p)
 	return flags
diff --git a/context.go b/context.go
index 4985d3e9..7a968951 100644
--- a/context.go
+++ b/context.go
@@ -34,6 +34,7 @@ type Context struct {
 	stopOnBattery int              // stop if running on battery
 	noLock        bool             // skip profile lock file
 	lockWait      time.Duration    // wait up to duration to acquire a lock
+	legacyArgs    bool             // I'm not even sure it's been used by anyone?
 }
 
 func CreateContext(flags commandLineFlags, global *config.Global, cfg *config.Config, ownCommands *OwnCommands) (*Context, error) {
@@ -63,6 +64,7 @@ func CreateContext(flags commandLineFlags, global *config.Global, cfg *config.Co
 		sigChan:       nil,
 		logTarget:     global.Log, // default to global (which can be empty)
 		commandOutput: global.CommandOutput,
+		legacyArgs:    global.LegacyArguments, // use the broken arguments escaping (before v0.15.0)
 	}
 	// own commands can check the context before running
 	if ownCommands.Exists(command, true) {
diff --git a/examples/dev.yaml b/examples/dev.yaml
index 4140ff20..bdaff402 100644
--- a/examples/dev.yaml
+++ b/examples/dev.yaml
@@ -118,11 +118,13 @@ self:
         schedule-ignore-on-battery: true
         schedule-after-network-online: true
 
+    run-before:
+        - "echo DOW=`date +\"%u\"` >> {{ env }}"
     run-after-fail:
         - "echo restic returned an error, command line = ${ERROR_COMMANDLINE}"
         - "echo restic stderr = ${RESTIC_STDERR}"
     check:
-        read-data: true
+        read-data-subset: "$DOW/7"
         schedule:
             - "*:15"
     retention:
@@ -139,6 +141,7 @@ self:
     snapshots:
         host: true
         run-before:
+          - "env"
           - "echo STEP=Before >> {{ env }}"
           - "echo \"$STEP ($Step)\""
         run-after:
diff --git a/main.go b/main.go
index 66d6c2b8..14a65853 100644
--- a/main.go
+++ b/main.go
@@ -494,11 +494,6 @@ func runProfile(ctx *Context) error {
 		changeLevelFilter(clog.LevelDebug)
 	}
 
-	// use the broken arguments escaping (before v0.15.0)
-	if ctx.global.LegacyArguments {
-		profile.SetLegacyArg(true)
-	}
-
 	// tell the profile what version of restic is in use
 	if e := profile.SetResticVersion(ctx.global.ResticVersion); e != nil {
 		clog.Warningf("restic version %q is no valid semver: %s", ctx.global.ResticVersion, e.Error())
diff --git a/mask/mask.go b/mask/mask.go
new file mode 100644
index 00000000..a1fb7f79
--- /dev/null
+++ b/mask/mask.go
@@ -0,0 +1,24 @@
+package mask
+
+import "regexp"
+
+const maskReplacement = "×××"
+
+var (
+	// httpHeaderNames     = regexp.MustCompile("(?i)^(Authorization)$")
+	RepositoryConfidentialPart = regexp.MustCompile("[:/][^:/@]+?:([^:@]+?)@[^:/@]+?") // user:pass@host
+	// urlEnvKeys          = regexp.MustCompile("(?i)^.+(_AUTH|_URL)$")
+	// hiddenEnvKeys       = regexp.MustCompile("(?i)^(.+_KEY|.+_TOKEN|.*PASSWORD.*|.*SECRET.*)$")
+)
+
+func Submatches(pattern *regexp.Regexp, value string) string {
+	if matches := pattern.FindStringSubmatchIndex(value); len(matches) > 2 {
+		for i := len(matches) - 2; i > 1; i -= 2 {
+			start := matches[i]
+			end := matches[i+1]
+
+			value = value[0:start] + maskReplacement + value[end:]
+		}
+	}
+	return value
+}
diff --git a/shell/arg.go b/shell/arg.go
index aaa41c2b..0099edee 100644
--- a/shell/arg.go
+++ b/shell/arg.go
@@ -1,8 +1,6 @@
 package shell
 
 import (
-	"crypto/rand"
-	"encoding/hex"
 	"fmt"
 	"regexp"
 	"strings"
@@ -12,8 +10,7 @@ import (
 
 var (
 	escapeNoGlobCharacters = []byte{'|', '&', ';', '<', '>', '(', ')', '$', '`', '\\', '"', '\'', ' ', '\t', '\r', '\n'}
-	// escapeGlobCharacters   = []byte{'*', '?', '['}
-	doubleQuotePattern = regexp.MustCompile(`[^\\][|&;<>()$'" \t\r\n*?[]`)
+	doubleQuotePattern     = regexp.MustCompile(`[^\\][|&;<>()$'" \t\r\n*?[]`)
 )
 
 type ArgType int
@@ -31,61 +28,90 @@ const (
 	ArgLegacyConfigBackupSource
 )
 
-var emptyArgValueMarker = func() string {
-	token := make([]byte, 16)
-	n, err := rand.Read(token)
-	if err == nil && n != 16 {
-		err = fmt.Errorf("insufficient random bytes %d", n)
+type filterFunc func(string) string
+
+type Arg struct {
+	value              string
+	argType            ArgType
+	empty              bool
+	confidentialFilter filterFunc
+}
+
+func NewArg(value string, argType ArgType, options ...ArgOption) Arg {
+	arg := Arg{
+		value:              value,
+		argType:            argType,
+		empty:              false,
+		confidentialFilter: nil,
 	}
-	if err != nil {
-		panic(err)
+	for _, option := range options {
+		option.setup(&arg)
 	}
-	return hex.EncodeToString(token)
-}()
-
-func EmptyArgValue() string {
-	return emptyArgValueMarker
+	return arg
 }
 
-func NewEmptyValueArg() Arg {
-	return NewArg(EmptyArgValue(), ArgConfigKeepGlobQuote)
+func NewArgsSlice(args []string, argType ArgType, options ...ArgOption) []Arg {
+	result := make([]Arg, len(args))
+	for i, arg := range args {
+		result[i] = NewArg(arg, argType, options...)
+	}
+	return result
 }
 
-type Arg struct {
-	raw     string
-	argType ArgType
+func NewEmptyValueArg() Arg {
+	return NewArg("", ArgConfigKeepGlobQuote, &EmptyArgOption{})
 }
 
-func NewArg(raw string, argType ArgType) Arg {
+func (a Arg) Clone() Arg {
 	return Arg{
-		raw:     raw,
-		argType: argType,
+		value:              a.value,
+		argType:            a.argType,
+		empty:              a.empty,
+		confidentialFilter: a.confidentialFilter,
 	}
 }
 
+// IsEmpty means the flag is specifically empty, not just a flag without a value
+// (e.g. --flag="")
+func (a Arg) IsEmptyValue() bool {
+	return a.empty
+}
+
+// HasValue returns true if the argument has a value (e.g. --flag=value).
+// The value could be empty (e.g. --flag="").
+// If false, the argument is a simple flag (e.g. --verbose).
 func (a Arg) HasValue() bool {
-	return a.raw != ""
+	return a.empty || a.value != ""
+}
+
+// IsConfidential returns true if the argument may contain credentials.
+func (a Arg) HasConfidentialFilter() bool {
+	return a.confidentialFilter != nil
 }
 
 func (a Arg) Value() string {
-	if a.raw == EmptyArgValue() {
-		return ""
-	}
-	return a.raw
+	return a.value
 }
 
 func (a Arg) Type() ArgType {
 	return a.argType
 }
 
-func (a Arg) String() string {
-	value := a.Value()
+func (a Arg) GetConfidentialValue() string {
+	if a.HasConfidentialFilter() {
+		return a.confidentialFilter(a.value)
+	}
+	return a.value
+}
 
+// String returns an escaped value to send to the command line
+func (a Arg) String() string {
 	if !a.HasValue() {
 		return ""
 	}
 
-	if a.Value() == "" {
+	value := a.Value()
+	if value == "" {
 		return `""`
 	}
 
diff --git a/shell/arg_option.go b/shell/arg_option.go
new file mode 100644
index 00000000..343bb29e
--- /dev/null
+++ b/shell/arg_option.go
@@ -0,0 +1,36 @@
+package shell
+
+import "github.com/creativeprojects/resticprofile/mask"
+
+type ArgOption interface {
+	setup(arg *Arg)
+}
+
+// EmptyArgOption is an option to create an specifically empty argument (e.g. --flag="")
+type EmptyArgOption struct{}
+
+func (o *EmptyArgOption) setup(arg *Arg) {
+	arg.empty = true
+}
+
+type ConfidentialArgOption struct {
+	ConfidentialFilter filterFunc
+}
+
+var confidentialFilterFunc filterFunc = func(value string) string {
+	return mask.Submatches(mask.RepositoryConfidentialPart, value)
+}
+
+func NewConfidentialArgOption(confidential bool) *ConfidentialArgOption {
+	var confidentialFilter filterFunc
+	if confidential {
+		confidentialFilter = confidentialFilterFunc
+	}
+	return &ConfidentialArgOption{
+		ConfidentialFilter: confidentialFilter,
+	}
+}
+
+func (o *ConfidentialArgOption) setup(arg *Arg) {
+	arg.confidentialFilter = o.ConfidentialFilter
+}
diff --git a/shell/args.go b/shell/args.go
index e701b98a..3a8adf44 100644
--- a/shell/args.go
+++ b/shell/args.go
@@ -7,12 +7,13 @@ import (
 	"github.com/creativeprojects/resticprofile/util/collect"
 )
 
+// Args is a collection of arguments that can be used to build a command line.
 type Args struct {
-	args   map[string][]Arg
-	more   []Arg
-	legacy bool
+	args map[string][]Arg
+	more []Arg
 }
 
+// NewArgs creates a new Args instance
 func NewArgs() *Args {
 	return &Args{
 		args: make(map[string][]Arg, 10),
@@ -29,7 +30,6 @@ func (a *Args) Clone() *Args {
 		clone.args[name] = args
 	}
 	clone.more = append(clone.more, a.more...)
-	clone.legacy = a.legacy
 	return clone
 }
 
@@ -47,43 +47,45 @@ func (a *Args) Walk(callback func(name string, arg *Arg) *Arg) {
 	processArgs("", a.more)
 }
 
-// SetLegacyArg is used to activate the legacy (broken) mode of sending arguments on the restic command line
-func (a *Args) SetLegacyArg(legacy bool) *Args {
-	a.legacy = legacy
-	return a
-}
-
-func (a *Args) addLegacy(argType ArgType) ArgType {
-	if a.legacy && argType <= ArgConfigBackupSource {
-		argType += ArgTypeCount
+// Modify returns a new Args with all arguments modified by the provided modifier
+func (a *Args) Modify(modifier ArgModifier) *Args {
+	processArgs := func(name string, args []Arg) []Arg {
+		newArgs := make([]Arg, len(args))
+		for i, arg := range args {
+			newArg, _ := modifier.Arg(name, &arg)
+			newArgs[i] = *newArg
+		}
+		return newArgs
+	}
+	newArgs := &Args{
+		args: make(map[string][]Arg, len(a.args)),
+		more: make([]Arg, 0, len(a.more)),
 	}
-	return argType
+	for name, args := range a.args {
+		newArgs.args[name] = processArgs(name, args)
+	}
+	newArgs.more = processArgs("", a.more)
+	return newArgs
 }
 
 // AddFlag adds a value to a flag
-func (a *Args) AddFlag(key, value string, argType ArgType) {
-	a.args[key] = []Arg{NewArg(value, a.addLegacy(argType))}
+func (a *Args) AddFlag(key string, arg Arg) {
+	a.args[key] = []Arg{arg}
 }
 
 // AddFlags adds a slice of values for the same flag
-func (a *Args) AddFlags(key string, values []string, argType ArgType) {
-	args := make([]Arg, len(values))
-	for i, value := range values {
-		args[i] = NewArg(value, a.addLegacy(argType))
-	}
+func (a *Args) AddFlags(key string, args []Arg) {
 	a.args[key] = args
 }
 
 // AddArg adds a single argument with no flag
-func (a *Args) AddArg(arg string, argType ArgType) {
-	a.more = append(a.more, NewArg(arg, a.addLegacy(argType)))
+func (a *Args) AddArg(arg Arg) {
+	a.more = append(a.more, arg)
 }
 
 // AddArgs adds multiple arguments not associated with a flag
-func (a *Args) AddArgs(args []string, argType ArgType) {
-	for _, arg := range args {
-		a.more = append(a.more, NewArg(arg, a.addLegacy(argType)))
-	}
+func (a *Args) AddArgs(args []Arg) {
+	a.more = append(a.more, args...)
 }
 
 // ToMap converts the arguments to a map.
@@ -122,13 +124,13 @@ func (a *Args) Rename(oldName, newName string) bool {
 	args, ok := a.Remove(oldName)
 	if ok {
 		for _, arg := range args {
-			a.AddFlag(newName, arg.Value(), arg.Type())
+			a.AddFlag(newName, NewArg(arg.Value(), arg.Type()))
 		}
 	}
 	args = a.RemoveArg(oldName)
 	ok = ok || len(args) > 0
 	for _, arg := range args {
-		a.AddArg(newName, arg.Type())
+		a.AddArg(NewArg(newName, arg.Type()))
 	}
 	return ok
 }
diff --git a/shell/confidential_modifier.go b/shell/confidential_modifier.go
new file mode 100644
index 00000000..29fb8fea
--- /dev/null
+++ b/shell/confidential_modifier.go
@@ -0,0 +1,23 @@
+package shell
+
+// ConfidentialArgModifier masks confidential information in arguments.
+type ConfidentialArgModifier struct {
+}
+
+var _ ArgModifier = (*ConfidentialArgModifier)(nil)
+
+func NewConfidentialArgModifier() *ConfidentialArgModifier {
+	return &ConfidentialArgModifier{}
+}
+
+// Arg returns either the same of a new argument if the value has changed.
+// A boolean value indicates if Arg has changed.
+func (m ConfidentialArgModifier) Arg(name string, arg *Arg) (*Arg, bool) {
+	if arg.HasConfidentialFilter() {
+		newArg := arg.Clone()
+		newArg.value = arg.GetConfidentialValue()
+		newArg.confidentialFilter = nil
+		return &newArg, true
+	}
+	return arg, false
+}
diff --git a/shell/expand_env_modifier.go b/shell/expand_env_modifier.go
new file mode 100644
index 00000000..7899b608
--- /dev/null
+++ b/shell/expand_env_modifier.go
@@ -0,0 +1,53 @@
+package shell
+
+import (
+	"os"
+	"strings"
+
+	"github.com/creativeprojects/resticprofile/platform"
+)
+
+type ExpandEnvModifier struct {
+	env map[string]string
+}
+
+var _ ArgModifier = (*ExpandEnvModifier)(nil)
+
+func NewExpandEnvModifier(environment []string) *ExpandEnvModifier {
+	envMap := make(map[string]string, len(environment))
+	for _, env := range environment {
+		envKey, envValue, found := strings.Cut(env, "=")
+		if !found {
+			continue
+		}
+		envMap[strings.TrimSpace(envKey)] = strings.TrimSpace(envValue)
+	}
+	return &ExpandEnvModifier{
+		env: envMap,
+	}
+}
+
+// Arg returns either the same of a new argument if the value was expanded.
+// A boolean value indicates if the argument was expanded.
+func (m ExpandEnvModifier) Arg(name string, arg *Arg) (*Arg, bool) {
+	if arg.HasValue() && arg.Type() == ArgConfigEscape && !platform.IsWindows() {
+		if value, changed := m.expandEnv(arg.Value()); changed {
+			newArg := arg.Clone()
+			newArg.value = value
+			return &newArg, true
+		}
+	}
+	return arg, false
+}
+
+func (m ExpandEnvModifier) expandEnv(value string) (string, bool) {
+	changed := false
+	return os.Expand(value, func(key string) string {
+		if envValue, found := m.env[key]; found {
+			changed = true
+			return envValue
+		}
+		// we don't want to replace the original value
+		return "${" + key + "}"
+	}), changed
+}
diff --git a/shell/legacy_arg_modifier.go b/shell/legacy_arg_modifier.go
new file mode 100644
index 00000000..6ec91ae8
--- /dev/null
+++ b/shell/legacy_arg_modifier.go
@@ -0,0 +1,35 @@
+package shell
+
+// LegacyArgModifier changes the type of arguments to a legacy type.
+type LegacyArgModifier struct {
+	legacy bool
+}
+
+var _ ArgModifier = (*LegacyArgModifier)(nil)
+
+func NewLegacyArgModifier(legacy bool) *LegacyArgModifier {
+	return &LegacyArgModifier{
+		legacy: legacy,
+	}
+}
+
+// Arg returns either the same of a new argument if the type has changed.
+// A boolean value indicates if Arg has changed.
+func (m LegacyArgModifier) Arg(name string, arg *Arg) (*Arg, bool) {
+	if m.legacy {
+		if newArgType, changed := addLegacy(arg.Type()); changed {
+			newArg := arg.Clone()
+			newArg.argType = newArgType
+			return &newArg, true
+		}
+	}
+	return arg, false
+}
+
+func addLegacy(argType ArgType) (ArgType, bool) {
+	if argType <= ArgConfigBackupSource {
+		argType += ArgTypeCount
+		return argType, true
+	}
+	return argType, false
+}
diff --git a/shell/modifier.go b/shell/modifier.go
new file mode 100644
index 00000000..05d31436
--- /dev/null
+++ b/shell/modifier.go
@@ -0,0 +1,7 @@
+package shell
+
+type ArgModifier interface {
+	// Arg returns either the same of a new Arg if the value was changed.
+	// A boolean also indicates if the value was changed.
+	Arg(name string, arg *Arg) (*Arg, bool)
+}
diff --git a/wrapper.go b/wrapper.go
index 457f4dcc..6a5454c3 100644
--- a/wrapper.go
+++ b/wrapper.go
@@ -363,19 +363,30 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
 		clog.Tracef("unfiltered extra flags: %s", strings.Join(config.GetNonConfidentialValues(r.profile, moreArgs), " "))
 		moreArgs = filter(moreArgs, allowExtraValues)
 	}
-	args.AddArgs(moreArgs, shell.ArgCommandLineEscape)
+	args.AddArgs(shell.NewArgsSlice(moreArgs, shell.ArgCommandLineEscape))
 
 	// Special case for backup command
 	var dir string
 	if command == constants.CommandBackup {
-		args.AddArgs(r.profile.GetBackupSource(), shell.ArgConfigBackupSource)
+		args.AddArgs(shell.NewArgsSlice(r.profile.GetBackupSource(), shell.ArgConfigBackupSource))
 		if r.profile.Backup != nil && r.profile.Backup.SourceRelative {
 			dir = r.profile.Backup.SourceBase
 		}
 	}
 	// Special case for copy command
 	if command == constants.CommandCopy {
-		args.AddArgs(r.profile.GetCopySnapshotIDs(), shell.ArgConfigEscape)
+		args.AddArgs(shell.NewArgsSlice(r.profile.GetCopySnapshotIDs(), shell.ArgConfigEscape))
+	}
+
+	env := r.getEnvironment(true)
+	env = append(env, r.getProfileEnvironment()...)
+
+	modifiers := []shell.ArgModifier{
+		shell.NewLegacyArgModifier(r.ctx.legacyArgs),
+		shell.NewExpandEnvModifier(env),
+	}
+	for _, modifier := range modifiers {
+		args = args.Modify(modifier)
 	}
 
 	// Add retry-lock (supported from restic 0.16, depends on filter being enabled)
@@ -388,7 +399,7 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
 		lockRetryTime = lockRetryTime.Truncate(time.Minute)
 
 		if lockRetryTime > 0 && !r.containsArguments(args.GetAll(), fmt.Sprintf("--%s", constants.ParameterRetryLock)) {
-			args.AddFlag(constants.ParameterRetryLock, fmt.Sprintf("%.0fm", lockRetryTime.Minutes()), shell.ArgConfigEscape)
+			args.AddFlag(constants.ParameterRetryLock, shell.NewArg(fmt.Sprintf("%.0fm", lockRetryTime.Minutes()), shell.ArgConfigEscape))
 		}
 	}
 
@@ -403,9 +414,6 @@ func (r *resticWrapper) prepareCommand(command string, args *shell.Args, allowEx
 	arguments = append([]string{command}, arguments...)
 	publicArguments = append([]string{command}, publicArguments...)
 
-	env := r.getEnvironment(true)
-	env = append(env, r.getProfileEnvironment()...)
-
 	clog.Debug(func() string {
 		wd := ""
 		if dir != "" {


Test patch:
diff --git a/config/confidential_test.go b/config/confidential_test.go
index eaf0e51d..4dbc2498 100644
--- a/config/confidential_test.go
+++ b/config/confidential_test.go
@@ -26,6 +26,8 @@ var (
 )
 
 func TestConfidentialHideAll(t *testing.T) {
+	t.Parallel()
+
 	value := NewConfidentialValue("val")
 
 	assert.Equal(t, value.String(), value.Value())
@@ -37,6 +39,8 @@ func TestConfidentialHideAll(t *testing.T) {
 }
 
 func TestConfidentialHideSubmatch(t *testing.T) {
+	t.Parallel()
+
 	value := NewConfidentialValue("some-vAl-with-sEcRet-parts")
 
 	assert.Equal(t, value.String(), value.Value())
@@ -50,6 +54,8 @@ func TestConfidentialHideSubmatch(t *testing.T) {
 }
 
 func TestUpdateConfidentialValue(t *testing.T) {
+	t.Parallel()
+
 	v := NewConfidentialValue("abc")
 
 	v.setValue("xyz")
@@ -65,6 +71,8 @@ func TestUpdateConfidentialValue(t *testing.T) {
 }
 
 func TestFmtStringDoesntLeakConfidentialValues(t *testing.T) {
+	t.Parallel()
+
 	value := NewConfidentialValue("secret")
 	value.hideValue()
 
@@ -75,6 +83,8 @@ func TestFmtStringDoesntLeakConfidentialValues(t *testing.T) {
 }
 
 func TestStringifyPassesConfidentialValues(t *testing.T) {
+	t.Parallel()
+
 	value := NewConfidentialValue("secret")
 	value.hideValue()
 
@@ -85,6 +95,8 @@ func TestStringifyPassesConfidentialValues(t *testing.T) {
 }
 
 func TestConfidentialURLs(t *testing.T) {
+	t.Parallel()
+
 	// https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html
 	urls := map[string]string{
 		"local:some/path":                                      "-",
@@ -122,6 +134,8 @@ repository = "%s"
 }
 
 func TestConfidentialEnvironment(t *testing.T) {
+	t.Parallel()
+
 	// https://restic.readthedocs.io/en/latest/030_preparing_a_new_repo.html
 	vars := map[string]string{
 		"MY_VALUE": "-",
@@ -194,6 +208,8 @@ func TestConfidentialEnvironment(t *testing.T) {
 }
 
 func TestShowConfigHidesConfidentialValues(t *testing.T) {
+	t.Parallel()
+
 	testConfig := `
 profile:
   repository: "local:user:pass@host"
@@ -223,6 +239,8 @@ profile:
 }
 
 func TestGetNonConfidentialValues(t *testing.T) {
+	t.Parallel()
+
 	testConfig := `
 profile:
   verbose: false
@@ -263,18 +281,51 @@ profile:
 }
 
 func TestGetNonConfidentialArgs(t *testing.T) {
+	t.Parallel()
+
 	repo := "local:user:%s@host/path with space"
 	testConfig := `
+global:
+  prevent-auto-repository-file: true
+profile:
+  repository: "` + fmt.Sprintf(repo, "password") + `"
+`
+	profile, err := getProfile("yaml", testConfig, "profile", "")
+	assert.NoError(t, err)
+	assert.NotNil(t, profile)
+
+	args := profile.GetCommandFlags(constants.CommandBackup)
+	result := GetNonConfidentialArgs(profile, args)
+
+	expectedSecret := shell.NewArg(fmt.Sprintf(repo, "password"), shell.ArgConfigEscape).String()
+	expectedPublic := shell.NewArg(fmt.Sprintf(repo, ConfidentialReplacement), shell.ArgConfigEscape).String()
+
+	assert.Equal(t, []string{"--repo=" + expectedSecret}, args.GetAll())
+	assert.Equal(t, []string{"--repo=" + expectedPublic}, result.GetAll())
+}
+
+func TestGetNonConfidentialArgsFromEnvironmentVariable(t *testing.T) {
+	t.Parallel()
+
+	if platform.IsWindows() {
+		t.Skip()
+	}
+	repo := "local:user:%s@host/path with space"
+	environment := []string{
+		"MY_REPOSITORY=" + fmt.Sprintf(repo, "password"),
+	}
+	testConfig := `
       global:
         prevent-auto-repository-file: true
       profile:
-        repository: "` + fmt.Sprintf(repo, "password") + `"
+        repository: $MY_REPOSITORY
       `
 	profile, err := getProfile("yaml", testConfig, "profile", "")
 	assert.NoError(t, err)
 	assert.NotNil(t, profile)
 
 	args := profile.GetCommandFlags(constants.CommandBackup)
+	args = args.Modify(shell.NewExpandEnvModifier(environment))
 	result := GetNonConfidentialArgs(profile, args)
 
 	expectedSecret := shell.NewArg(fmt.Sprintf(repo, "password"), shell.ArgConfigEscape).String()
@@ -376,6 +427,8 @@ func TestGetAutoRepositoryFileDisabledWithEnv(t *testing.T) {
 }
 
 func TestConfidentialToJSON(t *testing.T) {
+	t.Parallel()
+
 	t.Run("marshal", func(t *testing.T) {
 		value := NewConfidentialValue("plain")
 		assert.False(t, value.IsConfidential())
@@ -391,6 +444,8 @@ func TestConfidentialToJSON(t *testing.T) {
 	})
 
 	t.Run("unmarshal", func(t *testing.T) {
+		t.Parallel()
+
 		value := NewConfidentialValue("")
 		value.hideValue()
 		assert.True(t, value.IsConfidential())
diff --git a/config/profile_test.go b/config/profile_test.go
index 9714b390..59a7b955 100644
--- a/config/profile_test.go
+++ b/config/profile_test.go
@@ -1572,6 +1572,6 @@ func TestGetCopyStructFields(t *testing.T) {
 		p := &(*profile)
 		assert.Nil(t, p.GetCopyInitializeFlags())
 		p.Copy = copy
-		assert.Equal(t, copy.getInitFlags(p), p.GetCopyInitializeFlags())
+		assert.Equal(t, copy.getInitFlags(p).GetAll(), p.GetCopyInitializeFlags().GetAll())
 	})
 }
diff --git a/integration_test.go b/integration_test.go
index f92bb5d3..e19d0488 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -155,22 +155,21 @@ func TestFromConfigFileToCommandLine(t *testing.T) {
 					continue
 				}
 
-				// legacy test
+				// legacy args test
 				t.Run(fixture.profileName+"/"+fixture.commandName+"/legacy", func(t *testing.T) {
 					profile, err := cfg.GetProfile(fixture.profileName)
 					require.NoError(t, err)
 					require.NotNil(t, profile)
 
-					profile.SetLegacyArg(true)
-
 					ctx := &Context{
 						request: Request{
 							profile:   fixture.profileName,
 							arguments: fixture.cmdlineArgs,
 						},
-						binary:  echoBinary,
-						profile: profile,
-						command: fixture.commandName,
+						binary:     echoBinary,
+						profile:    profile,
+						command:    fixture.commandName,
+						legacyArgs: true,
 					}
 					wrapper := newResticWrapper(ctx)
 					buffer := &bytes.Buffer{}
diff --git a/shell/analyser_test.go b/shell/analyser_test.go
index 50736144..c275739b 100644
--- a/shell/analyser_test.go
+++ b/shell/analyser_test.go
@@ -18,6 +18,8 @@ the 'unlock' command can be used to remove stale locks
 `
 
 func TestRemoteLockFailure(t *testing.T) {
+	t.Parallel()
+
 	analysis := NewOutputAnalyser()
 	require.NoError(t, analysis.AnalyseStringLines(ResticLockFailureOutput))
 
@@ -43,6 +45,8 @@ func TestRemoteLockFailure(t *testing.T) {
 }
 
 func TestCustomErrorCallback(t *testing.T) {
+	t.Parallel()
+
 	var analyser *OutputAnalyser
 	invoked := 0
 	var cbError error
diff --git a/shell/arg_test.go b/shell/arg_test.go
index cb743cb9..5b804ca0 100644
--- a/shell/arg_test.go
+++ b/shell/arg_test.go
@@ -12,6 +12,8 @@ import (
 )
 
 func TestArgumentEscape(t *testing.T) {
+	t.Parallel()
+
 	if runtime.GOOS == "windows" {
 		t.Skip("Not running on Windows")
 	}
@@ -51,6 +53,8 @@ func TestArgumentEscape(t *testing.T) {
 			displayEscapedString(testItem.expectedEscape),
 			displayEscapedString(testItem.expectedNoGlobQuote))
 		t.Run(testItem.input, func(t *testing.T) {
+			t.Parallel()
+
 			assert.Equal(t, testItem.expectedEscape, NewArg(testItem.input, ArgConfigEscape).String())
 			assert.Equal(t, testItem.expectedNoGlobQuote, NewArg(testItem.input, ArgConfigKeepGlobQuote).String())
 		})
@@ -67,15 +71,49 @@ func displayEscapedString(input string) string {
 }
 
 func TestEmptyArgValue(t *testing.T) {
+	t.Parallel()
+
 	noValue := NewArg("", ArgConfigKeepGlobQuote)
-	emptyValue := NewArg(EmptyArgValue(), ArgConfigKeepGlobQuote)
+	emptyValue := NewEmptyValueArg()
 
 	assert.False(t, noValue.HasValue())
+	assert.False(t, noValue.IsEmptyValue())
 	assert.Equal(t, "", noValue.Value())
 
 	assert.True(t, emptyValue.HasValue())
+	assert.True(t, emptyValue.IsEmptyValue())
 	assert.Equal(t, "", emptyValue.Value())
 	if !platform.IsWindows() {
 		assert.Equal(t, `""`, emptyValue.String())
 	}
 }
+
+func TestArgClone(t *testing.T) {
+	t.Parallel()
+
+	args := []Arg{
+		NewEmptyValueArg(),
+		NewArg("value", ArgConfigEscape),
+		NewArg("non confidential", ArgConfigEscape, NewConfidentialArgOption(false)),
+		NewArg("confidential", ArgConfigEscape, NewConfidentialArgOption(true)),
+	}
+
+	for _, arg := range args {
+		t.Run(arg.Value(), func(t *testing.T) {
+			t.Parallel()
+
+			clone := arg.Clone()
+
+			if arg.confidentialFilter == nil || clone.confidentialFilter == nil {
+				assert.Equal(t, arg, clone)
+			} else {
+				// assert library cannot compare functions, so we need to check manually
+				assert.Equal(t, arg.Value(), clone.Value())
+				assert.Equal(t, arg.Type(), clone.Type())
+				assert.True(t, arg.HasConfidentialFilter())
+				assert.True(t, clone.HasConfidentialFilter())
+				assert.Equal(t, arg.GetConfidentialValue(), clone.GetConfidentialValue())
+			}
+		})
+	}
+}
diff --git a/shell/args_test.go b/shell/args_test.go
index 2d694860..019484f4 100644
--- a/shell/args_test.go
+++ b/shell/args_test.go
@@ -8,34 +8,40 @@ import (
 )
 
 func TestEmptyConversionToArgs(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs().GetAll()
 	assert.Equal(t, []string{}, args)
 }
 
 func TestConversionToArgsFromFlags(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddFlags("aaa", []string{"one", "two"}, ArgConfigEscape)
-	args.AddFlag("bbb", "three", ArgConfigEscape)
+	args.AddFlags("aaa", NewArgsSlice([]string{"one", "two"}, ArgConfigEscape))
+	args.AddFlag("bbb", NewArg("three", ArgConfigEscape))
 	assert.Equal(t, []string{"--aaa=one", "--aaa=two", "--bbb=three"}, args.GetAll())
 }
 
 func TestConversionToArgsNoFlag(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddArgs([]string{"one", "two"}, ArgConfigEscape)
-	args.AddArg("three", ArgConfigEscape)
+	args.AddArgs(NewArgsSlice([]string{"one", "two"}, ArgConfigEscape))
+	args.AddArg(NewArg("three", ArgConfigEscape))
 	assert.Equal(t, []string{"one", "two", "three"}, args.GetAll())
 }
 
 func TestClone(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddFlag("x", "y", ArgConfigEscape)
-	args.AddArg("more", ArgConfigEscape)
-	args.SetLegacyArg(true)
+	args.AddFlag("x", NewArg("y", ArgConfigEscape))
+	args.AddArg(NewArg("more", ArgConfigEscape))
 
 	clone := args.Clone()
 	assert.Equal(t, args.GetAll(), clone.GetAll())
 	assert.Equal(t, args.ToMap(), clone.ToMap())
-	assert.Equal(t, args.legacy, clone.legacy)
 
 	assert.NotSame(t, args, clone)
 	assert.NotSame(t, args.args, clone.args)
@@ -44,9 +50,11 @@ func TestClone(t *testing.T) {
 }
 
 func TestWalk(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddFlag("x", "y", ArgConfigEscape)
-	args.AddArg("more", ArgConfigEscape)
+	args.AddFlag("x", NewArg("y", ArgConfigEscape))
+	args.AddArg(NewArg("more", ArgConfigEscape))
 
 	var walked []string
 	args.Walk(func(name string, arg *Arg) *Arg {
@@ -63,9 +71,11 @@ func TestWalk(t *testing.T) {
 }
 
 func TestRenameAndRemove(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddFlag("x", "y", ArgConfigEscape)
-	args.AddArg("more", ArgConfigEscape)
+	args.AddFlag("x", NewArg("y", ArgConfigEscape))
+	args.AddArg(NewArg("more", ArgConfigEscape))
 
 	args.Rename("more", "new-more")
 	args.Rename("x", "new-x")
@@ -79,12 +89,14 @@ func TestRenameAndRemove(t *testing.T) {
 }
 
 func TestConversionToArgs(t *testing.T) {
+	t.Parallel()
+
 	args := NewArgs()
-	args.AddFlags("aaa", []string{"simple", "with space", "with\"quote"}, ArgConfigEscape)
-	args.AddFlags("bbb", []string{"simple", "with space", "with\"quote"}, ArgConfigKeepGlobQuote)
-	args.AddArgs([]string{"with space", "with\"quote", "with$variable"}, ArgConfigEscape)
-	args.AddArg("with space\"quote", ArgConfigKeepGlobQuote)
-	args.AddArg("with$variable", ArgConfigKeepGlobQuote)
+	args.AddFlags("aaa", NewArgsSlice([]string{"simple", "with space", "with\"quote"}, ArgConfigEscape))
+	args.AddFlags("bbb", NewArgsSlice([]string{"simple", "with space", "with\"quote"}, ArgConfigKeepGlobQuote))
+	args.AddArgs(NewArgsSlice([]string{"with space", "with\"quote", "with$variable"}, ArgConfigEscape))
+	args.AddArg(NewArg("with space\"quote", ArgConfigKeepGlobQuote))
+	args.AddArg(NewArg("with$variable", ArgConfigKeepGlobQuote))
 
 	expected := []string{
 		"--aaa=simple",
@@ -116,3 +128,31 @@ func TestConversionToArgs(t *testing.T) {
 	}
 	assert.Equal(t, expected, args.GetAll())
 }
+
+type testModifier struct{}
+
+func (m testModifier) Arg(name string, arg *Arg) (*Arg, bool) {
+	newArg := arg.Clone()
+	newArg.value = "modified-" + arg.Value()
+	return &newArg, true
+}
+
+func TestArgsModify(t *testing.T) {
+	t.Parallel()
+
+	args := &Args{
+		args: map[string][]Arg{
+			"aaa": {NewArg("value", ArgConfigEscape)},
+			"bbb": {NewArg("value", ArgConfigKeepGlobQuote)},
+		},
+		more: []Arg{
+			NewArg("value", ArgConfigEscape),
+			NewArg("value", ArgConfigKeepGlobQuote),
+		},
+	}
+
+	newArgs := args.Modify(testModifier{})
+
+	expected := []string{"--aaa=modified-value", "--bbb=modified-value", "modified-value", "modified-value"}
+	assert.Equal(t, expected, newArgs.GetAll())
+}
diff --git a/shell/confidential_modifier_test.go b/shell/confidential_modifier_test.go
new file mode 100644
index 00000000..0cd06689
--- /dev/null
+++ b/shell/confidential_modifier_test.go
@@ -0,0 +1,31 @@
+package shell
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConfidentialArgModifier(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		arg              Arg
+		expectedModified bool
+		expectedValue    string
+	}{
+		{NewEmptyValueArg(), false, ""},
+		{NewArg("value", ArgConfigEscape), false, "value"},
+		{NewArg("non confidential", ArgConfigEscape, NewConfidentialArgOption(false)), false, "non confidential"},
+		{NewArg("non confidential", ArgConfigEscape, NewConfidentialArgOption(true)), true, "non confidential"},
+		{NewArg("local:user:password@host/path", ArgConfigEscape, NewConfidentialArgOption(true)), true, "local:user:×××@host/path"},
+	}
+
+	modifier := NewConfidentialArgModifier()
+
+	for _, testCase := range testCases {
+		newArg, modified := modifier.Arg("name", &testCase.arg)
+		assert.Equal(t, testCase.expectedModified, modified)
+		assert.Equal(t, testCase.expectedValue, newArg.Value())
+	}
+}
diff --git a/shell/expand_env_modifier_test.go b/shell/expand_env_modifier_test.go
new file mode 100644
index 00000000..cea38eda
--- /dev/null
+++ b/shell/expand_env_modifier_test.go
@@ -0,0 +1,43 @@
+package shell
+
+import (
+	"testing"
+
+	"github.com/creativeprojects/resticprofile/platform"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestExpandEnvModifier(t *testing.T) {
+	t.Parallel()
+
+	if platform.IsWindows() {
+		t.Skip("Not running on Windows")
+	}
+
+	testCases := []struct {
+		environment  []string
+		value        string
+		expected     string
+		shouldChange bool
+	}{
+		{[]string{}, "", "", false},
+		{[]string{}, "something", "something", false},
+		{[]string{"wrong"}, "$wrong", "$wrong", false},                       // no environment variable
+		{[]string{}, "$notfound", "$notfound", false},                        // value not updated at all
+		{[]string{"found=true"}, "$notfound$found", "${notfound}true", true}, // value partially updated
+		{[]string{"empty="}, "$empty", "", true},
+		{[]string{"key=value"}, "$key", "value", true},
+		{[]string{"key = value"}, "$key", "value", true},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.value, func(t *testing.T) {
+			t.Parallel()
+
+			modifier := NewExpandEnvModifier(testCase.environment)
+			arg := NewArg(testCase.value, ArgConfigEscape)
+			newArg, changed := modifier.Arg("", &arg)
+			assert.Equal(t, testCase.shouldChange, changed)
+			assert.Equal(t, testCase.expected, newArg.Value())
+		})
+	}
+}
diff --git a/shell/json_summary_test.go b/shell/json_summary_test.go
index 562361a7..49f2a8f9 100644
--- a/shell/json_summary_test.go
+++ b/shell/json_summary_test.go
@@ -2,20 +2,18 @@ package shell
 
 import (
 	"os"
-	"runtime"
 	"strings"
 	"testing"
 
 	"github.com/creativeprojects/resticprofile/monitor"
+	"github.com/creativeprojects/resticprofile/platform"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
-const (
-	summary = `{"message_type":"summary","files_new":10,"files_changed":11,"files_unmodified":211,"dirs_new":0,"dirs_changed":12,"dirs_unmodified":58,"data_blobs":6,"tree_blobs":7,"data_added":8,"total_files_processed":232,"total_bytes_processed":362946952,"total_duration":0.220900201,"snapshot_id":"196f3b36"}`
-)
-
 func TestScanJsonSummary(t *testing.T) {
+	t.Parallel()
+
 	// example of restic output (beginning and end of the output)
 	resticOutput := `{"message_type":"status","percent_done":0,"total_files":1,"total_bytes":10244}
 {"message_type":"status","percent_done":0.028711419769115988,"total_files":213,"files_done":13,"total_bytes":362948126,"bytes_done":10420756,"current_files":["/go/src/github.com/creativeprojects/resticprofile/build/restic","/go/src/github.com/creativeprojects/resticprofile/build/resticprofile"]}
@@ -24,27 +22,29 @@ func TestScanJsonSummary(t *testing.T) {
 {"message_type":"summary","files_new":213,"files_changed":11,"files_unmodified":12,"dirs_new":58,"dirs_changed":2,"dirs_unmodified":3,"data_blobs":402,"tree_blobs":59,"data_added":296530781,"total_files_processed":236,"total_bytes_processed":362948126,"total_duration":1.009156009,"snapshot_id":"6daa8ef6"}
 `
 
-	if runtime.GOOS == "windows" {
+	if platform.IsWindows() {
 		// change the source
-		resticOutput = strings.ReplaceAll(resticOutput, "\n", eol)
+		resticOutput = strings.ReplaceAll(resticOutput, "\n", platform.LineSeparator)
 	}
 
 	reader, writer, err := os.Pipe()
 	require.NoError(t, err)
 
 	// Start writing into the pipe, line by line
-	go func() {
+	go func(t *testing.T) {
 		lines := strings.Split(resticOutput, "\n")
 		for _, line := range lines {
 			line = strings.TrimRight(line, "\r")
-			if runtime.GOOS == "windows" {
+			if platform.IsWindows() {
 				// https://github.com/restic/restic/issues/3111
-				writer.WriteString("\r\x1b[2K")
+				_, err := writer.WriteString("\r\x1b[2K")
+				assert.NoError(t, err)
 			}
-			writer.WriteString(line + eol)
+			_, err := writer.WriteString(line + platform.LineSeparator)
+			assert.NoError(t, err)
 		}
-		writer.Close()
-	}()
+		_ = writer.Close()
+	}(t)
 
 	// Read the stream and send back to output buffer
 	summary := &monitor.Summary{}
@@ -53,7 +53,7 @@ func TestScanJsonSummary(t *testing.T) {
 	require.NoError(t, err)
 
 	// Check what we read back is right (should be empty)
-	assert.Equal(t, eol, output.String())
+	assert.Equal(t, platform.LineSeparator, output.String())
 
 	// Check the values found are right
 	assert.Equal(t, 213, summary.FilesNew)
@@ -68,27 +68,30 @@ func TestScanJsonSummary(t *testing.T) {
 }
 
 func TestScanJsonError(t *testing.T) {
+	t.Parallel()
+
 	resticOutput := `Fatal: unable to open config file: Stat: stat /Volumes/RAMDisk/self/config: no such file or directory
 Is there a repository at the following location?
 /Volumes/RAMDisk/self
 `
-	if runtime.GOOS == "windows" {
+	if platform.IsWindows() {
 		// change the source
-		resticOutput = strings.ReplaceAll(resticOutput, "\n", eol)
+		resticOutput = strings.ReplaceAll(resticOutput, "\n", platform.LineSeparator)
 	}
 
 	reader, writer, err := os.Pipe()
 	require.NoError(t, err)
 
 	// Start writing into the pipe, line by line
-	go func() {
+	go func(t *testing.T) {
 		lines := strings.Split(resticOutput, "\n")
 		for _, line := range lines {
 			line = strings.TrimRight(line, "\r")
-			writer.WriteString(line + eol)
+			_, err := writer.WriteString(line + platform.LineSeparator)
+			assert.NoError(t, err)
 		}
-		writer.Close()
-	}()
+		_ = writer.Close()
+	}(t)
 
 	// Read the stream and send back to output buffer
 	summary := &monitor.Summary{}
@@ -97,7 +100,7 @@ Is there a repository at the following location?
 	require.NoError(t, err)
 
 	// Check what we read back is right
-	assert.Equal(t, resticOutput+eol, output.String())
+	assert.Equal(t, resticOutput+platform.LineSeparator, output.String())
 
 	// Check the values found are right
 	assert.Equal(t, 0, summary.FilesNew)
diff --git a/shell/legacy_arg_modifier_test.go b/shell/legacy_arg_modifier_test.go
new file mode 100644
index 00000000..eb5554b6
--- /dev/null
+++ b/shell/legacy_arg_modifier_test.go
@@ -0,0 +1,52 @@
+package shell
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestLegacyArgModifier(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		argValue              string
+		argType               ArgType
+		expectedLegacyArgType ArgType
+	}{
+		{"0 to 4", ArgConfigEscape, ArgLegacyEscape},
+		{"1 to 5", ArgConfigKeepGlobQuote, ArgLegacyKeepGlobQuote},
+		{"2 to 6", ArgCommandLineEscape, ArgLegacyCommandLineEscape},
+		{"3 to 7", ArgConfigBackupSource, ArgLegacyConfigBackupSource},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.argValue, func(t *testing.T) {
+			t.Parallel()
+
+			t.Run("ON", func(t *testing.T) {
+				t.Parallel()
+
+				modifier := NewLegacyArgModifier(true)
+				arg := NewArg(testCase.argValue, testCase.argType)
+				newArg, changed := modifier.Arg("", &arg)
+				assert.Equal(t, true, changed)
+				assert.Equal(t, testCase.expectedLegacyArgType, newArg.Type())
+				// run again to check if the type is not changed
+				newArgAgain, changed := modifier.Arg("", newArg)
+				assert.Equal(t, false, changed)
+				assert.Equal(t, testCase.expectedLegacyArgType, newArgAgain.Type())
+			})
+
+			t.Run("OFF", func(t *testing.T) {
+				t.Parallel()
+
+				modifier := NewLegacyArgModifier(false)
+				arg := NewArg(testCase.argValue, testCase.argType)
+				newArg, changed := modifier.Arg("", &arg)
+				assert.Equal(t, false, changed)
+				assert.Equal(t, testCase.argType, newArg.Type())
+			})
+		})
+	}
+}
diff --git a/shell/plain_summary_test.go b/shell/plain_summary_test.go
index 0dcaea68..cb4dffc4 100644
--- a/shell/plain_summary_test.go
+++ b/shell/plain_summary_test.go
@@ -7,11 +7,14 @@ import (
 	"testing"
 
 	"github.com/creativeprojects/resticprofile/monitor"
+	"github.com/creativeprojects/resticprofile/platform"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
 func TestPipeScanBackup(t *testing.T) {
+	t.Parallel()
+
 	source := `repository 2e92db7f opened successfully, password is correct
 created new cache in /Users/home/Library/Caches/restic
 
@@ -25,7 +28,7 @@ snapshot 07ab30a5 saved
 
 	if runtime.GOOS == "windows" {
 		// change the source
-		source = strings.ReplaceAll(source, "\n", eol)
+		source = strings.ReplaceAll(source, "\n", platform.LineSeparator)
 	}
 
 	reader, writer, err := os.Pipe()
@@ -36,7 +39,7 @@ snapshot 07ab30a5 saved
 		lines := strings.Split(source, "\n")
 		for _, line := range lines {
 			line = strings.TrimRight(line, "\r")
-			writer.WriteString(line + eol)
+			writer.WriteString(line + platform.LineSeparator)
 		}
 		writer.Close()
 	}()
@@ -48,7 +51,7 @@ snapshot 07ab30a5 saved
 	require.NoError(t, err)
 
 	// Check what we read back is right
-	assert.Equal(t, source+eol, output.String())
+	assert.Equal(t, source+platform.LineSeparator, output.String())
 
 	// Check the values found are right
 	assert.Equal(t, 209, summary.FilesNew)
diff --git a/shell/summary_test.go b/shell/summary_test.go
deleted file mode 100644
index de0edf5c..00000000
--- a/shell/summary_test.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package shell
-
-import "runtime"
-
-var (
-	eol = "\n"
-)
-
-func init() {
-	if runtime.GOOS == "windows" {
-		eol = "\r\n"
-	}
-}
diff --git a/wrapper_test.go b/wrapper_test.go
index e37b3d8b..2ff8aaec 100644
--- a/wrapper_test.go
+++ b/wrapper_test.go
@@ -33,6 +33,8 @@ import (
 )
 
 func TestValidResticArgumentsList(t *testing.T) {
+	t.Parallel()
+
 	wrapper := &resticWrapper{}
 
 	for _, command := range restic.CommandNames() {
@@ -69,6 +71,8 @@ func TestValidResticArgumentsList(t *testing.T) {
 }
 
 func TestVersionedResticArgumentsList(t *testing.T) {
+	t.Parallel()
+
 	wrapper := &resticWrapper{global: new(config.Global)}
 
 	wrapper.global.ResticVersion = "0.14"
@@ -83,6 +87,8 @@ func TestVersionedResticArgumentsList(t *testing.T) {
 }
 
 func TestValidArgumentsFilter(t *testing.T) {
+	t.Parallel()
+
 	wrapper := &resticWrapper{}
 	validArgs := collect.All(wrapper.validResticArgumentsList(constants.CommandBackup), func(arg string) bool {
 		return arg != "-x" && arg != "--xxx"
@@ -136,6 +142,8 @@ func TestValidArgumentsFilter(t *testing.T) {
 }
 
 func TestFilteredArgumentsRegression(t *testing.T) {
+	t.Parallel()
+
 	if platform.IsWindows() {
 		t.Skip()
 	}
@@ -190,6 +198,8 @@ func TestFilteredArgumentsRegression(t *testing.T) {
 }
 
 func TestGetEmptyEnvironment(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  "restic",
@@ -202,6 +212,8 @@ func TestGetEmptyEnvironment(t *testing.T) {
 }
 
 func TestGetSingleEnvironment(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Environment = map[string]config.ConfidentialValue{
 		"User": config.NewConfidentialValue("me"),
@@ -218,6 +230,8 @@ func TestGetSingleEnvironment(t *testing.T) {
 }
 
 func TestGetMultipleEnvironment(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Environment = map[string]config.ConfidentialValue{
 		"User":     config.NewConfidentialValue("me"),
@@ -247,6 +261,8 @@ func TestGetMultipleEnvironment(t *testing.T) {
 }
 
 func TestPreProfileScriptFail(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.RunBefore = []string{"exit 1"} // this should both work on unix shell and windows batch
 	ctx := &Context{
@@ -260,6 +276,8 @@ func TestPreProfileScriptFail(t *testing.T) {
 }
 
 func TestPostProfileScriptFail(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.RunAfter = []string{"exit 1"} // this should both work on unix shell and windows batch
 	ctx := &Context{
@@ -273,6 +291,8 @@ func TestPostProfileScriptFail(t *testing.T) {
 }
 
 func TestRunEchoProfile(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  "echo",
@@ -285,6 +305,8 @@ func TestRunEchoProfile(t *testing.T) {
 }
 
 func TestPostProfileAfterFail(t *testing.T) {
+	t.Parallel()
+
 	testFile := "TestPostProfileAfterFail.txt"
 	_ = os.Remove(testFile)
 	profile := config.NewProfile(nil, "name")
@@ -302,6 +324,8 @@ func TestPostProfileAfterFail(t *testing.T) {
 }
 
 func TestPostFailProfile(t *testing.T) {
+	t.Parallel()
+
 	testFile := "TestPostFailProfile.txt"
 	_ = os.Remove(testFile)
 	profile := config.NewProfile(nil, "name")
@@ -319,6 +343,8 @@ func TestPostFailProfile(t *testing.T) {
 }
 
 func TestFinallyProfile(t *testing.T) {
+	t.Parallel()
+
 	testFile := "TestFinallyProfile.txt"
 	defer os.Remove(testFile)
 
@@ -525,6 +551,8 @@ func TestEnvStderr(t *testing.T) {
 }
 
 func TestRunProfileWithSetPIDCallback(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Lock = filepath.Join(os.TempDir(), fmt.Sprintf("%s%d%d.tmp", "TestRunProfileWithSetPIDCallback", time.Now().UnixNano(), os.Getpid()))
 	t.Logf("lockfile = %s", profile.Lock)
@@ -539,6 +567,8 @@ func TestRunProfileWithSetPIDCallback(t *testing.T) {
 }
 
 func TestInitializeNoError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -551,6 +581,8 @@ func TestInitializeNoError(t *testing.T) {
 }
 
 func TestInitializeWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -564,6 +596,8 @@ func TestInitializeWithError(t *testing.T) {
 }
 
 func TestInitializeCopyNoError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Copy = &config.CopySection{InitializeCopyChunkerParams: maybe.False()}
 	ctx := &Context{
@@ -577,6 +611,8 @@ func TestInitializeCopyNoError(t *testing.T) {
 }
 
 func TestInitializeCopyWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Copy = &config.CopySection{InitializeCopyChunkerParams: maybe.False()}
 	ctx := &Context{
@@ -591,6 +627,8 @@ func TestInitializeCopyWithError(t *testing.T) {
 }
 
 func TestCheckNoError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -603,6 +641,8 @@ func TestCheckNoError(t *testing.T) {
 }
 
 func TestCheckWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -616,6 +656,8 @@ func TestCheckWithError(t *testing.T) {
 }
 
 func TestRetentionNoError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -628,6 +670,8 @@ func TestRetentionNoError(t *testing.T) {
 }
 
 func TestRetentionWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -641,6 +685,8 @@ func TestRetentionWithError(t *testing.T) {
 }
 
 func TestBackupWithStreamSource(t *testing.T) {
+	t.Parallel()
+
 	expected := "---Backup-Content---"
 	expectedInterruptedError := []string{
 		"stdin-test on profile 'name': exit status 128",
@@ -797,6 +843,8 @@ func TestBackupWithStreamSource(t *testing.T) {
 }
 
 func TestBackupWithSuccess(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Backup = &config.BackupSection{}
 	ctx := &Context{
@@ -810,6 +858,8 @@ func TestBackupWithSuccess(t *testing.T) {
 }
 
 func TestBackupWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Backup = &config.BackupSection{}
 	ctx := &Context{
@@ -824,6 +874,8 @@ func TestBackupWithError(t *testing.T) {
 }
 
 func TestBackupWithResticLockFailureRetried(t *testing.T) {
+	t.Parallel()
+
 	lockWait := constants.MinResticLockRetryDelay + time.Second
 	lockMessage := "unable to create lock in backend: repository is already locked exclusively by PID 60485 on VM by user (UID 503, GID 23)" + platform.LineSeparator +
 		"lock was created at 2023-09-24 15:29:57 (69.406ms ago)" + platform.LineSeparator +
@@ -858,6 +910,8 @@ func TestBackupWithResticLockFailureRetried(t *testing.T) {
 }
 
 func TestBackupWithResticLockFailureCancelled(t *testing.T) {
+	t.Parallel()
+
 	lockWait := constants.MinResticLockRetryDelay + time.Second
 	lockMessage := "unable to create lock in backend: repository is already locked exclusively by PID 60485 on VM by user (UID 503, GID 23)" + platform.LineSeparator +
 		"lock was created at 2023-09-24 15:29:57 (69.406ms ago)" + platform.LineSeparator +
@@ -896,6 +950,8 @@ func TestBackupWithResticLockFailureCancelled(t *testing.T) {
 }
 
 func TestBackupWithNoConfiguration(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -909,6 +965,8 @@ func TestBackupWithNoConfiguration(t *testing.T) {
 }
 
 func TestBackupWithNoConfigurationButStatusFile(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.StatusFile = "status.json"
 	ctx := &Context{
@@ -924,6 +982,8 @@ func TestBackupWithNoConfigurationButStatusFile(t *testing.T) {
 }
 
 func TestBackupWithWarningAsError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(nil, "name")
 	profile.Backup = &config.BackupSection{}
 	ctx := &Context{
@@ -938,6 +998,8 @@ func TestBackupWithWarningAsError(t *testing.T) {
 }
 
 func TestBackupWithSupressedWarnings(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Backup = &config.BackupSection{NoErrorOnWarning: true}
 	ctx := &Context{
@@ -952,6 +1014,8 @@ func TestBackupWithSupressedWarnings(t *testing.T) {
 }
 
 func TestRunShellCommands(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Backup = &config.BackupSection{}
 	profile.Check = &config.SectionWithScheduleAndMonitoring{}
@@ -1031,6 +1095,8 @@ func TestRunStreamErrorHandler(t *testing.T) {
 }
 
 func TestRunStreamErrorHandlerDoesNotBreakCommand(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Backup = &config.BackupSection{}
 	profile.StreamError = []config.StreamErrorSection{{Pattern: ".+error-line.+", Run: "exit 1"}}
@@ -1047,6 +1113,8 @@ func TestRunStreamErrorHandlerDoesNotBreakCommand(t *testing.T) {
 }
 
 func TestStreamErrorHandlerWithInvalidRegex(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Backup = &config.BackupSection{}
 	profile.StreamError = []config.StreamErrorSection{{Pattern: "(", Run: "echo pass"}}
@@ -1063,6 +1131,8 @@ func TestStreamErrorHandlerWithInvalidRegex(t *testing.T) {
 }
 
 func TestCanRetryAfterErrorDontFailWhenNoOutputAnalysis(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	ctx := &Context{
 		binary:  mockBinary,
@@ -1077,6 +1147,8 @@ func TestCanRetryAfterErrorDontFailWhenNoOutputAnalysis(t *testing.T) {
 }
 
 func TestCanRetryAfterRemoteStaleLockFailure(t *testing.T) {
+	t.Parallel()
+
 	lockedSince := time.Duration(0)
 	mockOutput := mocks.NewOutputAnalysis(t)
 	mockOutput.EXPECT().ContainsRemoteLockFailure().Return(true)
@@ -1134,10 +1206,12 @@ func TestCanRetryAfterRemoteStaleLockFailure(t *testing.T) {
 }
 
 func TestCanRetryAfterRemoteLockFailure(t *testing.T) {
+	t.Parallel()
+
 	lockFailure := false
 	mockOutput := mocks.NewOutputAnalysis(t)
 	mockOutput.EXPECT().ContainsRemoteLockFailure().RunAndReturn(func() bool { return lockFailure })
-	mockOutput.EXPECT().GetRemoteLockedBy().Return("TestCanRetryAfterRemoteLockFailure", true)
+	mockOutput.EXPECT().GetRemoteLockedBy().Return(t.Name(), true)
 	mockOutput.EXPECT().GetRemoteLockedSince().Return(5*time.Minute, true)
 
 	profile := config.NewProfile(&config.Config{}, "name")
@@ -1187,6 +1261,8 @@ func TestCanRetryAfterRemoteLockFailure(t *testing.T) {
 }
 
 func TestCanUseResticLockRetry(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Repository = config.NewConfidentialValue("my-repo")
 	emptyArgs := shell.NewArgs()
@@ -1350,6 +1426,8 @@ func TestLocksAndLockWait(t *testing.T) {
 }
 
 func TestGetContext(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "TestProfile")
 	ctx := &Context{
 		binary:  "",
@@ -1368,6 +1446,8 @@ func TestGetContext(t *testing.T) {
 }
 
 func TestGetContextWithError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "TestProfile")
 	ctx := &Context{
 		binary:  "",
@@ -1386,6 +1466,8 @@ func TestGetContextWithError(t *testing.T) {
 }
 
 func TestGetErrorContext(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1402,6 +1484,8 @@ func TestGetErrorContext(t *testing.T) {
 }
 
 func TestGetErrorContextWithStandardError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1418,6 +1502,8 @@ func TestGetErrorContextWithStandardError(t *testing.T) {
 }
 
 func TestGetErrorContextWithCommandError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1440,6 +1526,8 @@ func TestGetErrorContextWithCommandError(t *testing.T) {
 }
 
 func TestGetProfileEnvironment(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "TestProfile")
 	ctx := &Context{
 		binary:  "",
@@ -1454,6 +1542,8 @@ func TestGetProfileEnvironment(t *testing.T) {
 }
 
 func TestGetFailEnvironmentNoError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1468,6 +1558,8 @@ func TestGetFailEnvironmentNoError(t *testing.T) {
 }
 
 func TestGetFailEnvironmentWithStandardError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1482,6 +1574,8 @@ func TestGetFailEnvironmentWithStandardError(t *testing.T) {
 }
 
 func TestGetFailEnvironmentWithCommandError(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "")
 	ctx := &Context{
 		binary:  "",
@@ -1590,6 +1684,8 @@ func TestRunInitCopyCommand(t *testing.T) {
 }
 
 func TestCopyNoSnapshot(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Copy = &config.CopySection{}
 	ctx := &Context{
@@ -1604,6 +1700,8 @@ func TestCopyNoSnapshot(t *testing.T) {
 }
 
 func TestCopySnapshot(t *testing.T) {
+	t.Parallel()
+
 	profile := config.NewProfile(&config.Config{}, "name")
 	profile.Copy = &config.CopySection{Snapshots: []string{"snapshot1", "snapshot2"}}
 	ctx := &Context{