Files
larksuite--cli/shortcuts/sheets/lark_sheet_search_replace.go
wehub-resource-sync bf9395e022
CI / results (push) Blocked by required conditions
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / e2e-live (push) Waiting to run
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / deadcode (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

173 lines
5.7 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package sheets
import (
"context"
"strings"
"github.com/larksuite/cli/shortcuts/common"
)
// ─── lark_sheet_search_replace ────────────────────────────────────────
//
// Wraps search_data (read) and replace_data (write). Both tools take an
// `options` sub-object; the CLI flattens its common booleans
// (--match-case / --match-entire-cell / --regex / --include-formulas) into
// independent flags per the铁律.
// CellsSearch wraps search_data: find cell coordinates matching --find,
// with optional case / regex / whole-cell / formula-text controls.
var CellsSearch = common.Shortcut{
Service: "sheets",
Command: "+cells-search",
Description: "Find cells matching --find in a spreadsheet (case / regex / whole-cell / formula-text controls).",
Risk: "read",
Scopes: []string{"sheets:spreadsheet:read"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+cells-search"),
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, err := resolveSpreadsheetToken(runtime); err != nil {
return err
}
if _, _, err := resolveSheetSelector(runtime); err != nil {
return err
}
if strings.TrimSpace(runtime.Str("find")) == "" {
return sheetsValidationForFlag("find", "--find is required")
}
return nil
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
sheetID, sheetName, _ := resolveSheetSelector(runtime)
return invokeToolDryRun(token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
sheetID, sheetName, err := resolveSheetSelector(runtime)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindRead, "search_data", searchInput(runtime, token, sheetID, sheetName))
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
}
func searchInput(runtime *common.RuntimeContext, token, sheetID, sheetName string) map[string]interface{} {
input := map[string]interface{}{
"excel_id": token,
"search_term": runtime.Str("find"),
}
sheetSelectorForToolInput(input, sheetID, sheetName)
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
input["range"] = r
}
if runtime.Changed("offset") && runtime.Int("offset") > 0 {
input["offset"] = runtime.Int("offset")
}
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
input["options"] = opts
}
if n := runtime.Int("max-matches"); n > 0 {
input["max_matches"] = n
}
return input
}
// searchReplaceOptions packs the four shared boolean flags into the tool's
// `options` sub-object. Empty result → caller should omit the field.
func searchReplaceOptions(runtime flagView) map[string]interface{} {
opts := map[string]interface{}{}
if runtime.Bool("match-case") {
opts["match_case"] = true
}
if runtime.Bool("match-entire-cell") {
opts["match_entire_cell"] = true
}
if runtime.Bool("regex") {
opts["use_regex"] = true
}
if runtime.Bool("include-formulas") {
opts["match_formulas"] = true
}
return opts
}
// CellsReplace wraps replace_data: find and replace text across a
// spreadsheet, with the same option controls as +cells-search.
var CellsReplace = common.Shortcut{
Service: "sheets",
Command: "+cells-replace",
Description: "Find and replace text in a spreadsheet (case / regex / whole-cell / formula-text controls).",
Risk: "write",
Scopes: []string{"sheets:spreadsheet:write_only"},
AuthTypes: []string{"user", "bot"},
HasFormat: true,
Flags: flagsFor("+cells-replace"),
Validate: validateViaInput(replaceInput),
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
token, _ := resolveSpreadsheetToken(runtime)
sheetID, sheetName, _ := resolveSheetSelector(runtime)
input, _ := replaceInput(runtime, token, sheetID, sheetName)
return invokeToolDryRun(token, ToolKindWrite, "replace_data", input)
},
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
token, err := resolveSpreadsheetTokenExec(runtime)
if err != nil {
return err
}
sheetID, sheetName, err := resolveSheetSelector(runtime)
if err != nil {
return err
}
input, err := replaceInput(runtime, token, sheetID, sheetName)
if err != nil {
return err
}
out, err := callTool(ctx, runtime, token, ToolKindWrite, "replace_data", input)
if err != nil {
return err
}
runtime.Out(out, nil)
return nil
},
Tips: []string{
"Always preview with --dry-run before running — replace can mutate every matching cell across the sheet.",
},
}
func replaceInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
if err := requireSheetSelector(sheetID, sheetName); err != nil {
return nil, err
}
if strings.TrimSpace(runtime.Str("find")) == "" {
return nil, sheetsValidationForFlag("find", "--find is required")
}
if !runtime.Changed("replacement") {
return nil, sheetsValidationForFlag("replacement", "--replacement is required (pass an empty string to delete matches)")
}
input := map[string]interface{}{
"excel_id": token,
"search_term": runtime.Str("find"),
"replace_term": runtime.Str("replacement"),
}
sheetSelectorForToolInput(input, sheetID, sheetName)
if r := strings.TrimSpace(runtime.Str("range")); r != "" {
input["range"] = r
}
if opts := searchReplaceOptions(runtime); len(opts) > 0 {
input["options"] = opts
}
return input, nil
}