# Introduction to Go: Command Line
Command line apps are a fundamental part of software development
Go has a built in Commandline parser package. The package can be found [here](https://golang.org/pkg/flag/)
We simply have to import the `flag` package:
```
import (
"flag"
)
```
[In part 1](../readme.md), we covered the fundamentals of writing basic Go
[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.
[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
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
The `videos.go` file defines what a video `struct` looks like, a `getVideos()` function to retrieve
videos list as a slice and a `saveVideos()` function to save videos to a file locally.
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.
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
# 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 a 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
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 ` arguments:
```
getCmd.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
```