๐ Building Command Line Tools with Go: Lolcat ๐ฆ
Lolcat in Go: With more Rainbows and Unicorns!
Hey everyone ๐
This is UltiRequiem. I'm a 15-year-old, Peruvian ๐ต๐ช, full-stack web and system developer, and I have the intention to become a successful developer.
I'm a big fan of CLI Tools, I have a lot of them hosted on GitHub.
So naturally, when I started to learn Go (also known as Golang), I wanted to do CLI tools in Go too.
I consider myself a "Linux Rice Master", and as one I know a lot of terminal utilities, one of my favorites is lolcat
.
Today I will teach you how to do it on Go! ๐ฑ
Result
You can watch the final result to motivate yourself, or leave it for the end if you want!
Our Clone Features
Parse from
stdin
Parse from file(s)
Help Menu
Starting the project
File Structure
When I'm starting a project I like to do follow good practices from the start ๐
- Create a new directory where you will store the source code!
mkdir chigo
You can change Chigo for another thing if you want!
- Start a project using Go Modules
go mod init github.com/UltiRequiem/chigo
Change
UltiRequiem
by your name,chigo
by what you previously put, andgithub
forgitlab
or your preferred code hosting service!
Inside chigo/
create some other directories to split correctly the code
mkdir internal pkg cmd
Create some files
touch pkg/root.go internal/root.go cmd/root.go
You should have a structure like this
.
โโโ cmd
โ โโโ root.go
โโโ go.mod
โโโ internal
โ โโโ root.go
โโโ pkg
โโโ root.go
Let me explain these directories ๐
pkg/
: Here we will put what we think can be re-utilized in other projects.
eg. a function that generates RGB
internal/
: Internal logic of the program that is not intended to be used somewhere else.cmd/
: Here we will call all the logic frominternal/
and setup the CLI Tool
If you are using Windows, or want to give Windows Support in your version, check the Adding Windows Support section!
Understanding the workflow
Let's write some pseudo to understand how or the program will work ๐
from "internal" import printFromStdin, printWithColors
help, fileArguments, files = parametersAndFlags()
if help {
printHelp()
exit()
}
if fileArguments {
filesText = joinFiles(files)
printWitColors(filesText)
exit()
}
printFromStdin()
Knowing this we can start to code!
Writing Code
๐ฆ pkg/
In our root.go
file, let's make a function that generates an RGB coding receiving one number.
This was my first Idea ๐
package chigo
import "math"
func RGB(i int) (red int, green int, blue int) {
red = int(math.Sin(0.1*float64(i)+0)*127 + 128)
green = int(math.Sin(0.1*float64(i)+2*math.Pi/3)*127 + 128)
blue = int(math.Sin(0.1*float64(i)+4*math.Pi/3)*127 + 128)
return
}
Here I'm using "named return values"
This would work well, but I would prefer using some OOP ๐ช
package chigo
import "math"
type RGB struct {
Red, Green, Blue int
}
func NewRGB(i float64) RGB {
return RGB{
int(math.Sin(0.1*i)*127 + 128),
int(math.Sin(0.1*i+2*math.Pi/3)*127 + 128),
int(math.Sin(0.1*i+4*math.Pi/3)*127 + 128),
}
}
This is just a personal preference, but I think it will make more readable the parts where I'm calling this function.
๐ internal/
Remember this part of the pseudo code?
if fileArguments {
filesText = joinFiles(files)
printWitColors(filesText)
exit()
}
First, we need a function that receives a list of files and gets the text from them ๐
package internal
import "os"
func JoinFiles(files []string) (string, error) {
text := ""
for _, file := range files {
fileText, err := os.ReadFile(file)
if err != nil {
return "", err
}
text += string(fileText) + "\n"
}
return text, nil
}
While this could be on the internal/root.go
file I prefer to put it in internal/joinFiles.go
because is the unique part when we are doing something related to files.
Now let's write the functions to print with colors text and stdin
package internal
import (
"bufio"
"fmt"
"os"
"strings"
chigo "github.com/UltiRequiem/chigo/pkg"
)
func PrintWithScanner(text string) {
scanner := bufio.NewScanner(strings.NewReader(text))
for i := 1.0; scanner.Scan(); i++ {
rgb := chigo.NewRGB(i)
fmt.Printf("\033[38;2;%d;%d;%dm%s\033[0m\n", rgb.Red, rgb.Green, rgb.Blue, scanner.Text())
}
}
func StartProcessFromStdin() {
reader := bufio.NewReader(os.Stdin)
for i := 1.0; true; i++ {
input, _, err := reader.ReadRune()
if err != nil {
PrintWithScanner(err.Error())
break
}
rgb := chigo.NewRGB(i)
fmt.Printf("\033[38;2;%d;%d;%dm%s\033[0m", rgb.Red, rgb.Green, rgb.Blue, string(input))
}
}
๐ฅ cmd/
Let's get the parameters and flags ๐ดโโ ๏ธ
package cmd
import (
"flag"
)
func parametersAndFlags() (bool, bool, []string) {
help := flag.Bool("help", false, "Display Help")
helpShort := flag.Bool("h", false, "Display Help")
flag.Parse()
return *help || *helpShort, flag.NArg() > 0, flag.Args()
}
Also lets add a pretty help
function ๐
package cmd
import (
"flag"
"fmt"
"github.com/UltiRequiem/chigo/internal"
)
func parametersAndFlags() (bool, bool, []string) {
help := flag.Bool("help", false, "Display Help")
helpShort := flag.Bool("h", false, "Display Help")
flag.Usage = printHelp
flag.Parse()
return *help || *helpShort, flag.NArg() > 0, flag.Args()
}
func printHelp() {
internal.PrintWithScanner(fmt.Sprintf(HELP_MESSAGE, VERSION))
}
const VERSION = "1.0.0"
const HELP_MESSAGE = `Chigo %s
Concatenate FILE(s) or standard input to standard output.
When no FILE is passed read standard input.
Examples:
chigo fOne fTwo # Output fOne and fTwo contents.
chigo # Copy standard input to standard output.
echo "My Message" | chigo # Display "My message".
fortune | chigo # Display a rainbow cookie.
If you need more help, found a bug or want to suggest a new feature:
https://github.com/UltiRequiem/chigo`
While you can put all this part on
cmd/root.go
I decided to put it oncmd/parameters.go
just to modularize the code ๐น๐ฐ
Now let's call all our functions to see the result of our work! ๐
package cmd
import (
"github.com/UltiRequiem/chigo/internal"
)
func Main() {
help, fileArguments, files := parametersAndFlags()
if help {
printHelp()
return
}
if fileArguments {
data, error := internal.JoinFiles(files)
if error != nil {
internal.PrintWithScanner(error.Error())
return
}
internal.PrintWithScanner(data)
return
}
internal.StartProcessFromStdin()
}
You cannot run this yet, to do it create a file main.go
in the root of your project
touch main.go
And here we will simply import and call the Main
function from cmd/
package main
import "github.com/UltiRequiem/chigo/cmd"
func main() {
cmd.Main()
}
This is made so the user can
go install github.com/UltiRequiem/chigo@latest
Instead of
go install github.com/UltiRequiem/chigo/cmd@latest
If you don't care about this just rename the
Main
function ofcmd/root.go
tomain
๐
Adding Windows Support ๐ช
To support Windows, some extra work is needed ๐ฉ
Fortunately, The great mattn, core Go contributor, made a pull request with all the work done ๐
github.com/UltiRequiem/chigo/pull/1
All you need to do is add this dependence ๐๐ฝ go-colorable
go get github.com/mattn/go-colorable
Check the pull request to know what you need to do, is just an extra line of code!
package cmd
import (
"github.com/UltiRequiem/chigo/internal"
"github.com/mattn/go-colorable"
)
func Main() {
// Windows Support
defer colorable.EnableColorsStdout(nil)()
help, fileArguments, files := parametersAndFlags()
if help {
printHelp()
return
}
if fileArguments {
data, error := internal.JoinFiles(files)
if error != nil {
internal.PrintWithScanner(error.Error())
return
}
internal.PrintWithScanner(data)
return
}
internal.StartProcessFromStdin()
}
Notes
Do you want to chat with me? Join my Discord Server!
Connect with me on GitHub, Twitter, Instagram, Reddit and ProductHunt!
Visit my portfolio to see more of my projects!
Don't forget to follow me on Hashnode too!
Open an Issue on the repository or comment here if you find any errors!