diff --git a/.gitignore b/.gitignore index 8c2cd2af..226c9929 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Ignore Oasis CLI binary. oasis +# Ignore generated completions (created by goreleaser). +completions/ # Ignore Python cache directories. __pycache__/ # Ignore temporary files. diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 350b6d4f..ed36c6a6 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,6 +5,10 @@ project_name: Oasis CLI before: hooks: - go mod tidy + - mkdir -p completions + - sh -c "go run . completion bash > completions/oasis.bash" + - sh -c "go run . completion zsh > completions/oasis.zsh" + - sh -c "go run . completion fish > completions/oasis.fish" universal_binaries: - id: oasis-darwin-universal @@ -82,6 +86,8 @@ archives: format_overrides: - goos: windows formats: [ 'zip' ] + files: + - completions/* checksum: name_template: SHA256SUMS-{{.Version}}.txt diff --git a/cmd/account/allow.go b/cmd/account/allow.go index 08ba10a3..e8eb046a 100644 --- a/cmd/account/allow.go +++ b/cmd/account/allow.go @@ -14,9 +14,10 @@ import ( ) var allowCmd = &cobra.Command{ - Use: "allow ", - Short: "Configure beneficiary allowance", - Args: cobra.ExactArgs(2), + Use: "allow ", + Short: "Configure beneficiary allowance", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(0), // at position 1. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -63,6 +64,6 @@ var allowCmd = &cobra.Command{ } func init() { - allowCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(allowCmd) allowCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/amend_commission_schedule.go b/cmd/account/amend_commission_schedule.go index c263f46b..0387f0df 100644 --- a/cmd/account/amend_commission_schedule.go +++ b/cmd/account/amend_commission_schedule.go @@ -174,7 +174,7 @@ func init() { "The minimum rate is rate_min_numerator divided by %v, and the maximum rate is "+ "rate_max_numerator divided by %v", staking.CommissionRateDenominator, staking.CommissionRateDenominator, )) - amendCommissionScheduleCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(amendCommissionScheduleCmd) amendCommissionScheduleCmd.Flags().AddFlagSet(common.TxFlags) amendCommissionScheduleCmd.Flags().AddFlagSet(f) } diff --git a/cmd/account/burn.go b/cmd/account/burn.go index 1e14f376..df77ff3c 100644 --- a/cmd/account/burn.go +++ b/cmd/account/burn.go @@ -53,6 +53,6 @@ var burnCmd = &cobra.Command{ } func init() { - burnCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(burnCmd) burnCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/delegate.go b/cmd/account/delegate.go index 2e64bfa5..91f2107c 100644 --- a/cmd/account/delegate.go +++ b/cmd/account/delegate.go @@ -18,9 +18,10 @@ import ( ) var delegateCmd = &cobra.Command{ - Use: "delegate ", - Short: "Delegate given amount of tokens to an entity", - Args: cobra.ExactArgs(2), + Use: "delegate ", + Short: "Delegate given amount of tokens to an entity", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(1), // at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -122,6 +123,6 @@ var delegateCmd = &cobra.Command{ } func init() { - delegateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(delegateCmd) delegateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/account/deposit.go b/cmd/account/deposit.go index 6f6bdd0e..31931fe2 100644 --- a/cmd/account/deposit.go +++ b/cmd/account/deposit.go @@ -20,9 +20,10 @@ import ( ) var depositCmd = &cobra.Command{ - Use: "deposit [to]", - Short: "Deposit tokens into ParaTime", - Args: cobra.RangeArgs(1, 2), + Use: "deposit [to]", + Short: "Deposit tokens into ParaTime", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.AddressesAt(1), // [to] at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -116,7 +117,7 @@ var depositCmd = &cobra.Command{ } func init() { - depositCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(depositCmd) depositCmd.Flags().AddFlagSet(common.RuntimeTxFlags) depositCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/account/entity.go b/cmd/account/entity.go index e55b9ce2..2a7c4248 100644 --- a/cmd/account/entity.go +++ b/cmd/account/entity.go @@ -235,10 +235,10 @@ func init() { entityInitCmd.Flags().AddFlagSet(common.AccountFlag) entityInitCmd.Flags().AddFlagSet(common.AnswerYesFlag) - entityRegisterCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(entityRegisterCmd) entityRegisterCmd.Flags().AddFlagSet(common.TxFlags) - entityDeregisterCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(entityDeregisterCmd) entityDeregisterCmd.Flags().AddFlagSet(common.TxFlags) entityMetadataUpdateCmd.Flags().StringVarP(®istryPath, "registry-dir", "r", "", "path to the metadata registry directory") diff --git a/cmd/account/node_unfreeze.go b/cmd/account/node_unfreeze.go index 413a3804..f410aeeb 100644 --- a/cmd/account/node_unfreeze.go +++ b/cmd/account/node_unfreeze.go @@ -53,6 +53,6 @@ var nodeUnfreezeCmd = &cobra.Command{ } func init() { - nodeUnfreezeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(nodeUnfreezeCmd) nodeUnfreezeCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/account/show/show.go b/cmd/account/show/show.go index 9db3ec03..79caa4cf 100644 --- a/cmd/account/show/show.go +++ b/cmd/account/show/show.go @@ -26,10 +26,11 @@ var ( showDelegations bool Cmd = &cobra.Command{ - Use: "show [address]", - Short: "Show balance and other information", - Aliases: []string{"s", "balance", "b"}, - Args: cobra.MaximumNArgs(1), + Use: "show [address]", + Short: "Show balance and other information", + Aliases: []string{"s", "balance", "b"}, + Args: cobra.MaximumNArgs(1), + ValidArgsFunction: common.CompleteAccountAndAddressBookNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -241,7 +242,7 @@ var ( func init() { f := flag.NewFlagSet("", flag.ContinueOnError) f.BoolVar(&showDelegations, "show-delegations", false, "show incoming and outgoing delegations") - Cmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(Cmd) Cmd.Flags().AddFlagSet(common.HeightFlag) Cmd.Flags().AddFlagSet(f) } diff --git a/cmd/account/transfer.go b/cmd/account/transfer.go index 2ae52b6d..fbcc270a 100644 --- a/cmd/account/transfer.go +++ b/cmd/account/transfer.go @@ -18,10 +18,11 @@ import ( ) var transferCmd = &cobra.Command{ - Use: "transfer [] ", - Short: "Transfer given amount of tokens", - Aliases: []string{"t"}, - Args: cobra.RangeArgs(2, 3), + Use: "transfer [] ", + Short: "Transfer given amount of tokens", + Aliases: []string{"t"}, + Args: cobra.RangeArgs(2, 3), + ValidArgsFunction: common.AddressesAt(1, 2), // can be at position 2 or 3. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -120,7 +121,7 @@ var transferCmd = &cobra.Command{ func init() { transferCmd.Flags().AddFlagSet(SubtractFeeFlags) - transferCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(transferCmd) transferCmd.Flags().AddFlagSet(common.RuntimeTxFlags) transferCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/account/undelegate.go b/cmd/account/undelegate.go index 75b5bbac..789f40b0 100644 --- a/cmd/account/undelegate.go +++ b/cmd/account/undelegate.go @@ -18,9 +18,10 @@ import ( ) var undelegateCmd = &cobra.Command{ - Use: "undelegate ", - Short: "Undelegate given amount of shares from an entity", - Args: cobra.ExactArgs(2), + Use: "undelegate ", + Short: "Undelegate given amount of shares from an entity", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressesAt(1), // at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -118,6 +119,6 @@ var undelegateCmd = &cobra.Command{ } func init() { - undelegateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(undelegateCmd) undelegateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/account/withdraw.go b/cmd/account/withdraw.go index 7999131f..a6366588 100644 --- a/cmd/account/withdraw.go +++ b/cmd/account/withdraw.go @@ -19,9 +19,10 @@ import ( ) var withdrawCmd = &cobra.Command{ - Use: "withdraw [to]", - Short: "Withdraw tokens from ParaTime", - Args: cobra.RangeArgs(1, 2), + Use: "withdraw [to]", + Short: "Withdraw tokens from ParaTime", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.AddressesAt(1), // [to] at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -133,7 +134,7 @@ var withdrawCmd = &cobra.Command{ func init() { withdrawCmd.Flags().AddFlagSet(SubtractFeeFlags) - withdrawCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(withdrawCmd) withdrawCmd.Flags().AddFlagSet(common.RuntimeTxFlags) withdrawCmd.Flags().AddFlagSet(common.ForceFlag) } diff --git a/cmd/addressbook.go b/cmd/addressbook.go index 73a2b9af..117b9cb4 100644 --- a/cmd/addressbook.go +++ b/cmd/addressbook.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" "github.com/oasisprotocol/cli/table" ) @@ -70,9 +71,10 @@ var ( } abShowCmd = &cobra.Command{ - Use: "show ", - Short: "Show address information", - Args: cobra.ExactArgs(1), + Use: "show ", + Short: "Show address information", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAddressBookNames, Run: func(_ *cobra.Command, args []string) { name := args[0] abEntry, ok := config.Global().AddressBook.All[name] @@ -89,10 +91,11 @@ var ( } abRmCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an address from address book", - Args: cobra.ExactArgs(1), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an address from address book", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAddressBookNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() name := args[0] @@ -106,10 +109,11 @@ var ( } abRenameCmd = &cobra.Command{ - Use: "rename ", - Aliases: []string{"mv"}, - Short: "Rename address", - Args: cobra.ExactArgs(2), + Use: "rename ", + Aliases: []string{"mv"}, + Short: "Rename address", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AddressBookNamesAt(0), Run: func(_ *cobra.Command, args []string) { cfg := config.Global() oldName, newName := args[0], args[1] diff --git a/cmd/common/completion.go b/cmd/common/completion.go new file mode 100644 index 00000000..37536019 --- /dev/null +++ b/cmd/common/completion.go @@ -0,0 +1,173 @@ +package common + +import ( + "sort" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/cli/config" +) + +// CobraCompletionFunc is the function signature for cobra completions. +type CobraCompletionFunc = func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) + +// Helpers. + +func mapKeys[T any](m map[string]T) []string { + names := make([]string, 0, len(m)) + for name := range m { + names = append(names, name) + } + return names +} + +func noFileComp(names []string) ([]string, cobra.ShellCompDirective) { + sort.Strings(names) + return names, cobra.ShellCompDirectiveNoFileComp +} + +func deduplicate(names []string) []string { + seen := make(map[string]struct{}, len(names)) + result := make([]string, 0, len(names)) + for _, name := range names { + if _, ok := seen[name]; !ok { + seen[name] = struct{}{} + result = append(result, name) + } + } + return result +} + +func simpleComplete(fn func() []string) CobraCompletionFunc { + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return noFileComp(fn()) + } +} + +func completeAt(fn func() []string, positions ...int) CobraCompletionFunc { + return func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + for _, pos := range positions { + if len(args) == pos { + return noFileComp(fn()) + } + } + return nil, cobra.ShellCompDirectiveNoFileComp + } +} + +// Data sources. + +func accountNames() []string { + names := mapKeys(config.Global().Wallet.All) + names = append(names, testAccountAddresses...) + return names +} + +func addressBookNames() []string { + return mapKeys(config.Global().AddressBook.All) +} + +func addressNames() []string { + cfg := config.Global() + names := append(mapKeys(cfg.Wallet.All), mapKeys(cfg.AddressBook.All)...) + + // Add test accounts. + names = append(names, testAccountAddresses...) + + // Add paratime addresses. + names = append(names, ParaTimeAddresses(cfg)...) + + // Add pool addresses. + names = append(names, paraTimePoolAddresses...) + names = append(names, consensusPoolAddresses...) + + return deduplicate(names) +} + +func networkNames() []string { + return mapKeys(config.Global().Networks.All) +} + +// Simple completers - complete at any position. + +// CompleteAccountNames provides completion for wallet account names. +var CompleteAccountNames = simpleComplete(accountNames) + +// CompleteAddressBookNames provides completion for address book entry names. +var CompleteAddressBookNames = simpleComplete(addressBookNames) + +// CompleteAccountAndAddressBookNames provides completion for both wallet accounts +// and address book entries. Useful for commands that accept either as an address. +var CompleteAccountAndAddressBookNames = simpleComplete(addressNames) + +// CompleteNetworkNames provides completion for network names. +var CompleteNetworkNames = simpleComplete(networkNames) + +// CompleteParaTimeNames provides completion for ParaTime names. +// It uses the default network if --network flag is not set. +func CompleteParaTimeNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cfg := config.Global() + + // Get network from flag or use default. + networkName, _ := cmd.Flags().GetString("network") + if networkName == "" { + networkName = cfg.Networks.Default + } + if networkName == "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + network := cfg.Networks.All[networkName] + if network == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return noFileComp(mapKeys(network.ParaTimes.All)) +} + +// CompleteNetworkThenParaTime provides completion for commands that take +// as positional arguments. +func CompleteNetworkThenParaTime(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + // First argument: complete network names. + if len(args) == 0 { + return noFileComp(networkNames()) + } + + // Second argument: complete paratime names for the given network. + if len(args) == 1 { + network := config.Global().Networks.All[args[0]] + if network == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return noFileComp(mapKeys(network.ParaTimes.All)) + } + + return nil, cobra.ShellCompDirectiveNoFileComp +} + +// Position-aware completers - complete only at specified positions. + +// AddressesAt returns a completion function for wallet + address book names at specified positions. +func AddressesAt(positions ...int) CobraCompletionFunc { + return completeAt(addressNames, positions...) +} + +// AccountNamesAt returns a completion function for wallet account names at specified positions. +func AccountNamesAt(positions ...int) CobraCompletionFunc { + return completeAt(accountNames, positions...) +} + +// AddressBookNamesAt returns a completion function for address book names at specified positions. +func AddressBookNamesAt(positions ...int) CobraCompletionFunc { + return completeAt(addressBookNames, positions...) +} + +// NetworksAt returns a completion function for network names at specified positions. +func NetworksAt(positions ...int) CobraCompletionFunc { + return completeAt(networkNames, positions...) +} + +// StaticAt returns a completion function for static values at specified positions. +func StaticAt(values []string, positions ...int) CobraCompletionFunc { + return completeAt(func() []string { return values }, positions...) +} diff --git a/cmd/common/selector.go b/cmd/common/selector.go index 734ad8f7..03dfe1d3 100644 --- a/cmd/common/selector.go +++ b/cmd/common/selector.go @@ -128,6 +128,40 @@ func (npa *NPASelection) ConsensusDenomination() (denom types.Denomination) { return denom } +// AddSelectorFlags adds network/paratime/account selector flags with completions. +func AddSelectorFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("paratime", CompleteParaTimeNames) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + +// AddSelectorNPFlags adds network/paratime selector flags with completions. +func AddSelectorNPFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNPFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("paratime", CompleteParaTimeNames) +} + +// AddSelectorNAFlags adds network/account selector flags with completions. +func AddSelectorNAFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNAFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + +// AddSelectorNFlags adds network selector flags with completions. +func AddSelectorNFlags(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(SelectorNFlags) + _ = cmd.RegisterFlagCompletionFunc("network", CompleteNetworkNames) +} + +// AddAccountFlag adds account selector flag with completion. +func AddAccountFlag(cmd *cobra.Command) { + cmd.Flags().AddFlagSet(AccountFlag) + _ = cmd.RegisterFlagCompletionFunc("account", CompleteAccountNames) +} + func init() { AccountFlag = flag.NewFlagSet("", flag.ContinueOnError) AccountFlag.StringVar(&selectedAccount, "account", "", "explicitly set account to use") diff --git a/cmd/common/wallet.go b/cmd/common/wallet.go index ad77008b..54f3b9db 100644 --- a/cmd/common/wallet.go +++ b/cmd/common/wallet.go @@ -43,6 +43,65 @@ const ( poolPendingDelegation = "pending-delegation" ) +var ( + // paraTimePoolMap maps ParaTime pool names to their addresses. + paraTimePoolMap = map[string]types.Address{ + poolCommon: accounts.CommonPoolAddress, + poolFeeAccumulator: accounts.FeeAccumulatorAddress, + poolPendingDelegation: consensusaccounts.PendingDelegationAddress, + poolPendingWithdrawal: consensusaccounts.PendingWithdrawalAddress, + poolRewards: rewards.RewardPoolAddress, + } + + // consensusPoolMap maps consensus pool names to their addresses. + // Initialized in init() because addresses need conversion. + consensusPoolMap map[string]types.Address + + // testAccountAddresses contains all valid test account addresses in "test:" format. + testAccountAddresses []string + + // paraTimePoolAddresses contains all valid ParaTime pool addresses in "pool:paratime:" format. + paraTimePoolAddresses []string + + // consensusPoolAddresses contains all valid consensus pool addresses in "pool:consensus:" format. + consensusPoolAddresses []string +) + +func init() { + consensusPoolMap = map[string]types.Address{ + poolBurn: types.NewAddressFromConsensus(staking.BurnAddress), + poolCommon: types.NewAddressFromConsensus(staking.CommonPoolAddress), + poolFeeAccumulator: types.NewAddressFromConsensus(staking.FeeAccumulatorAddress), + poolGovernanceDeposits: types.NewAddressFromConsensus(staking.GovernanceDepositsAddress), + } + + // Build address lists for completion. + testPrefix := addressExplicitTest + addressExplicitSeparator + for name := range testing.TestAccounts { + testAccountAddresses = append(testAccountAddresses, testPrefix+name) + } + poolParaTimePrefix := addressExplicitPool + addressExplicitSeparator + addressExplicitParaTime + addressExplicitSeparator + for name := range paraTimePoolMap { + paraTimePoolAddresses = append(paraTimePoolAddresses, poolParaTimePrefix+name) + } + poolConsensusPrefix := addressExplicitPool + addressExplicitSeparator + addressExplicitConsensus + addressExplicitSeparator + for name := range consensusPoolMap { + consensusPoolAddresses = append(consensusPoolAddresses, poolConsensusPrefix+name) + } +} + +// ParaTimeAddresses returns all paratime addresses in "paratime:" format for the given config. +func ParaTimeAddresses(cfg *config.Config) []string { + var names []string + paraTimePrefix := addressExplicitParaTime + addressExplicitSeparator + for _, net := range cfg.Networks.All { + for ptName := range net.ParaTimes.All { + names = append(names, paraTimePrefix+ptName) + } + } + return names +} + // LoadAccount loads the given named account. func LoadAccount(cfg *config.Config, name string) wallet.Account { // Check if the specified account is a test account. @@ -183,35 +242,15 @@ func ResolveAddress(net *configSdk.Network, address string) (*types.Address, *et } switch poolKind { case addressExplicitParaTime: - switch poolName { - case poolCommon: - return &accounts.CommonPoolAddress, nil, nil - case poolFeeAccumulator: - return &accounts.FeeAccumulatorAddress, nil, nil - case poolPendingDelegation: - return &consensusaccounts.PendingDelegationAddress, nil, nil - case poolPendingWithdrawal: - return &consensusaccounts.PendingWithdrawalAddress, nil, nil - case poolRewards: - return &rewards.RewardPoolAddress, nil, nil - default: - return nil, nil, fmt.Errorf("unsupported ParaTime pool: %s", poolName) + if addr, ok := paraTimePoolMap[poolName]; ok { + return &addr, nil, nil } + return nil, nil, fmt.Errorf("unsupported ParaTime pool: %s", poolName) case addressExplicitConsensus: - var addr types.Address - switch poolName { - case poolBurn: - addr = types.NewAddressFromConsensus(staking.BurnAddress) - case poolCommon: - addr = types.NewAddressFromConsensus(staking.CommonPoolAddress) - case poolFeeAccumulator: - addr = types.NewAddressFromConsensus(staking.FeeAccumulatorAddress) - case poolGovernanceDeposits: - addr = types.NewAddressFromConsensus(staking.GovernanceDepositsAddress) - default: - return nil, nil, fmt.Errorf("unsupported consensus pool: %s", poolName) + if addr, ok := consensusPoolMap[poolName]; ok { + return &addr, nil, nil } - return &addr, nil, nil + return nil, nil, fmt.Errorf("unsupported consensus pool: %s", poolName) default: return nil, nil, fmt.Errorf("unsupported pool kind: %s. Please use pool::, for example pool:paratime:pending-withdrawal", poolKind) } diff --git a/cmd/completion.go b/cmd/completion.go new file mode 100644 index 00000000..815deff0 --- /dev/null +++ b/cmd/completion.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate shell completion script", + Long: `Generate shell completion script for the specified shell. + +To load completions: + +Bash: + $ source <(oasis completion bash) + + # To load completions for each session, execute once: + # Linux: + $ oasis completion bash > /etc/bash_completion.d/oasis + # macOS: + $ oasis completion bash > $(brew --prefix)/etc/bash_completion.d/oasis + +Zsh: + # If shell completion is not already enabled in your environment, + # you will need to enable it. You can execute the following once: + $ echo "autoload -U compinit; compinit" >> ~/.zshrc + + # To load completions for each session, execute once: + $ oasis completion zsh > "${fpath[1]}/_oasis" + + # You will need to start a new shell for this setup to take effect. + +Fish: + $ oasis completion fish | source + + # To load completions for each session, execute once: + $ oasis completion fish > ~/.config/fish/completions/oasis.fish + +PowerShell: + PS> oasis completion powershell | Out-String | Invoke-Expression + + # To load completions for every new session, run: + PS> oasis completion powershell > oasis.ps1 + # and source this file from your PowerShell profile. +`, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + out := cmd.OutOrStdout() + switch args[0] { + case "bash": + return cmd.Root().GenBashCompletion(out) + case "zsh": + return cmd.Root().GenZshCompletion(out) + case "fish": + return cmd.Root().GenFishCompletion(out, true) + case "powershell": + return cmd.Root().GenPowerShellCompletionWithDesc(out) + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(completionCmd) +} diff --git a/cmd/contract.go b/cmd/contract.go index 87d7d573..7961e90f 100644 --- a/cmd/contract.go +++ b/cmd/contract.go @@ -487,15 +487,15 @@ func parseTokens(pt *config.ParaTime, tokens []string) []types.BaseUnits { } func init() { - contractShowCmd.Flags().AddFlagSet(common.SelectorFlags) - contractShowCodeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractShowCmd) + common.AddSelectorFlags(contractShowCodeCmd) - contractDumpCodeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractDumpCodeCmd) contractsUploadFlags := flag.NewFlagSet("", flag.ContinueOnError) contractsUploadFlags.StringVar(&contractInstantiatePolicy, "instantiate-policy", "everyone", "contract instantiation policy") - contractUploadCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractUploadCmd) contractUploadCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractUploadCmd.Flags().AddFlagSet(contractsUploadFlags) @@ -505,16 +505,16 @@ func init() { contractsInstantiateFlags := flag.NewFlagSet("", flag.ContinueOnError) contractsInstantiateFlags.StringVar(&contractUpgradesPolicy, "upgrades-policy", "owner", "contract upgrades policy") - contractInstantiateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractInstantiateCmd) contractInstantiateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractInstantiateCmd.Flags().AddFlagSet(contractsInstantiateFlags) contractInstantiateCmd.Flags().AddFlagSet(contractsCallFlags) - contractCallCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractCallCmd) contractCallCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractCallCmd.Flags().AddFlagSet(contractsCallFlags) - contractChangeUpgradePolicyCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractChangeUpgradePolicyCmd) contractChangeUpgradePolicyCmd.Flags().AddFlagSet(common.RuntimeTxFlags) contractsStorageDumpCmdFlags := flag.NewFlagSet("", flag.ContinueOnError) @@ -526,10 +526,10 @@ func init() { ) contractsStorageDumpCmdFlags.Uint64Var(&contractStorageDumpLimit, "limit", 0, "result set limit") contractsStorageDumpCmdFlags.Uint64Var(&contractStorageDumpOffset, "offset", 0, "result set offset") - contractStorageDumpCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractStorageDumpCmd) contractStorageDumpCmd.Flags().AddFlagSet(contractsStorageDumpCmdFlags) - contractStorageGetCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(contractStorageGetCmd) contractStorageCmd.AddCommand(contractStorageDumpCmd) contractStorageCmd.AddCommand(contractStorageGetCmd) diff --git a/cmd/network/governance/create.go b/cmd/network/governance/create.go index c7387e58..81902993 100644 --- a/cmd/network/governance/create.go +++ b/cmd/network/governance/create.go @@ -205,13 +205,13 @@ var ( ) func init() { - govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalUpgradeCmd) govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) - govCreateProposalParameterChangeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalParameterChangeCmd) govCreateProposalUpgradeCmd.Flags().AddFlagSet(common.TxFlags) - govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCreateProposalCancelUpgradeCmd) govCreateProposalCancelUpgradeCmd.Flags().AddFlagSet(common.TxFlags) govCreateProposalCmd.AddCommand(govCreateProposalUpgradeCmd) diff --git a/cmd/network/governance/list.go b/cmd/network/governance/list.go index d4b71d89..0a114fc6 100644 --- a/cmd/network/governance/list.go +++ b/cmd/network/governance/list.go @@ -65,6 +65,6 @@ var govListCmd = &cobra.Command{ } func init() { - govListCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(govListCmd) govListCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/network/governance/show.go b/cmd/network/governance/show.go index f7d652f9..894848a6 100644 --- a/cmd/network/governance/show.go +++ b/cmd/network/governance/show.go @@ -464,6 +464,6 @@ func init() { showVotesFlag := flag.NewFlagSet("", flag.ContinueOnError) showVotesFlag.BoolVar(&showVotes, "show-votes", false, "individual entity votes") govShowCmd.Flags().AddFlagSet(showVotesFlag) - govShowCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(govShowCmd) govShowCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/network/governance/vote.go b/cmd/network/governance/vote.go index f5d82f14..d0a1a8ed 100644 --- a/cmd/network/governance/vote.go +++ b/cmd/network/governance/vote.go @@ -15,9 +15,10 @@ import ( ) var govCastVoteCmd = &cobra.Command{ - Use: "cast-vote { yes | no | abstain }", - Short: "Cast a governance vote on a proposal", - Args: cobra.ExactArgs(2), + Use: "cast-vote { yes | no | abstain }", + Short: "Cast a governance vote on a proposal", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.StaticAt([]string{"yes", "no", "abstain"}, 1), // Vote options at position 2. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() npa := common.GetNPASelection(cfg) @@ -65,6 +66,6 @@ var govCastVoteCmd = &cobra.Command{ } func init() { - govCastVoteCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(govCastVoteCmd) govCastVoteCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/network/remove.go b/cmd/network/remove.go index bbdbe1c1..e5f2a3a9 100644 --- a/cmd/network/remove.go +++ b/cmd/network/remove.go @@ -10,10 +10,11 @@ import ( ) var rmCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an existing network", - Args: cobra.ExactArgs(1), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an existing network", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_chain_context.go b/cmd/network/set_chain_context.go index 3c02f8c9..47391792 100644 --- a/cmd/network/set_chain_context.go +++ b/cmd/network/set_chain_context.go @@ -9,13 +9,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setChainContextCmd = &cobra.Command{ - Use: "set-chain-context [chain-context]", - Short: "Sets the chain context of the given network", - Args: cobra.RangeArgs(1, 2), + Use: "set-chain-context [chain-context]", + Short: "Sets the chain context of the given network", + Args: cobra.RangeArgs(1, 2), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_default.go b/cmd/network/set_default.go index 0f542956..ae997836 100644 --- a/cmd/network/set_default.go +++ b/cmd/network/set_default.go @@ -3,13 +3,15 @@ package network import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given network as the default network", - Args: cobra.ExactArgs(1), + Use: "set-default ", + Short: "Sets the given network as the default network", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name := args[0] diff --git a/cmd/network/set_rpc.go b/cmd/network/set_rpc.go index d71a4371..c10058da 100644 --- a/cmd/network/set_rpc.go +++ b/cmd/network/set_rpc.go @@ -5,13 +5,15 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setRPCCmd = &cobra.Command{ - Use: "set-rpc ", - Short: "Sets the RPC endpoint of the given network", - Args: cobra.ExactArgs(2), + Use: "set-rpc ", + Short: "Sets the RPC endpoint of the given network", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkNames, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() name, rpc := args[0], args[1] diff --git a/cmd/network/show.go b/cmd/network/show.go index 9ffe877f..09d2288f 100644 --- a/cmd/network/show.go +++ b/cmd/network/show.go @@ -552,7 +552,7 @@ func showParameters(ctx context.Context, npa *common.NPASelection, height int64, } func init() { - showCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(showCmd) showCmd.Flags().AddFlagSet(common.HeightFlag) showCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/network/status.go b/cmd/network/status.go index 5be03444..d0674b1b 100644 --- a/cmd/network/status.go +++ b/cmd/network/status.go @@ -218,5 +218,5 @@ var statusCmd = &cobra.Command{ func init() { statusCmd.Flags().AddFlagSet(common.FormatFlag) - statusCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(statusCmd) } diff --git a/cmd/network/trust.go b/cmd/network/trust.go index 163f656e..c9d67ecc 100644 --- a/cmd/network/trust.go +++ b/cmd/network/trust.go @@ -163,5 +163,5 @@ func calcTrustPeriod(debondingPeriod time.Duration) time.Duration { func init() { trustCmd.Flags().AddFlagSet(common.FormatFlag) - trustCmd.Flags().AddFlagSet(common.SelectorNFlags) + common.AddSelectorNFlags(trustCmd) } diff --git a/cmd/paratime/add.go b/cmd/paratime/add.go index f78aba8e..a86a48aa 100644 --- a/cmd/paratime/add.go +++ b/cmd/paratime/add.go @@ -19,9 +19,10 @@ var ( description string addCmd = &cobra.Command{ - Use: "add ", - Short: "Add a new ParaTime", - Args: cobra.ExactArgs(3), + Use: "add ", + Short: "Add a new ParaTime", + Args: cobra.ExactArgs(3), + ValidArgsFunction: common.NetworksAt(0), // at position 1. Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name, id := args[0], args[1], args[2] diff --git a/cmd/paratime/denomination/remove.go b/cmd/paratime/denomination/remove.go index 2088f29a..495cf4d6 100644 --- a/cmd/paratime/denomination/remove.go +++ b/cmd/paratime/denomination/remove.go @@ -8,13 +8,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var removeDenomCmd = &cobra.Command{ - Use: "remove ", - Short: "Remove denomination", - Args: cobra.ExactArgs(3), + Use: "remove ", + Short: "Remove denomination", + Args: cobra.ExactArgs(3), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, denomArg := args[0], args[1], args[2] diff --git a/cmd/paratime/denomination/set-native.go b/cmd/paratime/denomination/set-native.go index 0df8f335..ad0c4d73 100644 --- a/cmd/paratime/denomination/set-native.go +++ b/cmd/paratime/denomination/set-native.go @@ -8,13 +8,15 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setNativeDenomCmd = &cobra.Command{ - Use: "set-native ", - Short: "Set native denomination", - Args: cobra.ExactArgs(4), + Use: "set-native ", + Short: "Set native denomination", + Args: cobra.ExactArgs(4), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, symbolArg, decimalsArg := args[0], args[1], args[2], args[3] diff --git a/cmd/paratime/denomination/set.go b/cmd/paratime/denomination/set.go index 988c2db1..5bc685d9 100644 --- a/cmd/paratime/denomination/set.go +++ b/cmd/paratime/denomination/set.go @@ -9,6 +9,7 @@ import ( "github.com/oasisprotocol/oasis-sdk/client-sdk/go/config" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) @@ -16,9 +17,10 @@ var ( symbol string setDenomCmd = &cobra.Command{ - Use: "set [--symbol ]", - Short: "Set denomination", - Args: cobra.ExactArgs(4), + Use: "set [--symbol ]", + Short: "Set denomination", + Args: cobra.ExactArgs(4), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() networkArg, ptArg, denomArg, decimalsArg := args[0], args[1], args[2], args[3] diff --git a/cmd/paratime/register.go b/cmd/paratime/register.go index 776245f4..3715a3c4 100644 --- a/cmd/paratime/register.go +++ b/cmd/paratime/register.go @@ -57,6 +57,6 @@ var registerCmd = &cobra.Command{ } func init() { - registerCmd.Flags().AddFlagSet(common.SelectorNAFlags) + common.AddSelectorNAFlags(registerCmd) registerCmd.Flags().AddFlagSet(common.TxFlags) } diff --git a/cmd/paratime/remove.go b/cmd/paratime/remove.go index 81f9e9e9..53950afc 100644 --- a/cmd/paratime/remove.go +++ b/cmd/paratime/remove.go @@ -5,14 +5,16 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var removeCmd = &cobra.Command{ - Use: "remove ", - Aliases: []string{"rm"}, - Short: "Remove an existing ParaTime", - Args: cobra.ExactArgs(2), + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove an existing ParaTime", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name := args[0], args[1] diff --git a/cmd/paratime/set_default.go b/cmd/paratime/set_default.go index 45958bfd..c9cf041b 100644 --- a/cmd/paratime/set_default.go +++ b/cmd/paratime/set_default.go @@ -5,13 +5,15 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" cliConfig "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given ParaTime as the default ParaTime for the given network", - Args: cobra.ExactArgs(2), + Use: "set-default ", + Short: "Sets the given ParaTime as the default ParaTime for the given network", + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.CompleteNetworkThenParaTime, Run: func(_ *cobra.Command, args []string) { cfg := cliConfig.Global() network, name := args[0], args[1] diff --git a/cmd/paratime/show.go b/cmd/paratime/show.go index 8f8ce3ff..b2d7f6a8 100644 --- a/cmd/paratime/show.go +++ b/cmd/paratime/show.go @@ -538,6 +538,6 @@ func init() { roundFlag.Uint64Var(&selectedRound, "round", client.RoundLatest, "explicitly set block round to use") showCmd.Flags().AddFlagSet(common.FormatFlag) - showCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(showCmd) showCmd.Flags().AddFlagSet(roundFlag) } diff --git a/cmd/paratime/statistics.go b/cmd/paratime/statistics.go index 2b375967..566fac89 100644 --- a/cmd/paratime/statistics.go +++ b/cmd/paratime/statistics.go @@ -530,6 +530,6 @@ func (s *runtimeStats) printEntityStats() { } func init() { - statsCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(statsCmd) statsCmd.Flags().StringVarP(&fileCSV, "output-file", "o", "", "output statistics into specified CSV file") } diff --git a/cmd/rofl/machine/mgmt.go b/cmd/rofl/machine/mgmt.go index df58a425..dcf0c851 100644 --- a/cmd/rofl/machine/mgmt.go +++ b/cmd/rofl/machine/mgmt.go @@ -393,25 +393,25 @@ func showCommandArgs[V any](npa *common.NPASelection, raw []byte, args V) { } func init() { - restartCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(restartCmd) restartCmd.Flags().AddFlagSet(common.RuntimeTxFlags) restartCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) restartCmd.Flags().AddFlagSet(roflCommon.WipeFlags) - stopCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(stopCmd) stopCmd.Flags().AddFlagSet(common.RuntimeTxFlags) stopCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) stopCmd.Flags().AddFlagSet(roflCommon.WipeFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) removeCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - changeAdminCmd.Flags().AddFlagSet(common.AccountFlag) + common.AddAccountFlag(changeAdminCmd) changeAdminCmd.Flags().AddFlagSet(common.RuntimeTxFlags) changeAdminCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - topUpCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(topUpCmd) topUpCmd.Flags().AddFlagSet(common.RuntimeTxFlags) topUpCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) topUpCmd.Flags().AddFlagSet(roflCommon.TermFlags) diff --git a/cmd/rofl/machine/show.go b/cmd/rofl/machine/show.go index 1eaca3d8..08f9d5a3 100644 --- a/cmd/rofl/machine/show.go +++ b/cmd/rofl/machine/show.go @@ -221,6 +221,6 @@ func showMachinePorts(extraCfg *roflCmdBuild.AppExtraConfig, appID rofl.AppID, i } func init() { - showCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(showCmd) showCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) } diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go index 2b825f79..c6e5fe83 100644 --- a/cmd/rofl/mgmt.go +++ b/cmd/rofl/mgmt.go @@ -880,7 +880,7 @@ func init() { initCmd.Flags().BoolVar(&reset, "reset", false, "reset the existing ROFL manifest") initCmd.Flags().AddFlagSet(common.AnswerYesFlag) - createCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(createCmd) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) createCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) createCmd.Flags().AddFlagSet(roflCommon.NoUpdateFlag) @@ -890,11 +890,11 @@ func init() { updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) updateCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) removeCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) - showCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(showCmd) showCmd.Flags().AddFlagSet(roflCommon.DeploymentFlags) showCmd.Flags().AddFlagSet(common.FormatFlag) diff --git a/cmd/rofl/provider/list.go b/cmd/rofl/provider/list.go index e4cb81e0..a565439f 100644 --- a/cmd/rofl/provider/list.go +++ b/cmd/rofl/provider/list.go @@ -219,6 +219,6 @@ func ShowOfferSummary(npa *common.NPASelection, offer *roflmarket.Offer) { func init() { listCmd.Flags().AddFlagSet(roflCommon.ShowOffersFlag) - listCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(listCmd) listCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/rofl/provider/mgmt.go b/cmd/rofl/provider/mgmt.go index 49c82b7b..de345b2f 100644 --- a/cmd/rofl/provider/mgmt.go +++ b/cmd/rofl/provider/mgmt.go @@ -442,17 +442,17 @@ func maybeLoadManifestAndSetNPA(cfg *cliConfig.Config, npa *common.NPASelection) } func init() { - initCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(initCmd) - createCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(createCmd) createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - updateCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(updateCmd) updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - updateOffersCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(updateOffersCmd) updateOffersCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - removeCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(removeCmd) removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) } diff --git a/cmd/rofl/provider/show.go b/cmd/rofl/provider/show.go index c5b0a3c1..435de21d 100644 --- a/cmd/rofl/provider/show.go +++ b/cmd/rofl/provider/show.go @@ -154,6 +154,6 @@ func outputProviderText(npa *common.NPASelection, provider *roflmarket.Provider, } func init() { - showCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(showCmd) showCmd.Flags().AddFlagSet(common.FormatFlag) } diff --git a/cmd/rofl/trust_root.go b/cmd/rofl/trust_root.go index e99847f7..cb5a3056 100644 --- a/cmd/rofl/trust_root.go +++ b/cmd/rofl/trust_root.go @@ -47,6 +47,6 @@ var trustRootCmd = &cobra.Command{ } func init() { - trustRootCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(trustRootCmd) trustRootCmd.Flags().AddFlagSet(common.HeightFlag) } diff --git a/cmd/root.go b/cmd/root.go index 9fd244ab..e11eeded 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "io/fs" "os" "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -48,39 +49,96 @@ Go toolchain version: {{ toolchain }} `) } +// isCompletionCommand checks if the CLI is being invoked for shell completion. +// This is used to skip side effects (file creation, migrations) during tab-completion. +// We check only the first non-flag argument to avoid false positives when a +// positional argument happens to be named "completion" (e.g., an address book entry). +func isCompletionCommand() bool { + skipNext := false + for _, arg := range os.Args[1:] { + if skipNext { + skipNext = false + continue + } + if strings.HasPrefix(arg, "-") { + // Skip flag, and if it's not --flag=value format, skip the next arg too. + if !strings.Contains(arg, "=") { + skipNext = true + } + continue + } + // First non-flag arg is the subcommand. + return arg == "completion" || arg == "__complete" || arg == "__completeNoDesc" + } + return false +} + +// ensureConfigExists creates the config file with defaults if it doesn't exist. +func ensureConfigExists(v *viper.Viper, configPath string) { + if _, err := os.Stat(configPath); !errors.Is(err, fs.ErrNotExist) { + return + } + if _, err := os.Create(configPath); err != nil { + cobra.CheckErr(fmt.Errorf("failed to create configuration file: %w", err)) + } + config.ResetDefaults() + _ = config.Save(v) +} + func initConfig() { v := viper.New() - - if cfgFile != "" { - // Use config file from the flag. - v.SetConfigFile(cfgFile) + completionMode := isCompletionCommand() + + // cfgFile is set by cobra flag parsing, but OnInitialize runs before + // flags are parsed. For completion mode, manually check os.Args. + configPath := cfgFile + if configPath == "" && completionMode { + for i, arg := range os.Args { + if arg == "--config" && i+1 < len(os.Args) { + configPath = os.Args[i+1] + break + } + if strings.HasPrefix(arg, "--config=") { + configPath = strings.TrimPrefix(arg, "--config=") + break + } + } + } + if configPath != "" { + v.SetConfigFile(configPath) } else { const configFilename = "cli.toml" configDir := config.DefaultDirectory() - configPath := filepath.Join(configDir, configFilename) v.AddConfigPath(configDir) v.SetConfigType("toml") v.SetConfigName(configFilename) - // Ensure the configuration file exists. - _ = os.MkdirAll(configDir, 0o700) - if _, err := os.Stat(configPath); errors.Is(err, fs.ErrNotExist) { - if _, err := os.Create(configPath); err != nil { - cobra.CheckErr(fmt.Errorf("failed to create configuration file: %w", err)) - } + // Skip file creation during completion to avoid side effects. + if !completionMode { + _ = os.MkdirAll(configDir, 0o700) + ensureConfigExists(v, filepath.Join(configDir, configFilename)) + } + } - // Populate the initial configuration file with defaults. + if err := v.ReadInConfig(); err != nil { + // If config file doesn't exist and we're in completion mode, + // use defaults so completions still show default networks/paratimes. + if completionMode { config.ResetDefaults() - _ = config.Save(v) + return } } - _ = v.ReadInConfig() - - // Load and validate global configuration. + // Load global configuration. err := config.Load(v) cobra.CheckErr(err) + + // Skip migrations and validation during completion to avoid side effects. + if completionMode { + return + } + changes, err := config.Global().Migrate() cobra.CheckErr(err) if changes { diff --git a/cmd/tx.go b/cmd/tx.go index 15fab6ed..a44ec613 100644 --- a/cmd/tx.go +++ b/cmd/tx.go @@ -186,12 +186,12 @@ func tryDecodeTx(rawTx []byte) (any, error) { } func init() { - txSubmitCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(txSubmitCmd) - txSignCmd.Flags().AddFlagSet(common.SelectorFlags) + common.AddSelectorFlags(txSignCmd) txSignCmd.Flags().AddFlagSet(common.RuntimeTxFlags) - txShowCmd.Flags().AddFlagSet(common.SelectorNPFlags) + common.AddSelectorNPFlags(txShowCmd) txCmd.AddCommand(txSubmitCmd) txCmd.AddCommand(txSignCmd) diff --git a/cmd/wallet/export.go b/cmd/wallet/export.go index 2f6d1a62..08553726 100644 --- a/cmd/wallet/export.go +++ b/cmd/wallet/export.go @@ -10,9 +10,10 @@ import ( ) var exportCmd = &cobra.Command{ - Use: "export ", - Short: "Export secret account information", - Args: cobra.ExactArgs(1), + Use: "export ", + Short: "Export secret account information", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { name := args[0] diff --git a/cmd/wallet/remove.go b/cmd/wallet/remove.go index adaeda50..701a6fe3 100644 --- a/cmd/wallet/remove.go +++ b/cmd/wallet/remove.go @@ -13,10 +13,11 @@ import ( ) var rmCmd = &cobra.Command{ - Use: "remove [name ...]", - Aliases: []string{"rm"}, - Short: "Remove existing account(s)", - Args: cobra.MinimumNArgs(1), + Use: "remove [name ...]", + Aliases: []string{"rm"}, + Short: "Remove existing account(s)", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() diff --git a/cmd/wallet/rename.go b/cmd/wallet/rename.go index b7201c3b..27178888 100644 --- a/cmd/wallet/rename.go +++ b/cmd/wallet/rename.go @@ -5,14 +5,16 @@ import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" ) var renameCmd = &cobra.Command{ - Use: "rename ", - Short: "Rename an existing account", - Aliases: []string{"mv"}, - Args: cobra.ExactArgs(2), + Use: "rename ", + Short: "Rename an existing account", + Aliases: []string{"mv"}, + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AccountNamesAt(0), Run: func(_ *cobra.Command, args []string) { cfg := config.Global() oldName, newName := args[0], args[1] diff --git a/cmd/wallet/set_default.go b/cmd/wallet/set_default.go index 10218168..c8ae479c 100644 --- a/cmd/wallet/set_default.go +++ b/cmd/wallet/set_default.go @@ -3,13 +3,15 @@ package wallet import ( "github.com/spf13/cobra" + "github.com/oasisprotocol/cli/cmd/common" "github.com/oasisprotocol/cli/config" ) var setDefaultCmd = &cobra.Command{ - Use: "set-default ", - Short: "Sets the given account as the default account", - Args: cobra.ExactArgs(1), + Use: "set-default ", + Short: "Sets the given account as the default account", + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { cfg := config.Global() name := args[0] diff --git a/cmd/wallet/show.go b/cmd/wallet/show.go index 14dab7b0..c23a5fbf 100644 --- a/cmd/wallet/show.go +++ b/cmd/wallet/show.go @@ -11,10 +11,11 @@ import ( ) var showCmd = &cobra.Command{ - Use: "show ", - Short: "Show public account information", - Aliases: []string{"s"}, - Args: cobra.ExactArgs(1), + Use: "show ", + Short: "Show public account information", + Aliases: []string{"s"}, + Args: cobra.ExactArgs(1), + ValidArgsFunction: common.CompleteAccountNames, Run: func(_ *cobra.Command, args []string) { name := args[0] diff --git a/docs/setup.mdx b/docs/setup.mdx index 4313ba23..54873977 100644 --- a/docs/setup.mdx +++ b/docs/setup.mdx @@ -151,6 +151,87 @@ This command will check for a newer version on GitHub, show you the release notes, and ask for confirmation before downloading and replacing the current binary. +## Shell Completion + +The Oasis CLI supports shell completion for Bash, Zsh, Fish, and PowerShell. +Completions allow you to press `Tab` to auto-complete commands, subcommands, +and flags. + + + + #### Temporary (current session) + + ```shell + source <(oasis completion bash) + ``` + + #### Permanent + + **Linux:** + ```shell + oasis completion bash > /etc/bash_completion.d/oasis + ``` + + **macOS (Homebrew):** + ```shell + oasis completion bash > $(brew --prefix)/etc/bash_completion.d/oasis + ``` + + + + #### Temporary (current session) + + ```shell + source <(oasis completion zsh) + ``` + + #### Permanent + + First, ensure completion is enabled in your `~/.zshrc`: + ```shell + echo "autoload -U compinit; compinit" >> ~/.zshrc + ``` + + Then install the completion: + ```shell + oasis completion zsh > "${fpath[1]}/_oasis" + ``` + + Start a new shell for the changes to take effect. + + + + #### Temporary (current session) + + ```shell + oasis completion fish | source + ``` + + #### Permanent + + ```shell + oasis completion fish > ~/.config/fish/completions/oasis.fish + ``` + + + + #### Temporary (current session) + + ```powershell + oasis completion powershell | Out-String | Invoke-Expression + ``` + + #### Permanent + + Add the following to your PowerShell profile: + ```powershell + oasis completion powershell | Out-String | Invoke-Expression + ``` + + To find your profile path, run `echo $PROFILE` in PowerShell. + + + ## Configuration When running the Oasis CLI for the first time, it will generate a configuration diff --git a/examples/completion/00-wallet-rename-second-arg.in b/examples/completion/00-wallet-rename-second-arg.in new file mode 100644 index 00000000..015ce8f3 --- /dev/null +++ b/examples/completion/00-wallet-rename-second-arg.in @@ -0,0 +1 @@ +oasis __complete wallet rename oldname '' diff --git a/examples/completion/00-wallet-rename-second-arg.out b/examples/completion/00-wallet-rename-second-arg.out new file mode 100644 index 00000000..b6f86717 --- /dev/null +++ b/examples/completion/00-wallet-rename-second-arg.out @@ -0,0 +1 @@ +:4 diff --git a/examples/completion/01-addressbook-rename-second-arg.in b/examples/completion/01-addressbook-rename-second-arg.in new file mode 100644 index 00000000..8a084b66 --- /dev/null +++ b/examples/completion/01-addressbook-rename-second-arg.in @@ -0,0 +1 @@ +oasis __complete addressbook rename oldname '' diff --git a/examples/completion/01-addressbook-rename-second-arg.out b/examples/completion/01-addressbook-rename-second-arg.out new file mode 100644 index 00000000..b6f86717 --- /dev/null +++ b/examples/completion/01-addressbook-rename-second-arg.out @@ -0,0 +1 @@ +:4 diff --git a/examples/completion/02-network-show-flags.in b/examples/completion/02-network-show-flags.in new file mode 100644 index 00000000..43b67988 --- /dev/null +++ b/examples/completion/02-network-show-flags.in @@ -0,0 +1 @@ +oasis __complete network show -- diff --git a/examples/completion/02-network-show-flags.out b/examples/completion/02-network-show-flags.out new file mode 100644 index 00000000..46a8fc48 --- /dev/null +++ b/examples/completion/02-network-show-flags.out @@ -0,0 +1,5 @@ +--format output format [text,json] +--height explicitly set block height to use +--help help for show +--network explicitly set network to use +:4 diff --git a/examples/completion/03-network-flag-value.in b/examples/completion/03-network-flag-value.in new file mode 100644 index 00000000..ef71e334 --- /dev/null +++ b/examples/completion/03-network-flag-value.in @@ -0,0 +1 @@ +oasis __complete network show --network '' diff --git a/examples/completion/03-network-flag-value.out b/examples/completion/03-network-flag-value.out new file mode 100644 index 00000000..03cd61fe --- /dev/null +++ b/examples/completion/03-network-flag-value.out @@ -0,0 +1,3 @@ +mainnet +testnet +:4 diff --git a/examples/completion/04-paratime-flag-value.in b/examples/completion/04-paratime-flag-value.in new file mode 100644 index 00000000..ebebd7bf --- /dev/null +++ b/examples/completion/04-paratime-flag-value.in @@ -0,0 +1 @@ +oasis __complete account show --paratime '' diff --git a/examples/completion/04-paratime-flag-value.out b/examples/completion/04-paratime-flag-value.out new file mode 100644 index 00000000..4d810a7e --- /dev/null +++ b/examples/completion/04-paratime-flag-value.out @@ -0,0 +1,4 @@ +cipher +emerald +sapphire +:4 diff --git a/examples/completion/05-wallet-show-completion.in b/examples/completion/05-wallet-show-completion.in new file mode 100644 index 00000000..59e398b0 --- /dev/null +++ b/examples/completion/05-wallet-show-completion.in @@ -0,0 +1 @@ +oasis __complete wallet show '' diff --git a/examples/completion/05-wallet-show-completion.out b/examples/completion/05-wallet-show-completion.out new file mode 100644 index 00000000..9df3074e --- /dev/null +++ b/examples/completion/05-wallet-show-completion.out @@ -0,0 +1,11 @@ +alice +bob +test:alice +test:bob +test:charlie +test:cory +test:dave +test:erin +test:frank +test:grace +:4 diff --git a/examples/completion/06-addressbook-show-completion.in b/examples/completion/06-addressbook-show-completion.in new file mode 100644 index 00000000..20fa9f63 --- /dev/null +++ b/examples/completion/06-addressbook-show-completion.in @@ -0,0 +1 @@ +oasis __complete addressbook show '' diff --git a/examples/completion/06-addressbook-show-completion.out b/examples/completion/06-addressbook-show-completion.out new file mode 100644 index 00000000..00dd17ba --- /dev/null +++ b/examples/completion/06-addressbook-show-completion.out @@ -0,0 +1,2 @@ +treasury +:4 diff --git a/examples/completion/config/cli.toml b/examples/completion/config/cli.toml new file mode 100644 index 00000000..986c604f --- /dev/null +++ b/examples/completion/config/cli.toml @@ -0,0 +1,137 @@ +last_migration = 1 + +[address_book] +[address_book.treasury] +address = 'oasis1qz78phkdan64g040cvqvqpwkplfqf6tj6uwcsh30' +description = '' +eth_address = '' + +[networks] +default = 'mainnet' + +[networks.mainnet] +chain_context = 'bb3d748def55bdfb797a2ac53ee6ee141e54cd2ab2dc2375f4a0703a178e6e55' +description = '' +rpc = 'grpc.oasis.io:443' + +[networks.mainnet.denomination] +decimals = 9 +symbol = 'ROSE' + +[networks.mainnet.paratimes] +default = 'sapphire' + +[networks.mainnet.paratimes.cipher] +consensus_denomination = '_' +description = '' +id = '000000000000000000000000000000000000000000000000e199119c992377cb' + +[networks.mainnet.paratimes.cipher.denominations] +[networks.mainnet.paratimes.cipher.denominations._] +decimals = 9 +symbol = 'ROSE' + +[networks.mainnet.paratimes.emerald] +consensus_denomination = '_' +description = '' +id = '000000000000000000000000000000000000000000000000e2eaa99fc008f87f' + +[networks.mainnet.paratimes.emerald.denominations] +[networks.mainnet.paratimes.emerald.denominations._] +decimals = 18 +symbol = 'ROSE' + +[networks.mainnet.paratimes.sapphire] +consensus_denomination = '_' +description = '' +id = '000000000000000000000000000000000000000000000000f80306c9858e7279' + +[networks.mainnet.paratimes.sapphire.denominations] +[networks.mainnet.paratimes.sapphire.denominations._] +decimals = 18 +symbol = 'ROSE' + +[networks.testnet] +chain_context = '0b91b8e4e44b2003a7c5e23ddadb5e14ef5345c0ebcb3ddcae07fa2f244cab76' +description = '' +rpc = 'testnet.grpc.oasis.io:443' + +[networks.testnet.denomination] +decimals = 9 +symbol = 'TEST' + +[networks.testnet.paratimes] +default = 'sapphire' + +[networks.testnet.paratimes.cipher] +consensus_denomination = '_' +description = '' +id = '0000000000000000000000000000000000000000000000000000000000000000' + +[networks.testnet.paratimes.cipher.denominations] +[networks.testnet.paratimes.cipher.denominations._] +decimals = 9 +symbol = 'TEST' + +[networks.testnet.paratimes.emerald] +consensus_denomination = '_' +description = '' +id = '00000000000000000000000000000000000000000000000072c8215e60d5bca7' + +[networks.testnet.paratimes.emerald.denominations] +[networks.testnet.paratimes.emerald.denominations._] +decimals = 18 +symbol = 'TEST' + +[networks.testnet.paratimes.pontusx_dev] +consensus_denomination = 'TEST' +description = 'Pontus-X Devnet' +id = '0000000000000000000000000000000000000000000000004febe52eb412b421' + +[networks.testnet.paratimes.pontusx_dev.denominations] +[networks.testnet.paratimes.pontusx_dev.denominations._] +decimals = 18 +symbol = 'EUROe' + +[networks.testnet.paratimes.pontusx_dev.denominations.test] +decimals = 18 +symbol = 'TEST' + +[networks.testnet.paratimes.pontusx_test] +consensus_denomination = 'TEST' +description = 'Pontus-X Testnet' +id = '00000000000000000000000000000000000000000000000004a6f9071c007069' + +[networks.testnet.paratimes.pontusx_test.denominations] +[networks.testnet.paratimes.pontusx_test.denominations._] +decimals = 18 +symbol = 'EUROe' + +[networks.testnet.paratimes.pontusx_test.denominations.test] +decimals = 18 +symbol = 'TEST' + +[networks.testnet.paratimes.sapphire] +consensus_denomination = '_' +description = '' +id = '000000000000000000000000000000000000000000000000a6d1e3ebf60dff6c' + +[networks.testnet.paratimes.sapphire.denominations] +[networks.testnet.paratimes.sapphire.denominations._] +decimals = 18 +symbol = 'TEST' + +[wallets] +default = 'alice' + +[wallets.alice] +address = 'oasis1qrec770vrek0a9a5lcrv0zvt22504k68svq7kzve' +algorithm = 'ed25519-adr8' +description = '' +kind = 'file' + +[wallets.bob] +address = 'oasis1qrydpazemvuwtnp3efm7vmfvg3tde044qg6cxwzx' +algorithm = 'ed25519-adr8' +description = '' +kind = 'file' diff --git a/examples/setup/first-run.out b/examples/setup/first-run.out index 7126bdb3..55f4c702 100644 --- a/examples/setup/first-run.out +++ b/examples/setup/first-run.out @@ -6,7 +6,7 @@ Usage: Available Commands: account Account operations addressbook Manage addresses in the local address book - completion Generate the autocompletion script for the specified shell + completion Generate shell completion script contract WebAssembly smart contracts operations help Help about any command network Consensus layer operations diff --git a/scripts/gen_example.sh b/scripts/gen_example.sh index cd748e35..72e58692 100755 --- a/scripts/gen_example.sh +++ b/scripts/gen_example.sh @@ -76,7 +76,14 @@ function init_cfg() { init_cfg # Replace "oasis" with the actual path to the Oasis CLI executable. -CMD="$(sed "s#oasis#$OASIS_CMD#" $IN) ${CFG_FLAG} ${YES_FLAG}" +# For __complete commands, insert --config right after the executable +# (appending at end breaks completion parsing). +CMD_CONTENT=$(cat $IN) +if [[ "$CMD_CONTENT" == *"__complete"* ]]; then + CMD="$(sed "s#oasis#$OASIS_CMD ${CFG_FLAG}#" $IN)" +else + CMD="$(sed "s#oasis#$OASIS_CMD#" $IN) ${CFG_FLAG} ${YES_FLAG}" +fi echo " ${CMD}" # Use UTC timezone.