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)