Files
dingtalk-workspace-cli/pkg/cmdutil/sticky_suffix.go
T
6707e56f9c feat(cli): schema-aware sticky flag splitting and structured unknown-flag recovery (#272)
* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* 1.0.19 changelog

* stick opt

* fix(cli): utf-8 safe sticky suffix guard + changelog (#272)

SuffixLooksLikeValue used to read suffix[0] (a single byte) for both the
uuid format branch and the fallback "is the first rune a letter?" check.
For multi-byte UTF-8 leading runes — common in dws because value text is
often Chinese — this picked up only the first byte (0xE0..0xF4 lead),
which is not a letter and not a hex digit, so the function silently
returned true and let glued tokens like --name<CJK> get split into
--name <CJK>... This switches both branches to utf8.DecodeRuneInString
and adds a utf8.RuneError guard so invalid UTF-8 input is rejected too.

Also locks down the new behaviour in CHANGELOG ## [Unreleased]:
- Changed: schema-aware sticky flag splitting
- Added: available_flags field on unknown-flag errors

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 10:56:02 +08:00

134 lines
3.4 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmdutil
import (
"strings"
"unicode"
"unicode/utf8"
)
// SuffixLooksLikeValue decides whether a candidate suffix from a glued
// "--flagsuffix" token plausibly represents a value for a flag's
// declared type/format/enum. Shared by StickyHandler (PreParse) and
// SuggestFlagFix (unknown-flag recovery).
//
// typ is a pflag value type string (e.g. "int", "bool", "string");
// format is JSON Schema "format" when present (e.g. "date-time");
// enum is the schema enum list when present.
func SuffixLooksLikeValue(suffix, typ, format string, enum []string) bool {
if suffix == "" {
return false
}
switch typ {
case "int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"integer", "number", "float", "float32", "float64",
"count":
return startsWithNumericSuffix(suffix)
case "bool", "boolean":
return isBoolLiteralSuffix(suffix)
case "duration":
return startsWithDigitSuffix(suffix) || startsWithSignSuffix(suffix)
case "stringSlice", "stringArray", "intSlice", "boolSlice", "float32Slice",
"float64Slice", "uintSlice", "durationSlice", "ipSlice", "array":
return false
case "object":
return false
}
if len(enum) > 0 {
return matchesEnumSuffix(suffix, enum)
}
switch strings.ToLower(format) {
case "date", "date-time", "datetime", "time":
return startsWithDigitSuffix(suffix)
case "duration":
return startsWithDigitSuffix(suffix) || startsWithSignSuffix(suffix)
case "email":
return strings.Contains(suffix, "@")
case "uri", "url":
lower := strings.ToLower(suffix)
return strings.HasPrefix(lower, "http") ||
strings.HasPrefix(lower, "ftp") ||
strings.HasPrefix(lower, "mailto:")
case "ipv4", "ipv6", "hostname":
return startsWithDigitSuffix(suffix)
case "uuid":
first, _ := utf8.DecodeRuneInString(suffix)
return isHexRuneSuffix(first)
}
first, _ := utf8.DecodeRuneInString(suffix)
if first == utf8.RuneError || unicode.IsLetter(first) {
return false
}
return true
}
func startsWithDigitSuffix(s string) bool {
if s == "" {
return false
}
c := s[0]
return c >= '0' && c <= '9'
}
func startsWithSignSuffix(s string) bool {
if s == "" {
return false
}
c := s[0]
return c == '+' || c == '-'
}
func startsWithNumericSuffix(s string) bool {
if startsWithDigitSuffix(s) {
return true
}
if startsWithSignSuffix(s) && len(s) > 1 {
c := s[1]
return c >= '0' && c <= '9'
}
return false
}
func isBoolLiteralSuffix(s string) bool {
switch strings.ToLower(s) {
case "true", "false", "1", "0", "t", "f", "yes", "no", "on", "off", "y", "n":
return true
}
return false
}
func matchesEnumSuffix(s string, enum []string) bool {
lower := strings.ToLower(s)
for _, e := range enum {
if strings.ToLower(e) == lower {
return true
}
}
return false
}
func isHexRuneSuffix(r rune) bool {
return (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
}