Compare commits

...

4 Commits

Author SHA1 Message Date
Abin Simon
6e7b74cb6d Please vale lint 2023-11-16 19:49:31 +05:30
Abin Simon
78f2dbdccb Add documentation for completion command 2023-11-16 19:46:05 +05:30
Abin Simon
84f4400635 Update CHANGELOG and known issues 2023-11-16 19:06:26 +05:30
Abin Simon
bfea3dea34 Add autocompletion for cli commands and flags
This add autocompletion for all flags and commands in the cli.
To use this you'll have to source the completions file generated by
`corso completion [bash|zsh|fish|powershell]`.

You can do that by doing the following (example for bash):

``` bash
corso completion bash > /tmp/corso_completions
source /tmp/corso_completions
```
2023-11-16 18:43:14 +05:30
36 changed files with 524 additions and 52 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added export support for emails in exchange backups as `.eml` files
- More colorful and informational cli output
- CLI completions for corso commands and flags (bash, zsh, fish, powershell)
### Changed
- Change file extension of messages export to json to match the content
@ -19,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Automatically re-run a full delta query on incremental if the prior backup is found to have malformed prior-state information.
- Retry drive item permission downloads during long-running backups after the jwt token expires and refreshes.
### Known issues
- CLI completions cannot autocomplete multiple values for flags
## [v0.15.0] (beta) - 2023-10-31
### Added

View File

@ -75,7 +75,7 @@ func backupCmd() *cobra.Command {
Short: "Backup your service data",
Long: `Backup the data stored in one of your M365 services.`,
RunE: handleBackupCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -94,7 +94,7 @@ func createCmd() *cobra.Command {
Use: createCommand,
Short: "Backup an M365 Service",
RunE: handleCreateCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -113,7 +113,7 @@ func listCmd() *cobra.Command {
Use: listCommand,
Short: "List the history of backups",
RunE: handleListCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -132,7 +132,7 @@ func detailsCmd() *cobra.Command {
Use: detailsCommand,
Short: "Shows the details of a backup",
RunE: handleDetailsCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -151,7 +151,7 @@ func deleteCmd() *cobra.Command {
Use: deleteCommand,
Short: "Deletes a backup",
RunE: handleDeleteCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}

View File

@ -77,7 +77,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// Flags addition ordering should follow the order we want them to appear in help and docs:
// More generic (ex: --user) and more frequently used flags take precedence.
flags.AddMailBoxFlag(c)
flags.AddMailBoxFlag(c, utils.MailboxCompletionFunc(path.ExchangeService))
flags.AddDataFlag(c, []string{dataEmail, dataContacts, dataEvents}, false)
flags.AddFetchParallelismFlag(c)
flags.AddDisableDeltaFlag(c)
@ -90,7 +90,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
c, fs = utils.AddCommand(cmd, exchangeListCmd())
fs.SortFlags = false
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.ExchangeService))
flags.AddAllBackupListFlags(c)
case detailsCommand:
@ -104,7 +104,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// Flags addition ordering should follow the order we want them to appear in help and docs:
// More generic (ex: --user) and more frequently used flags take precedence.
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.ExchangeService))
flags.AddExchangeDetailsAndRestoreFlags(c, false)
case deleteCommand:
@ -115,7 +115,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
c.Example = exchangeServiceCommandDeleteExamples
flags.AddMultipleBackupIDsFlag(c, false)
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.ExchangeService))
}
return c

View File

@ -70,7 +70,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
c.Example = groupsServiceCommandCreateExamples
// Flags addition ordering should follow the order we want them to appear in help and docs:
flags.AddGroupFlag(c)
flags.AddGroupFlag(c, utils.GroupsCompletionFunc())
flags.AddDataFlag(c, []string{flags.DataLibraries, flags.DataMessages}, false)
flags.AddFetchParallelismFlag(c)
flags.AddDisableDeltaFlag(c)
@ -80,7 +80,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
c, fs = utils.AddCommand(cmd, groupsListCmd(), utils.MarkPreviewCommand())
fs.SortFlags = false
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.GroupsService))
flags.AddAllBackupListFlags(c)
case detailsCommand:
@ -94,7 +94,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
// Flags addition ordering should follow the order we want them to appear in help and docs:
// More generic (ex: --user) and more frequently used flags take precedence.
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.GroupsService))
flags.AddGroupDetailsAndRestoreFlags(c)
flags.AddSharePointDetailsAndRestoreFlags(c)
@ -106,7 +106,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
c.Example = groupsServiceCommandDeleteExamples
flags.AddMultipleBackupIDsFlag(c, false)
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.GroupsService))
}
return c

