Skip to content

Commit

Permalink
Add multiline support (#64)
Browse files Browse the repository at this point in the history
* Fix assignment of key

* Add functions `last`, `first`, `parent`, `root`, and `key`

* Add multiline support for CLI
  • Loading branch information
spyzhov authored Jul 24, 2023
1 parent 8c52398 commit d59d485
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 94 deletions.
113 changes: 61 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,20 @@ func main() {
You can download `ajson` cli from the [release page](https://github.com/spyzhov/ajson/releases), or install from the source:

```shell script
go get github.com/spyzhov/ajson/cmd/ajson@v0.8.0
go get github.com/spyzhov/ajson/cmd/ajson@v0.9.0
```

Usage:

```
Usage: ajson "jsonpath" ["input"]
Usage: ajson [-mq] "jsonpath" ["input"]
Read JSON and evaluate it with JSONPath.
Parameters:
-m, --multiline Input file/stream will be read as a multiline JSON. Each line should have a full valid JSON.
-q, --quiet Do not print errors into the STDERR.
Argument:
jsonpath Valid JSONPath or evaluate string (Examples: "$..[?(@.price)]", "$..price", "avg($..price)")
input Path to the JSON file. Leave it blank to use STDIN.
jsonpath Valid JSONPath or evaluate string (Examples: "$..[?(@.price)]", "$..price", "avg($..price)")
input Path to the JSON file. Leave it blank to use STDIN.
```

Examples:
Expand All @@ -74,6 +77,7 @@ Examples:
curl -s "https://randomuser.me/api/?results=10" | ajson "$..coordinates"
ajson "$" example.json
echo "3" | ajson "2 * pi * $"
docker logs image-name -f | ajson -qm 'root($[?(@=="ERROR" && key(@)=="severity")])'
```

# JSONPath
Expand Down Expand Up @@ -291,54 +295,59 @@ JSON: {"name":"Foo","mail":"foo@example.com"}

Package has several predefined functions.

abs math.Abs integers, floats
acos math.Acos integers, floats
acosh math.Acosh integers, floats
asin math.Asin integers, floats
asinh math.Asinh integers, floats
atan math.Atan integers, floats
atanh math.Atanh integers, floats
avg Average array of integers or floats
b64decode b64 Decoding string
b64encode b64 Encoding string
b64encoden b64 Encoding (no padding) string
cbrt math.Cbrt integers, floats
ceil math.Ceil integers, floats
cos math.Cos integers, floats
cosh math.Cosh integers, floats
erf math.Erf integers, floats
erfc math.Erfc integers, floats
erfcinv math.Erfcinv integers, floats
erfinv math.Erfinv integers, floats
exp math.Exp integers, floats
exp2 math.Exp2 integers, floats
expm1 math.Expm1 integers, floats
factorial N! unsigned integer
floor math.Floor integers, floats
gamma math.Gamma integers, floats
j0 math.J0 integers, floats
j1 math.J1 integers, floats
length len array
log math.Log integers, floats
log10 math.Log10 integers, floats
log1p math.Log1p integers, floats
log2 math.Log2 integers, floats
logb math.Logb integers, floats
not not any
pow10 math.Pow10 integer
rand N*rand.Float64 float
randint rand.Intn integer
round math.Round integers, floats
roundtoeven math.RoundToEven integers, floats
sin math.Sin integers, floats
sinh math.Sinh integers, floats
sum Sum array of integers or floats
sqrt math.Sqrt integers, floats
tan math.Tan integers, floats
tanh math.Tanh integers, floats
trunc math.Trunc integers, floats
y0 math.Y0 integers, floats
y1 math.Y1 integers, floats
abs math.Abs integers, floats
acos math.Acos integers, floats
acosh math.Acosh integers, floats
asin math.Asin integers, floats
asinh math.Asinh integers, floats
atan math.Atan integers, floats
atanh math.Atanh integers, floats
avg Average array of integers or floats
b64decode b64 Decoding string
b64encode b64 Encoding string
b64encoden b64 Encoding (no padding) string
cbrt math.Cbrt integers, floats
ceil math.Ceil integers, floats
cos math.Cos integers, floats
cosh math.Cosh integers, floats
erf math.Erf integers, floats
erfc math.Erfc integers, floats
erfcinv math.Erfcinv integers, floats
erfinv math.Erfinv integers, floats
exp math.Exp integers, floats
exp2 math.Exp2 integers, floats
expm1 math.Expm1 integers, floats
factorial N! unsigned integer
first Get first element any
floor math.Floor integers, floats
gamma math.Gamma integers, floats
j0 math.J0 integers, floats
j1 math.J1 integers, floats
key Key of element string
last Get last element any
length len array
log math.Log integers, floats
log10 math.Log10 integers, floats
log1p math.Log1p integers, floats
log2 math.Log2 integers, floats
logb math.Logb integers, floats
not not any
parent Get parent element any
pow10 math.Pow10 integer
rand N*rand.Float64 float
randint rand.Intn integer
root Get root element any
round math.Round integers, floats
roundtoeven math.RoundToEven integers, floats
sin math.Sin integers, floats
sinh math.Sinh integers, floats
sum Sum array of integers or floats
sqrt math.Sqrt integers, floats
tan math.Tan integers, floats
tanh math.Tanh integers, floats
trunc math.Trunc integers, floats
y0 math.Y0 integers, floats
y1 math.Y1 integers, floats

You are free to add new one with function `AddFunction`:

Expand Down
162 changes: 132 additions & 30 deletions cmd/ajson/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"bufio"
"errors"
"fmt"
"io"
"log"
Expand All @@ -11,22 +13,27 @@ import (
"github.com/spyzhov/ajson"
)

var version = "v0.8.0"
var version = "v0.9.0"

func usage() {
text := ``
if inArgs("-h", "-help", "--help", "help") || len(os.Args) > 3 {
text = `Usage: ajson "jsonpath" ["input"]
if inArgs("-h", "-help", "--help", "help") {
text = `Usage: ajson [-mq] "jsonpath" ["input"]
Read JSON and evaluate it with JSONPath.
Parameters:
-m, --multiline Input file/stream will be read as a multiline JSON. Each line should have a full valid JSON.
-q, --quiet Do not print errors into the STDERR.
-man Display man page with "man ajson -man"
Argument:
jsonpath Valid JSONPath or evaluate string (Examples: "$..[?(@.price)]", "$..price", "avg($..price)")
input Path to the JSON file. Leave it blank to use STDIN.
jsonpath Valid JSONPath or evaluate string (Examples: "$..[?(@.price)]", "$..price", "avg($..price)")
input Path to the JSON file. Leave it blank to use STDIN.
Examples:
ajson "avg($..registered.age)" "https://randomuser.me/api/?results=5000"
ajson "$.results.*.name" "https://randomuser.me/api/?results=10"
curl -s "https://randomuser.me/api/?results=10" | ajson "$..coordinates"
ajson "$" example.json
echo "3" | ajson "2 * pi * $"`
echo "3" | ajson "2 * pi * $"
docker logs image-name -f | ajson -m 'root($[?(@=="ERROR" && key(@)=="severity")])'`
} else if inArgs("version", "-version", "--version") {
text = fmt.Sprintf(`ajson: Version %s
Copyright (c) 2020 Pyzhov Stepan
Expand All @@ -43,63 +50,98 @@ There is NO WARRANTY, to the extent permitted by law.`, version)
func main() {
log.SetFlags(0)
usage()
if len(os.Args) < 2 {
log.Fatalf("JSONPath was not set")
cfg := getConfig()
if cfg.jsonpath == "" {
pFatal("JSONPath was not set")
}
path := os.Args[1]
input := getInput()
input := getInput(cfg)
defer func() {
_ = input.Close()
}()
data, err := io.ReadAll(input)

if err != nil {
log.Fatalf("error reading source: %s", err)
if cfg.multiline {
reader := bufio.NewReader(input)
for {
data, err := reader.ReadBytes('\n')
if err != nil {
if !errors.Is(err, io.EOF) {
mlError(cfg, "unable to read input: %s", err)
}
return
}
apply(cfg, data)
}
} else {
data, err := io.ReadAll(input)
if err != nil {
pFatal("error reading source: %s", err)
}
apply(cfg, data)
}
}

func apply(cfg config, data []byte) {
var result *ajson.Node

root, err := ajson.Unmarshal(data)
if err != nil {
log.Fatalf("error parsing JSON: %s", err)
mlFatal(cfg, "error parsing JSON: %s", err)
return
}

var nodes []*ajson.Node
nodes, err = root.JSONPath(path)
nodes, err = root.JSONPath(cfg.jsonpath)
result = ajson.ArrayNode("", nodes)
if err != nil {
result, err = ajson.Eval(root, path)
result, err = ajson.Eval(root, cfg.jsonpath)
}
if err != nil {
log.Fatalf("error: %s", err)
mlFatal(cfg, "jsonpath error: %s", err)
return
}

if cfg.multiline {
if (result.IsArray() || result.IsObject()) && result.Empty() {
return
}
if result.IsString() && result.MustString() == "" {
return
}
if result.IsNull() {
return
}
}

data, err = ajson.Marshal(result)
if err != nil {
log.Fatalf("error preparing JSON: %s", err)
mlFatal(cfg, "error preparing JSON: %s", err)
}
fmt.Printf("%s\n", data)
pPrint("%s\n", data)
}

func getInput() io.ReadCloser {
if len(os.Args) < 3 {
func getInput(cfg config) io.ReadCloser {
if cfg.input == "" {
return os.Stdin
}

input := os.Args[2]
if strings.HasPrefix(input, "http://") || strings.HasPrefix(input, "https://") {
resp, err := http.DefaultClient.Get(input)
if strings.HasPrefix(cfg.input, "http://") || strings.HasPrefix(cfg.input, "https://") {
resp, err := http.DefaultClient.Get(cfg.input)
if err != nil {
log.Fatalf("Error on getting data from '%s': %s", input, err)
pFatal("Error on getting data from '%s': %s", cfg.input, err)
}
if resp.StatusCode >= 400 {
log.Printf("WARNING: status code is '%s'", resp.Status)
if resp.StatusCode >= 300 {
if !cfg.quiet {
pError("WARNING: status code is '%s'", resp.Status)
}
}
return resp.Body
}
file, err := os.Open(input)

file, err := os.Open(cfg.input)
if err != nil {
log.Fatalf("Error on open file '%s': %s", input, err)
pFatal("Error on open file '%s': %s", cfg.input, err)
}

return file
}

Expand All @@ -108,10 +150,70 @@ func inArgs(value ...string) bool {
for _, val := range value {
index[val] = true
}
for _, val := range os.Args {
args := os.Args
for _, val := range args {
if index[val] {
return true
}
}
return false
}

type config struct {
jsonpath string
input string
multiline bool
quiet bool
}

func getConfig() (cfg config) {
for _, val := range os.Args[1:] {
switch val {
case "-m", "--multiline":
cfg.multiline = true
case "-q", "--quiet":
cfg.quiet = true
case "-mq", "-qm":
cfg.multiline = true
cfg.quiet = true
default:
if cfg.jsonpath == "" {
cfg.jsonpath = val
} else if cfg.input == "" {
cfg.input = val
} else {
pFatal("Wrong arguments count, unknown flag %q", val)
}
}
}
return
}

func pPrint(format string, args ...interface{}) {
_, _ = fmt.Fprintf(os.Stdout, format, args...)
}

func pError(format string, args ...interface{}) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", args...)
}

func pFatal(format string, args ...interface{}) {
pError(format, args...)
os.Exit(1)
}

func mlFatal(cfg config, format string, args ...interface{}) {
if cfg.multiline {
if !cfg.quiet {
pError(format, args...)
}
} else {
pFatal(format, args...)
}
}

func mlError(cfg config, format string, args ...interface{}) {
if !cfg.quiet {
pError(format, args...)
}
}
Loading

0 comments on commit d59d485

Please sign in to comment.