DEV Community

Nestor Gutiérrez
Nestor Gutiérrez

Posted on

Creating a command Cli with Golang

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:

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

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:

  cloneaws clone —profile=myProfile —url=“ssh-Url-or-https-Url” —projectName=demo
Enter fullscreen mode Exit fullscreen mode

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:

  cloneAwsCommand := flag.NewFlagSet("clone", flag.ExitOnError)
Enter fullscreen mode Exit fullscreen mode

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

  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")
Enter fullscreen mode Exit fullscreen mode

Validate if has set any subcommand:

if len(os.Args) < 2 {
  log.Fatalln("Subcommand is required")
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:


  // 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)
Enter fullscreen mode Exit fullscreen mode

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

// 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"`
}
Enter fullscreen mode Exit fullscreen mode

Now I'll explain the function findCredentials

/*
  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{}
}
Enter fullscreen mode Exit fullscreen mode

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

/*
  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)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"

Top comments (0)