DEV Community

Cover image for Building CLI Tools with Go (Golang): A JSON File Formatter

Building CLI Tools with Go (Golang): A JSON File Formatter

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
Enter fullscreen mode Exit fullscreen mode

You should get a response like

go version go1.22.1 linux/amd64
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, δ½ ε₯½")
}
Enter fullscreen mode Exit fullscreen mode

To execute the following code in your CLI run

go run main.go
Enter fullscreen mode Exit fullscreen mode

Which would give us the output

Hello, δ½ ε₯½
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)