Introduction
Go is a simple language, it supports concurrency out of the box and compiles to executable files so users do not need to have Go installed to run our apps, this makes Go ideally suited for building CLI tools. In this article, we will be going over how to build a CLI utility to format JSON files in Go.
Prerequisites
A working Go installation. You can find instructions here to set up Go if you do not.
A code editor (VsCode is a popular choice).
This article assumes you have basic knowledge of programming concepts and the CLI.
Getting ready
To confirm you have Go installed and ready to use run
go version
You should get a response like
go version go1.22.1 linux/amd64
If you did not, please follow the instructions here to install Go.
With that out of the way here is the flow of our tool:
// 1. Get the path for our input file from
// the command line arguments
// 2. Check if the input file is readable and contains
// valid JSON
// 3. Format the contents of the file
// 4. Save formatted JSON to another file
// 5. Compile our binary
Step 0:
In a folder of your choosing, create a main.go file using your code editor. Here are the contents of the file:
// 1. Get the path for our input file from
// the command line arguments
// 2. Check if the input file is readable and contains
// valid JSON
// 3. Format the contents of the file
// 4. Save formatted JSON to another file
// 5. Compile our binary
package main
import "fmt"
func main() {
fmt.Println("Hello, 你好")
}
To execute the following code in your CLI run
go run main.go
Which would give us the output
Hello, 你好
Step 1:
First, we check that we have at least 1 argument passed to our program; the input file. To do this we would need to import the OS module
import (
"fmt"
"os"
)
We check that at least 1 argument was passed and display an error otherwise:
// Get the arguments passed to our program
arguments := os.Args[1:]
// Check that at least 1 argument was passed
if len(arguments) < 1 {
// Display error message and exit the program
fmt.Println("Missing required arguments")
fmt.Println("Usage: go run main.go input_file.json")
return
}
Step 2
Next, we confirm that our input file is readable and contains valid JSON
// We add the encoding/json, errors and bytes module
import (
"bytes"
"encoding/json"
"errors"
...
)
// A function to check if a string is valid JSON
// src: https://stackoverflow.com/a/22129435
func isJSON(s string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(s), &js) == nil
}
// We define a function to check if the file exists
func FileExists(name string) (bool, error) {
_, err := os.Stat(name)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, err
}
// A function to pretty print JSON strings
// src: https://stackoverflow.com/a/36544455 (modified)
func jsonPrettyPrint(in string) string {
var out bytes.Buffer
err := json.Indent(&out, []byte(in), "", " ")
if err != nil {
return in
}
return out.String()
}
// In our main function, we check if the file exists
func main() {
....
// Call FileExists function with the file path
exists, err := FileExists(arguments[0])
// Check the result
if exists != true {
fmt.Println("Sorry the file", arguments[0], "does not exist!")
return
}
if err != nil {
fmt.Println("Error:", err)
return
}
raw, err := os.ReadFile(arguments[0]) // just pass the file name
if err != nil {
fmt.Print(err)
}
// convert the files contents to string
contents := string(raw)
// check if the string is valid json
if contents == "" || isJSON(contents) != true {
fmt.Println("Invalid or empty JSON file")
return
}
}
Step 3
We format the contents of the file and display it
// print the formatted string; this output can be piped to an output file
// no prefix and indenting with 4 spaces
formatted := jsonPrettyPrint(contents)
// Display the formatted string
fmt.Println(formatted)
Step 4
Save formatted JSON to another file, and to do that we pipe the output to a file
go run main.go input.json > out.json
The angle brackets >
redirects the output of our program to a file out.json
Step 5
Compile our code to a single binary. To do this we run
# Build our code
go build main.go
# list the contents of the current directory
# we would have an executable binary called "main"
ls
# main main.go
#We will rename our executable prettyJson
mv main prettyJson
We can run our new binary as we would any other executable
Let's update our usage instructions
// fmt.Println("Usage: go run main.go input_file.json")
// becomes
fmt.Println("Usage: ./prettyJson input_file.json")
Here is the completed code
// 1. Get the path for our input file from
// the command line arguments
// 2. Check if the input file is readable and contains
// valid JSON
// 3. Format the contents of the file
// 4. Save formatted JSON to another file
// 5. Compile our binary
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
)
// A function to check if a string is valid JSON
// src: https://stackoverflow.com/a/22129435 (modified)
func isJSON(s string) bool {
var js map[string]interface{}
return json.Unmarshal([]byte(s), &js) == nil
}
func FileExists(name string) (bool, error) {
_, err := os.Stat(name)
if err == nil {
return true, nil
}
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, err
}
// A function to pretty print JSON strings
// src: https://stackoverflow.com/a/36544455
func jsonPrettyPrint(in string) string {
var out bytes.Buffer
err := json.Indent(&out, []byte(in), "", "\t")
if err != nil {
return in
}
return out.String()
}
func main() {
// Get the arguments passed to our program
arguments := os.Args[1:]
// Check that at least 1 argument was passed
if len(arguments) < 1 {
// Display error message and exit the program
fmt.Println("Missing required argument")
fmt.Println("Usage: ./prettyJson input_file.json")
return
}
// Call FileExists function with the file path
exists, err := FileExists(arguments[0])
// Check the result
if exists != true {
fmt.Println("Sorry the file", arguments[0], "does not exist!")
return
}
if err != nil {
fmt.Println("Error:", err)
return
}
raw, err := os.ReadFile(arguments[0]) // just pass the file name
if err != nil {
fmt.Print(err)
}
// convert the files contents to string
contents := string(raw)
// check if the string is valid json
if contents == "" || isJSON(contents) != true {
fmt.Println("Invalid or empty JSON file")
return
}
// print the formatted string; this output can be piped to an output file
// no prefix and indenting with 4 spaces
formatted := jsonPrettyPrint(contents)
// Display the formatted string
fmt.Println(formatted)
}
Conclusion
We have seen how to create a JSON formatter utility with Go. The code is available as a GitHub gist here. Feel free to make improvements.