Hello, I am working on a CLI tool using the standard library.
I am trying to implement a feature to a new flag that takes an option.
i.e., poke-cli pokemon tyranitar --image=lg
This flag generates an image when called.
I have three other flags implemented but they just take a Bool
value.
There are three different files in play here:
./flags/pokemonflagset.go
where I create the flags and their functions../cmd/pokemon.go
where I call the flags../cli.go
where I validate and call the commands.
In ./flags/pokemonflagset.go
, I have the function defined that generates the image:
func ImageFlag(endpoint string, pokemonName string, size string) error {
baseURL := "https://pokeapi.co/api/v2/"
pokemonStruct, _, _, _, _ := connections.PokemonApiCall(endpoint, pokemonName, baseURL)
header("Image")
// Anonymous function to transform the image to a string
// ToString generates an ASCII representation of the image with color
ToString := func(width int, height int, img image.Image) string {
// Resize the image to the specified width, preserving aspect ratio
img = imaging.Resize(img, width, height, imaging.NearestNeighbor)
b := img.Bounds()
imageWidth := b.Max.X - 2 // Adjust width to exclude margins
h := b.Max.Y - 4 // Adjust height to exclude margins
str := strings.Builder{}
// Loop through the image pixels, two rows at a time
for heightCounter := 2; heightCounter < h; heightCounter += 2 {
for x := 1; x < imageWidth; x++ {
// Get the color of the current and next row's pixels
c1, _ := colorful.MakeColor(img.At(x, heightCounter))
color1 := lipgloss.Color(c1.Hex())
c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
color2 := lipgloss.Color(c2.Hex())
// Render the half-block character with the two colors
str.WriteString(lipgloss.NewStyle().
Foreground(color1).
Background(color2).
Render("▀"))
}
// Add a newline after each row
str.WriteString("\n")
}
return str.String()
}
imageResp, err := http.Get(pokemonStruct.Sprites.FrontDefault)
if err != nil {
fmt.Println("Error downloading sprite image:", err)
os.Exit(1)
}
defer imageResp.Body.Close()
img, err := imaging.Decode(imageResp.Body)
if err != nil {
fmt.Println("Error decoding image:", err)
os.Exit(1)
}
// Define size map
sizeMap := map[string][2]int{
"lg": {120, 120},
"md": {90, 90},
"sm": {55, 55},
}
// Validate size
dimensions, exists := sizeMap[strings.ToLower(size)]
if !exists {
errMessage := errorBorder.Render(errorColor.Render("Error!"), "\nInvalid image size. Valid sizes are: lg, md, sm")
return fmt.Errorf("%s", errMessage)
}
imgStr := ToString(dimensions[0], dimensions[1], img)
fmt.Println(imgStr)
return nil
}
In ./cmd/pokemon.go
, I have the flags listed to call their relative function when used:
// PokemonCommand processes the Pokémon command
func PokemonCommand() {
flag.Usage = func() {
helpMessage := helpBorder.Render(
"Get details about a specific Pokémon.\n\n",
styleBold.Render("USAGE:"),
fmt.Sprintf("\n\t%s %s %s %s", "poke-cli", styleBold.Render("pokemon"), "<pokemon-name>", "[flag]"),
fmt.Sprintf("\n\t%-30s", styleItalic.Render("Use a hyphen when typing a name with a space.")),
"\n\n",
styleBold.Render("FLAGS:"),
fmt.Sprintf("\n\t%-30s %s", "-a, --abilities", "Prints the Pokémon's abilities."),
fmt.Sprintf("\n\t%-30s %s", "-i, --image", "Prints out the Pokémon's default sprite."),
fmt.Sprintf("\n\t%-30s %s", "-s, --stats", "Prints the Pokémon's base stats."),
fmt.Sprintf("\n\t%-30s %s", "-t, --types", "Prints the Pokémon's typing."),
fmt.Sprintf("\n\t%-30s %s", "-h, --help", "Prints the help menu."),
)
fmt.Println(helpMessage)
}
flag.Parse()
pokeFlags, abilitiesFlag, shortAbilitiesFlag, imageFlag, shortImageFlag, statsFlag, shortStatsFlag, typesFlag, shortTypesFlag := flags.SetupPokemonFlagSet()
args := os.Args
err := ValidatePokemonArgs(args)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
endpoint := strings.ToLower(args[1])
pokemonName := strings.ToLower(args[2])
if err := pokeFlags.Parse(args[3:]); err != nil {
fmt.Printf("error parsing flags: %v\n", err)
pokeFlags.Usage()
os.Exit(1)
}
_, pokemonName, pokemonID, pokemonWeight, pokemonHeight := connections.PokemonApiCall(endpoint, pokemonName, "https://pokeapi.co/api/v2/")
capitalizedString := cases.Title(language.English).String(strings.Replace(pokemonName, "-", " ", -1))
// Weight calculation
weightKilograms := float64(pokemonWeight) / 10
weightPounds := float64(weightKilograms) * 2.20462
// Height calculation
heightMeters := float64(pokemonHeight) / 10
heightFeet := heightMeters * 3.28084
feet := int(heightFeet)
inches := int(math.Round((heightFeet - float64(feet)) * 12)) // Use math.Round to avoid truncation
// Adjust for rounding to 12 inches (carry over to the next foot)
if inches == 12 {
feet++
inches = 0
}
fmt.Printf(
"Your selected Pokémon: %s\nNational Pokédex #: %d\nWeight: %.1fkg (%.1f lbs)\nHeight: %.1fm (%d′%02d″)\n",
capitalizedString, pokemonID, weightKilograms, weightPounds, heightFeet, feet, inches,
)
if *abilitiesFlag || *shortAbilitiesFlag {
if err := flags.AbilitiesFlag(endpoint, pokemonName); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
if *imageFlag != "md" || *shortImageFlag != "md" {
// Determine the size based on the provided flags
size := *imageFlag
if *shortImageFlag != "" {
size = *shortImageFlag
}
// Call the ImageFlag function with the specified size
if err := flags.ImageFlag(endpoint, pokemonName, size); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
if *typesFlag || *shortTypesFlag {
if err := flags.TypesFlag(endpoint, pokemonName); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
if *statsFlag || *shortStatsFlag {
if err := flags.StatsFlag(endpoint, pokemonName); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
}
Finally, in ./cli.go
, I parse through the arguments and check to make sure the user is typing in a valid command:
//.....
commands := map[string]func(){
"pokemon": cmd.PokemonCommand,
"natures": cmd.NaturesCommand,
"types": cmd.TypesCommand,
}
if len(os.Args) < 2 {
mainFlagSet.Usage()
return 1
} else if *latestFlag || *shortLatestFlag {
flags.LatestFlag()
return 0
} else if *currentVersionFlag || *shortCurrentVersionFlag {
currentVersion()
return 0
} else if cmdFunc, exists := commands[os.Args[1]]; exists {
cmdFunc()
return 0
} else {
command := os.Args[1]
errMessage := errorBorder.Render(
errorColor.Render("Error!"),
fmt.Sprintf("\n\t%-15s", fmt.Sprintf("'%s' is not a valid command.\n", command)),
styleBold.Render("\nCommands:"),
fmt.Sprintf("\n\t%-15s %s", "natures", "Get details about Pokémon natures"),
fmt.Sprintf("\n\t%-15s %s", "pokemon", "Get details about a specific Pokémon"),
fmt.Sprintf("\n\t%-15s %s", "types", "Get details about a specific typing"),
fmt.Sprintf("\n\nAlso run %s for more info!", styleBold.Render("poke-cli -h")),
)
fmt.Printf("%s\n", errMessage)
return 1
}
//.....
Where I having issues is using the -i | --image
flag and passing the size options. Specifically here:
if *imageFlag != "" || *shortImageFlag != "" {
// Determine the size based on the provided flags
size := *imageFlag
if *shortImageFlag != "" {
size = *shortImageFlag
}
// Call the ImageFlag function with the specified size
if err := flags.ImageFlag(endpoint, pokemonName, size); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}
With it set like this (empty string or any other character besides [sm, md, lg]
) and run the tool without the -i | --image
flag, the image still prints:
poke-cli pokemon tyranitar
However, using a size option works. For example:
poke-cli pokemon tyranitar -i=md | --image=lg
If I do set the if *imageFlag != "md" || *shortImageFlag != "md"
with a value from the available options, then that image size doesn’t print anything but the others will print. (`–image=sm | -i=lg print just fine).
Not sure if I am missing something simple or if the whole calling of the flags is wrong. I have other flags that are flag.Bool
and work with this:
if *abilitiesFlag || *shortAbilitiesFlag {
if err := flags.AbilitiesFlag(endpoint, pokemonName); err != nil {
fmt.Printf("Error: %s\n", err)
os.Exit(1)
}
}