View File

@ -65,14 +65,14 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
c.Use = c.Use + " " + oneDriveServiceCommandCreateUseSuffix
c.Example = oneDriveServiceCommandCreateExamples
flags.AddUserFlag(c)
flags.AddUserFlag(c, utils.UsersCompletionFunc(path.OneDriveService))
flags.AddGenericBackupFlags(c)
case listCommand:
c, fs = utils.AddCommand(cmd, oneDriveListCmd())
fs.SortFlags = false
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.OneDriveService))
flags.AddAllBackupListFlags(c)
case detailsCommand:
@ -83,7 +83,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
c.Example = oneDriveServiceCommandDetailsExamples
flags.AddSkipReduceFlag(c)
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.OneDriveService))
flags.AddOneDriveDetailsAndRestoreFlags(c)
case deleteCommand:
@ -94,7 +94,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
c.Example = oneDriveServiceCommandDeleteExamples
flags.AddMultipleBackupIDsFlag(c, false)
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.OneDriveService))
}
return c

View File

@ -76,7 +76,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
c.Use = c.Use + " " + sharePointServiceCommandCreateUseSuffix
c.Example = sharePointServiceCommandCreateExamples
flags.AddSiteFlag(c, true)
flags.AddSiteFlag(c, true, utils.SitesCompletionFunc())
flags.AddSiteIDFlag(c, true)
flags.AddDataFlag(c, []string{flags.DataLibraries}, true)
flags.AddGenericBackupFlags(c)
@ -85,7 +85,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
c, fs = utils.AddCommand(cmd, sharePointListCmd())
fs.SortFlags = false
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.SharePointService))
flags.AddAllBackupListFlags(c)
case detailsCommand:
@ -96,7 +96,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
c.Example = sharePointServiceCommandDetailsExamples
flags.AddSkipReduceFlag(c)
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.SharePointService))
flags.AddSharePointDetailsAndRestoreFlags(c)
case deleteCommand:
@ -107,7 +107,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
c.Example = sharePointServiceCommandDeleteExamples
flags.AddMultipleBackupIDsFlag(c, false)
flags.AddBackupIDFlag(c, false)
flags.AddBackupIDFlag(c, false, utils.BackupIDCompletionFunc(path.SharePointService))
}
return c

View File

@ -2,6 +2,7 @@ package cli
import (
"context"
"fmt"
"os"
"regexp"
"strings"
@ -54,7 +55,19 @@ func preRun(cc *cobra.Command, args []string) error {
}
avoidTheseCommands := []string{
"corso", "env", "help", "backup", "details", "list", "restore", "export", "delete", "repo", "init", "connect",
"corso",
"env",
"help",
"backup",
"details",
"list",
"restore",
"export",
"delete",
"repo",
"init",
"connect",
"completion [bash|zsh|fish|powershell]",
}
if len(logger.ResolvedLogFile) > 0 && !slices.Contains(avoidTheseCommands, cc.Use) {
@ -121,6 +134,7 @@ func BuildCommandTree(cmd *cobra.Command) {
cmd.SetUsageTemplate(indentExamplesTemplate(corsoCmd.UsageTemplate()))
cmd.CompletionOptions.DisableDefaultCmd = true
cmd.SuggestionsMinimumDistance = 2 // default
repo.AddCommands(cmd)
backup.AddCommands(cmd)
@ -128,6 +142,53 @@ func BuildCommandTree(cmd *cobra.Command) {
export.AddCommands(cmd)
debug.AddCommands(cmd)
help.AddCommands(cmd)
AddCompletion(cmd)
}
// We are not using the default completion command as it will be
// harder to control it, for example skipping printing "Logging to file"
// message
func AddCompletion(cmd *cobra.Command) {
longMessage := `Generate shell completion script for Corso.
These need to be hooked into your shell to enable completions for Corso.
For bash, add the following line to your ` + "`~/.bashrc` \n" + // two spaces for markdown and \n for go
"`eval \"$(corso completion bash)\"` \n" + `
For zsh, add the following line to your ` + "`~/.zshrc` \n" +
"`eval \"$(corso completion zsh)\"` \n" + `
For fish, add the following line to your ` + "`~/.config/fish/config.fish` \n" +
"`corso completion fish | source` \n" + `
For powershell, add the following to your ` + "`$PROFILE` \n" +
"`Invoke-Expression \"$(corso completion powershell)\"`"
completion := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: longMessage,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
}
return fmt.Errorf("unknown shell type %q", args[0])
},
}
cmd.AddCommand(completion)
}
// ------------------------------------------------------------------------------------------

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -26,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.ExchangeService))
}
return c

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -26,7 +27,7 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.GroupsService))
}
return c

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -26,7 +27,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.OneDriveService))
}
return c

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -26,7 +27,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.SharePointService))
}
return c

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
)
// called by export.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.ExchangeService))
flags.AddExchangeDetailsAndRestoreFlags(c, true)
flags.AddExportConfigFlags(c)
flags.AddFailFastFlag(c)

