fix: normalize escaped sheet range separators (#207)

Accept escaped and full-width sheet/range separators in sheets shortcuts.
Normalize range parsing in the shared sheets helper so read, find, write,
and append handle \!, \!, and ! consistently.
Add regression tests for separator normalization in dry-run paths.
This commit is contained in:
caojie0621
2026-04-02 15:51:22 +08:00
committed by GitHub
parent 102ee51914
commit 0f96bdf5e8
2 changed files with 163 additions and 5 deletions

View File

@@ -23,6 +23,8 @@ var (
cellRefPattern = regexp.MustCompile(`^([A-Za-z]+)([1-9][0-9]*)$`)
)
var sheetRangeSeparatorReplacer = strings.NewReplacer(`\`, "!", `\!`, "!", "", "!")
// getFirstSheetID queries the spreadsheet and returns the first sheet's ID.
func getFirstSheetID(runtime *common.RuntimeContext, spreadsheetToken string) (string, error) {
data, err := runtime.CallAPI("GET", fmt.Sprintf("/open-apis/sheets/v3/spreadsheets/%s/sheets/query", validate.EncodePathSegment(spreadsheetToken)), nil, nil)
@@ -56,7 +58,7 @@ func extractSpreadsheetToken(input string) string {
}
func normalizeSheetRange(sheetID, input string) string {
input = strings.TrimSpace(input)
input = normalizeSheetRangeSeparators(input)
if input == "" || strings.Contains(input, "!") || sheetID == "" {
return input
}
@@ -80,7 +82,7 @@ func normalizePointRange(sheetID, input string) string {
func normalizeWriteRange(sheetID, input string, values interface{}) string {
rows, cols := matrixDimensions(values)
input = strings.TrimSpace(input)
input = normalizeSheetRangeSeparators(input)
if input == "" {
return buildRectRange(sheetID, "A1", rows, cols)
}
@@ -97,7 +99,7 @@ func normalizeWriteRange(sheetID, input string, values interface{}) string {
}
func validateSheetRangeInput(sheetID, input string) error {
input = strings.TrimSpace(input)
input = normalizeSheetRangeSeparators(input)
if input == "" || strings.Contains(input, "!") || sheetID != "" {
return nil
}
@@ -108,7 +110,7 @@ func validateSheetRangeInput(sheetID, input string) error {
}
func looksLikeRelativeRange(input string) bool {
input = strings.TrimSpace(input)
input = normalizeSheetRangeSeparators(input)
if input == "" {
return false
}
@@ -120,13 +122,21 @@ func looksLikeRelativeRange(input string) bool {
}
func splitSheetRange(input string) (sheetID, subRange string, ok bool) {
parts := strings.SplitN(strings.TrimSpace(input), "!", 2)
parts := strings.SplitN(normalizeSheetRangeSeparators(input), "!", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return "", "", false
}
return parts[0], parts[1], true
}
func normalizeSheetRangeSeparators(input string) string {
input = strings.TrimSpace(input)
if input == "" {
return input
}
return sheetRangeSeparatorReplacer.Replace(input)
}
func buildRectRange(sheetID, anchor string, rows, cols int) string {
if sheetID == "" {
return ""

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/larksuite/cli/shortcuts/common"
"github.com/spf13/cobra"
)
func mustMarshalSheetsDryRun(t *testing.T, v interface{}) string {
t.Helper()
b, err := json.Marshal(v)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
return string(b)
}
func newSheetsTestRuntime(t *testing.T, stringFlags map[string]string, boolFlags map[string]bool) *common.RuntimeContext {
t.Helper()
cmd := &cobra.Command{Use: "test"}
for name := range stringFlags {
cmd.Flags().String(name, "", "")
}
for name := range boolFlags {
cmd.Flags().Bool(name, false, "")
}
if err := cmd.ParseFlags(nil); err != nil {
t.Fatalf("ParseFlags() error = %v", err)
}
for name, value := range stringFlags {
if err := cmd.Flags().Set(name, value); err != nil {
t.Fatalf("Flags().Set(%q) error = %v", name, err)
}
}
for name, value := range boolFlags {
if err := cmd.Flags().Set(name, map[bool]string{true: "true", false: "false"}[value]); err != nil {
t.Fatalf("Flags().Set(%q) error = %v", name, err)
}
}
return &common.RuntimeContext{Cmd: cmd}
}
func TestNormalizeSheetRangeSeparators(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
want string
}{
{name: "standard", input: "sheet_123!A1:B2", want: "sheet_123!A1:B2"},
{name: "escaped ascii", input: `sheet_123\!A1:B2`, want: "sheet_123!A1:B2"},
{name: "fullwidth", input: "sheet_123A1:B2", want: "sheet_123!A1:B2"},
{name: "escaped fullwidth", input: `sheet_123\A1:B2`, want: "sheet_123!A1:B2"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := normalizeSheetRangeSeparators(tt.input); got != tt.want {
t.Fatalf("normalizeSheetRangeSeparators(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}
func TestValidateSheetRangeInputAcceptsEscapedSeparator(t *testing.T) {
t.Parallel()
if err := validateSheetRangeInput("", `sheet_123\A1:B2`); err != nil {
t.Fatalf("validateSheetRangeInput() error = %v, want nil", err)
}
}
func TestSheetReadDryRunNormalizesEscapedSeparator(t *testing.T) {
t.Parallel()
runtime := newSheetsTestRuntime(t, map[string]string{
"spreadsheet-token": "sht_test",
"range": `sheet_123\A1`,
"sheet-id": "",
}, nil)
got := mustMarshalSheetsDryRun(t, SheetRead.DryRun(context.Background(), runtime))
if !strings.Contains(got, `"range":"sheet_123!A1:A1"`) {
t.Fatalf("SheetRead.DryRun() = %s, want normalized escaped separator", got)
}
}
func TestSheetWriteDryRunNormalizesEscapedSeparator(t *testing.T) {
t.Parallel()
runtime := newSheetsTestRuntime(t, map[string]string{
"spreadsheet-token": "sht_test",
"range": `sheet_123\A1:B2`,
"values": `[[1,2],[3,4]]`,
}, nil)
got := mustMarshalSheetsDryRun(t, SheetWrite.DryRun(context.Background(), runtime))
if !strings.Contains(got, `"range":"sheet_123!A1:B2"`) {
t.Fatalf("SheetWrite.DryRun() = %s, want normalized escaped separator", got)
}
}
func TestSheetAppendDryRunNormalizesEscapedSeparator(t *testing.T) {
t.Parallel()
runtime := newSheetsTestRuntime(t, map[string]string{
"spreadsheet-token": "sht_test",
"range": `sheet_123\A1:B2`,
"values": `[["foo","bar"]]`,
}, nil)
got := mustMarshalSheetsDryRun(t, SheetAppend.DryRun(context.Background(), runtime))
if !strings.Contains(got, `"range":"sheet_123!A1:B2"`) {
t.Fatalf("SheetAppend.DryRun() = %s, want normalized escaped separator", got)
}
}
func TestSheetFindDryRunNormalizesEscapedSeparator(t *testing.T) {
t.Parallel()
runtime := newSheetsTestRuntime(t, map[string]string{
"spreadsheet-token": "sht_test",
"sheet-id": "sheet_123",
"find": "target",
"range": `sheet_123\A1:B2`,
}, map[string]bool{
"ignore-case": false,
"match-entire-cell": false,
"search-by-regex": false,
"include-formulas": false,
})
got := mustMarshalSheetsDryRun(t, SheetFind.DryRun(context.Background(), runtime))
if !strings.Contains(got, `"range":"sheet_123!A1:B2"`) {
t.Fatalf("SheetFind.DryRun() = %s, want normalized escaped separator", got)
}
}