Repository: creativeprojects/resticprofile (language: python)

Issue:
[Suggestion] Add --profile as an alias to --name
I get why it's 'name'. The tool itself is called so and so profile. But it sounds more descriptive with 'profile' I think. (Bit more discoverable too). Especially since within profiles.yaml each section is referred mostly as a profile regardless of the binary's name. An alias would be more than enough to cover this if it makes sense for people.


Discussion / hints:
Yes, we can easily add an alias 😉 

BTW there's also another syntax you might like: `<profile>.<command>`.

Like:
```
resticprofile root_profile.backup
```

@creativeprojects , it might make sense to support `backup.profile` also. With command completion I often first think of the task (e.g. backup) and then of the profile. Would be good if either order is supported.
Yeah I agree. I started to use `<command>@<profile>` for the `run-schedule` command (for `v1`), which is not something the user would see anyway, but we could also accept it.

Fix patch:
diff --git a/config/mocks/NamedPropertySet.go b/config/mocks/NamedPropertySet.go
index 4462f2da..dd922974 100644
--- a/config/mocks/NamedPropertySet.go
+++ b/config/mocks/NamedPropertySet.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 
diff --git a/config/mocks/ProfileInfo.go b/config/mocks/ProfileInfo.go
index cd6669d4..f60e7e12 100644
--- a/config/mocks/ProfileInfo.go
+++ b/config/mocks/ProfileInfo.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 
diff --git a/config/mocks/PropertyInfo.go b/config/mocks/PropertyInfo.go
index 757cd1ec..0741bfa6 100644
--- a/config/mocks/PropertyInfo.go
+++ b/config/mocks/PropertyInfo.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 
diff --git a/config/mocks/SectionInfo.go b/config/mocks/SectionInfo.go
index 537019a3..d7ef7ff5 100644
--- a/config/mocks/SectionInfo.go
+++ b/config/mocks/SectionInfo.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 
diff --git a/flags.go b/flags.go
index fa999a1f..174ce88f 100644
--- a/flags.go
+++ b/flags.go
@@ -114,6 +114,14 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
 	flagset.IntVar(&flags.ignoreOnBattery, "ignore-on-battery", flags.ignoreOnBattery, "don't start the profile when the computer is running on battery. You can specify a value to ignore only when the % charge left is less or equal than the value")
 	flagset.Lookup("ignore-on-battery").NoOptDefVal = "100" // 0 is flag not set, 100 is for a flag with no value (meaning just battery discharge)
 
+	flagset.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
+		switch name {
+		case "profile":
+			name = "name"
+		}
+		return pflag.NormalizedName(name)
+	})
+
 	if platform.IsWindows() {
 		// flag for internal use only
 		flagset.BoolVar(&flags.isChild, constants.FlagAsChild, false, "run as an elevated user child process")
@@ -148,27 +156,50 @@ func loadFlags(args []string) (*pflag.FlagSet, commandLineFlags, error) {
 
 	// parse first positional argument as <profile>.<command> if the profile was not set via name
 	nameFlag := flagset.Lookup("name")
-	if (nameFlag == nil || !nameFlag.Changed) && strings.Contains(flags.resticArgs[0], ".") {
-		// split first argument at `.`
-		profileAndCommand := strings.Split(flags.resticArgs[0], ".")
-		// last element will be used as restic command
-		command := profileAndCommand[len(profileAndCommand)-1]
-		// remaining elements will be stiched together with `.` and used as profile name
-		profile := strings.Join(profileAndCommand[0:len(profileAndCommand)-1], ".")
-
-		// set command
-		if len(command) == 0 {
-			// take default command by removing it from resticArgs
-			flags.resticArgs = flags.resticArgs[1:]
-		} else {
-			flags.resticArgs[0] = command
-		}
+	if nameFlag == nil || !nameFlag.Changed {
+		if strings.Contains(flags.resticArgs[0], ".") {
+			// split first argument at `.`
+			profileAndCommand := strings.Split(flags.resticArgs[0], ".")
+			// last element will be used as restic command
+			command := profileAndCommand[len(profileAndCommand)-1]
+			// remaining elements will be stitched together with `.` and used as profile name
+			profile := strings.Join(profileAndCommand[0:len(profileAndCommand)-1], ".")
+
+			// set command
+			if len(command) == 0 {
+				// take default command by removing it from resticArgs
+				flags.resticArgs = flags.resticArgs[1:]
+			} else {
+				flags.resticArgs[0] = command
+			}
 
-		// set profile
-		if len(profile) == 0 {
-			profile = constants.DefaultProfileName
+			// set profile
+			if len(profile) == 0 {
+				profile = constants.DefaultProfileName
+			}
+			flags.name = profile
+		} else if strings.Contains(flags.resticArgs[0], "@") {
+			// split first argument at `@`
+			commandAndProfile := strings.Split(flags.resticArgs[0], "@")
+			// first element will be used as restic command
+			command := commandAndProfile[0]
+			// last element(s) will be used as profile name
+			profile := strings.Join(commandAndProfile[1:], "@")
+
+			// set command
+			if len(command) == 0 {
+				// take default command by removing it from resticArgs
+				flags.resticArgs = flags.resticArgs[1:]
+			} else {
+				flags.resticArgs[0] = command
+			}
+
+			// set profile
+			if len(profile) == 0 {
+				profile = constants.DefaultProfileName
+			}
+			flags.name = profile
 		}
-		flags.name = profile
 	}
 
 	return flagset, flags, nil
diff --git a/go.mod b/go.mod
index f4501214..e973295a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/creativeprojects/resticprofile
 
-go 1.21
+go 1.22
 
 require (
 	github.com/Masterminds/semver/v3 v3.2.1
diff --git a/monitor/mocks/OutputAnalysis.go b/monitor/mocks/OutputAnalysis.go
index 20ba9538..28dd1393 100644
--- a/monitor/mocks/OutputAnalysis.go
+++ b/monitor/mocks/OutputAnalysis.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 
diff --git a/schedule/mocks/Handler.go b/schedule/mocks/Handler.go
index 7f7567d5..ab2c8b9c 100644
--- a/schedule/mocks/Handler.go
+++ b/schedule/mocks/Handler.go
@@ -1,4 +1,4 @@
-// Code generated by mockery v2.42.0. DO NOT EDIT.
+// Code generated by mockery v2.42.1. DO NOT EDIT.
 
 package mocks
 


Test patch:
diff --git a/flags_test.go b/flags_test.go
index 0806ebc6..b491c3c0 100644
--- a/flags_test.go
+++ b/flags_test.go
@@ -3,6 +3,7 @@ package main
 import (
 	"fmt"
 	"os"
+	"strings"
 	"testing"
 	"time"
 
@@ -115,84 +116,141 @@ func TestEnvOverridesError(t *testing.T) {
 	assert.Contains(t, mem.Logs(), `cannot convert env variable RESTICPROFILE_LOCK_WAIT="no-valid-duration": time: invalid duration "no-valid-duration"`)
 }
 
-func TestProfileCommandWithProfileNamePrecedence(t *testing.T) {
-	_, flags, err := loadFlags([]string{"-n", "profile2", "-v", "some.command"})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, "profile2")
-	assert.True(t, flags.verbose)
-	assert.Equal(t, flags.resticArgs, []string{"some.command"})
-}
-
-func TestProfileCommandWithProfileNamePrecedenceWithDefaultProfile(t *testing.T) {
-	_, flags, err := loadFlags([]string{"-n", constants.DefaultProfileName, "-v", "some.other.command"})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, constants.DefaultProfileName)
-	assert.True(t, flags.verbose)
-	assert.Equal(t, flags.resticArgs, []string{"some.other.command"})
-}
-
-func TestProfileCommandWithResticVerbose(t *testing.T) {
-	_, flags, err := loadFlags([]string{"profile1.check", "--", "-v"})
-	require.NoError(t, err)
-	assert.False(t, flags.help)
-	assert.Equal(t, flags.name, "profile1")
-	assert.False(t, flags.verbose)
-	assert.Equal(t, flags.resticArgs, []string{"check", "--", "-v"})
-}
-
-func TestProfileCommandWithResticprofileVerbose(t *testing.T) {
-	_, flags, err := loadFlags([]string{"-v", "pro.file1.backup"})
-	require.NoError(t, err)
-	assert.True(t, flags.verbose)
-	assert.Equal(t, flags.name, "pro.file1")
-	assert.Equal(t, flags.resticArgs, []string{"backup"})
-}
-
-func TestProfileCommandThreePart(t *testing.T) {
-	_, flags, err := loadFlags([]string{"bla.foo.backup"})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, "bla.foo")
-	assert.Equal(t, flags.resticArgs, []string{"backup"})
-}
-
-func TestProfileCommandTwoPartCommandMissing(t *testing.T) {
-	_, flags, err := loadFlags([]string{"bar."})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, "bar")
-	assert.Equal(t, flags.resticArgs, []string{})
-}
-
-func TestProfileCommandTwoPartProfileMissing(t *testing.T) {
-	_, flags, err := loadFlags([]string{".baz"})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, constants.DefaultProfileName)
-	assert.Equal(t, flags.resticArgs, []string{"baz"})
-}
-
-func TestProfileCommandTwoPartDotPrefix(t *testing.T) {
-	_, flags, err := loadFlags([]string{".baz.qux"})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, ".baz")
-	assert.Equal(t, flags.resticArgs, []string{"qux"})
-}
-
-func TestProfileCommandThreePartCommandMissing(t *testing.T) {
-	_, flags, err := loadFlags([]string{"quux.quuz."})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, "quux.quuz")
-	assert.Equal(t, flags.resticArgs, []string{})
-}
-
-func TestProfileCommandTwoPartDotPrefixCommandMissing(t *testing.T) {
-	_, flags, err := loadFlags([]string{".corge."})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, ".corge")
-	assert.Equal(t, flags.resticArgs, []string{})
-}
-
-func TestProfileCommandTwoPartProfileMissingCommandMissing(t *testing.T) {
-	_, flags, err := loadFlags([]string{"."})
-	require.NoError(t, err)
-	assert.Equal(t, flags.name, constants.DefaultProfileName)
-	assert.Equal(t, flags.resticArgs, []string{})
+func TestCommandProfileOrProfileCommandShortcuts(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		sourceArgs      []string
+		expectedProfile string
+		expectedArgs    []string
+		expectedVerbose bool
+	}{
+		{
+			sourceArgs:      []string{"."},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{".command"},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"profile."},
+			expectedProfile: "profile",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{".profile."},
+			expectedProfile: ".profile",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{"pro.file."},
+			expectedProfile: "pro.file",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{".profile.command"},
+			expectedProfile: ".profile",
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"pro.file.command"},
+			expectedProfile: "pro.file",
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"-v", "pro.file1.backup"},
+			expectedProfile: "pro.file1",
+			expectedArgs:    []string{"backup"},
+			expectedVerbose: true,
+		},
+		{
+			sourceArgs:      []string{"profile1.check", "--", "-v"},
+			expectedProfile: "profile1",
+			expectedArgs:    []string{"check", "--", "-v"},
+			expectedVerbose: false,
+		},
+		{
+			sourceArgs:      []string{"-n", constants.DefaultProfileName, "-v", "some.other.command"},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{"some.other.command"},
+			expectedVerbose: true,
+		},
+		{
+			sourceArgs:      []string{"-n", "profile2", "-v", "some.command"},
+			expectedProfile: "profile2",
+			expectedArgs:    []string{"some.command"},
+			expectedVerbose: true,
+		},
+		{
+			sourceArgs:      []string{"@"},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{"command@"},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"@profile"},
+			expectedProfile: "profile",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{"@profile@"},
+			expectedProfile: "profile@",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{"command@profile@"},
+			expectedProfile: "profile@",
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"@pro@file"},
+			expectedProfile: "pro@file",
+			expectedArgs:    []string{},
+		},
+		{
+			sourceArgs:      []string{"command@pro@file"},
+			expectedProfile: "pro@file",
+			expectedArgs:    []string{"command"},
+		},
+		{
+			sourceArgs:      []string{"-v", "backup@pro@file1"},
+			expectedProfile: "pro@file1",
+			expectedArgs:    []string{"backup"},
+			expectedVerbose: true,
+		},
+		{
+			sourceArgs:      []string{"check@profile1", "--", "-v"},
+			expectedProfile: "profile1",
+			expectedArgs:    []string{"check", "--", "-v"},
+			expectedVerbose: false,
+		},
+		{
+			sourceArgs:      []string{"-n", constants.DefaultProfileName, "-v", "some@other@command"},
+			expectedProfile: constants.DefaultProfileName,
+			expectedArgs:    []string{"some@other@command"},
+			expectedVerbose: true,
+		},
+		{
+			sourceArgs:      []string{"-n", "profile2", "-v", "some@command"},
+			expectedProfile: "profile2",
+			expectedArgs:    []string{"some@command"},
+			expectedVerbose: true,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(strings.Join(testCase.sourceArgs, " "), func(t *testing.T) {
+			t.Parallel()
+			_, flags, err := loadFlags(testCase.sourceArgs)
+			require.NoError(t, err)
+			assert.Equal(t, flags.name, testCase.expectedProfile)
+			assert.Equal(t, flags.resticArgs, testCase.expectedArgs)
+			assert.Equal(t, flags.verbose, testCase.expectedVerbose)
+		})
+	}
 }