View File

@ -49,7 +49,7 @@ func exportCmd() *cobra.Command {
Short: "Export your service data",
Long: `Export the data stored in one of your M365 services.`,
RunE: handleExportCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
// called by export.go to map subcommands to provider-specific handling.
@ -27,8 +28,8 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddSiteFlag(c, false)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.GroupsService))
flags.AddSiteFlag(c, false, utils.SitesCompletionFunc())
flags.AddSiteIDFlag(c, false)
flags.AddSharePointDetailsAndRestoreFlags(c)
flags.AddGroupDetailsAndRestoreFlags(c)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
)
// called by export.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.OneDriveService))
flags.AddOneDriveDetailsAndRestoreFlags(c)
flags.AddExportConfigFlags(c)
flags.AddFailFastFlag(c)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
)
// called by export.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.SharePointService))
flags.AddSharePointDetailsAndRestoreFlags(c)
flags.AddExportConfigFlags(c)
flags.AddFailFastFlag(c)

View File

@ -68,9 +68,14 @@ func AddGroupDetailsAndRestoreFlags(cmd *cobra.Command) {
// Mail is most accurate, MailNickame is accurate and shorter, but the end user
// may not see either one visibly.
// https://learn.microsoft.com/en-us/graph/api/group-list?view=graph-rest-1.0&tabs=http
func AddGroupFlag(cmd *cobra.Command) {
func AddGroupFlag(
cmd *cobra.Command,
completionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective),
) {
cmd.Flags().StringSliceVar(
&GroupFV,
GroupFN, nil,
"Backup data by group; accepts '"+Wildcard+"' to select all groups.")
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(GroupFN, completionFunc))
}

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
)
var CategoryDataFV []string
@ -39,4 +40,36 @@ func AddDataFlag(cmd *cobra.Command, allowed []string, hide bool) {
if hide {
cobra.CheckErr(fs.MarkHidden(CategoryDataFN))
}
// TODO(meain): This is a hacky way to get it to autocomplete multiple items
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(
CategoryDataFN,
func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
added := strings.Split(toComplete, ",")
last := added[len(added)-1]
added = added[:len(added)-1]
if slices.Contains(added, last) {
added = append(added, last)
last = ""
}
pending := make([]string, 0, len(allowed)-len(added))
for _, a := range allowed {
if !slices.Contains(added, a) && strings.HasPrefix(a, last) {
pending = append(pending, a)
}
}
completions := []string{}
for _, p := range pending {
completions = append(completions, strings.Join(append(added, p), ","))
}
return completions, cobra.ShellCompDirectiveNoSpace
}))
}

View File

