Skip to content

Commit

Permalink
Merge pull request #169 from danielgtaylor/exit-from-status-code
Browse files Browse the repository at this point in the history
feat: set exit code from status code, fixes #125
  • Loading branch information
danielgtaylor authored Jan 19, 2023
2 parents c6a3394 + e73426b commit 9687640
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 6 deletions.
10 changes: 10 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ Not after (expires): %s (%s)
AddGlobalFlag("rsh-client-cert", "", "Path to a PEM encoded client certificate", "", false)
AddGlobalFlag("rsh-client-key", "", "Path to a PEM encoded private key", "", false)
AddGlobalFlag("rsh-ca-cert", "", "Path to a PEM encoded CA cert", "", false)
AddGlobalFlag("rsh-ignore-status-code", "", "Do not set exit code from HTTP status code", false, false)

Root.RegisterFlagCompletionFunc("rsh-output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"auto", "json", "yaml"}, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -818,3 +819,12 @@ func Run() (returnErr error) {

return returnErr
}

// GetExitCode returns the exit code to use based on the last HTTP status code.
func GetExitCode() int {
if s := GetLastStatus() / 100; s > 2 && !viper.GetBool("rsh-ignore-status-code") {
return s
}

return 0
}
21 changes: 20 additions & 1 deletion cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ func expectJSON(t *testing.T, cmd string, expected string) {
assert.JSONEq(t, expected, captured)
}

func expectExitCode(t *testing.T, expected int) {
assert.Equal(t, expected, GetExitCode())
}

