iOfficeAI/OfficeCLI · error · ArgumentException

Anchor must be a row path like /{sheetName}/row[K], got: {an

Error message

Anchor must be a row path like /{sheetName}/row[K], got: {anchorPath}

What it means

Thrown by the FindAnchorRow helper inside AddRow when the --before/--after anchor path does not have at least two segments after trimming the leading slash. An anchor must be a full row path of the form '/<sheetName>/row[K]'. If the split produces fewer than two segments (e.g. the path is just '/Sheet1' with no row[K] suffix), the anchor is malformed and cannot be resolved to a row number.

Source

Thrown at src/officecli/Handlers/Excel/ExcelHandler.Add.Cells.cs:192

            ?? throw new ArgumentException($"Sheet not found: {sheetName}");
        var sheetData = GetSheet(worksheet).GetFirstChild<SheetData>()
            ?? GetSheet(worksheet).AppendChild(new SheetData());

        // Resolve --before / --after anchors (same shape as Excel CopyFrom):
        // anchor must be /<sheetName>/row[K] in the same sheet.
        // CONSISTENCY(zero-based-index): per project convention, position.Index
        // is 0-based across all formats (--index 0 = head, --index 1 = before
        // 2nd slot). xlsx Row uses a 1-based RowIndex internally, so +1 here
        // and let the existing branch keep treating `index` as a 1-based row
        // number (which is also what the anchor branch below produces).
        int? index = position?.Index.HasValue == true ? position!.Index!.Value + 1 : (int?)null;
        if (index == null && position != null && (position.After != null || position.Before != null))
        {
            int FindAnchorRow(string anchorPath)
            {
                var aSegs = anchorPath.TrimStart('/').Split('/', 2);
                if (aSegs.Length < 2)
                    throw new ArgumentException(
                        $"Anchor must be a row path like /{sheetName}/row[K], got: {anchorPath}");
                if (!aSegs[0].Equals(sheetName, StringComparison.OrdinalIgnoreCase))
                    throw new ArgumentException(
                        $"Anchor sheet '{aSegs[0]}' must match target sheet '{sheetName}'");
                var am = Regex.Match(aSegs[1], @"^row\[(\d+)\]$");
                if (!am.Success)
                    throw new ArgumentException(
                        $"Anchor must be a row path like /{sheetName}/row[K], got: {anchorPath}");
                return (int)uint.Parse(am.Groups[1].Value);
            }
            // For row insertion, --before /Sheet1/row[5] means "the new row
            // takes the row[5] slot, original row[5] shifts to row[6]". So
            // resolved index == anchor row number. --after /Sheet1/row[5]
            // means index == anchor + 1.
            if (position.Before != null) index = FindAnchorRow(position.Before);
            else index = FindAnchorRow(position.After!) + 1;
        }

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Format the anchor as '/<sheetName>/row[K]' — e.g. '/Sheet1/row[5]'.
  2. For --before: the new row takes the row[K] slot, pushing the original row[K] to row[K+1].
  3. For --after: the new row is inserted immediately after row[K].
  4. Use --index N as an alternative to anchors for simple positional insertion.

Example fix

// before: anchor missing the row[K] suffix
handler.Add("/Sheet1", "row", null, new { before = "/Sheet1" }); // malformed

// after: full row path as anchor
handler.Add("/Sheet1", "row", null, new { before = "/Sheet1/row[5]" });
Defensive patterns

Strategy: validation

Validate before calling

// Validate anchor path has the required two segments
static bool IsValidRowAnchor(string? anchor, string sheetName)
{
    if (string.IsNullOrEmpty(anchor)) return false;
    var segs = anchor.TrimStart('/').Split('/', 2);
    return segs.Length >= 2 && !string.IsNullOrEmpty(segs[1]);
}

Type guard

static bool IsCompleteRowAnchor(string anchor)
{
    if (string.IsNullOrEmpty(anchor)) return false;
    var segs = anchor.TrimStart('/').Split('/', 2);
    return segs.Length >= 2 && System.Text.RegularExpressions.Regex.IsMatch(segs[1], @"^row\[\d+\]$");
}

Try / catch

try
{
    handler.Add($"/{sheetName}", "row", new InsertPosition { Before = anchor }, properties);
}
catch (ArgumentException ex) when (ex.Message.Contains("Anchor must be a row path"))
{
    // Anchor is missing the row[K] component — reformat it
    logger.LogError("Anchor '{Anchor}' must be '/{Sheet}/row[K]'.", anchor, sheetName);
    throw;
}

Prevention

When it happens

Trigger: Passing a --before or --after anchor that is missing the row[K] component, such as '/Sheet1' instead of '/Sheet1/row[5]'. The anchor path must include both the sheet name and the row bracket notation for the resolver to extract a row index.

Common situations: A user passing a sheet path instead of a row path as the anchor; a script that builds the anchor by concatenating the sheet name but forgetting to append '/row[K]'; confusion between the parent path format ('/<sheet>') and the anchor format ('/<sheet>/row[K]').

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/1deeadfd66ad3899. Report an issue: GitHub.