Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Structured success output is rolling out command by command. Currently migrated

Command results and JSON errors are written to stdout. Status, progress, human-facing warnings, diagnostics, and verbose logs are written to stderr. JSON errors include a `warnings` array for machine-actionable warnings; it is `[]` when no warnings are present. New operation-style JSON payloads should use the same `warnings` field.

Successful JSON responses are command-specific. Operation commands return an `input` object, a `results` array, and a `warnings` array. For commands such as `mkdir`, each result reports what happened to the requested path:
Successful JSON responses are command-specific. Operation-style commands return an `input` object, a `results` array, and a `warnings` array. For commands such as `mkdir`, each result reports what happened to the requested path:

```json
{
Expand Down Expand Up @@ -299,28 +299,45 @@ Commands that return entry lists, such as `ls`, `search`, and `revs`, return an
}
```

Account and usage commands return command-specific objects:
Account and usage commands use the operation-style wrapper with a single result:

```json
{
"input": {},
"account": {
"type": "full",
"account_id": "dbid:...",
"email": "user@example.com",
"email_verified": true,
"disabled": false
}
"results": [
{
"kind": "account",
"input": {},
"result": {
"type": "full",
"account_id": "dbid:...",
"email": "user@example.com",
"email_verified": true,
"disabled": false
}
}
],
"warnings": []
}
```

```json
{
"used": 123,
"allocation": {
"type": "individual",
"allocated": 100000
}
"input": {},
"results": [
{
"kind": "space_usage",
"input": {},
"result": {
"used": 123,
"allocation": {
"type": "individual",
"allocated": 100000
}
}
}
],
"warnings": []
}
```

Expand Down
29 changes: 14 additions & 15 deletions cmd/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ type accountInput struct {
AccountID string `json:"account_id,omitempty"`
}

type accountOutput struct {
Input accountInput `json:"input"`
Account jsonAccount `json:"account"`
}

type jsonAccount struct {
Type string `json:"type"`
AccountID string `json:"account_id"`
Expand Down Expand Up @@ -64,6 +59,8 @@ type jsonAccountTeam struct {
MemberID string `json:"member_id,omitempty"`
}

const accountKindAccount = "account"

// renderFullAccount prints the account details returned by GetCurrentAccount.
func renderFullAccount(out io.Writer, fa *users.FullAccount) error {
w := new(tabwriter.Writer)
Expand Down Expand Up @@ -117,12 +114,10 @@ func account(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
input := accountInput{}
return out.Render(func(w io.Writer) error {
return renderFullAccount(w, res)
}, accountOutput{
Input: accountInput{},
Account: jsonFullAccount(res),
})
}, newAccountOperationOutput(input, jsonFullAccount(res)))
}

// Otherwise look up an account with the provided ID
Expand All @@ -131,14 +126,18 @@ func account(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
input := accountInput{
AccountID: args[0],
}
return out.Render(func(w io.Writer) error {
return renderBasicAccount(w, res)
}, accountOutput{
Input: accountInput{
AccountID: args[0],
},
Account: jsonBasicAccount(res),
})
}, newAccountOperationOutput(input, jsonBasicAccount(res)))
}

func newAccountOperationOutput(input accountInput, account jsonAccount) jsonOperationOutput {
return newJSONOperationOutput(input, []jsonOperationResult{
newJSONOperationResult("", accountKindAccount, input, account),
}, nil)
}

