# 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 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
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: ``` 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 ```