mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
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:
@@ -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 ""
|
||||
|
||||
148
shortcuts/sheets/sheet_ranges_test.go
Normal file
148
shortcuts/sheets/sheet_ranges_test.go
Normal 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_123!A1: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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user