func jsonFullAccount(fa *users.FullAccount) jsonAccount {
Expand Down
45 changes: 40 additions & 5 deletions cmd/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ func TestAccountCurrentJSONOutputsAccount(t *testing.T) {
if got.Input.AccountID != "" {
t.Fatalf("input.account_id = %q, want empty for current account", got.Input.AccountID)
}
account := got.Account
result := got.Results[0]
if result.Kind != accountKindAccount {
t.Fatalf("kind = %q, want account", result.Kind)
}
if result.Input.AccountID != "" {
t.Fatalf("result input.account_id = %q, want empty for current account", result.Input.AccountID)
}
account := result.Result
if account.Type != "full" || account.AccountID != "dbid:current" || account.Email != "test@example.com" {
t.Fatalf("account = %#v, want current full account", account)
}
Expand Down Expand Up @@ -90,7 +97,14 @@ func TestAccountLookupJSONUsesAccountID(t *testing.T) {
if got.Input.AccountID != "dbid:lookup" {
t.Fatalf("input.account_id = %q, want dbid:lookup", got.Input.AccountID)
}
account := got.Account
result := got.Results[0]
if result.Kind != accountKindAccount {
t.Fatalf("kind = %q, want account", result.Kind)
}
if result.Input.AccountID != "dbid:lookup" {
t.Fatalf("result input.account_id = %q, want dbid:lookup", result.Input.AccountID)
}
account := result.Result
if account.Type != "basic" || account.AccountID != "dbid:lookup" || account.Email != "lookup@example.com" {
t.Fatalf("account = %#v, want lookup basic account", account)
}
Expand Down Expand Up @@ -137,13 +151,34 @@ func setAccountOutputJSON(t *testing.T, cmd *cobra.Command) {
}
}

func decodeAccountOutput(t *testing.T, out *bytes.Buffer) accountOutput {
type accountJSONOutput struct {
Input accountInput `json:"input"`
Results []accountJSONResult `json:"results"`
Warnings []jsonWarning `json:"warnings"`
}

type accountJSONResult struct {
Kind string `json:"kind"`
Input accountInput `json:"input"`
Result jsonAccount `json:"result"`
}

func decodeAccountOutput(t *testing.T, out *bytes.Buffer) accountJSONOutput {
t.Helper()

var got accountOutput
if err := json.NewDecoder(out).Decode(&got); err != nil {
var got accountJSONOutput
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
t.Fatalf("decode JSON output: %v\noutput: %s", err, out.String())
}
if got.Warnings == nil {
t.Fatalf("warnings = nil, want empty array")
}
if len(got.Warnings) != 0 {
t.Fatalf("warnings = %+v, want empty", got.Warnings)
}
if len(got.Results) != 1 {
t.Fatalf("results len = %d, want 1", len(got.Results))
}
return got
}

Expand Down
186 changes: 186 additions & 0 deletions cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright © 2016 Dropbox, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

func newCompletionCmd() *cobra.Command {
noDesc := false
programName := RootCmd.Name()

completionCmd := &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate the autocompletion script for the specified shell",
Long: completionLong(programName),
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}

addCompletionNoDescFlag := func(cmd *cobra.Command) {
cmd.Flags().BoolVar(&noDesc, "no-descriptions", false, "disable completion descriptions")
}

bash := &cobra.Command{
Use: "bash",
Short: "Generate the autocompletion script for bash",
Long: bashCompletionLong(programName),
Args: cobra.NoArgs,
DisableFlagsInUseLine: true,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Root().GenBashCompletionV2(cmd.Root().OutOrStdout(), !noDesc)
},
}
addCompletionNoDescFlag(bash)

zsh := &cobra.Command{
Use: "zsh",
Short: "Generate the autocompletion script for zsh",
Long: zshCompletionLong(programName),
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
if noDesc {
return cmd.Root().GenZshCompletionNoDesc(cmd.Root().OutOrStdout())
}
return cmd.Root().GenZshCompletion(cmd.Root().OutOrStdout())
},
}
addCompletionNoDescFlag(zsh)

fish := &cobra.Command{
Use: "fish",
Short: "Generate the autocompletion script for fish",
Long: fishCompletionLong(programName),
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Root().GenFishCompletion(cmd.Root().OutOrStdout(), !noDesc)
},
}
addCompletionNoDescFlag(fish)

powershell := &cobra.Command{
Use: "powershell",
Short: "Generate the autocompletion script for powershell",
Long: powershellCompletionLong(programName),
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
RunE: func(cmd *cobra.Command, args []string) error {
if noDesc {
return cmd.Root().GenPowerShellCompletion(cmd.Root().OutOrStdout())
}
return cmd.Root().GenPowerShellCompletionWithDesc(cmd.Root().OutOrStdout())
},
}
addCompletionNoDescFlag(powershell)

completionCmd.AddCommand(bash, zsh, fish, powershell)
return completionCmd
}

func completionLong(programName string) string {
return fmt.Sprintf(`Generate the autocompletion script for %s for the specified shell.
See each sub-command's help for details on how to use the generated script.
`, programName)
}

func bashCompletionLong(programName string) string {
return fmt.Sprintf(`Generate the autocompletion script for the bash shell.

This script depends on the 'bash-completion' package.
If it is not installed already, you can install it via your OS's package manager.

To load completions in your current shell session:

source <(%[1]s completion bash)

To load completions for every new session, execute once:

#### Linux:

%[1]s completion bash > /etc/bash_completion.d/%[1]s

#### macOS:

%[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s

You will need to start a new shell for this setup to take effect.
`, programName)
}

func zshCompletionLong(programName string) string {
return fmt.Sprintf(`Generate the autocompletion script for the zsh shell.

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 in your current shell session:

source <(%[1]s completion zsh)

To load completions for every new session, execute once:

#### Linux:

%[1]s completion zsh > "${fpath[1]}/_%[1]s"

#### macOS:

%[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s

You will need to start a new shell for this setup to take effect.
`, programName)
}

func fishCompletionLong(programName string) string {
return fmt.Sprintf(`Generate the autocompletion script for the fish shell.

To load completions in your current shell session:

%[1]s completion fish | source

To load completions for every new session, execute once:

%[1]s completion fish > ~/.config/fish/completions/%[1]s.fish

You will need to start a new shell for this setup to take effect.
`, programName)
}

func powershellCompletionLong(programName string) string {
return fmt.Sprintf(`Generate the autocompletion script for powershell.

To load completions in your current shell session:

%[1]s completion powershell | Out-String | Invoke-Expression

To load completions for every new session, add the output of the above command
to your powershell profile.
`, programName)
}

func init() {
RootCmd.AddCommand(newCompletionCmd())
}
Loading
Loading