Golang中CLI标志参数问题求助

Golang中CLI标志参数问题求助 我正在开发一个使用标准库的命令行界面工具。

我尝试为一个新标志实现一个功能,该标志接受一个选项。

例如:poke-cli pokemon tyranitar --image=lg

调用此标志时会生成一张图片。

我已经实现了另外三个标志,但它们只接受 Bool 值。

这里涉及三个不同的文件:

  • ./flags/pokemonflagset.go:我在此创建标志及其相关函数。
  • ./cmd/pokemon.go:我在此调用标志。
  • ./cli.go:我在此验证并调用命令。

./flags/pokemonflagset.go 中,我定义了生成图片的函数:

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
}

./cmd/pokemon.go 中,我列出了标志,以便在使用时调用其相关函数:

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

最后,在 ./cli.go 中,我解析参数并检查用户输入的命令是否有效:

//.....
	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
	}

//.....

我遇到问题的地方是在使用 -i | --image 标志并传递尺寸选项时。具体是在这里:

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

像这样设置(空字符串或除 [sm, md, lg] 之外的任何字符)并在不使用 -i | --image 标志的情况下运行工具时,图片仍然会打印出来: poke-cli pokemon tyranitar

image

但是,使用尺寸选项是有效的。例如: poke-cli pokemon tyranitar -i=md | --image=lg

如果我确实设置了 if *imageFlag != "md" || *shortImageFlag != "md" 并使用可用选项中的一个值,那么该图片尺寸不会打印任何内容,但其他尺寸会正常打印(–image=sm | -i=lg 打印正常)。

我不确定是遗漏了一些简单的东西,还是整个标志的调用方式错了。我有其他使用 flag.Bool 的标志,它们可以正常工作:

	if *abilitiesFlag || *shortAbilitiesFlag {
		if err := flags.AbilitiesFlag(endpoint, pokemonName); err != nil {
			fmt.Printf("Error: %s\n", err)
			os.Exit(1)
		}
	}

更多关于Golang中CLI标志参数问题求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

我想我现在已经让它正常工作了。

更多关于Golang中CLI标志参数问题求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我曾尝试集成自己的 CLI 框架,但显然在短时间内无法满足各种需求。如果 CLI 设计较为复杂,我建议使用现有的 CLI 开发框架,例如:

GitHub - spf13/cobra: 用于现代 Go CLI 交互的指挥官

用于现代 Go CLI 交互的指挥官

你的问题在于标志初始化和条件判断逻辑。在 pokemon.go 中,imageFlagshortImageFlag 被初始化为默认值 "md",导致条件判断出错。

首先检查 ./flags/pokemonflagset.go 中的标志定义:

func SetupPokemonFlagSet() (*flag.FlagSet, *bool, *bool, *string, *string, *bool, *bool, *bool, *bool) {
	pokeFlags := flag.NewFlagSet("pokemon", flag.ExitOnError)
	
	abilitiesFlag := pokeFlags.Bool("abilities", false, "Prints the Pokémon's abilities.")
	shortAbilitiesFlag := pokeFlags.Bool("a", false, "Prints the Pokémon's abilities.")
	
	imageFlag := pokeFlags.String("image", "md", "Prints out the Pokémon's default sprite. Options: sm, md, lg")
	shortImageFlag := pokeFlags.String("i", "md", "Prints out the Pokémon's default sprite. Options: sm, md, lg")
	
	statsFlag := pokeFlags.Bool("stats", false, "Prints the Pokémon's base stats.")
	shortStatsFlag := pokeFlags.Bool("s", false, "Prints the Pokémon's base stats.")
	
	typesFlag := pokeFlags.Bool("types", false, "Prints the Pokémon's typing.")
	shortTypesFlag := pokeFlags.Bool("t", false, "Prints the Pokémon's typing.")
	
	return pokeFlags, abilitiesFlag, shortAbilitiesFlag, imageFlag, shortImageFlag, statsFlag, shortStatsFlag, typesFlag, shortTypesFlag
}

问题在于 imageFlagshortImageFlag 的默认值都是 "md"。当用户不提供 -i--image 标志时,这些标志的值仍然是 "md",而不是空字符串。

修改 ./cmd/pokemon.go 中的条件判断:

// 修改前的错误逻辑
if *imageFlag != "" || *shortImageFlag != "" {
    // 这会触发,因为默认值是 "md"
}

// 修改后的正确逻辑
if pokeFlags.Lookup("image").Changed || pokeFlags.Lookup("i").Changed {
    // 确定尺寸
    size := *imageFlag
    if *shortImageFlag != "md" && *shortImageFlag != "" {
        size = *shortImageFlag
    }
    
    // 调用 ImageFlag 函数
    if err := flags.ImageFlag(endpoint, pokemonName, size); err != nil {
        fmt.Printf("Error: %s\n", err)
        os.Exit(1)
    }
}

或者使用更简洁的方法,检查标志是否被显式设置:

// 检查是否提供了 image 标志
imageProvided := false
pokeFlags.Visit(func(f *flag.Flag) {
    if f.Name == "image" || f.Name == "i" {
        imageProvided = true
    }
})

if imageProvided {
    // 确定尺寸
    size := *imageFlag
    if *shortImageFlag != "md" {
        size = *shortImageFlag
    }
    
    // 调用 ImageFlag 函数
    if err := flags.ImageFlag(endpoint, pokemonName, size); err != nil {
        fmt.Printf("Error: %s\n", err)
        os.Exit(1)
    }
}

另一种解决方案是修改标志定义,使用空字符串作为默认值:

// 在 SetupPokemonFlagSet 函数中修改
imageFlag := pokeFlags.String("image", "", "Prints out the Pokémon's default sprite. Options: sm, md, lg")
shortImageFlag := pokeFlags.String("i", "", "Prints out the Pokémon's default sprite. Options: sm, md, lg")

然后保持原来的条件判断:

if *imageFlag != "" || *shortImageFlag != "" {
    // 确定尺寸
    size := *imageFlag
    if *shortImageFlag != "" {
        size = *shortImageFlag
    }
    
    // 调用 ImageFlag 函数
    if err := flags.ImageFlag(endpoint, pokemonName, size); err != nil {
        fmt.Printf("Error: %s\n", err)
        os.Exit(1)
    }
}

这样当用户不提供 -i--image 标志时,条件判断为 false,不会执行图片生成代码。当用户提供标志时(如 -i=md--image=lg),条件判断为 true,会执行相应的图片生成逻辑。

回到顶部