marcel-dempers 138a127ac2 go-cmd
2021-01-20 12:33:42 +11:00

416 lines
9.3 KiB
Markdown

# Introduction to Go: Command Line
Command line apps are a fundamental part of software development <br/>
Go has a built in Commandline parser package. The package can be found [here](https://golang.org/pkg/flag/) <br/>
We simply have to import the `flag` package:
```
import (
"flag"
)
```
[In part 1](../readme.md), we covered the fundamentals of writing basic Go <br/>
[In part 2](../part-2.json/readme.md), we've learn how to use basic data structures like `json` so we can send\receive data. <br/>
[Part 3](../part-3.http/readme.md) was about exposing data via a Web server.
We'll be combining these techniques so we can serve our `videos` data over a commandline application.
As always, let's start with our `dockerfile` , `main.go` and `videos.go` we created in Part 2
## Dev Environment
The same as Part 1+2+3, we start with a [dockerfile](./dockerfile) where we declare our version of `go`.
```
cd golang\introduction\part-4.commandline
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
```
## Create our App
Create a new directory that holds defines our `repository` and holds our `module`
```
mkdir videos
cd videos
```
* Define a module path
```
# create a go module file
go mod init videos
```
## Create our base code
We start out all our applications with a `main.go` defining our `package`, declaring our `import` dependencies <br/>
and our entrypoint `main()` function
```
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
```
## Create our Videos app
Firstly, we create a seperate code file `videos.go` that deals with our YouTube videos <br/>
The `videos.go` file defines what a video `struct` looks like, a `getVideos()` function to retrieve <br/>
videos list as a slice and a `saveVideos()` function to save videos to a file locally. <br/>
Let's copy the following content from Part 2 and create `videos.go` :
We want `videos.go` to be part of package main:
```
package main
```
We import 2 packages, 1 for reading and writing files, and another for dealing with `json`
```
import (
"io/ioutil"
"encoding/json"
)
```
Then we define what a video `struct` looks like:
```
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
```
We have a function for retrieving `video` objects as a list of type `slice` :
```
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
```
We also need to copy our `videos.json` file which contains our video data. <br/>
And finally, we have a function that accepts a list of type `slice` and stores the videos to a local file
```
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}
```
## Flag Package
https://golang.org/pkg/flag/
Package flag implements command-line flag parsing.
So we can run our videos app and pass it inputs like any other CLI.
Let's build a CLI tool that users our `getVideos()` and `saveVideos` functions.
To get all videos, perhaps we'd like a command
```
# get all videos
videos get --all
# get video by ID
videos get -id <video-id>
# add a video to our list
videos add -id -title -url -imageurl -desc
```
To start, we import package `flag`
```
import (
"flag"
"fmt"
)
```
Let's define our subcommands in `main.go` :
```
//'videos get' subcommand
getCmd := flag.NewFlagSet("get", flag.ExitOnError)
```
`videos get` command will need two inputs, `--all` if the user wants to return all videos, or `--id` if the user only wants a specific video
```
// inputs for `videos get` command
getAll := getCmd.Bool("all", false, "Get all videos")
getID := getCmd.String("id", "", "YouTube video ID")
```
`videos add` command will take a bit more inputs to be able to add a video to our storage.
```
addCmd := flag.NewFlagSet("add", flag.ExitOnError)
addID := addCmd.String("id", "", "YouTube video ID")
addTitle := addCmd.String("title", "", "YouTube video Title")
addUrl := addCmd.String("url", "", "YouTube video URL")
addImageUrl := addCmd.String("imageurl", "", "YouTube video Image URL")
addDesc := addCmd.String("desc", "", "YouTube video description")
```
When a user runs our videos CLI tool, we may need to validate that
our application receives the right subcommands. So lets ensure add simple validation to check if the user has passed a subcommand
To check the arguments passed to our CLI, we use the ["os"](https://golang.org/pkg/os/) package. Check the Args variable, it holds usefull information passed to our application including its name.
`var Args []string`
We can do a simple check by ensuring the length of `os.Args` is atleast 2.
We firstly need to add `os` to our import section </br>
Followed by our check:
```
if len(os.Args) < 2 {
fmt.Println("expected 'get' or 'add' subcommands")
os.Exit(1)
}
```
## Handling our subcommands
So to handle each sub command like `get` and `add`, we add a simple
`switch` statement that can branch into different pathways of execution,
based on a variables content.
Let's take a look at this simple `switch` statement:
```
//look at the 2nd argument's value
switch os.Args[1] {
case "get": // if its the 'get' command
//hande get here
case "add": // if its the 'add' command
//hande add here
default: // if we don't understand the input
}
```
Let's create seperate handler functions for each sub command to keep our code tidy:
```
func HandleGet(getCmd *flag.FlagSet, all *bool, id *string){
}
func HandleAdd(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
}
```
Now that we have seperate functions for each subcommand, we can take appropriate actions in each. Let's firstly parse the command flags for each subcommand:
This allows us to parse everything after the `videos <subcommand>` arguments:
```
addCmd.Parse(os.Args[2:])
```
## Input Validation
For our `HandleGet` function, let's validate input to ensure its correct.
```
if *all == false && *id == "" {
fmt.Print("id is required or specify --all for all videos")
getCmd.PrintDefaults()
os.Exit(1)
}
```
Let's handle the scenario if user passing `--all` flag:
```
if *all {
//return all videos
videos := getVideos()
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
for _, video := range videos {
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
return
}
```
Let's handle when user is searching for a video by ID
```
if *id != "" {
videos := getVideos()
id := *id
for _, video := range videos {
if id == video.Id {
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
}
}
```
## Parsing multiple fields
For our `HandleAdd` function, we need to validate multiple inputs, create a `video` struct, append it to the existing video list and save it back to file
Let's create a `ValidateVideo()` function with similar inputs to our `HandleAdd()`:
```
func ValidateVideo(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
}
```
Let's simply validate all fields since they are all required:
```
if *id == "" || *title == "" || *url == "" || *imageUrl == "" || *description == "" {
fmt.Print("all fields are required for adding a video")
addCmd.PrintDefaults()
os.Exit(1)
}
```
And we can now call this function in our add function:
```
ValidateVideo(addCmd, id,title,url, imageUrl, description)
```
## Adding our video
Now that we have some basic validation, not perfect, but good enough to get started, let's add our video to the existing file.
Define a video struct with the CLI input:
```
video := video{
Id: *id,
Title: *title,
Description: *description,
Imageurl: *imageUrl,
Url: *url,
}
```
Get the existing videos:
```
videos := getVideos()
```
Append our video to the list:
```
videos = append(videos,video)
```
Save the new updated video list:
```
saveVideos(videos)
```
# Build our Docker container
Let's uncomment all the build lines in the `dockerfile`
Full `dockerfile` :
```
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /usr/local/bin/videos
COPY ./videos/videos.json /
COPY run.sh /
RUN chmod +x /run.sh
ENTRYPOINT [ "./run.sh" ]
```
For our entrypoint, we need to create a shell script to accept all the arguments:
Let's create a script called `run.sh`
```
#!/bin/sh
videos $@
```
Build :
```
cd golang\introduction\part-4.commandline
docker build . -t videos
```
Run :
```
docker run -it videos get --help
```