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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `dbxcli`: Dropbox from the command line

[![CI](https://github.com/dropbox/dbxcli/actions/workflows/ci.yml/badge.svg)](https://github.com/dropbox/dbxcli/actions/workflows/ci.yml)
[![Go Report Card](https://goreportcard.com/badge/github.com/dropbox/dbxcli)](https://goreportcard.com/report/github.com/dropbox/dbxcli)
[![Go Report Card](https://goreportcard.com/badge/github.com/dropbox/dbxcli/v3)](https://goreportcard.com/report/github.com/dropbox/dbxcli/v3)

`dbxcli` is a scriptable Dropbox CLI for files, shared links, teams, and
automation workflows. It is built for humans at the terminal, scripts, CI jobs,
Expand Down Expand Up @@ -126,7 +126,7 @@ Release assets include:
### Build from source

```sh
go install github.com/dropbox/dbxcli@latest
go install github.com/dropbox/dbxcli/v3@latest
```

Or build from a clone:
Expand Down
2 changes: 1 addition & 1 deletion cmd/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"fmt"
"strings"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/spf13/cobra"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"testing"
"time"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/spf13/cobra"
Expand Down
2 changes: 1 addition & 1 deletion cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"strings"
"time"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/dustin/go-humanize"
"github.com/mitchellh/ioprogress"
Expand Down
2 changes: 1 addition & 1 deletion cmd/help_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"sort"
"strings"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand Down
3 changes: 2 additions & 1 deletion cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"strings"
"testing"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -519,6 +519,7 @@ func expectedJSONErrorCodes() []string {
jsonErrorCodeEnvTokenStillActive,
jsonErrorCodeInvalidArguments,
jsonErrorCodeNotFound,
jsonErrorCodePartialTransfer,
jsonErrorCodePathConflict,
jsonErrorCodePermissionDenied,
jsonErrorCodeRateLimited,
Expand Down
2 changes: 1 addition & 1 deletion cmd/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"os"
"sort"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/auth"
"github.com/spf13/cobra"
Expand Down
2 changes: 1 addition & 1 deletion cmd/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"strings"
"text/tabwriter"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/dustin/go-humanize"
"github.com/spf13/cobra"
Expand Down
2 changes: 1 addition & 1 deletion cmd/mv.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"fmt"
"strings"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/spf13/cobra"
)
Expand Down
53 changes: 51 additions & 2 deletions cmd/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"reflect"
"strings"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
dropboxauth "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/auth"
"github.com/spf13/cobra"
Expand All @@ -25,6 +25,7 @@ const (
jsonErrorCodeEnvTokenStillActive = "env_token_still_active"
jsonErrorCodeInvalidArguments = "invalid_arguments"
jsonErrorCodeNotFound = "not_found"
jsonErrorCodePartialTransfer = "partial_transfer"
jsonErrorCodePathConflict = "path_conflict"
jsonErrorCodePermissionDenied = "permission_denied"
jsonErrorCodeRateLimited = "rate_limited"
Expand All @@ -33,6 +34,18 @@ const (
jsonErrorCodeUnknownFlag = "unknown_flag"
)

const (
exitCodeSuccess = 0
exitCodeGenericError = 1
exitCodeAuthFailure = 2
exitCodePermissionDenied = 3
exitCodeNotFound = 4
exitCodeConflict = 5
exitCodeRateLimited = 6
exitCodeValidationError = 7
exitCodePartialTransfer = 8
)

type jsonCodedError interface {
error
JSONErrorCode() string
Expand Down Expand Up @@ -202,7 +215,11 @@ func parseOutputFormat(value string) (output.Format, error) {
case output.FormatJSON:
return output.FormatJSON, nil
default:
return "", fmt.Errorf("unsupported output format %q: use text or json", value)
return "", invalidArgumentsErrorfWithDetails(
"unsupported output format %q: use text or json",
flagValueErrorDetails(outputFlag, value),
value,
)
}
}

Expand Down Expand Up @@ -276,6 +293,38 @@ func renderCommandErrorWithJSON(cmd *cobra.Command, err error, forceJSON bool) {
}
}

func exitCodeForError(err error) int {
if err == nil {
return exitCodeSuccess
}

switch jsonErrorCode(err) {
case jsonErrorCodeAppKeyRequired,
jsonErrorCodeAuthExchangeFailed,
jsonErrorCodeAuthRefreshFailed,
jsonErrorCodeAuthRequired,
jsonErrorCodeEnvTokenStillActive:
return exitCodeAuthFailure
case jsonErrorCodePermissionDenied:
return exitCodePermissionDenied
case jsonErrorCodeNotFound:
return exitCodeNotFound
case jsonErrorCodePathConflict:
return exitCodeConflict
case jsonErrorCodeRateLimited:
return exitCodeRateLimited
case jsonErrorCodeInvalidArguments,
jsonErrorCodeStructuredOutputUnsupported,
jsonErrorCodeUnknownCommand,
jsonErrorCodeUnknownFlag:
return exitCodeValidationError
case jsonErrorCodePartialTransfer:
return exitCodePartialTransfer
default:
return exitCodeGenericError
}
}

// outputJSONRequested is a narrow pre-parse fallback for errors that happen
// before Cobra has resolved a command/flag context, such as unknown commands.
func outputJSONRequested(args []string) bool {
Expand Down
122 changes: 121 additions & 1 deletion cmd/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strings"
"testing"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
dropboxauth "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/auth"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
Expand Down Expand Up @@ -770,6 +770,11 @@ func TestJSONErrorCodeUsesCodedErrors(t *testing.T) {
err: authRefreshFailedErrorf("refresh saved Dropbox credentials: failed"),
want: jsonErrorCodeAuthRefreshFailed,
},
{
name: "partial transfer",
err: partialStdoutError(12),
want: jsonErrorCodePartialTransfer,
},
}

for _, tt := range tests {
Expand All @@ -781,6 +786,121 @@ func TestJSONErrorCodeUsesCodedErrors(t *testing.T) {
}
}

func TestExitCodeForStableJSONErrorCodes(t *testing.T) {
tests := []struct {
code string
want int
}{
{jsonErrorCodeAppKeyRequired, exitCodeAuthFailure},
{jsonErrorCodeAuthExchangeFailed, exitCodeAuthFailure},
{jsonErrorCodeAuthRefreshFailed, exitCodeAuthFailure},
{jsonErrorCodeAuthRequired, exitCodeAuthFailure},
{jsonErrorCodeCommandFailed, exitCodeGenericError},
{jsonErrorCodeDropboxAPIError, exitCodeGenericError},
{jsonErrorCodeEnvTokenStillActive, exitCodeAuthFailure},
{jsonErrorCodeInvalidArguments, exitCodeValidationError},
{jsonErrorCodeNotFound, exitCodeNotFound},
{jsonErrorCodePartialTransfer, exitCodePartialTransfer},
{jsonErrorCodePathConflict, exitCodeConflict},
{jsonErrorCodePermissionDenied, exitCodePermissionDenied},
{jsonErrorCodeRateLimited, exitCodeRateLimited},
{jsonErrorCodeStructuredOutputUnsupported, exitCodeValidationError},
{jsonErrorCodeUnknownCommand, exitCodeValidationError},
{jsonErrorCodeUnknownFlag, exitCodeValidationError},
}

if got := exitCodeForError(nil); got != exitCodeSuccess {
t.Fatalf("exitCodeForError(nil) = %d, want %d", got, exitCodeSuccess)
}

for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
err := newCodedError(tt.code, errors.New(tt.code))
if got := exitCodeForError(err); got != tt.want {
t.Fatalf("exitCodeForError(%s) = %d, want %d", tt.code, got, tt.want)
}
})
}
}

func TestExitCodeForCommonFailures(t *testing.T) {
tests := []struct {
name string
err error
want int
}{
{
name: "generic",
err: errors.New("unexpected local failure"),
want: exitCodeGenericError,
},
{
name: "auth",
err: authRequiredErrorf("no saved Dropbox credentials"),
want: exitCodeAuthFailure,
},
{
name: "permission denied",
err: dropboxauth.AccessAPIError{},
want: exitCodePermissionDenied,
},
{
name: "not found",
err: files.GetMetadataAPIError{APIError: dropbox.APIError{ErrorSummary: "path/not_found/"}},
want: exitCodeNotFound,
},
{
name: "conflict",
err: pathConflictErrorWithPath("/file", "path exists and is not a folder: /file"),
want: exitCodeConflict,
},
{
name: "rate limited",
err: dropboxauth.RateLimitAPIError{},
want: exitCodeRateLimited,
},
{
name: "validation",
err: invalidArgumentsErrorWithDetails("missing path", argumentErrorDetails("path")),
want: exitCodeValidationError,
},
{
name: "partial transfer",
err: partialStdoutError(12),
want: exitCodePartialTransfer,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := exitCodeForError(tt.err); got != tt.want {
t.Fatalf("exitCodeForError(%v) = %d, want %d", tt.err, got, tt.want)
}
})
}
}

func TestParseOutputFormatInvalidValueIsValidationError(t *testing.T) {
_, err := parseOutputFormat("yaml")
if err == nil {
t.Fatal("expected invalid output format error")
}
if got := jsonErrorCode(err); got != jsonErrorCodeInvalidArguments {
t.Fatalf("jsonErrorCode = %q, want %q", got, jsonErrorCodeInvalidArguments)
}
if got := exitCodeForError(err); got != exitCodeValidationError {
t.Fatalf("exitCodeForError = %d, want %d", got, exitCodeValidationError)
}

details := jsonErrorDetails(err)
if details["flag"] != outputFlag {
t.Fatalf("details[flag] = %v, want %q", details["flag"], outputFlag)
}
if details["value"] != "yaml" {
t.Fatalf("details[value] = %v, want yaml", details["value"])
}
}

func TestJSONErrorCodeClassifiesDropboxAPIErrors(t *testing.T) {
expiredAuth := dropboxauth.AuthAPIError{AuthError: &dropboxauth.AuthError{}}
expiredAuth.AuthError.Tag = dropboxauth.AuthErrorExpiredAccessToken
Expand Down
2 changes: 1 addition & 1 deletion cmd/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
"sync"
"time"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/dustin/go-humanize"
Expand Down
2 changes: 1 addition & 1 deletion cmd/revs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"io"
"text/tabwriter"

"github.com/dropbox/dbxcli/internal/output"
"github.com/dropbox/dbxcli/v3/internal/output"
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
"github.com/spf13/cobra"
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func Execute() {
cmd, err := RootCmd.ExecuteC()
if err != nil {
renderCommandErrorWithJSON(cmd, err, jsonErrorOutput)
os.Exit(1)
os.Exit(exitCodeForError(err))
}
}

Expand Down
Loading
Loading