Skip to content

Commit

Permalink
feat: adds flag to install specified version (#7)
Browse files Browse the repository at this point in the history
* feat: add flag to install specified version

* fix: typo

* fix: linter
  • Loading branch information
VassilisPallas authored Oct 28, 2023
1 parent 70dabaf commit 27d93e9
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 46 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
- [Use the dropdown to select a version](#use-the-dropdown-to-select-a-version)
- [See all versions including release candidates (rc)](#see-all-versions-including-release-candidates-rc)
- [Install latest version](#install-latest-version)
- [Install specific version](#install-specific-version)
- [Delete unused versions](#delete-unused-versions)
- [Refresh version list](#refresh-version-list)
- [Help](#help)
- [Contributions](#contributions)
Expand Down Expand Up @@ -81,7 +83,7 @@ $ brew install VassilisPallas/tap/gvs
Installation for other linux operation systems.

```sh
$ curl -L https://raw.githubusercontent.com/VassilisPallas/gvs/main/install.sh | bash
$ curl -L https://raw.githubusercontent.com/VassilisPallas/gvs/HEAD/install.sh | bash
```

### Install from source
Expand Down Expand Up @@ -151,6 +153,26 @@ Installing version...
1.21.3 version is installed!
```

### Install specific version

In order to install a specific version without using the dropdown, use the `--install-version=value`.

```sh
$ gvs --install-version=1.21.3
Downloading...
Compare Checksums...
Unzipping...
Installing version...
1.21.3 version is installed!
```

If the `Minor` version is not specified (`--install-version=1`), the latest `Minor` version is selected from the given `Major` version.

If the `Patch` version is not specified (`--install-version=1.21`), the latest `Patch` version is selected from the given version.

You can also pass Release Candidates, like `1.21rc2`.


### Delete unused versions

Every time you install a new version, gvs keeps the previous installed versions, so you can easily change between them. If you want to delete all the unused versions and keep only the current one, use the `--delete-unused` flag.
Expand Down
76 changes: 33 additions & 43 deletions cmd/gvs/main.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
package main

import (
"flag"
"fmt"
"net/http"
"os"
"runtime"
"time"

terminalColors "github.com/fatih/color"

"github.com/VassilisPallas/gvs/api_client"
"github.com/VassilisPallas/gvs/clock"
cf "github.com/VassilisPallas/gvs/config"
"github.com/VassilisPallas/gvs/files"
"github.com/VassilisPallas/gvs/flags"
"github.com/VassilisPallas/gvs/install"
"github.com/VassilisPallas/gvs/logger"
"github.com/VassilisPallas/gvs/pkg/unzip"
Expand All @@ -26,48 +23,18 @@ var (
installLatest = false
deleteUnused = false
showAllVersions = false
specificVersion = ""
)

func getBold() *terminalColors.Color {
return terminalColors.New().Add(terminalColors.Bold)
}

func parseFlags() {
flag.BoolVar(&showAllVersions, "show-all", false, "Show both stable and unstable versions.")
flag.BoolVar(&installLatest, "install-latest", false, "Install latest stable version.")
flag.BoolVar(&deleteUnused, "delete-unused", false, "Delete all unused versions that were installed before.")
flag.BoolVar(&refreshVersions, "refresh-versions", false, "Fetch again go versions in case the cached ones are stale.")

flag.Usage = func() {
bold := getBold()
gvsMessage := bold.Sprint("gvs")

flagSet := flag.CommandLine

fmt.Println()
bold.Println("NAME")
fmt.Printf(" gvs\n\n")

bold.Println("DESCRIPTION")
fmt.Printf(" the %s CLI is a command line tool to manage multiple active Go versions.\n\n", gvsMessage)

bold.Println("SYNOPSIS")
fmt.Printf(" gvs\n [--show-all]\n [--install-latest]\n [--delete-unused]\n [--refresh-versions]\n\n")

bold.Println("FLAGS")
flags := []string{"show-all", "install-latest", "delete-unused", "refresh-versions"}
for _, name := range flags {
flag := flagSet.Lookup(name)
fmt.Printf(" --%s\n\t%s\n", flag.Name, flag.Usage)
}
fmt.Println()

fmt.Printf("Before start using the %s CLI, make sure to delete all the existing go versions\n", gvsMessage)
fmt.Printf("and append to your profile file the export: %q.\n", "export PATH=$PATH:$HOME/bin")
fmt.Printf("The profile file could be one of: (%s)\n", "~/.bash_profile, ~/.zshrc, ~/.profile, or ~/.bashrc")
}

flag.Parse()
set := flags.FlagSet{}
set.FlagBool(&showAllVersions, "show-all", false, "Show both stable and unstable versions.")
set.FlagBool(&installLatest, "install-latest", false, "Install latest stable version.")
set.FlagBool(&deleteUnused, "delete-unused", false, "Delete all unused versions that were installed before.")
set.FlagBool(&refreshVersions, "refresh-versions", false, "Fetch again go versions in case the cached ones are stale.")
set.FlagStr(&specificVersion, "install-version", "", "Pass the version you want to install instead of selecting from the dropdown. If you do not specify the minor or the patch version, the latest one will be selected.")

set.Parse()
}

func main() {
Expand Down Expand Up @@ -110,6 +77,29 @@ func main() {
}

switch {
case specificVersion != "":
semver := &version.Semver{}
err := version.ParseSemver(specificVersion, semver)
if err != nil {
log.PrintError(err.Error())
os.Exit(1)
return
}

selectedVersion := versioner.FindVersionBasedOnSemverName(versions, semver)
if selectedVersion == nil {
log.PrintError("%s is not a valid version.", semver.GetVersion())
os.Exit(1)
return
}

err = versioner.Install(selectedVersion, runtime.GOOS, runtime.GOARCH)

if err != nil {
log.PrintError(err.Error())
os.Exit(1)
return
}
case deleteUnused:
log.Info("deleteUnused option selected")

Expand Down
107 changes: 107 additions & 0 deletions flags/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package flags

import (
"flag"
"fmt"

terminalColors "github.com/fatih/color"
)

// Flag contains the information for the flag.
type Flag struct {
// The name of the flag
name string

// A boolean that represents if the flag expects a value to be passed.
//
// e.g. --install-version 21.3
acceptsVale bool
}

// getHelpName returns the name for the flag and an indicator
// if the flag expects a value of not.
//
// For example, when a flag does not expect a value the result will be like: --some-flag.
// If the flag expects a value, the result will be like: --some-flag=value
func (f Flag) getHelpName() string {
flagName := fmt.Sprintf("--%s", f.name)
if f.acceptsVale {
flagName += "=value"
}

return flagName
}

// FlagSet is the struct that will be used to set up the flags for the CLI.
type FlagSet struct {
// An array of the flags that are available.
// flags is used as a store to easy iterate on the flags.
flags []Flag
}

// FlagBool defines a bool flag with specified name, default value, and usage string.
// The argument p points to a bool variable in which to store the value of the flag.
// FlagBool also appends the flag to the FlagSet array.
func (s *FlagSet) FlagBool(p *bool, name string, value bool, usage string) {
s.flags = append(s.flags, Flag{name: name, acceptsVale: false})
flag.BoolVar(p, name, value, usage)
}

// StringVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a string variable in which to store the value of the flag.
// FlagBool also appends the flag to the FlagSet array.
func (s *FlagSet) FlagStr(p *string, name string, value string, usage string) {
s.flags = append(s.flags, Flag{name: name, acceptsVale: true})
flag.StringVar(p, name, value, usage)
}

// printSynopsis returns back all the available flags without any description.
// All the flags are iterated from FlagSet array that contains the flags.
func (s *FlagSet) printSynopsis() {
msg := " gvs\n"
for _, flag := range s.flags {
msg += fmt.Sprintf(" [%s]\n", flag.getHelpName())
}

fmt.Printf("%s\n", msg)
}

// printFlags returns back all the available flags wiath a description.
// All the flags are iterated from FlagSet array that contains the flags.
func (s *FlagSet) printFlags() {
flagSet := flag.CommandLine

for _, flag := range s.flags {
flagInfo := flagSet.Lookup(flag.name)
fmt.Printf(" %s\n\t%s\n", flag.getHelpName(), flagInfo.Usage)
}
}

// Parse is preparing the help command and parses the flags.
func (s *FlagSet) Parse() {
flag.Usage = func() {
bold := terminalColors.New().Add(terminalColors.Bold)

gvsMessage := bold.Sprint("gvs")

fmt.Println()
bold.Println("NAME")
fmt.Printf(" gvs - go version manager\n\n")

bold.Println("DESCRIPTION")
fmt.Printf(" the %s CLI is a command line tool to manage multiple active Go versions.\n\n", gvsMessage)

bold.Println("SYNOPSIS")
s.printSynopsis()

bold.Println("FLAGS")
s.printFlags()

fmt.Println()
fmt.Printf("Before start using the %s CLI, make sure to delete all the existing go versions\n", gvsMessage)
fmt.Printf("and append to your profile file the export: %q.\n", "export PATH=$PATH:$HOME/bin")
fmt.Printf("The profile file could be one of: (%s)\n", "~/.bash_profile, ~/.zshrc, ~/.profile, or ~/.bashrc")
}

flag.Parse()
}
2 changes: 0 additions & 2 deletions install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package install

import (
"context"
"fmt"
"io"

"github.com/VassilisPallas/gvs/api_client"
Expand Down Expand Up @@ -112,7 +111,6 @@ func (i Install) newVersionHandler(checksum string, goVersionName string) func(c

i.log.PrintMessage("Unzipping...\n")
if err = i.fileHelpers.UnzipTarFile(); err != nil {
fmt.Println(err)
return err
}

Expand Down
98 changes: 98 additions & 0 deletions version/semver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Package version provides an interface to make handle
// the CLI logic for the versions.
package version

import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)

// Semver contains the version split into semantic version structure.
// Semver can have either a patch or a release candidate value, not both at the same time.
type Semver struct {
// Contains the major version of the semantic structure.
Major *uint64

// Contains the minor version of the semantic structure.
Minor *uint64

// Contains the patch version of the semantic structure.
Patch *uint64

// Contains the release candidate number of the semantic structure.
ReleaseCandidate *uint64
}

// GetVersion returns back the stringified representation of the semantic version structure.
func (s Semver) GetVersion() string {
semver := ""

if s.Major == nil {
return ""
}

semver += fmt.Sprint(*s.Major)

if s.Minor != nil {
semver += fmt.Sprintf(".%d", *s.Minor)
}

if s.Patch != nil {
semver += fmt.Sprintf(".%d", *s.Patch)
} else if s.ReleaseCandidate != nil {
semver += fmt.Sprintf("rc%d", *s.ReleaseCandidate)
}

return semver
}

// parseNumber converts a string into *uint64.
// In case of an error while converting the value, parseNumber return nil.
func parseNumber(str string) *uint64 {
num, err := strconv.ParseUint(str, 10, 64)

if err != nil {
return nil
}

return &num
}

// ParseSemver parses the given stringified version into a semantic version structure.
//
// If the parse is successful it will store it
// in the value pointed to by semver.
//
// If the parse fails, ParseSemver will return an error.
func ParseSemver(version string, semver *Semver) error {
var major *uint64
var minor *uint64
var patch *uint64
var rc *uint64

r := regexp.MustCompile(`(\d{1,2})(\.?\d{1,2})?(\.?\d{1,2}|rc\d{1,2})?`)

if !r.MatchString(version) {
return errors.New("invalid Go version")
}

groups := r.FindStringSubmatch(version)[1:]
major = parseNumber(groups[0])
minor = parseNumber(strings.TrimPrefix(groups[1], "."))

if strings.HasPrefix(groups[2], "rc") {
rc = parseNumber(strings.TrimPrefix(groups[2], "rc"))
} else {
patch = parseNumber(strings.TrimPrefix(groups[2], "."))
}

semver.Major = major
semver.Minor = minor
semver.Patch = patch
semver.ReleaseCandidate = rc

return nil
}
Loading

0 comments on commit 27d93e9

Please sign in to comment.