@ -22,16 +22,24 @@ var (
)
// AddUserFlag adds the --user flag.
func AddUserFlag(cmd *cobra.Command) {
func AddUserFlag(
cmd *cobra.Command,
completionFunc func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective),
) {
cmd.Flags().StringSliceVar(
&UserFV,
UserFN, nil,
"Backup a specific user's data; accepts '"+Wildcard+"' to select all users.")
cobra.CheckErr(cmd.MarkFlagRequired(UserFN))
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(UserFN, completionFunc))
}
// AddMailBoxFlag adds the --user and --mailbox flag.
func AddMailBoxFlag(cmd *cobra.Command) {
func AddMailBoxFlag(
cmd *cobra.Command,
completionFunc func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective),
) {
flags := cmd.Flags()
flags.StringSliceVar(
@ -41,10 +49,22 @@ func AddMailBoxFlag(cmd *cobra.Command) {
cobra.CheckErr(flags.MarkDeprecated(UserFN, fmt.Sprintf("use --%s instead", MailBoxFN)))
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(UserFN,
func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
message := fmt.Sprintf("This flag is deprecated, Use --%s instead", MailBoxFN)
return cobra.AppendActiveHelp(nil, message), cobra.ShellCompDirectiveNoFileComp
}))
flags.StringSliceVar(
&UserFV,
MailBoxFN, nil,
"Backup a specific mailbox's data; accepts '"+Wildcard+"' to select all mailbox.")
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(MailBoxFN, completionFunc))
}
// AddAzureCredsFlags adds M365 cred flags

View File

@ -41,8 +41,13 @@ func AddMultipleBackupIDsFlag(cmd *cobra.Command, require bool) {
}
// AddBackupIDFlag adds the --backup flag.
func AddBackupIDFlag(cmd *cobra.Command, require bool) {
func AddBackupIDFlag(
cmd *cobra.Command,
require bool,
completionFunc func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective),
) {
cmd.Flags().StringVar(&BackupIDFV, BackupFN, "", "ID of the backup to retrieve.")
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(BackupFN, completionFunc))
if require {
cobra.CheckErr(cmd.MarkFlagRequired(BackupFN))

View File

@ -95,6 +95,7 @@ func AddSharePointDetailsAndRestoreFlags(cmd *cobra.Command) {
// AddSiteIDFlag adds the --site-id flag, which accepts site ID values.
// This flag is hidden, since we expect users to prefer the --site url
// and do not want to encourage confusion.
// TODO(meain): --site is the primary one, but it would be useful to have comepletion for this as well
func AddSiteIDFlag(cmd *cobra.Command, multiple bool) {
fs := cmd.Flags()
@ -112,11 +113,17 @@ func AddSiteIDFlag(cmd *cobra.Command, multiple bool) {
}
// AddSiteFlag adds the --site flag, which accepts webURL values.
func AddSiteFlag(cmd *cobra.Command, multiple bool) {
func AddSiteFlag(
cmd *cobra.Command,
multiple bool,
completionFunc func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective),
) {
message := "Web URL of the site to operate on"
if multiple {
message += "; accepts '" + Wildcard + "' to select all sites."
}
cmd.Flags().StringSliceVar(&WebURLFV, SiteFN, nil, message)
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(SiteFN, completionFunc))
}

View File

@ -4,6 +4,7 @@ import (
"github.com/spf13/cobra"
. "github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/cli/utils"
)
// AddCommands attaches all `corso env * *` commands to the parent.
@ -19,7 +20,7 @@ func envCmd() *cobra.Command {
Short: "env var guide",
Long: `A guide to using environment variables in Corso.`,
RunE: handleEnvCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
envCmd.SetHelpFunc(envGuide)

View File

@ -77,7 +77,7 @@ func repoCmd() *cobra.Command {
Short: "Manage your repositories",
Long: `Initialize, configure, connect and update to your account backup repositories`,
RunE: handleRepoCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -95,7 +95,7 @@ func initCmd() *cobra.Command {
Short: "Initialize a repository.",
Long: `Create a new repository to store your backups.`,
RunE: handleInitCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -112,7 +112,7 @@ func connectCmd() *cobra.Command {
Short: "Connect to a repository.",
Long: `Connect to an existing repository.`,
RunE: handleConnectCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}
@ -127,7 +127,7 @@ func maintenanceCmd() *cobra.Command {
Short: "Run maintenance on an existing repository",
Long: `Run maintenance on an existing repository to optimize performance and storage use`,
RunE: handleMaintenanceCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
}
}

View File

@ -6,6 +6,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/path"
)
// called by restore.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addExchangeCommands(cmd *cobra.Command) *cobra.Command {
// general flags
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.ExchangeService))
flags.AddExchangeDetailsAndRestoreFlags(c, false)
flags.AddRestoreConfigFlags(c, true)
flags.AddFailFastFlag(c)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/path"
)
// called by restore.go to map subcommands to provider-specific handling.
@ -26,8 +27,8 @@ func addGroupsCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddSiteFlag(c, false)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.GroupsService))
flags.AddSiteFlag(c, false, utils.SitesCompletionFunc())
flags.AddSiteIDFlag(c, false)
flags.AddNoPermissionsFlag(c)
flags.AddSharePointDetailsAndRestoreFlags(c)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/path"
)
// called by restore.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addOneDriveCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --user) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.OneDriveService))
flags.AddOneDriveDetailsAndRestoreFlags(c)
flags.AddNoPermissionsFlag(c)
flags.AddRestoreConfigFlags(c, true)

