mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 01:51:27 +08:00
refactor(sheets): switch dim-* / rows-cols-resize to A1-string range schema
The 9 row/column-region shortcuts used to share two int flags --start /
--end with inconsistent end semantics across commands — +dim-insert /
-delete / -hide / -unhide / -group / -ungroup treated --end as exclusive,
while +dim-move / +rows-resize / +cols-resize treated it as inclusive.
The skill reference even called this out as "the highest-frequency
off-by-one source", patched in docs rather than at the surface. Three
underlying tool schemas (position+count, A1 range string, 0-based int
pair) were all flattened onto the same --start/--end pair, which forced
a different normaliser per command and pushed mental math (count =
end - start) onto every caller.
Schema (sourced from base, regenerated via sheet-skill-spec, mirrored
into shortcuts/sheets/data/ and skills/lark-sheets/):
+dim-insert --position + --count
rows: "3"; columns: "C". --count rows/columns
inserted *before* --position.
+dim-delete / -hide / -unhide / -group / -ungroup
--range
+rows-resize / +cols-resize --range
A1 closed range. Rows: "3:7" or "5". Columns: "C:F" or "C".
Mixing letters and digits in one range is rejected.
+dim-move --source-range + --target
--target must match --source-range's dimension (both row or both
column). The move places the source block *before* --target.
Wire-shape preserved: modify_sheet_structure still receives `position`
+ `count` (insert) or a `range` A1 string (other dim-* ops); v3
move_dimension still receives 0-based inclusive ints (CLI parses the
A1 strings into them); resize_range still receives a two-sided A1
range (single-element form is expanded to "N:N" before send).
This is a flag-surface break (--start / --end / --dimension flags
removed from these 9 shortcuts); --dimension stays only on +dim-freeze
since it has no range to derive from.
Code: A1 parser added (parseA1Range / parseA1Position /
letterToColumnIndex reused from write_cells); dimRange / dimRangeFull /
dimPosition deleted; dim-move switches to source-range + target parsing;
resize gains a same-dimension guard so +rows-resize rejects "A:C" with
a clear "+rows-resize expects row numbers" message.
Tests: TestSheetStructureShortcuts_DryRun / TestDimMove_DryRun /
TestDimMove_Column / TestDimMove_MismatchedDimension /
TestDimRange_Validation / TestParseA1Range / TestResize_TypeAndSizeGuards
/ TestRangeOperationsShortcuts_DryRun all rewritten against the new
schema. Batch contract trio (BodyMatchesStandalone /
ErrorEquivalence / RejectsBadSubOpInput) and
TestBatchOp_DispatchCoversReportedBugs likewise. Full
`go test ./shortcuts/sheets/` passes.
This commit is contained in:
@@ -72,20 +72,20 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) {
|
||||
{
|
||||
shortcut: "+dim-insert",
|
||||
sc: DimInsert,
|
||||
args: []string{"--sheet-id", "sh1", "--dimension", "row", "--start", "10", "--end", "12", "--inherit-style", "before"},
|
||||
subInput: `{"sheet-id":"sh1","dimension":"row","start":10,"end":12,"inherit-style":"before"}`,
|
||||
args: []string{"--sheet-id", "sh1", "--position", "11", "--count", "2", "--inherit-style", "before"},
|
||||
subInput: `{"sheet-id":"sh1","position":"11","count":2,"inherit-style":"before"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+dim-delete",
|
||||
sc: DimDelete,
|
||||
args: []string{"--sheet-id", "sh1", "--dimension", "column", "--start", "2", "--end", "4"},
|
||||
subInput: `{"sheet-id":"sh1","dimension":"column","start":2,"end":4}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "C:D"},
|
||||
subInput: `{"sheet-id":"sh1","range":"C:D"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+dim-hide",
|
||||
sc: DimHide,
|
||||
args: []string{"--sheet-id", "sh1", "--dimension", "row", "--start", "1", "--end", "3"},
|
||||
subInput: `{"sheet-id":"sh1","dimension":"row","start":1,"end":3}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "2:3"},
|
||||
subInput: `{"sheet-id":"sh1","range":"2:3"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+dim-freeze",
|
||||
@@ -96,20 +96,20 @@ func TestBatchOp_BodyMatchesStandalone(t *testing.T) {
|
||||
{
|
||||
shortcut: "+dim-group",
|
||||
sc: DimGroup,
|
||||
args: []string{"--sheet-id", "sh1", "--dimension", "row", "--start", "1", "--end", "5", "--group-state", "fold"},
|
||||
subInput: `{"sheet-id":"sh1","dimension":"row","start":1,"end":5,"group-state":"fold"}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "2:5", "--group-state", "fold"},
|
||||
subInput: `{"sheet-id":"sh1","range":"2:5","group-state":"fold"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+rows-resize",
|
||||
sc: RowsResize,
|
||||
args: []string{"--sheet-id", "sh1", "--start", "0", "--end", "0", "--type", "pixel", "--size", "30"},
|
||||
subInput: `{"sheet-id":"sh1","start":0,"end":0,"type":"pixel","size":30}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "1", "--type", "pixel", "--size", "30"},
|
||||
subInput: `{"sheet-id":"sh1","range":"1","type":"pixel","size":30}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+cols-resize",
|
||||
sc: ColsResize,
|
||||
args: []string{"--sheet-id", "sh1", "--start", "1", "--end", "3", "--type", "standard"},
|
||||
subInput: `{"sheet-id":"sh1","start":1,"end":3,"type":"standard"}`,
|
||||
args: []string{"--sheet-id", "sh1", "--range", "B:D", "--type", "standard"},
|
||||
subInput: `{"sheet-id":"sh1","range":"B:D","type":"standard"}`,
|
||||
},
|
||||
{
|
||||
shortcut: "+range-move",
|
||||
@@ -377,25 +377,25 @@ func TestBatchOp_ErrorEquivalence(t *testing.T) {
|
||||
{
|
||||
name: "+dim-insert missing sheet selector",
|
||||
shortcut: DimInsert,
|
||||
args: []string{"--dimension", "row", "--start", "0", "--end", "1"},
|
||||
args: []string{"--position", "1", "--count", "1"},
|
||||
subShortcut: "+dim-insert",
|
||||
subInput: `{"dimension":"row","start":0,"end":1}`,
|
||||
subInput: `{"position":"1","count":1}`,
|
||||
wantContains: "specify at least one of --sheet-id or --sheet-name",
|
||||
},
|
||||
{
|
||||
name: "+dim-insert --end <= --start",
|
||||
name: "+dim-insert count <= 0",
|
||||
shortcut: DimInsert,
|
||||
args: []string{"--sheet-id", "sh1", "--dimension", "row", "--start", "5", "--end", "3"},
|
||||
args: []string{"--sheet-id", "sh1", "--position", "5", "--count", "0"},
|
||||
subShortcut: "+dim-insert",
|
||||
subInput: `{"sheet-id":"sh1","dimension":"row","start":5,"end":3}`,
|
||||
wantContains: "must be greater than --start",
|
||||
subInput: `{"sheet-id":"sh1","position":"5","count":0}`,
|
||||
wantContains: "--count must be > 0",
|
||||
},
|
||||
{
|
||||
name: "+rows-resize --type pixel without --size",
|
||||
shortcut: RowsResize,
|
||||
args: []string{"--sheet-id", "sh1", "--start", "0", "--end", "1", "--type", "pixel"},
|
||||
args: []string{"--sheet-id", "sh1", "--range", "1:2", "--type", "pixel"},
|
||||
subShortcut: "+rows-resize",
|
||||
subInput: `{"sheet-id":"sh1","start":0,"end":1,"type":"pixel"}`,
|
||||
subInput: `{"sheet-id":"sh1","range":"1:2","type":"pixel"}`,
|
||||
wantContains: "--type pixel requires --size",
|
||||
},
|
||||
{
|
||||
@@ -485,15 +485,15 @@ func TestBatchOp_RejectsBadSubOpInput(t *testing.T) {
|
||||
"--range is required",
|
||||
},
|
||||
{
|
||||
"+dim-insert missing --dimension",
|
||||
"+dim-insert missing --position",
|
||||
"+dim-insert",
|
||||
`{"sheet-id":"sh1","start":0,"end":1}`,
|
||||
"--dimension is required",
|
||||
`{"sheet-id":"sh1","count":1}`,
|
||||
"--position is required",
|
||||
},
|
||||
{
|
||||
"+rows-resize missing --type",
|
||||
"+rows-resize",
|
||||
`{"sheet-id":"sh1","start":0,"end":0}`,
|
||||
`{"sheet-id":"sh1","range":"1:1"}`,
|
||||
"--type is required",
|
||||
},
|
||||
{
|
||||
@@ -622,10 +622,12 @@ func TestBatchOp_DispatchCoversReportedBugs(t *testing.T) {
|
||||
t.Errorf("+range-copy operation = %v, want copy", copyIn["operation"])
|
||||
}
|
||||
|
||||
// +rows-resize → resize_range with range + resize_height (not raw start/end).
|
||||
// +rows-resize → resize_range with range + resize_height. The CLI's single
|
||||
// "23" input must be expanded to "23:23" because resize_range rejects
|
||||
// bare single-element ranges.
|
||||
body = parseDryRunBody(t, BatchUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--operations", `[{"shortcut":"+rows-resize","input":{"sheet-id":"sh1","start":22,"end":22,"type":"pixel","size":40}}]`,
|
||||
"--operations", `[{"shortcut":"+rows-resize","input":{"sheet-id":"sh1","range":"23","type":"pixel","size":40}}]`,
|
||||
"--yes",
|
||||
})
|
||||
ops = decodeToolInput(t, body, "batch_update")["operations"].([]interface{})
|
||||
|
||||
@@ -607,31 +607,6 @@
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Insert start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Insert end position (exclusive)"
|
||||
},
|
||||
{
|
||||
"name": "inherit-style",
|
||||
"kind": "own",
|
||||
@@ -645,6 +620,20 @@
|
||||
"none"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "position",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Insert position (1-based row number like `3` or column letter like `C`); new rows/columns are inserted *before* this position"
|
||||
},
|
||||
{
|
||||
"name": "count",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Number of rows/columns to insert (must be > 0)"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -686,29 +675,11 @@
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End position (exclusive)"
|
||||
"desc": "Row/column closed range to delete; rows use 1-based numbers like `3:7` or `5` (single row), columns use letters like `C:F` or `C`"
|
||||
},
|
||||
{
|
||||
"name": "yes",
|
||||
@@ -758,29 +729,11 @@
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End position (exclusive)"
|
||||
"desc": "Row/column closed range to hide; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
@@ -823,29 +776,11 @@
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End position (exclusive)"
|
||||
"desc": "Row/column closed range to unhide; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
@@ -945,31 +880,6 @@
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End position (exclusive)"
|
||||
},
|
||||
{
|
||||
"name": "depth",
|
||||
"kind": "own",
|
||||
@@ -990,6 +900,13 @@
|
||||
"fold"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Row/column closed range to group; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -1030,31 +947,6 @@
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End position (exclusive)"
|
||||
},
|
||||
{
|
||||
"name": "depth",
|
||||
"kind": "own",
|
||||
@@ -1063,6 +955,13 @@
|
||||
"desc": "Group nesting level to ungroup; default 1 (outermost)",
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Row/column closed range to ungroup; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -1104,36 +1003,18 @@
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "dimension",
|
||||
"name": "source-range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Dimension (row or column)",
|
||||
"enum": [
|
||||
"row",
|
||||
"column"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Source range start position (0-based)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Source range end position (inclusive)"
|
||||
"desc": "Source row/column closed range to move; rows use 1-based numbers like `3:7`, columns use letters like `C:F`"
|
||||
},
|
||||
{
|
||||
"name": "target",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Destination position (move target inserts before this index; 0-based)"
|
||||
"desc": "Destination position (the moved rows/columns are placed *before* this position); rows use 1-based row number like `12`, columns use column letter like `H`. Must match the dimension of --source-range"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
@@ -2201,20 +2082,6 @@
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start row (0-based, inclusive)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End row (0-based, inclusive)"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"kind": "own",
|
||||
@@ -2235,6 +2102,13 @@
|
||||
"desc": "Row height in pixels (e.g. 30 / 40 / 60); required when `--type pixel`, ignored otherwise",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Row closed range to resize; 1-based row numbers like `2:10` or `5` (single row)"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -2275,20 +2149,6 @@
|
||||
"required": "xor",
|
||||
"desc": "Sheet name (XOR with `--sheet-id`)"
|
||||
},
|
||||
{
|
||||
"name": "start",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "Start column (0-based, inclusive)"
|
||||
},
|
||||
{
|
||||
"name": "end",
|
||||
"kind": "own",
|
||||
"type": "int",
|
||||
"required": "required",
|
||||
"desc": "End column (0-based, inclusive)"
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"kind": "own",
|
||||
@@ -2308,6 +2168,13 @@
|
||||
"desc": "Column width in pixels (e.g. 80 / 120 / 200); required when `--type pixel`, ignored otherwise",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "range",
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "required",
|
||||
"desc": "Column closed range to resize; column letters like `A:E` or `C` (single column)"
|
||||
},
|
||||
{
|
||||
"name": "dry-run",
|
||||
"kind": "system",
|
||||
@@ -3269,7 +3136,7 @@
|
||||
"kind": "own",
|
||||
"type": "string",
|
||||
"required": "optional",
|
||||
"desc": "Top-left cell within the target sub-sheet (A1 notation, e.g. `A1`); pairs with `--target-sheet-id`, maps to the top-level `target_position`, default `A1` (not sent when the value is A1). It and `--range` both express placement but map to different wire fields — avoid passing conflicting values for both.",
|
||||
"desc": "Top-left cell within the target sub-sheet (A1 notation, e.g. `A1`); maps to the top-level `target_position`, default `A1` (not sent when the value is A1). It and `--range` both express placement but map to different wire fields — avoid passing conflicting values for both.",
|
||||
"default": "A1"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -317,8 +317,9 @@ func TestExecute_WorkbookCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestExecute_DimMove covers the native v3 move_dimension call. CLI's
|
||||
// 0-based inclusive --start/--end pass straight through to v3's
|
||||
// source.{start_index,end_index} (also 0-based inclusive).
|
||||
// --source-range "1:3" (1-based inclusive) is parsed into v3's
|
||||
// source.{start_index=0,end_index=2} (0-based inclusive); --target "11" is
|
||||
// parsed into destination_index=10.
|
||||
func TestExecute_DimMove(t *testing.T) {
|
||||
t.Parallel()
|
||||
move := &httpmock.Stub{
|
||||
@@ -332,7 +333,7 @@ func TestExecute_DimMove(t *testing.T) {
|
||||
}
|
||||
_, err := runShortcutWithStubs(t, DimMove, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--dimension", "row", "--start", "0", "--end", "2", "--target", "10",
|
||||
"--source-range", "1:3", "--target", "11",
|
||||
}, move)
|
||||
if err != nil {
|
||||
t.Fatalf("execute failed: %v", err)
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestBatchUpdate_TranslatesShortcutToToolName(t *testing.T) {
|
||||
"--url", testURL,
|
||||
"--operations", `[
|
||||
{"shortcut":"+cells-set","input":{"sheet_id":"sh1","range":"A1","cells":[[{"value":42}]]}},
|
||||
{"shortcut":"+dim-insert","input":{"sheet_id":"sh1","dimension":"row","start":0,"end":3}}
|
||||
{"shortcut":"+dim-insert","input":{"sheet_id":"sh1","position":"1","count":3}}
|
||||
]`,
|
||||
"--continue-on-error",
|
||||
"--yes",
|
||||
@@ -353,7 +353,7 @@ func TestBatchUpdate_TranslatorRejects(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "user filled operation manually",
|
||||
opsJSON: `[{"shortcut":"+dim-insert","input":{"operation":"delete","range":"1:1"}}]`,
|
||||
opsJSON: `[{"shortcut":"+dim-insert","input":{"operation":"delete","position":"1","count":1}}]`,
|
||||
wantMatch: "do not pass input.operation",
|
||||
},
|
||||
{
|
||||
@@ -430,7 +430,7 @@ func TestBatchUpdate_ResizeNoOperationField(t *testing.T) {
|
||||
t.Parallel()
|
||||
body := parseDryRunBody(t, BatchUpdate, []string{
|
||||
"--url", testURL,
|
||||
"--operations", `[{"shortcut":"+rows-resize","input":{"sheet_id":"sh1","start":0,"end":2,"type":"pixel","size":30}}]`,
|
||||
"--operations", `[{"shortcut":"+rows-resize","input":{"sheet_id":"sh1","range":"1:3","type":"pixel","size":30}}]`,
|
||||
"--yes",
|
||||
})
|
||||
input := decodeToolInput(t, body, "batch_update")
|
||||
|
||||
@@ -180,25 +180,24 @@ func mergeInput(runtime flagView, token, sheetID, sheetName, op string, withMerg
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// resize_range now exposes two CLI shortcuts:
|
||||
// resize_range exposes two CLI shortcuts:
|
||||
//
|
||||
// +rows-resize / +cols-resize — set row heights / column widths. The new
|
||||
// --type enum (pixel / standard / [auto]) replaces the old --size/--reset
|
||||
// pair; --type pixel still takes a --size pixel value, --type standard
|
||||
// restores the sheet default, --type auto auto-fits row heights (rows only).
|
||||
// +rows-resize / +cols-resize — set row heights / column widths. --type
|
||||
// enum (pixel / standard / [auto]) controls how: --type pixel needs --size,
|
||||
// --type standard restores the sheet default, --type auto auto-fits row
|
||||
// heights (rows only). --range is an A1 closed range ("2:10" / "5" rows or
|
||||
// "A:E" / "C" columns); single-element form is expanded to "N:N" before
|
||||
// send because resize_range rejects bare single-element ranges.
|
||||
//
|
||||
// Wire shape: resize_height / resize_width carries { type, value? }, e.g.
|
||||
// { "type": "pixel", "value": 30 } or { "type": "standard" }.
|
||||
//
|
||||
// Both shortcuts share the underlying resize_range tool; --end is inclusive
|
||||
// in the new CLI surface (was exclusive in the legacy +dim-resize).
|
||||
|
||||
// RowsResize wraps resize_range for row heights. --type auto enables
|
||||
// auto-fit (rows only); --type pixel requires --size.
|
||||
var RowsResize = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+rows-resize",
|
||||
Description: "Resize rows by pixel / standard / auto (--type pixel needs --size; --start/--end are 0-based inclusive).",
|
||||
Description: "Resize rows by pixel / standard / auto (--type pixel needs --size; --range is 1-based A1 like \"2:10\" or \"5\").",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
@@ -238,7 +237,7 @@ var RowsResize = common.Shortcut{
|
||||
var ColsResize = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+cols-resize",
|
||||
Description: "Resize columns by pixel / standard (--type pixel needs --size; --start/--end are 0-based inclusive; no auto for cols).",
|
||||
Description: "Resize columns by pixel / standard (--type pixel needs --size; --range is column letters like \"A:E\" or \"C\"; no auto for cols).",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
@@ -275,7 +274,7 @@ var ColsResize = common.Shortcut{
|
||||
|
||||
// validateViaResize wires the standalone Validate to resizeInput so both
|
||||
// paths (standalone + batch sub-op) emit the same error for missing --type,
|
||||
// out-of-range --start/--end, or --type auto on columns.
|
||||
// malformed --range, or --type auto on columns.
|
||||
func validateViaResize(dimension string) func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
token, err := resolveSpreadsheetToken(runtime)
|
||||
@@ -297,20 +296,42 @@ func autoSuffix(dimension string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// commandForDimension returns the shortcut command name a given dimension
|
||||
// belongs to; used in error messages so users see "+rows-resize" / "+cols-resize"
|
||||
// instead of the internal "row" / "column" tag.
|
||||
func commandForDimension(dimension string) string {
|
||||
if dimension == "row" {
|
||||
return "+rows-resize"
|
||||
}
|
||||
return "+cols-resize"
|
||||
}
|
||||
|
||||
// resizeInput builds the resize_range tool input. dimension is "row" /
|
||||
// "column"; --end is inclusive on the CLI surface, dimRangeFull wants
|
||||
// exclusive end, so it is bumped by one here. dimRangeFull (not dimRange) is
|
||||
// used so a single row/column still emits "N:N" — resize_range rejects a bare
|
||||
// "N".
|
||||
// "column" (selected by the calling shortcut); --range must match that
|
||||
// dimension (row → digits like "2:10" / "5"; column → letters like "A:E" /
|
||||
// "C"). Single-element form is expanded to "N:N" because resize_range
|
||||
// rejects bare single-element ranges.
|
||||
func resizeInput(runtime flagView, token, sheetID, sheetName, dimension string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !runtime.Changed("start") || !runtime.Changed("end") {
|
||||
return nil, common.FlagErrorf("--start and --end are required")
|
||||
if !runtime.Changed("range") {
|
||||
return nil, common.FlagErrorf("--range is required")
|
||||
}
|
||||
if runtime.Int("start") < 0 || runtime.Int("end") < runtime.Int("start") {
|
||||
return nil, common.FlagErrorf("invalid range: --start (%d) must be >= 0 and --end (%d) must be >= --start", runtime.Int("start"), runtime.Int("end"))
|
||||
rangeStr := strings.TrimSpace(runtime.Str("range"))
|
||||
parsedDim, _, _, err := parseA1Range(rangeStr)
|
||||
if err != nil {
|
||||
return nil, common.FlagErrorf("invalid --range %q: %v", rangeStr, err)
|
||||
}
|
||||
if parsedDim != dimension {
|
||||
want := "row numbers (e.g. \"2:10\")"
|
||||
if dimension == "column" {
|
||||
want = "column letters (e.g. \"A:E\")"
|
||||
}
|
||||
return nil, common.FlagErrorf("--range %q is a %s range; %s expects %s", rangeStr, parsedDim, commandForDimension(dimension), want)
|
||||
}
|
||||
if !strings.Contains(rangeStr, ":") {
|
||||
rangeStr = rangeStr + ":" + rangeStr
|
||||
}
|
||||
typ := strings.TrimSpace(runtime.Str("type"))
|
||||
if typ == "" {
|
||||
@@ -326,7 +347,6 @@ func resizeInput(runtime flagView, token, sheetID, sheetName, dimension string)
|
||||
if typ != "pixel" && hasSize {
|
||||
return nil, common.FlagErrorf("--size is only valid with --type pixel")
|
||||
}
|
||||
rangeStr := dimRangeFull(dimension, runtime.Int("start"), runtime.Int("end")+1)
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"range": rangeStr,
|
||||
|
||||
@@ -67,9 +67,9 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+rows-resize --type pixel --size 200",
|
||||
name: "+rows-resize --range 1:5 pixel 200",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "4", "--type", "pixel", "--size", "200"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--type", "pixel", "--size", "200"},
|
||||
toolName: "resize_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -82,9 +82,9 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+rows-resize single row (start==end) keeps N:N range",
|
||||
name: "+rows-resize single row \"1\" expands to \"1:1\"",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "0", "--type", "auto"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1", "--type", "auto"},
|
||||
toolName: "resize_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"range": "1:1",
|
||||
@@ -92,9 +92,9 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+cols-resize --type standard (reset to default)",
|
||||
name: "+cols-resize --range B:D standard",
|
||||
sc: ColsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "1", "--end", "3", "--type", "standard"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "B:D", "--type", "standard"},
|
||||
toolName: "resize_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -106,9 +106,9 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+cols-resize --type pixel --size 120",
|
||||
name: "+cols-resize --range A:C pixel 120",
|
||||
sc: ColsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "2", "--type", "pixel", "--size", "120"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A:C", "--type", "pixel", "--size", "120"},
|
||||
toolName: "resize_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"range": "A:C",
|
||||
@@ -118,6 +118,16 @@ func TestRangeOperationsShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+cols-resize single column \"C\" expands to \"C:C\"",
|
||||
sc: ColsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "C", "--type", "standard"},
|
||||
toolName: "resize_range",
|
||||
wantInput: map[string]interface{}{
|
||||
"range": "C:C",
|
||||
"resize_width": map[string]interface{}{"type": "standard"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+range-move cross-sheet",
|
||||
sc: RangeMove,
|
||||
@@ -257,26 +267,38 @@ func TestResize_TypeAndSizeGuards(t *testing.T) {
|
||||
{
|
||||
name: "+rows-resize --type pixel without --size",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "3", "--type", "pixel"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--type", "pixel"},
|
||||
want: "--type pixel requires --size",
|
||||
},
|
||||
{
|
||||
name: "+rows-resize --type standard with --size",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "3", "--type", "standard", "--size", "30"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--type", "standard", "--size", "30"},
|
||||
want: "--size is only valid with --type pixel",
|
||||
},
|
||||
{
|
||||
name: "+cols-resize rejects --type auto",
|
||||
sc: ColsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "0", "--end", "2", "--type", "auto"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A:C", "--type", "auto"},
|
||||
want: "auto", // cobra Enum gate kicks first with "valid values are: pixel, standard"
|
||||
},
|
||||
{
|
||||
name: "--end < --start",
|
||||
name: "+rows-resize given column range",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--start", "5", "--end", "3", "--type", "standard"},
|
||||
want: "must be >= --start",
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A:C", "--type", "standard"},
|
||||
want: "+rows-resize expects row numbers",
|
||||
},
|
||||
{
|
||||
name: "+cols-resize given row range",
|
||||
sc: ColsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--type", "standard"},
|
||||
want: "+cols-resize expects column letters",
|
||||
},
|
||||
{
|
||||
name: "+rows-resize end < start",
|
||||
sc: RowsResize,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "5:3", "--type", "standard"},
|
||||
want: "end position is before start",
|
||||
},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
|
||||
@@ -6,6 +6,7 @@ package sheets
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/validate"
|
||||
@@ -15,9 +16,12 @@ import (
|
||||
// ─── lark_sheet_sheet_structure ───────────────────────────────────────
|
||||
//
|
||||
// Wraps get_sheet_structure (read) and modify_sheet_structure (write,
|
||||
// operation-enum dispatch). CLI's --start/--end are 0-based with exclusive
|
||||
// end; the tool wants 1-based inclusive row numbers ("3:7") or column
|
||||
// letters ("C:F"). The conversion lives in dimRange / dimPosition below.
|
||||
// operation-enum dispatch). All region/position arguments use A1-style
|
||||
// strings (1-based row numbers like "3:7" / "5", or column letters like
|
||||
// "C:F" / "C"); dim-* / resize never expose 0-based int indices on the CLI
|
||||
// surface, so there is no inclusive/exclusive ambiguity across commands.
|
||||
// parseA1Range / parseA1Position handle parsing into the 0-based ints that
|
||||
// dim-move's native v3 endpoint expects.
|
||||
//
|
||||
// +rows-resize / +cols-resize live in lark_sheet_range_operations (different
|
||||
// tool); they are only grouped under "工作表" for discoverability.
|
||||
@@ -118,7 +122,7 @@ func infoTypeFromInclude(include []string) string {
|
||||
var DimInsert = common.Shortcut{
|
||||
Service: "sheets",
|
||||
Command: "+dim-insert",
|
||||
Description: "Insert blank rows or columns at a given range.",
|
||||
Description: "Insert blank rows or columns at a given position.",
|
||||
Risk: "write",
|
||||
Scopes: []string{"sheets:spreadsheet:write_only"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
@@ -153,21 +157,31 @@ var DimInsert = common.Shortcut{
|
||||
},
|
||||
}
|
||||
|
||||
// dimInsertInput passes --position (1-based row number "3" or column letter
|
||||
// "C") straight to the tool's `position` field; --count maps to `count`.
|
||||
func dimInsertInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := requireDimRange(runtime); err != nil {
|
||||
return nil, err
|
||||
if !runtime.Changed("position") {
|
||||
return nil, common.FlagErrorf("--position is required")
|
||||
}
|
||||
if !runtime.Changed("count") {
|
||||
return nil, common.FlagErrorf("--count is required")
|
||||
}
|
||||
position := strings.TrimSpace(runtime.Str("position"))
|
||||
if _, _, err := parseA1Position(position); err != nil {
|
||||
return nil, common.FlagErrorf("invalid --position %q: %v", position, err)
|
||||
}
|
||||
count := runtime.Int("count")
|
||||
if count <= 0 {
|
||||
return nil, common.FlagErrorf("--count must be > 0 (got %d)", count)
|
||||
}
|
||||
dim := runtime.Str("dimension")
|
||||
start := runtime.Int("start")
|
||||
end := runtime.Int("end")
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": "insert",
|
||||
"position": dimPosition(dim, start),
|
||||
"count": end - start,
|
||||
"position": position,
|
||||
"count": count,
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
switch runtime.Str("inherit-style") {
|
||||
@@ -338,40 +352,25 @@ func dimFreezeInput(runtime flagView, token, sheetID, sheetName string) (map[str
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// requireDimRange validates the dimension/start/end triple shared by
|
||||
// insert/delete/hide/unhide/group/ungroup. Pure flag-level checks — the sheet
|
||||
// selector and token live in their own helpers.
|
||||
func requireDimRange(runtime flagView) error {
|
||||
if !runtime.Changed("dimension") {
|
||||
return common.FlagErrorf("--dimension is required")
|
||||
}
|
||||
if !runtime.Changed("start") || !runtime.Changed("end") {
|
||||
return common.FlagErrorf("--start and --end are required")
|
||||
}
|
||||
start := runtime.Int("start")
|
||||
end := runtime.Int("end")
|
||||
if start < 0 {
|
||||
return common.FlagErrorf("--start must be >= 0")
|
||||
}
|
||||
if end <= start {
|
||||
return common.FlagErrorf("--end (%d) must be greater than --start (%d)", end, start)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dimRangeOpInput builds the tool input for delete/hide/unhide which all
|
||||
// take a `range` field. dimRange handles 0-based exclusive → 1-based inclusive.
|
||||
// dimRangeOpInput builds the tool input for delete/hide/unhide/group/ungroup
|
||||
// which all take a `range` string field. --range is a 1-based A1 closed range
|
||||
// ("3:7" / "5" for rows, "C:F" / "C" for columns) and passes straight through
|
||||
// after format validation.
|
||||
func dimRangeOpInput(runtime flagView, token, sheetID, sheetName, op string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := requireDimRange(runtime); err != nil {
|
||||
return nil, err
|
||||
if !runtime.Changed("range") {
|
||||
return nil, common.FlagErrorf("--range is required")
|
||||
}
|
||||
rangeStr := strings.TrimSpace(runtime.Str("range"))
|
||||
if _, _, _, err := parseA1Range(rangeStr); err != nil {
|
||||
return nil, common.FlagErrorf("invalid --range %q: %v", rangeStr, err)
|
||||
}
|
||||
input := map[string]interface{}{
|
||||
"excel_id": token,
|
||||
"operation": op,
|
||||
"range": dimRange(runtime.Str("dimension"), runtime.Int("start"), runtime.Int("end")),
|
||||
"range": rangeStr,
|
||||
}
|
||||
sheetSelectorForToolInput(input, sheetID, sheetName)
|
||||
return input, nil
|
||||
@@ -475,48 +474,75 @@ func dimGroupInput(runtime flagView, token, sheetID, sheetName, op string) (map[
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// ─── dimension formatting helpers ─────────────────────────────────────
|
||||
// ─── A1 parsing helpers ───────────────────────────────────────────────
|
||||
|
||||
// dimRange formats a CLI (0-based exclusive end) range as the tool's
|
||||
// 1-based inclusive A1-style range string. row → "3:7", column → "C:F".
|
||||
// A single-element range collapses to "3" / "C".
|
||||
func dimRange(dimension string, start, end int) string {
|
||||
if dimension == "column" {
|
||||
startLetter := columnIndexToLetter(start)
|
||||
endLetter := columnIndexToLetter(end - 1)
|
||||
if start == end-1 {
|
||||
return startLetter
|
||||
// parseA1Range parses an A1 closed range ("3:7" / "5" / "C:F" / "C") into
|
||||
// the inferred dimension ("row" or "column") and 0-based inclusive indices.
|
||||
// Single-element form yields startIdx == endIdx. Mixing digits and letters
|
||||
// across the two sides ("3:C") is rejected.
|
||||
func parseA1Range(s string) (dimension string, startIdx, endIdx int, err error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "", 0, 0, fmt.Errorf("range is empty")
|
||||
}
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) > 2 {
|
||||
return "", 0, 0, fmt.Errorf("expected \"start:end\" or single element")
|
||||
}
|
||||
dim1, idx1, err := parseA1Position(parts[0])
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return dim1, idx1, idx1, nil
|
||||
}
|
||||
dim2, idx2, err := parseA1Position(parts[1])
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
if dim1 != dim2 {
|
||||
return "", 0, 0, fmt.Errorf("cannot mix row (digits) and column (letters) in one range")
|
||||
}
|
||||
if idx2 < idx1 {
|
||||
return "", 0, 0, fmt.Errorf("end position is before start")
|
||||
}
|
||||
return dim1, idx1, idx2, nil
|
||||
}
|
||||
|
||||
// parseA1Position parses a single A1 position element: pure digits → row
|
||||
// (1-based number, returned as 0-based idx); pure letters → column (letters
|
||||
// case-insensitive, "A" → 0, "AA" → 26).
|
||||
func parseA1Position(s string) (dimension string, idx int, err error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return "", 0, fmt.Errorf("position is empty")
|
||||
}
|
||||
isDigits := true
|
||||
isLetters := true
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
isDigits = false
|
||||
}
|
||||
if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z')) {
|
||||
isLetters = false
|
||||
}
|
||||
return startLetter + ":" + endLetter
|
||||
}
|
||||
if start == end-1 {
|
||||
return fmt.Sprintf("%d", start+1)
|
||||
if isDigits {
|
||||
n, _ := strconv.Atoi(s)
|
||||
if n <= 0 {
|
||||
return "", 0, fmt.Errorf("row number must be >= 1 (got %q)", s)
|
||||
}
|
||||
return "row", n - 1, nil
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", start+1, end)
|
||||
}
|
||||
|
||||
// dimRangeFull is like dimRange but never collapses a single-element range to
|
||||
// a bare index — it always emits the two-sided "N:N" / "C:C" form. resize_range
|
||||
// rejects a bare index ("23" → Invalid range), so single-row/column resizes
|
||||
// must keep both sides.
|
||||
func dimRangeFull(dimension string, start, end int) string {
|
||||
if dimension == "column" {
|
||||
return columnIndexToLetter(start) + ":" + columnIndexToLetter(end-1)
|
||||
if isLetters {
|
||||
return "column", letterToColumnIndex(s), nil
|
||||
}
|
||||
return fmt.Sprintf("%d:%d", start+1, end)
|
||||
}
|
||||
|
||||
// dimPosition formats a single CLI 0-based index as the tool's 1-based row
|
||||
// number string or column letter.
|
||||
func dimPosition(dimension string, idx int) string {
|
||||
if dimension == "column" {
|
||||
return columnIndexToLetter(idx)
|
||||
}
|
||||
return fmt.Sprintf("%d", idx+1)
|
||||
return "", 0, fmt.Errorf("expected pure digits (row number) or letters (column letter), got %q", s)
|
||||
}
|
||||
|
||||
// columnIndexToLetter converts a 0-based column index to the spreadsheet
|
||||
// letter notation (0 → "A", 25 → "Z", 26 → "AA", 701 → "ZZ", 702 → "AAA").
|
||||
// Used by +workbook helpers that need to format absolute column references.
|
||||
func columnIndexToLetter(idx int) string {
|
||||
if idx < 0 {
|
||||
return ""
|
||||
@@ -535,13 +561,9 @@ func columnIndexToLetter(idx int) string {
|
||||
//
|
||||
// Moves a contiguous block of rows or columns to a new index in the same
|
||||
// sheet via the native v3 move_dimension endpoint (not the One-OpenAPI
|
||||
// dispatcher). CLI's --start / --end are 0-based inclusive; v3
|
||||
// move_dimension's source.{start_index,end_index} are likewise 0-based
|
||||
// inclusive, so they pass straight through. The earlier build POSTed a
|
||||
// {source,destinationIndex} body to the v2 dimension_range endpoint, which
|
||||
// is the add/update/delete surface and expects a `dimension` object —
|
||||
// hence the server rejected it with "[9499] Missing required parameter:
|
||||
// Dimension".
|
||||
// dispatcher). CLI accepts --source-range (A1 closed range like "3:7" or
|
||||
// "C:F") + --target (A1 single position like "12" or "H"); both are parsed
|
||||
// into the 0-based int indices that v3 move_dimension expects.
|
||||
|
||||
var DimMove = common.Shortcut{
|
||||
Service: "sheets",
|
||||
@@ -559,16 +581,8 @@ var DimMove = common.Shortcut{
|
||||
if _, _, err := resolveSheetSelector(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
if !runtime.Changed("dimension") || !runtime.Changed("start") || !runtime.Changed("end") || !runtime.Changed("target") {
|
||||
return common.FlagErrorf("--dimension / --start / --end / --target are all required")
|
||||
}
|
||||
if runtime.Int("start") < 0 || runtime.Int("end") < runtime.Int("start") {
|
||||
return common.FlagErrorf("--end (%d) must be >= --start (%d) (both 0-indexed, inclusive)", runtime.Int("end"), runtime.Int("start"))
|
||||
}
|
||||
if runtime.Int("target") < 0 {
|
||||
return common.FlagErrorf("--target must be >= 0")
|
||||
}
|
||||
return nil
|
||||
_, err := buildDimMovePlan(runtime)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
@@ -606,6 +620,36 @@ var DimMove = common.Shortcut{
|
||||
},
|
||||
}
|
||||
|
||||
// dimMovePlan is the parsed form of --source-range / --target.
|
||||
type dimMovePlan struct {
|
||||
dimension string // "row" / "column"
|
||||
startIdx int // 0-based inclusive
|
||||
endIdx int // 0-based inclusive
|
||||
targetIdx int // 0-based; destination position (move inserts before this)
|
||||
}
|
||||
|
||||
// buildDimMovePlan parses --source-range + --target and enforces that the
|
||||
// target dimension matches the source. Used by both Validate and Execute.
|
||||
func buildDimMovePlan(runtime flagView) (*dimMovePlan, error) {
|
||||
if !runtime.Changed("source-range") || !runtime.Changed("target") {
|
||||
return nil, common.FlagErrorf("--source-range and --target are required")
|
||||
}
|
||||
src := strings.TrimSpace(runtime.Str("source-range"))
|
||||
dim, startIdx, endIdx, err := parseA1Range(src)
|
||||
if err != nil {
|
||||
return nil, common.FlagErrorf("invalid --source-range %q: %v", src, err)
|
||||
}
|
||||
tgt := strings.TrimSpace(runtime.Str("target"))
|
||||
tgtDim, tgtIdx, err := parseA1Position(tgt)
|
||||
if err != nil {
|
||||
return nil, common.FlagErrorf("invalid --target %q: %v", tgt, err)
|
||||
}
|
||||
if tgtDim != dim {
|
||||
return nil, common.FlagErrorf("--target %q dimension (%s) must match --source-range %q dimension (%s)", tgt, tgtDim, src, dim)
|
||||
}
|
||||
return &dimMovePlan{dimension: dim, startIdx: startIdx, endIdx: endIdx, targetIdx: tgtIdx}, nil
|
||||
}
|
||||
|
||||
// dimMovePath builds the native v3 move_dimension endpoint. sheet_id lives in
|
||||
// the path (unlike the v2 dimension_range body that the earlier build used).
|
||||
func dimMovePath(token, sheetID string) string {
|
||||
@@ -614,16 +658,22 @@ func dimMovePath(token, sheetID string) string {
|
||||
}
|
||||
|
||||
func dimMoveBody(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
plan, err := buildDimMovePlan(runtime)
|
||||
if err != nil {
|
||||
// Validate has already rejected this case; emit an empty body
|
||||
// rather than panic on the dry-run path.
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
dim := "ROWS"
|
||||
if runtime.Str("dimension") == "column" {
|
||||
if plan.dimension == "column" {
|
||||
dim = "COLUMNS"
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"source": map[string]interface{}{
|
||||
"major_dimension": dim,
|
||||
"start_index": runtime.Int("start"),
|
||||
"end_index": runtime.Int("end"), // both CLI --end and v3 end_index are 0-based inclusive
|
||||
"start_index": plan.startIdx,
|
||||
"end_index": plan.endIdx,
|
||||
},
|
||||
"destination_index": runtime.Int("target"),
|
||||
"destination_index": plan.targetIdx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
)
|
||||
|
||||
// TestSheetStructureShortcuts_DryRun covers all 8 shortcuts in
|
||||
// lark_sheet_sheet_structure (sheet-info + 7 dim-*) and verifies the
|
||||
// CLI 0-based exclusive-end → tool 1-based inclusive A1 conversion.
|
||||
// lark_sheet_sheet_structure (sheet-info + 7 dim-*) and verifies that the
|
||||
// CLI's A1-style --range / --position / --count flags map straight through
|
||||
// to the tool's `range` / `position` / `count` fields (or are normalised
|
||||
// per shortcut's wire shape).
|
||||
func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -46,9 +48,9 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-insert row 5..8 inherit-before → position 6 + count 3 + side",
|
||||
name: "+dim-insert row position=6 count=3 inherit-before",
|
||||
sc: DimInsert,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--start", "5", "--end", "8", "--inherit-style", "before"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "6", "--count", "3", "--inherit-style", "before"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -60,9 +62,22 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-delete column B..D",
|
||||
name: "+dim-insert column position=C count=2",
|
||||
sc: DimInsert,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "C", "--count", "2"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
"operation": "insert",
|
||||
"sheet_id": testSheetID,
|
||||
"position": "C",
|
||||
"count": float64(2),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-delete column B:D",
|
||||
sc: DimDelete,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "column", "--start", "1", "--end", "4"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "B:D"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -72,9 +87,9 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-hide row 2..5 → range 3:5",
|
||||
name: "+dim-hide row 3:5",
|
||||
sc: DimHide,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--start", "2", "--end", "5"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:5"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -84,9 +99,9 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-unhide column 26..29 → AA:AC",
|
||||
name: "+dim-unhide column AA:AC",
|
||||
sc: DimUnhide,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "column", "--start", "26", "--end", "29"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "AA:AC"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -119,9 +134,9 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-group with state",
|
||||
name: "+dim-group row 1:5 fold",
|
||||
sc: DimGroup,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--start", "0", "--end", "5", "--group-state", "fold"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--group-state", "fold"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -132,9 +147,9 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "+dim-ungroup",
|
||||
name: "+dim-ungroup row 1:5",
|
||||
sc: DimUngroup,
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--start", "0", "--end", "5"},
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5"},
|
||||
toolName: "modify_sheet_structure",
|
||||
wantInput: map[string]interface{}{
|
||||
"excel_id": testToken,
|
||||
@@ -155,29 +170,56 @@ func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDimRange_StartEndValidation(t *testing.T) {
|
||||
// TestDimRange_Validation covers the A1 range parser's edge cases routed
|
||||
// through +dim-hide (any --range shortcut works; we just need to exercise
|
||||
// the validator).
|
||||
func TestDimRange_Validation(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdout, stderr, err := runShortcutCapturingErr(t, DimHide, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--dimension", "row", "--start", "5", "--end", "3", "--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr)
|
||||
cases := []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "end before start",
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "5:3", "--dry-run"},
|
||||
want: "end position is before start",
|
||||
},
|
||||
{
|
||||
name: "mix row+column",
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:C", "--dry-run"},
|
||||
want: "cannot mix row",
|
||||
},
|
||||
{
|
||||
name: "invalid characters",
|
||||
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:B2", "--dry-run"},
|
||||
want: "expected pure digits",
|
||||
},
|
||||
}
|
||||
if !strings.Contains(stdout+stderr+err.Error(), "must be greater than --start") {
|
||||
t.Errorf("expected end>start guard; got=%s|%s|%v", stdout, stderr, err)
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdout, stderr, err := runShortcutCapturingErr(t, DimHide, tt.args)
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout+stderr+err.Error(), tt.want) {
|
||||
t.Errorf("expected %q substring; got=%s|%s|%v", tt.want, stdout, stderr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDimMove_DryRun verifies the native v3 move_dimension payload shape.
|
||||
// CLI's 0-based inclusive (--start / --end) maps straight through to v3's
|
||||
// source.{start_index,end_index} (also 0-based inclusive), and sheet_id is
|
||||
// carried in the path, not the body.
|
||||
// CLI's --source-range "1:3" (1-based inclusive) is parsed into
|
||||
// source.{start_index=0, end_index=2} (0-based inclusive), and sheet_id is
|
||||
// carried in the path, not the body. --target "11" → destination_index=10.
|
||||
func TestDimMove_DryRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
calls := parseDryRunAPI(t, DimMove, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--dimension", "row", "--start", "0", "--end", "2", "--target", "10",
|
||||
"--source-range", "1:3", "--target", "11",
|
||||
})
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("api calls = %d, want 1", len(calls))
|
||||
@@ -196,15 +238,96 @@ func TestDimMove_DryRun(t *testing.T) {
|
||||
t.Errorf("start_index = %v, want 0", src["start_index"])
|
||||
}
|
||||
if src["end_index"].(float64) != 2 {
|
||||
t.Errorf("end_index = %v, want 2 (0-based inclusive, passes straight through)", src["end_index"])
|
||||
t.Errorf("end_index = %v, want 2 (0-based inclusive)", src["end_index"])
|
||||
}
|
||||
if body["destination_index"].(float64) != 10 {
|
||||
t.Errorf("destination_index = %v, want 10", body["destination_index"])
|
||||
t.Errorf("destination_index = %v, want 10 (target \"11\" → 0-based 10)", body["destination_index"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestColumnIndexToLetter exercises the corner cases of the letter helper:
|
||||
// single, double, and triple-letter spans.
|
||||
// TestDimMove_Column exercises the column path: --source-range "C:F" →
|
||||
// COLUMNS / start=2 / end=5; --target "H" → destination_index=7.
|
||||
func TestDimMove_Column(t *testing.T) {
|
||||
t.Parallel()
|
||||
calls := parseDryRunAPI(t, DimMove, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--source-range", "C:F", "--target", "H",
|
||||
})
|
||||
c := calls[0].(map[string]interface{})
|
||||
body, _ := c["body"].(map[string]interface{})
|
||||
src, _ := body["source"].(map[string]interface{})
|
||||
if src["major_dimension"] != "COLUMNS" {
|
||||
t.Errorf("major_dimension = %v, want COLUMNS", src["major_dimension"])
|
||||
}
|
||||
if src["start_index"].(float64) != 2 || src["end_index"].(float64) != 5 {
|
||||
t.Errorf("source = %v, want start=2 end=5", src)
|
||||
}
|
||||
if body["destination_index"].(float64) != 7 {
|
||||
t.Errorf("destination_index = %v, want 7", body["destination_index"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDimMove_MismatchedDimension verifies that mixing source row + target
|
||||
// column (or vice versa) is rejected at Validate.
|
||||
func TestDimMove_MismatchedDimension(t *testing.T) {
|
||||
t.Parallel()
|
||||
stdout, stderr, err := runShortcutCapturingErr(t, DimMove, []string{
|
||||
"--url", testURL, "--sheet-id", testSheetID,
|
||||
"--source-range", "1:3", "--target", "H", "--dry-run",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error; stdout=%s stderr=%s", stdout, stderr)
|
||||
}
|
||||
if !strings.Contains(stdout+stderr+err.Error(), "must match --source-range") {
|
||||
t.Errorf("expected dimension-mismatch guard; got=%s|%s|%v", stdout, stderr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseA1Range covers parser edge cases directly.
|
||||
func TestParseA1Range(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
in string
|
||||
dim string
|
||||
start int
|
||||
end int
|
||||
wantErr bool
|
||||
}{
|
||||
{"3:7", "row", 2, 6, false},
|
||||
{"5", "row", 4, 4, false},
|
||||
{"C:F", "column", 2, 5, false},
|
||||
{"C", "column", 2, 2, false},
|
||||
{"aa:ac", "column", 26, 28, false}, // lower-case letters accepted
|
||||
{"", "", 0, 0, true},
|
||||
{"3:C", "", 0, 0, true},
|
||||
{"7:3", "", 0, 0, true},
|
||||
{"A1", "", 0, 0, true}, // cell ref, not a row/col range
|
||||
{"3:5:7", "", 0, 0, true},
|
||||
{"0", "", 0, 0, true}, // rows are 1-based
|
||||
}
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.in, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dim, start, end, err := parseA1Range(c.in)
|
||||
if c.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("parseA1Range(%q) = (%q, %d, %d, nil), want error", c.in, dim, start, end)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("parseA1Range(%q) unexpected error: %v", c.in, err)
|
||||
}
|
||||
if dim != c.dim || start != c.start || end != c.end {
|
||||
t.Errorf("parseA1Range(%q) = (%q, %d, %d), want (%q, %d, %d)", c.in, dim, start, end, c.dim, c.start, c.end)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestColumnIndexToLetter exercises the corner cases of the letter helper
|
||||
// (still in use by lark_sheet_workbook.go for absolute column refs).
|
||||
func TestColumnIndexToLetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
|
||||
@@ -123,10 +123,9 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--start` | int | required | 起始行(0-based, inclusive) |
|
||||
| `--end` | int | required | 结束行(0-based, inclusive) |
|
||||
| `--type` | string | required | 尺寸方式 enum:`pixel`(指定 px 像素值,需配 `--size`)/ `standard`(重置为默认标准行高)/ `auto`(自动适应内容)(可选值:`pixel` / `standard` / `auto`) |
|
||||
| `--size` | int | optional | 行高(像素,例:30 / 40 / 60);`--type pixel` 时必填,其它 type 忽略 |
|
||||
| `--range` | string | required | 要调整行高的行闭区间;1-based 行号如 `2:10` 或单行 `5` |
|
||||
|
||||
### `+cols-resize`
|
||||
|
||||
@@ -134,10 +133,9 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--start` | int | required | 起始列(0-based, inclusive) |
|
||||
| `--end` | int | required | 结束列(0-based, inclusive) |
|
||||
| `--type` | string | required | 尺寸方式 enum:`pixel`(指定 px 像素值,需配 `--size`)/ `standard`(重置为默认标准列宽)(可选值:`pixel` / `standard`) |
|
||||
| `--size` | int | optional | 列宽(像素,例:80 / 120 / 200);`--type pixel` 时必填,其它 type 忽略 |
|
||||
| `--range` | string | required | 要调整列宽的列闭区间;列字母如 `A:E` 或单列 `C` |
|
||||
|
||||
### `+range-move`
|
||||
|
||||
@@ -224,16 +222,16 @@ lark-cli sheets +cells-unmerge --url "..." --sheet-id "$SID" --range "A1:C100"
|
||||
|
||||
```bash
|
||||
# 把第 2-10 行设为固定 30 px
|
||||
lark-cli sheets +rows-resize --url "..." --sheet-id "$SID" --start 2 --end 10 --type pixel --size 30
|
||||
lark-cli sheets +rows-resize --url "..." --sheet-id "$SID" --range "2:10" --type pixel --size 30
|
||||
|
||||
# 把 A-C 列设为固定 120 px
|
||||
lark-cli sheets +cols-resize --url "..." --sheet-id "$SID" --start 0 --end 2 --type pixel --size 120
|
||||
lark-cli sheets +cols-resize --url "..." --sheet-id "$SID" --range "A:C" --type pixel --size 120
|
||||
|
||||
# 行高自动适应内容(列宽不支持 auto)
|
||||
lark-cli sheets +rows-resize --url "..." --sheet-id "$SID" --start 0 --end 0 --type auto
|
||||
# 第 1 行行高自动适应内容(列宽不支持 auto)
|
||||
lark-cli sheets +rows-resize --url "..." --sheet-id "$SID" --range "1" --type auto
|
||||
|
||||
# 重置为默认
|
||||
lark-cli sheets +cols-resize --url "..." --sheet-id "$SID" --start 0 --end 5 --type standard
|
||||
# 重置 A-E 列为默认列宽
|
||||
lark-cli sheets +cols-resize --url "..." --sheet-id "$SID" --range "A:E" --type standard
|
||||
```
|
||||
|
||||
> 同时出现在 `lark-sheets-sheet-structure.md` —— 行高 / 列宽调整也算行列结构层动作。
|
||||
|
||||
@@ -17,31 +17,31 @@
|
||||
| 操作需求 | 使用工具 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 查看子表布局 | `+sheet-info` | 获取行高、列宽、隐藏行列、行列分组、合并单元格等信息 |
|
||||
| 变更子表结构 | `+dim-{insert|delete|hide|unhide|freeze|group|ungroup}` | 插入/删除/隐藏/取消隐藏/冻结行列、行列分组操作 |
|
||||
| 变更子表结构 | `+dim-{insert|delete|hide|unhide|freeze|group|ungroup|move}` | 插入/删除/隐藏/取消隐藏/冻结/分组/移动行列 |
|
||||
|
||||
注意:
|
||||
|
||||
- 当表格存在合并单元格时,应结合返回的 `merged_cells` 判断表头、分组标题和区域语义
|
||||
- 不要把合并区域中非左上角的空白单元格理解为"无内容";通常应将左上角单元格的内容视为整个合并区域的语义内容
|
||||
- 插入用 `+dim-insert`:`--dimension`(`row`/`column`)+ `--start`(插入起始 index,0-based)+ `--end`(结束 index,exclusive);插入行/列数 = `--end` − `--start`。新行/列样式继承用 `--inherit-style`(`before`/`after`/`none`)
|
||||
- 处理"在第 N 行后追加"这类请求时,注意 `--start` 是 0-based 索引、`--end` 是 exclusive,换算时避免 off-by-one
|
||||
- 例如"在第 20 行后新增 116 行":`--dimension row --start 21 --end 137`("第 20 行后"即从 index 21 起插入,`--end` = `--start` + 116)
|
||||
- 插入用 `+dim-insert`:`--position`(插入位置;行用 1-based 行号如 `3`,列用字母如 `C`,新行/列插在此位置**之前**)+ `--count`(插入数量,>0)。新行/列样式继承用 `--inherit-style`(`before`/`after`/`none`)
|
||||
- 例如"在第 20 行后新增 116 行":`--position 21 --count 116`("第 20 行后"即 1-based 行号 21)
|
||||
|
||||
**⚠️ `--end` 区间端点语义对照(跨命令不一致,最高发的 off-by-one 来源)**:同样叫 `--start` / `--end`、同样作用于行/列区间,但 `--end` 含义因命令而异,构造参数前务必对照本表:
|
||||
**区间表达统一为 A1 风格**:所有涉及"一段连续行/列"的 shortcut 都用同一套 A1 闭区间字符串语法,**不存在 inclusive / exclusive / 0-based / 1-based 跨命令差异**:
|
||||
|
||||
| 命令 | `--end` 语义 | 备注 |
|
||||
| 命令 | 用什么 flag 表达区间 / 位置 | 例子 |
|
||||
| --- | --- | --- |
|
||||
| `+dim-insert` / `+dim-delete` / `+dim-hide` / `+dim-unhide` / `+dim-group` / `+dim-ungroup` | **exclusive**(不含 end) | 操作行/列数 = `--end` − `--start` |
|
||||
| `+dim-move` | **inclusive**(含 end) | ⚠️ 与同族 `+dim-*` **相反**!`--start`/`--end` 是**源区间**(闭区间),目标位置另用 `--target` |
|
||||
| `+rows-resize` / `+cols-resize` | **inclusive**(含 end) | `--start`/`--end` 均为 0-based 闭区间 |
|
||||
| `+dim-insert` | `--position` + `--count` | `--position 3 --count 5`(在第 3 行前插 5 行)/ `--position C --count 2`(在 C 列前插 2 列) |
|
||||
| `+dim-delete` / `+dim-hide` / `+dim-unhide` / `+dim-group` / `+dim-ungroup` / `+rows-resize` / `+cols-resize` | `--range` | `"3:7"`(第 3-7 行,闭区间)/ `"C:F"`(C-F 列,闭区间)/ `"5"` 或 `"C"`(单行/列) |
|
||||
| `+dim-move` | `--source-range`(源区间)+ `--target`(目标位置) | `--source-range "3:7" --target 12`(把第 3-7 行移到第 12 行前)/ `--source-range "C:F" --target H` |
|
||||
|
||||
把 `+dim-insert` / `+dim-delete` 的 exclusive 习惯照搬到 `+dim-move` / `+rows-resize` / `+cols-resize`(或反过来)会少算/多算一行/一列——动手前先在本表确认目标命令的 `--end` 端点语义。
|
||||
行用 1-based 数字、列用字母——跟 Excel / 飞书 UI 看到的行号、列字母完全一致。
|
||||
|
||||
**常见配置错误(必须注意)**:
|
||||
- **插入列位置偏移**:插入列时 `--start` 是基于 0 的列索引,不是列字母。插入前先通过 `+workbook-info` 或读取表头确认目标位置的实际列索引,不要凭猜测
|
||||
- **插入后引用偏移**:插入行/列后,原有数据的行列号会发生偏移。如果插入后还需要对原有区域执行写入操作,必须重新计算偏移后的行列号
|
||||
- **删除行列前先确认范围**:删除操作不可逆,执行前应确认 `--start` / `--end` 精确无误(`+dim-delete` 用 `--dimension` + `--start`(0-based)+ `--end`(exclusive))。可先用 `+csv-get` 读取目标区域验证内容
|
||||
- **"在左侧新增一列"的正确写法**:用户说"在 D 列左侧新增一列"时,`--dimension column`、`--start` 取 D 列的 0-based 索引(新列插在该 index 之前)、`--end = --start + 1`;要继承左侧列样式加 `--inherit-style before`
|
||||
- **插入列直接用字母**:`+dim-insert` 的 `--position` 在列场景直接传字母(如 `C`),不要把列字母换算成 0-based 索引
|
||||
- **插入后引用偏移**:插入行/列后,原有数据的行号 / 列字母会发生偏移。如果插入后还需要对原有区域执行写入操作,必须重新计算偏移后的位置
|
||||
- **删除行列前先确认范围**:删除操作不可逆,执行前应确认 `--range` 精确无误。可先用 `+csv-get` 读取目标区域验证内容
|
||||
- **"在 D 列左侧新增一列"的正确写法**:`--position D --count 1`(新列插在 D 列之前);要继承左侧列样式加 `--inherit-style before`
|
||||
- **`+dim-move` 同维度约束**:`--source-range` 是行区间时 `--target` 必须是行号(数字),是列区间时 `--target` 必须是列字母——不可一行一列混用
|
||||
- **插入列后必须检查多行表头合并区域**:很多表格有 2-3 行的合并表头。插入列后,原有的合并区域不会自动扩展到新列。必须先用 `+sheet-info --include merges` 读取合并区域,插入后将跨越插入位置的合并区域重新设置(用 `+cells-{merge|unmerge}`),否则新列的表头会是空的、格式不连续
|
||||
- **公式写入范围跳过表头行**:写入公式时从数据行开始(不是第 1 行)。先确认表头占几行(可能 1-3 行),公式的起始行 = 表头行数 + 1
|
||||
|
||||
@@ -76,10 +76,9 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 插入起始位置(0-based) |
|
||||
| `--end` | int | required | 插入结束位置(exclusive) |
|
||||
| `--inherit-style` | string | optional | 新行/列样式继承策略 enum:`before`(继承前一行/列)/ `after`(继承后一行/列)/ `none`(默认)(可选值:`before` / `after` / `none`) |
|
||||
| `--position` | string | required | 插入位置(在此行/列**之前**插入):行用 1-based 行号如 `3`;列用字母如 `C` |
|
||||
| `--count` | int | required | 插入数量(>0) |
|
||||
|
||||
### `+dim-delete`
|
||||
|
||||
@@ -87,9 +86,7 @@ _公共四件套 · 系统:`--yes`、`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 起始位置(0-based) |
|
||||
| `--end` | int | required | 结束位置(exclusive) |
|
||||
| `--range` | string | required | 要删除的行/列闭区间;行用 1-based 数字如 `3:7` 或单行 `5`,列用字母如 `C:F` 或单列 `C` |
|
||||
|
||||
### `+dim-hide`
|
||||
|
||||
@@ -97,9 +94,7 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 起始位置(0-based) |
|
||||
| `--end` | int | required | 结束位置(exclusive) |
|
||||
| `--range` | string | required | 要隐藏的行/列闭区间;行如 `3:7`,列如 `C:F` |
|
||||
|
||||
### `+dim-unhide`
|
||||
|
||||
@@ -107,9 +102,7 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 起始位置(0-based) |
|
||||
| `--end` | int | required | 结束位置(exclusive) |
|
||||
| `--range` | string | required | 要取消隐藏的行/列闭区间;行如 `3:7`,列如 `C:F` |
|
||||
|
||||
### `+dim-freeze`
|
||||
|
||||
@@ -126,11 +119,9 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 起始位置(0-based) |
|
||||
| `--end` | int | required | 结束位置(exclusive) |
|
||||
| `--depth` | int | optional | 嵌套分组的层级(创建到第几层),默认 1 |
|
||||
| `--group-state` | string | optional | 分组初始展开状态(可选值:`expand` / `fold`)(默认 `expand`) |
|
||||
| `--range` | string | required | 要创建分组的行/列闭区间;行如 `3:7`,列如 `C:F` |
|
||||
|
||||
### `+dim-ungroup`
|
||||
|
||||
@@ -138,10 +129,8 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 起始位置(0-based) |
|
||||
| `--end` | int | required | 结束位置(exclusive) |
|
||||
| `--depth` | int | optional | 要取消的分组层级,默认 1(最外层) |
|
||||
| `--range` | string | required | 要取消分组的行/列闭区间;行如 `3:7`,列如 `C:F` |
|
||||
|
||||
### `+dim-move`
|
||||
|
||||
@@ -149,10 +138,8 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
| Flag | Type | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--dimension` | string | required | 维度方向(行或列)(可选值:`row` / `column`) |
|
||||
| `--start` | int | required | 源起止区间的起始位置(0-based) |
|
||||
| `--end` | int | required | 源起止区间的结束位置(inclusive) |
|
||||
| `--target` | int | required | 目标位置(move 到该 index 之前;0-based) |
|
||||
| `--source-range` | string | required | 要移动的源行/列闭区间;行如 `3:7`,列如 `C:F` |
|
||||
| `--target` | string | required | 目标位置(移到此行/列**之前**):行用 1-based 行号如 `12`,列用字母如 `H`。必须与 `--source-range` 同维度(行/列) |
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -164,26 +151,41 @@ _公共四件套 · 系统:`--dry-run`_
|
||||
|
||||
### `+dim-insert`
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
# 在第 10 行前插 3 行,继承上方样式
|
||||
lark-cli sheets +dim-insert --url "https://example.feishu.cn/sheets/shtXXX" \
|
||||
--sheet-id "$SID" --dimension row --start 10 --end 13 --inherit-style before
|
||||
--sheet-id "$SID" --position 10 --count 3 --inherit-style before
|
||||
|
||||
# 在 C 列前插 2 列
|
||||
lark-cli sheets +dim-insert --url "..." --sheet-id "$SID" --position C --count 2
|
||||
```
|
||||
|
||||
### `+dim-delete`
|
||||
|
||||
```bash
|
||||
# 删除第 5-7 行(0-based,--end 为 exclusive)
|
||||
lark-cli sheets +dim-delete --url "..." --sheet-id "$SID" --dimension row --start 5 --end 8 --yes
|
||||
# 删除第 5-7 行
|
||||
lark-cli sheets +dim-delete --url "..." --sheet-id "$SID" --range "5:7" --yes
|
||||
|
||||
# 删除 D-F 列
|
||||
lark-cli sheets +dim-delete --url "..." --sheet-id "$SID" --range "D:F" --yes
|
||||
```
|
||||
|
||||
### `+dim-hide` / `+dim-unhide`
|
||||
|
||||
```bash
|
||||
lark-cli sheets +dim-hide --url "..." --sheet-id "$SID" --dimension row --start 5 --end 8
|
||||
lark-cli sheets +dim-unhide --url "..." --sheet-id "$SID" --dimension row --start 5 --end 8
|
||||
lark-cli sheets +dim-hide --url "..." --sheet-id "$SID" --range "5:7"
|
||||
lark-cli sheets +dim-unhide --url "..." --sheet-id "$SID" --range "5:7"
|
||||
lark-cli sheets +dim-hide --url "..." --sheet-id "$SID" --range "C:F"
|
||||
```
|
||||
|
||||
### `+dim-move`
|
||||
|
||||
```bash
|
||||
# 把第 3-7 行移到第 12 行前
|
||||
lark-cli sheets +dim-move --url "..." --sheet-id "$SID" --source-range "3:7" --target 12
|
||||
|
||||
# 把 C-F 列移到 H 列前
|
||||
lark-cli sheets +dim-move --url "..." --sheet-id "$SID" --source-range "C:F" --target H
|
||||
```
|
||||
|
||||
### `+rows-resize` / `+cols-resize`
|
||||
@@ -205,6 +207,6 @@ lark-cli sheets +dim-freeze --url "..." --sheet-id "$SID" --dimension row --coun
|
||||
|
||||
### Validate / DryRun / Execute 约束
|
||||
|
||||
- `Validate`:XOR 公共四件套;`--start ≤ --end`;`+dim-delete` 强制 `--yes` 或 `--dry-run`;`+rows-resize` / `+cols-resize` 的 `--type` 必填,`--type pixel` 时 `--size` 必填、其它 type 时 `--size` 会被忽略(传了无害);`+rows-resize` / `+cols-resize` 的行 vs 列 `--type` 差异详见 `lark-sheets-range-operations.md`。
|
||||
- `DryRun`:写操作输出"将要 PATCH 的 dimension 区间 + 目标参数"。
|
||||
- `Validate`:XOR 公共四件套;`--range` / `--source-range` 必须是合法 A1 闭区间(行用数字、列用字母,不可混用);`+dim-insert` 的 `--count` > 0;`+dim-move` 的 `--target` 必须与 `--source-range` 同维度(行 vs 列);`+dim-delete` 强制 `--yes` 或 `--dry-run`;`+rows-resize` / `+cols-resize` 的 `--type` 必填,`--type pixel` 时 `--size` 必填、其它 type 时 `--size` 会被忽略(传了无害);`+rows-resize` / `+cols-resize` 的行 vs 列 `--type` 差异详见 `lark-sheets-range-operations.md`。
|
||||
- `DryRun`:写操作输出"将要 PATCH 的目标范围 + 目标参数"。
|
||||
- `Execute`:写后不自动回读;如需确认,自行调用 `+sheet-info --include row_heights,col_widths,hidden_rows,hidden_cols,groups,frozen` 查看受影响的范围。
|
||||
|
||||
Reference in New Issue
Block a user