Skip to content

Commit

Permalink
fix halt and halt_error to stop command immediately (close #244)
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Mar 24, 2024
1 parent 971f5d6 commit 857f6a7
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 50 deletions.
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -116,6 +116,12 @@ func main() {
break
}
if err, ok := v.(error); ok {
if err, ok := err.(gojq.HaltError); ok {
if err.Value() != nil {
log.Fatalln(err)
}
break
}
log.Fatalln(err)
}
fmt.Printf("%#v\n", v)
Expand All @@ -129,7 +135,12 @@ func main() {
- or alternatively, compile the query using [`gojq.Compile`](https://pkg.go.dev/github.com/itchyny/gojq#Compile) and then [`code.Run`](https://pkg.go.dev/github.com/itchyny/gojq#Code.Run) or [`code.RunWithContext`](https://pkg.go.dev/github.com/itchyny/gojq#Code.RunWithContext). You can reuse the `*Code` against multiple inputs to avoid compilation of the same query. But for arguments of `code.Run`, do not give values sharing same data between multiple calls.
- In either case, you cannot use custom type values as the query input. The type should be `[]any` for an array and `map[string]any` for a map (just like decoded to an `any` using the [encoding/json](https://golang.org/pkg/encoding/json/) package). You can't use `[]int` or `map[string]string`, for example. If you want to query your custom struct, marshal to JSON, unmarshal to `any` and use it as the query input.
- Thirdly, iterate through the results using [`iter.Next() (any, bool)`](https://pkg.go.dev/github.com/itchyny/gojq#Iter). The iterator can emit an error so make sure to handle it. The method returns `true` with results, and `false` when the iterator terminates.
- The return type is not `(any, error)` because iterators can emit multiple errors and you can continue after an error. It is difficult for the iterator to tell the termination in this situation.
- The return type is not `(any, error)` because the iterator may emit multiple errors. The `jq` and `gojq` commands stop the iteration on the first error, but the library user can choose to stop the iteration on errors, or to continue until it terminates.
- In any case, it is recommended to stop the iteration on [`HaltError`](https://pkg.go.dev/github.com/itchyny/gojq#HaltError), which is emitted on `halt` and `halt_error` functions, although these functions are not commonly used.
If the error value of `HaltError` is `nil`, stop the iteration without handling the error.
Technically speaking, we can fix the iterator to terminate on the halting error, but it does not terminate at the moment.
The `halt` function in jq not only stops the iteration, but also terminates the command execution, even if there are still input values.
So, gojq leaves it up to the library user how to handle the halting error.
- Note that the result iterator may emit infinite number of values; `repeat(0)` and `range(infinite)`. It may stuck with no output value; `def f: f; f`. Use `RunWithContext` when you want to limit the execution time.
[`gojq.Compile`](https://pkg.go.dev/github.com/itchyny/gojq#Compile) allows to configure the following compiler options.
Expand Down
52 changes: 27 additions & 25 deletions cli/cli.go
Expand Up @@ -85,7 +85,9 @@ var addDefaultModulePaths = true

func (cli *cli) run(args []string) int {
if err := cli.runInternal(args); err != nil {
cli.printError(err)
if _, ok := err.(interface{ isEmptyError() }); !ok {
fmt.Fprintf(cli.errStream, "%s: %s\n", name, err)
}
if err, ok := err.(interface{ ExitCode() int }); ok {
return err.ExitCode()
}
Expand Down Expand Up @@ -323,18 +325,35 @@ func (cli *cli) process(iter inputIter, code *gojq.Code) error {
for {
v, ok := iter.Next()
if !ok {
return err
break
}
if er, ok := v.(error); ok {
cli.printError(er)
err = &emptyError{er}
if e, ok := v.(error); ok {
fmt.Fprintf(cli.errStream, "%s: %s\n", name, e)
err = e
continue
}
if er := cli.printValues(code.Run(v, cli.argvalues...)); er != nil {
cli.printError(er)
err = &emptyError{er}
if e := cli.printValues(code.Run(v, cli.argvalues...)); e != nil {
if e, ok := e.(gojq.HaltError); ok {
if v := e.Value(); v != nil {
if str, ok := v.(string); ok {
cli.errStream.Write([]byte(str))
} else {
bs, _ := gojq.Marshal(v)
cli.errStream.Write(bs)
cli.errStream.Write([]byte{'\n'})
}
}
err = e
break
}
fmt.Fprintf(cli.errStream, "%s: %s\n", name, e)
err = e
}
}
if err != nil {
return &emptyError{err}
}
return nil
}

func (cli *cli) printValues(iter gojq.Iter) error {
Expand Down Expand Up @@ -410,20 +429,3 @@ func (cli *cli) funcStderr(v any, _ []any) any {
}
return v
}

func (cli *cli) printError(err error) {
if er, ok := err.(interface{ IsEmptyError() bool }); !ok || !er.IsEmptyError() {
if er, ok := err.(interface{ IsHaltError() bool }); !ok || !er.IsHaltError() {
fmt.Fprintf(cli.errStream, "%s: %s\n", name, err)
} else if er, ok := err.(gojq.ValueError); ok {
v := er.Value()
if str, ok := v.(string); ok {
cli.errStream.Write([]byte(str))
} else {
bs, _ := gojq.Marshal(v)
cli.errStream.Write(bs)
cli.errStream.Write([]byte{'\n'})
}
}
}
}
8 changes: 2 additions & 6 deletions cli/error.go
Expand Up @@ -19,9 +19,7 @@ func (*emptyError) Error() string {
return ""
}

func (*emptyError) IsEmptyError() bool {
return true
}
func (*emptyError) isEmptyError() {}

func (err *emptyError) ExitCode() int {
if err, ok := err.err.(interface{ ExitCode() int }); ok {
Expand All @@ -38,9 +36,7 @@ func (err *exitCodeError) Error() string {
return "exit code: " + strconv.Itoa(err.code)
}

func (err *exitCodeError) IsEmptyError() bool {
return true
}
func (err *exitCodeError) isEmptyError() {}

func (err *exitCodeError) ExitCode() int {
return err.code
Expand Down
51 changes: 41 additions & 10 deletions cli/test.yaml
Expand Up @@ -5598,11 +5598,13 @@
- name: halt function and raw string input option
args:
- -R
- 'halt'
- '., halt, .'
input: |
1
2
3
foo
bar
baz
expected: |
"foo"
- name: halt function and json file arguments
args:
Expand All @@ -5626,6 +5628,16 @@
null
exit_code: 5

- name: halt_error/0 function on string
args:
- 'halt_error'
input: '"foo\nbar\nbaz\n"'
error: |
foo
bar
baz
exit_code: 5

- name: halt_error/0 function and try catch
args:
- 'range(5) | try halt_error catch .'
Expand All @@ -5649,19 +5661,17 @@
3
error: |
1
2
3
exit_code: 5

- name: halt_error/0 function and raw string input option
args:
- -R
- 'halt_error'
input: |
1
2
3
error: 123
foo
bar
baz
error: foo
exit_code: 5

- name: halt_error/1 function
Expand All @@ -5670,6 +5680,27 @@
input: 'null'
exit_code: 42

- name: halt_error/1 function on string
args:
- 'halt_error(42)'
input: '"foo\nbar\nbaz\n"'
error: |
foo
bar
baz
exit_code: 42

- name: halt_error/1 function with multiple input values
args:
- 'halt_error(.)'
input: |
1
2
3
error: |
1
exit_code: 1

- name: builtins function
args:
- 'builtins | length > 100'
Expand Down
22 changes: 14 additions & 8 deletions error.go
Expand Up @@ -11,6 +11,18 @@ type ValueError interface {
Value() any
}

// HaltError is an interface for errors of halt and halt_error functions.
// Any HaltError is [ValueError], and if the value is nil, discard the
// error and stop the iteration. Consider a query like "1, halt, 2";
// the first value is 1, and the second value is a HaltError with nil value.
// You might think the iterator should not emit an error this case, but it
// should so that we can recognize the halt error to stop the outer loop
// of iterating input values; echo 1 2 3 | gojq "., halt".
type HaltError interface {
ValueError
isHaltError()
}

type expectedObjectError struct {
v any
}
Expand Down Expand Up @@ -183,11 +195,7 @@ func (err *exitCodeError) ExitCode() int {
type haltError exitCodeError

func (err *haltError) Error() string {
return (*exitCodeError)(err).Error()
}

func (err *haltError) IsEmptyError() bool {
return err.value == nil
return "halt " + (*exitCodeError)(err).Error()
}

func (err *haltError) Value() any {
Expand All @@ -198,9 +206,7 @@ func (err *haltError) ExitCode() int {
return (*exitCodeError)(err).ExitCode()
}

func (err *haltError) IsHaltError() bool {
return true
}
func (err *haltError) isHaltError() {}

type flattenDepthError struct {
v float64
Expand Down
48 changes: 48 additions & 0 deletions query_test.go
Expand Up @@ -93,6 +93,54 @@ func TestQueryRun_Errors(t *testing.T) {
}
}

func TestQueryRun_Halt(t *testing.T) {
query, err := gojq.Parse("0, halt, 1")
if err != nil {
t.Fatal(err)
}
iter := query.Run(nil)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
if _, ok := err.(gojq.HaltError); ok {
break
}
t.Errorf("should emit a halt error but got: %v", err)
} else if expected := 0; v != expected {
t.Errorf("expected: %#v, got: %#v", expected, v)
}
}
}

func TestQueryRun_HaltError(t *testing.T) {
query, err := gojq.Parse(".[] | halt_error")
if err != nil {
t.Fatal(err)
}
iter := query.Run([]any{"foo", "bar", "baz"})
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
if _, ok := err.(gojq.HaltError); ok {
if expected := "halt error: foo"; err.Error() != expected {
t.Errorf("expected: %v, got: %v", expected, err)
}
break
} else {
t.Errorf("should emit a halt error but got: %v", err)
}
} else {
t.Errorf("should emit an error but got: %v", v)
}
}
}

func TestQueryRun_ObjectError(t *testing.T) {
query, err := gojq.Parse(".[] | {(.): 1}")
if err != nil {
Expand Down

0 comments on commit 857f6a7

Please sign in to comment.