View File

@ -74,7 +74,7 @@ func restoreCmd() *cobra.Command {
Short: "Restore your service data",
Long: `Restore the data stored in one of your M365 services.`,
RunE: handleRestoreCmd,
Args: cobra.NoArgs,
Args: utils.SubcommandsRequiredWithSuggestions,
Example: restoreCommandExamples,
}
}

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/path"
)
// called by restore.go to map subcommands to provider-specific handling.
@ -26,7 +27,7 @@ func addSharePointCommands(cmd *cobra.Command) *cobra.Command {
// More generic (ex: --site) and more frequently used flags take precedence.
fs.SortFlags = false
flags.AddBackupIDFlag(c, true)
flags.AddBackupIDFlag(c, true, utils.BackupIDCompletionFunc(path.SharePointService))
flags.AddSharePointDetailsAndRestoreFlags(c)
flags.AddNoPermissionsFlag(c)
flags.AddRestoreConfigFlags(c, true)

View File

@ -0,0 +1,194 @@
package utils
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/pkg/backup"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/services/m365"
"github.com/alcionai/corso/src/pkg/store"
)
func GetBackups(ctx context.Context, cmd *cobra.Command, service path.ServiceType) ([]*backup.Backup, error) {
r, _, err := GetAccountAndConnect(ctx, cmd, service)
if err != nil {
return nil, err
}
defer CloseRepo(ctx, r)
return r.BackupsByTag(ctx, store.Service(service))
}
type completionFunc func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)
func BackupIDCompletionFunc(service path.ServiceType) completionFunc {
return func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
bs, err := GetBackups(cmd.Context(), cmd, service)
if err != nil {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("Unable to fetch %s backups", service)), cobra.ShellCompDirectiveNoFileComp
}
if len(bs) == 0 {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("No %s backups found", service)), cobra.ShellCompDirectiveNoFileComp
}
backups := make([]string, len(bs))
for _, b := range bs {
backups = append(backups, fmt.Sprintf("%s\tCreated at %s", b.GetID(), b.CreationTime))
}
return cobra.AppendActiveHelp(backups, "Choose backup ID to use"), cobra.ShellCompDirectiveNoFileComp
}
}
func UsersCompletionFunc(service path.ServiceType) completionFunc {
return func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
r, acct, err := AccountConnectAndWriteRepoConfig(ctx, cmd, service)
if err != nil {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("Unable to fetch %s users", service)), cobra.ShellCompDirectiveNoFileComp
}
ins, err := UsersMap(ctx, *acct, Control(), r.Counter(), fault.New(true))
if err != nil {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("Unable to fetch %s users", service)), cobra.ShellCompDirectiveNoFileComp
}
if len(ins.IDs()) == 0 {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("No %s users found", service)), cobra.ShellCompDirectiveNoFileComp
}
backups := make([]string, len(ins.IDs()))
for _, u := range ins.IDs() {
name, _ := ins.NameOf(u)
backups = append(backups, fmt.Sprintf("%s\t%s", u, name))
}
return cobra.AppendActiveHelp(backups, "Choose user ID to use"), cobra.ShellCompDirectiveNoFileComp
}
}
func MailboxCompletionFunc(service path.ServiceType) completionFunc {
return func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
r, acct, err := AccountConnectAndWriteRepoConfig(ctx, cmd, service)
if err != nil {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("Unable to fetch %s mailboxes", service)), cobra.ShellCompDirectiveNoFileComp
}
ins, err := UsersMap(ctx, *acct, Control(), r.Counter(), fault.New(true))
if err != nil {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("Unable to fetch %s mailboxes", service)), cobra.ShellCompDirectiveNoFileComp
}
if len(ins.IDs()) == 0 {
return cobra.AppendActiveHelp(
nil,
fmt.Sprintf("No %s mailboxes found", service)), cobra.ShellCompDirectiveNoFileComp
}
backups := ins.Names()
return cobra.AppendActiveHelp(backups, "Choose mailbox to use"), cobra.ShellCompDirectiveNoFileComp
}
}
func GroupsCompletionFunc() completionFunc {
return func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
_, acct, err := AccountConnectAndWriteRepoConfig(ctx, cmd, path.GroupsService)
if err != nil {
return cobra.AppendActiveHelp(nil, "Unable to fetch Groups"), cobra.ShellCompDirectiveNoFileComp
}
ins, err := m365.GroupsMap(ctx, *acct, fault.New(true))
if err != nil {
return cobra.AppendActiveHelp(nil, "Unable to fetch Groups"), cobra.ShellCompDirectiveNoFileComp
}
if len(ins.IDs()) == 0 {
return cobra.AppendActiveHelp(nil, "No Groups found"), cobra.ShellCompDirectiveNoFileComp
}
backups := make([]string, len(ins.IDs()))
for _, u := range ins.IDs() {
name, _ := ins.NameOf(u)
backups = append(backups, fmt.Sprintf("%s\t%s", u, name))
}
return cobra.AppendActiveHelp(backups, "Choose Group to use"), cobra.ShellCompDirectiveNoFileComp
}
}
func SitesCompletionFunc() completionFunc {
return func(
cmd *cobra.Command,
args []string,
toComplete string,
) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
_, acct, err := AccountConnectAndWriteRepoConfig(ctx, cmd, path.SharePointService)
if err != nil {
return cobra.AppendActiveHelp(nil, "Unable to fetch Sites"), cobra.ShellCompDirectiveNoFileComp
}
ins, err := m365.SitesMap(ctx, *acct, fault.New(true))
if err != nil {
return cobra.AppendActiveHelp(nil, "Unable to fetch Sites"), cobra.ShellCompDirectiveNoFileComp
}
if len(ins.IDs()) == 0 {
return cobra.AppendActiveHelp(nil, "No Sites found"), cobra.ShellCompDirectiveNoFileComp
}
backups := make([]string, len(ins.IDs()))
for _, u := range ins.IDs() {
name, _ := ins.NameOf(u)
backups = append(backups, fmt.Sprintf("%s\t%s", u, name))
}
return cobra.AppendActiveHelp(backups, "Choose Site to use"), cobra.ShellCompDirectiveNoFileComp
}
}

