I recently created a Go language port of the cli tool jo.
The jo command is a tool for creating a JSON easily. It can be nested and used for the request body of the curl command. I actually remember the day it was published and wondered why nobody could come up with the idea before. I looked into the code and sent some small pull requests, but somehow I didn’t used for a few years. I would like to respect the original tool jo and to the idea of the author, to the effort to make the tool available on various OSs.
Recently I’m interested in porting useful cli tools to Go language. I think the language is the best choice for creating cli tools these days. The popularity is still growing rapidly so we can expect many developers to install with go get and contribute to the OSS community (honestly speaking, I love Rust for its memory safety and traits feature. But it is difficult to expect such a large number of developers for contribution. I choose Rust or Haskell when the language is suitable to achieve what I want).
$ gojo foo=bar qux=quux
{"foo":"bar","qux":"quux"}
$ gojo -p foo=bar qux=quux
{
"foo": "bar",
"qux": "quux"
}
$ gojo -a foo bar baz
["foo","bar","baz"]
$ seq 10 | gojo -a
[1,2,3,4,5,6,7,8,9,10]
$ gojo -p foo=$(gojo bar=$(gojo baz=100))
{
"foo": {
"bar": {
"baz": 100
}
}
}
$ gojo -p res[foo][][id]=10 res[foo][][id]=20 res[cnt]=2
{
"res": {
"foo": [
{
"id": 10
},
{
"id": 20
}
],
"cnt": 2
}
}
I implemented gojo using the test cases of jo as reference. Especially I enjoyed implementing deeply nested keys using recursions. Another language leads to another beautiful structure of code so it’s always interesting to implement a well known tool with different language.
The original tool jo has many options to change its type guessing behaviors. I don’t implement many of them yet because I don’t still understand such many options are required for this tool. I didn’t like the syntax for reading file contents; jo foo=@sample.txt
, because it can go against UNIX philosophy. But I finally implemented the syntax in the latest version, because gojo foo=@sample.txt
is surely easier to type than gojo foo="$(<sample.txt)"
and it is actually difficult to write the correct corresponding command when the target file is a binary file.
I’ve been using Go language for over four years and created many small cli tools. I made many mistakes in my experience with Go language. Untestable code, cli tool with only main package, less testability due to the lack of api client mocks, etc.
When I create a cli tool with Go, I create a reusable package at the top directory in the repository from the beginning. This package should be independent from the command line flags (i.e. os.Args
) and output/input streams (i.e. os.Stdout
, os.Stderr
and os.Stdin
). When created carefully, the package can be imported and configurable enough to output to any files or used as response of a http server. Next I create a cli directory which parses the command line flags and inject the streams. I define a single exported function cli.Run
, which returns the exit code. The cli package can depend on os.Args
and streams, but it should still be testable (test flags, streams and exit code) and I never call os.Exit
in this package. The code of the main package is func main() { os.Exit(cli.Run()) }
and that’s all.
Anyway, creating a cli tool with Go is so fun. Consider reusability even when you create a cli tool. Porting something to another language is a good exercise for holidays of programmers. Have fun.