Help with CLI flag issue

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)
		}
	}

I think I have this working now.

I have tried to integrate my own cli framework, but it is obviously not enough to meet various needs in a short time. If the cli design is more complicated, I suggest using an existing cli development framework, such as: