├── .circleci └── config.yml ├── .github └── workflows │ └── go.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── main.go ├── main_test.go ├── screenshot.png └── testdata └── test.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/golang:latest 6 | working_directory: /go/src/github.com/skanehira/gjo 7 | steps: 8 | - checkout 9 | - run: 10 | name: Unit test 11 | command: go test -v 12 | - run: 13 | name: Build 14 | command: go build -v 15 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push] 4 | 5 | env: 6 | GOPATH: ${{ github.workspace }}/.. 7 | 8 | jobs: 9 | 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | 16 | - name: Set up Go 1.13.5 17 | uses: actions/setup-go@v1 18 | with: 19 | go-version: 1.13.5 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v1 24 | with: 25 | path: src/github.com/${{ github.repository }} 26 | 27 | - name: Unit test 28 | run: go test -v 29 | 30 | - name: Build 31 | run: go build -v 32 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build gjo 2 | FROM golang AS builder 3 | ENV GOPATH /go 4 | ENV GOOS linux 5 | ENV GOARCH amd64 6 | ENV CGO_ENABLED 0 7 | ENV GO111MODULE on 8 | COPY . ./src/github.com/skanehira/gjo 9 | WORKDIR /go/src/github.com/skanehira/gjo 10 | RUN go build 11 | 12 | # copy artifact from the build stage 13 | FROM busybox 14 | COPY --from=builder /go/src/github.com/skanehira/gjo/gjo /usr/local/bin/gjo 15 | 16 | ENTRYPOINT ["gjo"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 skanehira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOBUILD=go build 3 | GOCLEAN=go clean 4 | BINARY_NAME=gjo 5 | 6 | export GO111MODULE=on 7 | 8 | all: build 9 | 10 | clean: 11 | $(GOCLEAN) 12 | 13 | build: clean 14 | $(GOBUILD) -o $(BINARY_NAME) 15 | 16 | # copy to $GOBIN 17 | install: build 18 | cp -f $(BINARY_NAME) $(GOBIN)/ 19 | 20 | # build release binary 21 | release: clean 22 | GOOS=darwin GOARCH=amd64 $(GOBUILD) && zip MacOS.zip $(BINARY_NAME) && rm -rf $(BINARY_NAME) 23 | GOOS=linux GOARCH=amd64 $(GOBUILD) && zip Linux.zip $(BINARY_NAME) && rm -rf $(BINARY_NAME) 24 | GOOS=windows GOARCH=amd64 $(GOBUILD) && zip Windows.zip $(BINARY_NAME).exe && rm -rf $(BINARY_NAME).exe 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gjo 2 | 3 | [![GitHub Actions status](https://github.com/skanehira/gjo/workflows/Go/badge.svg)](https://github.com/skanehira/gjo/actions) 4 | [![CircleCI](https://img.shields.io/circleci/project/github/skanehira/gjo/master.svg)](https://circleci.com/gh/skanehira/gjo/tree/master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/skanehira/gjo)](https://goreportcard.com/report/github.com/skanehira/gjo) 6 | 7 | Small utility to create JSON objects. 8 | This was inspired by [jpmens/jo](https://github.com/jpmens/jo). 9 | 10 | ![sreenshot](./screenshot.png) 11 | 12 | ## Support OS 13 | - Mac 14 | - Linux 15 | - Windows 16 | 17 | ## Requirements 18 | - Go 1.1.14~ 19 | - Git 20 | 21 | ## Installtion 22 | ### Build 23 | ```sh 24 | $ git clone https://github.com/skanehira/gjo.git 25 | $ cd gjo 26 | $ GO111MODULE=on go install 27 | ``` 28 | 29 | ### Binary 30 | Please download from [releases](https://github.com/skanehira/gjo/releases) 31 | 32 | ## Usage 33 | ### Mac and Linux 34 | ```sh 35 | $ gjo -p status=$(gjo name=gorilla age=26 lang=$(gjo -a Go Java PHP)) 36 | { 37 | "status": { 38 | "age": 26, 39 | "lang": [ 40 | "Go", 41 | "Java", 42 | "PHP" 43 | ], 44 | "name": "gorilla" 45 | } 46 | } 47 | $ gjo -h 48 | Usage of gjo: 49 | -a creates an array of words 50 | -p pretty-prints 51 | -v show version 52 | ``` 53 | 54 | ### Windows 55 | If you want to use `$()` on the Windows, please install [shellwrap](https://github.com/mattn/shellwrap). 56 | 57 | ```sh 58 | shellwrap gjo -p status=$(gjo name=gorilla age=26) 59 | ``` 60 | 61 | ## Author 62 | gorilla0513 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/skanehira/gjo 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | // Version gjo version info 14 | type Version struct { 15 | Program string `json:"program"` 16 | Description string `json:"description"` 17 | Author string `json:"author"` 18 | Repo string `json:"repo"` 19 | Version string `json:"version"` 20 | } 21 | 22 | var ( 23 | array = flag.Bool("a", false, "creates an array of words") 24 | pretty = flag.Bool("p", false, "pretty-prints") 25 | version = flag.Bool("v", false, "show version") 26 | 27 | stdin io.Reader = os.Stdin 28 | stdout io.Writer = os.Stdout 29 | stderr io.Writer = os.Stderr 30 | ) 31 | 32 | func isRawString(s string) bool { 33 | if strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}") { 34 | return true 35 | } 36 | if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { 37 | return true 38 | } 39 | if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") { 40 | return true 41 | } 42 | return false 43 | } 44 | 45 | func parseValue(s string) interface{} { 46 | if s == "" { 47 | return "" 48 | } 49 | if isRawString(s) { 50 | return json.RawMessage(s) 51 | } 52 | if s == "true" { 53 | return true 54 | } 55 | if s == "false" { 56 | return false 57 | } 58 | if s == "null" { 59 | return nil 60 | } 61 | 62 | f, err := strconv.ParseFloat(s, 64) 63 | if err == nil { 64 | return f 65 | } 66 | return s 67 | } 68 | 69 | func readFile(fname string) (interface{}, error) { 70 | f, err := os.Open(fname) 71 | if err != nil { 72 | return nil, err 73 | } 74 | var v interface{} 75 | err = json.NewDecoder(f).Decode(&v) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return v, nil 80 | } 81 | 82 | func doArray(args []string) (interface{}, error) { 83 | jsons := []interface{}{} 84 | for _, value := range args { 85 | jsons = append(jsons, parseValue(value)) 86 | } 87 | return jsons, nil 88 | } 89 | 90 | func isKeyFile(s string) bool { 91 | pos := strings.IndexRune(s, ':') 92 | return pos > 0 && pos == len(s)-1 93 | } 94 | 95 | func doObject(args []string) (interface{}, error) { 96 | jsons := make(map[string]interface{}, len(args)) 97 | for _, arg := range args { 98 | kv := strings.SplitN(arg, "=", 2) 99 | s := "" 100 | if len(kv) > 0 { 101 | s = kv[0] 102 | } 103 | if len(kv) != 2 { 104 | return nil, fmt.Errorf("Argument %q is not k=v", s) 105 | } 106 | if isKeyFile(kv[0]) { 107 | // For argument a:=b, read value from file "b". 108 | v, err := readFile(kv[1]) 109 | if err != nil { 110 | return v, err 111 | } 112 | key := kv[0][:len(kv[0])-1] 113 | jsons[key] = v 114 | } else { 115 | jsons[kv[0]] = parseValue(kv[1]) 116 | } 117 | } 118 | 119 | return jsons, nil 120 | } 121 | 122 | func doVersion() error { 123 | enc := json.NewEncoder(stdout) 124 | if *pretty { 125 | enc.SetIndent("", " ") 126 | } 127 | return enc.Encode(&Version{ 128 | Program: "gjo", 129 | Description: "This is inspired by jpmens/jo", 130 | Author: "skanehira", 131 | Repo: "https://github.com/skanehira/gjo", 132 | Version: "1.0.3", 133 | }) 134 | } 135 | 136 | func run() int { 137 | flag.Parse() 138 | if *version { 139 | err := doVersion() 140 | if err != nil { 141 | fmt.Fprintln(stderr, err) 142 | return 1 143 | } 144 | return 0 145 | } 146 | 147 | args := flag.Args() 148 | if len(args) == 0 { 149 | flag.Usage() 150 | return 2 151 | } 152 | 153 | var value interface{} 154 | var err error 155 | 156 | if *array { 157 | value, err = doArray(args) 158 | } else { 159 | value, err = doObject(args) 160 | } 161 | if err != nil { 162 | fmt.Fprintln(stderr, err) 163 | return 1 164 | } 165 | enc := json.NewEncoder(stdout) 166 | if *pretty { 167 | enc.SetIndent("", " ") 168 | } 169 | err = enc.Encode(value) 170 | if err != nil { 171 | fmt.Fprintln(stderr, err) 172 | return 1 173 | } 174 | return 0 175 | } 176 | 177 | func main() { 178 | os.Exit(run()) 179 | } 180 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "flag" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestObject(t *testing.T) { 13 | tests := []struct { 14 | input []string 15 | want string 16 | err string 17 | }{ 18 | {input: []string{``}, want: ``, err: `Argument "" is not k=v`}, 19 | {input: []string{`a`}, want: ``, err: `Argument "a" is not k=v`}, 20 | {input: []string{`a=`}, want: `{"a":""}`, err: ``}, 21 | {input: []string{`a=1`}, want: `{"a":1}`, err: ``}, 22 | {input: []string{`a=1.1`}, want: `{"a":1.1}`, err: ``}, 23 | {input: []string{`a=true`}, want: `{"a":true}`, err: ``}, 24 | {input: []string{`a=false`}, want: `{"a":false}`, err: ``}, 25 | {input: []string{`a=null`}, want: `{"a":null}`, err: ``}, 26 | {input: []string{`a=s`}, want: `{"a":"s"}`, err: ``}, 27 | {input: []string{`a={"a":"s"}`}, want: `{"a":{"a":"s"}}`, err: ``}, 28 | {input: []string{`a=["a","s"]`}, want: `{"a":["a","s"]}`, err: ``}, 29 | {input: []string{`a:=testdata/test.json`}, want: `{"a":{"foo":true}}`, err: ``}, 30 | {input: []string{`a="+123456"`}, want: `{"a":"+123456"}`, err: ``}, 31 | } 32 | 33 | for _, test := range tests { 34 | value, err := doObject(test.input) 35 | if err != nil { 36 | if err.Error() != test.err { 37 | t.Fatal(err) 38 | } 39 | } else { 40 | var buf bytes.Buffer 41 | err = json.NewEncoder(&buf).Encode(value) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | got := strings.TrimSpace(buf.String()) 46 | if got != test.want { 47 | t.Fatalf("want %q, but got %q", test.want, got) 48 | } 49 | } 50 | } 51 | } 52 | 53 | func TestArray(t *testing.T) { 54 | tests := []struct { 55 | input []string 56 | want string 57 | err string 58 | }{ 59 | {input: []string{``}, want: `[""]`, err: ``}, 60 | {input: []string{`a`}, want: `["a"]`, err: ``}, 61 | {input: []string{`1`}, want: `[1]`, err: ``}, 62 | {input: []string{`1.1`}, want: `[1.1]`, err: ``}, 63 | {input: []string{`true`}, want: `[true]`, err: ``}, 64 | {input: []string{`false`}, want: `[false]`, err: ``}, 65 | {input: []string{`false`, `1`, `a`}, want: `[false,1,"a"]`, err: ``}, 66 | } 67 | 68 | for _, test := range tests { 69 | value, err := doArray(test.input) 70 | if err != nil { 71 | if err.Error() != test.err { 72 | t.Fatal(err) 73 | } 74 | } else { 75 | var buf bytes.Buffer 76 | err = json.NewEncoder(&buf).Encode(value) 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | got := strings.TrimSpace(buf.String()) 81 | if got != test.want { 82 | t.Fatalf("want %q, but got %q", test.want, got) 83 | } 84 | } 85 | } 86 | } 87 | 88 | func TestVersion(t *testing.T) { 89 | tests := []struct { 90 | pretty bool 91 | want error 92 | }{ 93 | {pretty: false, want: nil}, 94 | {pretty: true, want: nil}, 95 | } 96 | 97 | var bufstdout, bufstderr bytes.Buffer 98 | oldstdout, oldstderr := stdout, stderr 99 | stdout, stderr = &bufstdout, &bufstderr 100 | defer func() { stdout, stderr = oldstdout, oldstderr }() 101 | 102 | for _, test := range tests { 103 | *pretty = test.pretty 104 | got := doVersion() 105 | if got != test.want { 106 | t.Fatalf("want %q, but get %q", test.want, got) 107 | } 108 | } 109 | 110 | } 111 | 112 | func TestRun(t *testing.T) { 113 | tests := []struct { 114 | args map[string]string 115 | input []string 116 | want int 117 | }{ 118 | {args: map[string]string{"a": "true", "p": "flase", "v": "false"}, input: []string{`gorilla`, `dog`}, want: 0}, 119 | {args: map[string]string{"p": "true", "a": "flase", "v": "false"}, input: []string{`name=gorilla`}, want: 0}, 120 | {args: map[string]string{"p": "true", "a": "true", "v": "false"}, input: []string{`gorilla`, `cat`}, want: 0}, 121 | {args: map[string]string{"v": "true"}, input: []string{""}, want: 0}, 122 | {args: map[string]string{"p": "false", "a": "false", "v": "false"}, input: []string{`gorilla`}, want: 1}, 123 | } 124 | 125 | var bufstdout, bufstderr bytes.Buffer 126 | oldstdout, oldstderr := stdout, stderr 127 | stdout, stderr = &bufstdout, &bufstderr 128 | defer func() { stdout, stderr = oldstdout, oldstderr }() 129 | 130 | for _, test := range tests { 131 | os.Args = append([]string{""}, test.input...) 132 | 133 | for arg, value := range test.args { 134 | flag.CommandLine.Set(arg, value) 135 | } 136 | 137 | got := run() 138 | if got != test.want { 139 | t.Fatalf("want %v, but get %v", test.want, got) 140 | } 141 | } 142 | } 143 | 144 | func TestIsKeyFile(t *testing.T) { 145 | tests := []struct { 146 | arg string 147 | want bool 148 | }{ 149 | {arg: "", want: false}, 150 | {arg: ":", want: false}, 151 | {arg: "a", want: false}, 152 | {arg: "a:", want: true}, 153 | {arg: "a:=", want: false}, 154 | {arg: "a::", want: false}, 155 | } 156 | 157 | for _, test := range tests { 158 | got := isKeyFile(test.arg) 159 | if got != test.want { 160 | t.Fatalf("want %v, but get %v", test.want, got) 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skanehira/gjo/15ee8be380a53bb6a51672a56cd134bb755077d4/screenshot.png -------------------------------------------------------------------------------- /testdata/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": true 3 | } 4 | --------------------------------------------------------------------------------