larksuite/cli · error

%s got both %q and %q — keep %q and drop the other

Error message

%s got both %q and %q — keep %q and drop the other

What it means

normalizeSubOpInputKeys converts a kebab-case vocab key to its canonical snake_case target. If the input already contains the snake_case target while the current kebab-case key is also present, both would fight for the same logical field, so the dispatcher errors and asks you to keep the snake_case key.

Source

Thrown at shortcuts/sheets/batch_op_dispatch.go:450

			}
			// Normalize the surviving spelling to the underscore form the tool
			// bodies use, so exactly one key reaches the flag view.
			if target := strings.ReplaceAll(hv, "-", "_"); target != k {
				if _, taken := input[target]; !taken {
					input[target] = input[k]
					delete(input, k)
					canonical[hv] = target
				}
			}
			continue
		}
		if kebab := camelToKebab(k); kebab != "" && vocab[kebab] {
			if err := claim(kebab, k); err != nil {
				return err
			}
			target := strings.ReplaceAll(kebab, "-", "_")
			if _, taken := input[target]; taken {
				return fmt.Errorf("%s got both %q and %q — keep %q and drop the other", sc, k, target, target) //nolint:forbidigo // intermediate error; the batch dispatcher wraps it into a typed operations validation error
			}
			if _, taken := input[kebab]; taken && kebab != target {
				return fmt.Errorf("%s got both %q and %q — keep %q and drop the other", sc, k, kebab, kebab) //nolint:forbidigo // intermediate error; the batch dispatcher wraps it into a typed operations validation error
			}
			input[target] = input[k]
			delete(input, k)
			canonical[kebab] = target
			continue
		}
		if target, ok := aliases[strings.ToLower(hv)]; ok && vocab[target] {
			if err := claim(target, k); err != nil {
				return err
			}
			underscored := strings.ReplaceAll(target, "-", "_")
			_, hyphenTaken := input[target]
			_, underscoreTaken := input[underscored]
			if !hyphenTaken && !underscoreTaken {
				input[target] = input[k]

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Drop the kebab-case spelling and keep the snake_case key (`target`) with your intended value.
  2. If both are intended for the same field, copy the kebab value into the snake_case key and delete the kebab one.
  3. Normalize your payload generator to emit snake_case keys only.

Example fix

// before
{"values": [...], "cell-range": "A1:B2", "cell_range": "C3:D4"}
// after
{"values": [...], "cell_range": "C3:D4"}
Defensive patterns

Strategy: validation

Validate before calling

func hasKebabSnakeConflict(op map[string]interface{}) bool {
    for k := range op {
        if strings.Contains(k, "-") {
            if _, exists := op[strings.ReplaceAll(k, "-", "_")]; exists {
                return true
            }
        }
    }
    return false
}

Prevention

When it happens

Trigger: Sub-op input contains e.g. both `cell-range` and `cell_range` for a vocab-defined kebab key. The camelToKebab normalization produced a kebab spelling whose snake_case form already exists in the input with its own value.

Common situations: Mixing old kebab-case-style payloads with the canonical snake_case format in the same batch op; merging two JSON fragments each using a different convention for the same flag.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/9a4369be584c9c5a. Report an issue: GitHub.