func TestGetURI(t *testing.T) {
defer gock.Off()

Expand All @@ -63,6 +67,7 @@ func TestGetURI(t *testing.T) {
expectJSON(t, "http://example.com/foo", `{
"Hello": "World"
}`)
expectExitCode(t, 0)
}

func TestPostURI(t *testing.T) {
Expand All @@ -82,13 +87,27 @@ func TestPostURI(t *testing.T) {
func TestPutURI400(t *testing.T) {
defer gock.Off()

gock.New("http://example.com").Put("/foo/1").Reply(400).JSON(map[string]interface{}{
gock.New("http://example.com").Put("/foo/1").Reply(422).JSON(map[string]interface{}{
"detail": "Invalid input",
})

expectJSON(t, "put http://example.com/foo/1 value: 123", `{
"detail": "Invalid input"
}`)
expectExitCode(t, 4)
}

func TestIgnoreStatusCodeExit(t *testing.T) {
defer gock.Off()

gock.New("http://example.com").Put("/foo/1").Reply(400).JSON(map[string]interface{}{
"detail": "Invalid input",
})

expectJSON(t, "put http://example.com/foo/1 value: 123 --rsh-ignore-status-code", `{
"detail": "Invalid input"
}`)
expectExitCode(t, 0)
}

func TestHeaderWithComma(t *testing.T) {
Expand Down
36 changes: 31 additions & 5 deletions cli/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ import (
"github.com/spf13/viper"
)

// lastStatus is the last HTTP status code returned by a request.
var lastStatus int

// GetLastStatus returns the last HTTP status code returned by a request. A
// request can opt out of this via the IgnoreStatus option.
func GetLastStatus() int {
return lastStatus
}

// FixAddress can convert `:8000` or `example.com` to a full URL.
func FixAddress(addr string) string {
return fixAddress(addr)
Expand Down Expand Up @@ -60,8 +69,9 @@ func fixAddress(addr string) string {
}

type requestOption struct {
client *http.Client
disableLog bool
client *http.Client
disableLog bool
ignoreStatus bool
}

// WithClient sets the client to use for the request.
Expand All @@ -78,6 +88,13 @@ func WithoutLog() requestOption {
}
}

// IgnoreStatus ignores the response status code.
func IgnoreStatus() requestOption {
return requestOption{
ignoreStatus: true,
}
}

// MakeRequest makes an HTTP request using the default client. It adds the
// user-agent, auth, and any passed headers or query params to the request
// before sending it out on the wire. If verbose mode is enabled, it will
Expand Down Expand Up @@ -226,6 +243,7 @@ func MakeRequest(req *http.Request, options ...requestOption) (*http.Response, e
}

log := true
setStatus := true
for _, option := range options {
if option.client != nil {
client = option.client
Expand All @@ -234,6 +252,10 @@ func MakeRequest(req *http.Request, options ...requestOption) (*http.Response, e
if option.disableLog {
log = false
}

if option.ignoreStatus {
setStatus = false
}
}

if log {
Expand All @@ -245,6 +267,10 @@ func MakeRequest(req *http.Request, options ...requestOption) (*http.Response, e
return nil, err
}

if setStatus {
lastStatus = resp.StatusCode
}

if log {
LogDebugResponse(start, resp)
}
Expand Down Expand Up @@ -350,8 +376,8 @@ func ParseResponse(resp *http.Response) (Response, error) {
// GetParsedResponse makes a request and gets the parsed response back. It
// handles any auto-pagination or linking that needs to be done and may
// return a psuedo-responsse that is a combination of all responses.
func GetParsedResponse(req *http.Request) (Response, error) {
resp, err := MakeRequest(req)
func GetParsedResponse(req *http.Request, options ...requestOption) (Response, error) {
resp, err := MakeRequest(req, options...)
if err != nil {
return Response{}, err
}
Expand Down Expand Up @@ -388,7 +414,7 @@ func GetParsedResponse(req *http.Request) (Response, error) {
next = base.ResolveReference(next)
req, _ = http.NewRequest(http.MethodGet, next.String(), nil)

resp, err = MakeRequest(req)
resp, err = MakeRequest(req, options...)
if err != nil {
return Response{}, err
}
Expand Down
38 changes: 38 additions & 0 deletions cli/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,41 @@ func TestAuthHookFailure(t *testing.T) {
MakeRequest(r)
})
}

func TestGetStatus(t *testing.T) {
defer gock.Off()

reset(false)
lastStatus = 0

gock.New("http://example.com").
Get("/").
Reply(http.StatusOK)

req, _ := http.NewRequest(http.MethodGet, "http://example.com/", nil)
resp, err := MakeRequest(req)

assert.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)

assert.Equal(t, http.StatusOK, GetLastStatus())
}

func TestIgnoreStatus(t *testing.T) {
defer gock.Off()

reset(false)
lastStatus = 0

gock.New("http://example.com").
Get("/").
Reply(http.StatusOK)

req, _ := http.NewRequest(http.MethodGet, "http://example.com/", nil)
resp, err := MakeRequest(req, IgnoreStatus())

assert.NoError(t, err)
assert.Equal(t, resp.StatusCode, http.StatusOK)

assert.Equal(t, 0, GetLastStatus())
}
15 changes: 15 additions & 0 deletions docs/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,18 @@ $ restish api.rest.sh/types -H Accept:application/json -r >types.json
```

?> Raw mode without filtering will not parse the response, but _will_ decode it if compressed (e.g. with gzip or brotli).

## Exit Status Codes

Restish will exit with the following status codes by default in order to facilitate scripting. The most recent HTTP status code is used when a command makes more than one request.

| Code | Description |
| ---- | -------------------- |
| 0 | Success |
| 1 | Unrecoverable errors |
| 2 | - |
| 3 | 3xx HTTP response |
| 4 | 4xx HTTP response |
| 5 | 5xx HTTP response |

Use the `--rsh-ignore-status-code` option or `RSH_IGNORE_STATUS_CODE=1` environment variable to ignore the exit status code and always return 0 for 3xx/4xx/5xx responses.
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ func main() {
if err := cli.Run(); err != nil {
os.Exit(1)
}

// Exit based on the status code of the last request.
os.Exit(cli.GetExitCode())
}

0 comments on commit 9687640

Please sign in to comment.