mirror of
https://github.com/marcel-dempers/docker-development-youtube-series.git
synced 2025-06-06 17:01:30 +00:00
416 lines
9.3 KiB
Markdown
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
|
|
``` |