View File

@ -0,0 +1,115 @@
package utils
// The code in this file is mostly lifted out of cobra itself. It as
// of now has some issue with how it handles completions for
// subcommands and only autocompletes for top level commands.
// https://github.com/spf13/cobra/issues/981#issuecomment-547003669
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
// SubcommandsRequiredWithSuggestions will ensure we have a subcommand provided by the user and augments it with
// suggestion for commands, alias and help on root command.
func SubcommandsRequiredWithSuggestions(cmd *cobra.Command, args []string) error {
requireMsg := "%s requires a valid subcommand"
// This will be triggered if cobra didn't find any subcommands.
// Find some suggestions.
var suggestions []string
if len(args) != 0 && !cmd.DisableSuggestions {
typedName := args[0]
if cmd.SuggestionsMinimumDistance <= 0 {
cmd.SuggestionsMinimumDistance = 2
}
// subcommand suggestions
suggestions = cmd.SuggestionsFor(args[0])
// subcommand alias suggestions (with distance, not exact)
for _, c := range cmd.Commands() {
if c.IsAvailableCommand() {
for _, alias := range c.Aliases {
levenshteinDistance := levenshteinDistance(typedName, alias, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(alias), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, alias)
}
}
}
}
// help for root command
if !cmd.HasParent() {
help := "help"
levenshteinDistance := levenshteinDistance(typedName, help, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(help), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, help)
}
}
}
var suggestionsMsg string
if len(suggestions) > 0 {
suggestionsMsg += "\n\nDid you mean this?\n"
for _, s := range suggestions {
suggestionsMsg += fmt.Sprintf("\t%v\n", s)
}
}
if len(suggestionsMsg) > 0 {
requireMsg = fmt.Sprintf("%s. %s", requireMsg, suggestionsMsg)
}
return fmt.Errorf(requireMsg, cmd.Name())
}
// levenshteinDistance compares two strings and returns the levenshtein distance between them.
func levenshteinDistance(s, t string, ignoreCase bool) int {
if ignoreCase {
s = strings.ToLower(s)
t = strings.ToLower(t)
}
d := make([][]int, len(s)+1)
for i := range d {
d[i] = make([]int, len(t)+1)
}
for i := range d {
d[i][0] = i
}
for j := range d[0] {
d[0][j] = j
}
for j := 1; j <= len(t); j++ {
for i := 1; i <= len(s); i++ {
if s[i-1] == t[j-1] {
d[i][j] = d[i-1][j-1]
} else {
min := d[i-1][j]
if d[i][j-1] < min {
min = d[i][j-1]
}
if d[i-1][j-1] < min {
min = d[i-1][j-1]
}
d[i][j] = min + 1
}
}
}
return d[len(s)][len(t)]
}

