Creating a command Cli with Golang
Tags: go, git
Published: 2020-08-22 03:28:19

# Creating a command Cli with Golang to clone repositories from _AWS CodeCommit_

## My Problem

I’m starting in the AWS world and right now in the project I’m working we need to clone many repositories from AWS CodeCommit, to clone any repo we need to write something like that:

```shell
  git clone “https://username:pass@git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-with-super-extense-name
```

but the username and password need to be URL encoded before cloning the repo, so it was so boring, really I need to do something like this

* Url encode the username
* Url encode the password
* Cut the URL just using the way I need
* Create the `https://username:pass@git-codecommit.us-east-1.amazonaws.com/v1/repos/demo-with-super-extense-name`

Just before cloning a repository? So I decided to create my script to clone all the repositories I want from AWS CodeCommit just writing something like this:

```shell
  cloneaws clone —profile=myProfile —url=“ssh-Url-or-https-Url” —projectName=demo
```

This is more friendly and so easy without URL encode first the username and password

Before start, I had months trying to learn Go, because I feel empathy with the pet of the language (just kidding I just want to learn another language, but the pet is so cute), so I decide to write a command CLI. with Go

## Solution

Well, the principal idea is clone a repository from AWS CodeCommit without writing the user and password, just set a **label** to find in a list of credentials.

First, we create a subcommand with the code below:

```go
  cloneAwsCommand := flag.NewFlagSet("clone", flag.ExitOnError)
```

Add the few parameters needed by the command with the next code:

```go
  cloneUrl := cloneAwsCommand.String("url", "", "Repository HTTPS/SSH URL (Required)")
  cloneProfile := cloneAwsCommand.String("profile", "", "Profile name to use cloning repository (Required)")
  cloneProjectName := cloneAwsCommand.String("projectName", "", "Folder name to copy repo (optional)")
  cloneHelp := cloneAwsCommand.Bool("help", false, "Show usage for subcommand")
```

Validate if has set any subcommand:

```go
if len(os.Args) < 2 {
  log.Fatalln("Subcommand is required")
}
```

Now I'll check what subcommand to use, for now just has `clone` but I pretend to add many other functionalities like "add a new profile, modified profile"

To check the subcommand we use a switch with the bode below:

```go
switch os.Args[1] {
    case "clone":
        // Clone block
    default:
        fmt.Println("  clone string")
        fmt.Println("   Subcommand for clone")
        log.Fatalln("Subcommand is required")
    }
```

the switch has a default, this will print in console an error with the message "subcommand is required".

Now I want to explain the code in the clone block:

```go

  // First we parse the arguments for clone subcommand.
  cloneAwsCommand.Parse(os.Args[2:])

  // Validate if set the flag "help" to display the clone subcommand usage.
  if *cloneHelp == true {
      cloneAwsCommand.Usage()
      os.Exit(1)
  }

  /*
    Validate if the profile and URL flag is an empty string.
    If one of them is empty print default values.
  */
  if *cloneProfile == "" || *cloneUrl == "" {
      cloneAwsCommand.PrintDefaults()
      log.Fatalln("No flags set for clone subcommand")
  }

  //Validate again if profile is an empty string.
  if *cloneProfile == "" {
      cloneAwsCommand.PrintDefaults()
      log.Fatalln("No profile value to find")
  }
  /*
    Call findCredentials function to get the username and password.
    I'll explain this function next.
  */
  credentials := findCredentials(*cloneProfile)

  // Validate again if clone URL is and empty string.
  if *cloneUrl == "" {
      cloneAwsCommand.PrintDefaults()
      log.Fatalln("")
  }

  // Get the URL substring.
  urlRepo := getRepoPath(*cloneUrl)

  // URL encode the email and password from credentials.
  userEncode := url.QueryEscape(credentials.Email)
  passwordEncode := url.QueryEscape(credentials.Password)

  // Set the URL to clone
  fullUrlEncode := "https://" + userEncode + ":" + passwordEncode + "@" + urlRepo

  /*
    Execute the clone command.
    I'll explain this function next.
  */
  executeCloneCommand(fullUrlEncode, *cloneProjectName)
```

Before creating the function we will create a Struct with the structure of a `user` and `Users`what is an array of many users.

```go
// Users struct, Array of users.
type Users struct {
  Users []User `json:"users"`
}

// User struct, JSON with attributes.
type User struct {
  Alias    string `json:"alias"`
  Email    string `json:"email"`
  Password string `json:"password"`
}
```

Now I'll explain the function `findCredentials`

```go
/*
  Create a function, this receives one parameter:
  * alias - String

  The function will return a User struct.
*/
func findCredentials(alias string) User {
  // Get the current user, the user who call the command.
  user, err := user.Current()
  // create the credentials file path.
  credentialsPath := user.HomeDir + "/.codecommit/" + "credentials.json"
  // Open the credentials file.
  jsonFile, err := os.Open(credentialsPath)
  // Validate if hasn't error opening the file.
  if err != nil {
    log.Fatalln(err)
  }

  defer jsonFile.Close()
  // Read the file.
  byteValue, _ := ioutil.ReadAll(jsonFile)

  // Set a variable using the Users Struct.
  var users Users

  json.Unmarshal(byteValue, &users)

  // Make a for to find the right alias in the file.
  for i := 0; i < len(users.Users); i++ {
    if alias == users.Users[i].Alias {
      return users.Users[i]
    }
  }

  // If no exist the profile, send an error indicating what to do for add a new profile.
  fmt.Println("No profile with the value:", alias)
  log.Fatalln("Please add the user/password/alias in the credentials.json file inside" + user.HomeDir + "/.codecommit folder")
  return User{}
}
```

The last step is `executeCloneCommand` and I'll explain how to do this below

```go
/*
  Create a function, this receives two parameter:
  * URL - String
  * projectName - String
*/
func executeCloneCommand(url string, projectName string) {
  /*
    Validate if the parameter "projectName" is a empty to set the parameter and execute the comand `git clone` else execute the gommand `git clone`without this patameter.
  */
  if projectName != "" {
    /*
      Execute the command with the project name, this will execute a command like this:
      `git clone https://url test`

      If executing the command has an error print it in console an exit the program.
    */
    if err := exec.Command("git", "clone", url, projectName).Run(); err != nil {
      fmt.Fprintln(os.Stderr, err)
      os.Exit(1)
    }
  } else {
    /*
      Execute the command without project name, this will execute a command like this:
      `git clone https://url test`

      If executing the command has an error print it in console an exit the program.
    */
    if err := exec.Command("git", "clone", url).Run(); err != nil {
      fmt.Fprintln(os.Stderr, err)
      os.Exit(1)
    }
  }
}
```

After writing all this you can test the command using `go run main.go clone --profile="your-profile" --url="https://url" --projectName="test"` and clone any project you have in _AWS CodeCommit_.

However if you want install the command go to root project and execute `go install` and now can clone from _AWS CodeCommit_ using `cloneaws clone --profile="your-profile" --url="https://url" --projectName="test"`