View File

@ -89,7 +89,8 @@ func fatal(err error) {
// Adapted from https://github.com/spf13/cobra/blob/main/doc/md_docs.go for Corso specific formatting
func genMarkdownCorso(cmd *cobra.Command, dir string) error {
for _, c := range cmd.Commands() {
if !isAvailableCommand(c) || c.IsAdditionalHelpTopicCommand() {
if c.Use != "completion [bash|zsh|fish|powershell]" &&
(!isAvailableCommand(c) || c.IsAdditionalHelpTopicCommand()) {
continue
}

View File

@ -101,6 +101,16 @@ func AddLoggingFlags(cmd *cobra.Command) {
addFlags(fs, "corso-<timestamp>.log")
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(LogLevelFN, cobra.FixedCompletions(
[]string{string(LLDebug), string(LLInfo), string(LLWarn), string(LLError), string(LLDisabled)},
cobra.ShellCompDirectiveNoFileComp)))
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(LogFormatFN, cobra.FixedCompletions(
[]string{string(LFText), string(LFJSON)},
cobra.ShellCompDirectiveNoFileComp)))
cobra.CheckErr(cmd.RegisterFlagCompletionFunc(MaskSensitiveDataFN, cobra.FixedCompletions(
[]string{string(PIIPlainText), string(PIIMask), string(PIIHash)},
cobra.ShellCompDirectiveNoFileComp)))
//nolint:errcheck
fs.MarkHidden(ReadableLogsFN)
}

View File

@ -55,8 +55,8 @@ _validatemdgen: # in case we have a different architecture
${MDGEN_BINARY}: $(shell find ${CORSO_LOCAL_PATH}/src -type f -name *.go) $(shell find ${CORSO_LOCAL_PATH}/src -type d )
@echo 'Re-building Corso CLI docs auto-gen tooling...'
$(GOC) go mod download
$(GOC) go build -o ${MDGEN_BINARY} ${MDGEN_SRC}
$(GOC) go mod download
$(GOC) go build -o ${MDGEN_BINARY} ${MDGEN_SRC}
clean:
$(WEBC) rm -rf docs/cli build node_modules

View File

@ -35,3 +35,5 @@ Below is a list of known Corso issues and limitations:
* Groups and Teams support is available in an early-access status, and may be subject to breaking changes.
* Restoring the data into a different Group from the one it was backed up from isn't currently supported
* CLI completions can't autocomplete multiple values for flags

View File

@ -54,7 +54,8 @@ const sidebars = {
'cli/corso-repo-connect-filesystem',
'cli/corso-repo-maintenance',
'cli/corso-repo-update-passphrase',
'cli/corso-env']
'cli/corso-env',
'cli/corso-completion']
},
{
type: 'category',

View File

@ -60,3 +60,4 @@ subtrees
anonymized
unreferenced
hostname
zsh