xlsx

Create, read, edit Excel .xlsx workbooks and CSVs.

  • excel
  • spreadsheet
  • xlsx
  • csv
  • openpyxl
  • productivity

Declared platforms: linux · macos · windows

Install
npx skills add 'https://github.com/NousResearch/hermes-agent/tree/main/skills/productivity/xlsx'
Download bundle ↓
main · 24fd22bScanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗
View on GitHub
← Back to SKILL.md
#!/usr/bin/env python3"""Reference-aware row/column insert and delete for .xlsx workbooks. Unlike plain openpyxl insert_rows/delete_cols (and xlsx_edit.py's thinwrappers), this script also rewrites everything that points at the movedcells:   * formula references in ALL sheets, including absolute refs ($B$2),    ranges (B2:B9), and cross-sheet refs ('My Sheet'!A1 / Data!$B$8).    References into a deleted region become #REF!.  * merged-cell ranges (shifted; expanded when they span the insertion    point; removed when fully deleted)  * autofilter range, freeze panes, data-validation ranges,    conditional-formatting applied ranges (sqref)  * native table (ListObject) refs on the edited sheet  * workbook-scope defined names that point at the edited sheet  * row heights / column widths It prints a JSON report of every rewrite it made and lists what itcould NOT shift (chart anchors, images, conditional-format RULEformulas). Full rules and limits: references/restructuring.md. One structural operation per invocation: Usage:  xlsx_restructure.py book.xlsx --sheet Data --insert-rows 3:2  xlsx_restructure.py book.xlsx --sheet Data --delete-rows 5  xlsx_restructure.py book.xlsx --sheet Data --insert-cols B:1 --out new.xlsx  xlsx_restructure.py book.xlsx --sheet Data --delete-cols 4:2"""from __future__ import annotations import argparseimport jsonimport reimport sys from openpyxl import load_workbookfrom openpyxl.formatting.formatting import ConditionalFormattingListfrom openpyxl.utils import (column_index_from_string, get_column_letter,                            range_boundaries) # A1-style reference, optionally sheet-qualified, optionally a range.# Guards: not preceded by a word char/$/. (avoids ABC123 identifiers) and# not followed by a word char or "(" (avoids function names like LOG10().REF_RE = re.compile(    r"(?<![\w$.:])"    r"(?P<sheet>(?:'(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!)?"    r"(?P<start>\$?[A-Za-z]{1,3}\$?[0-9]{1,7})"    r"(?::(?P<end>\$?[A-Za-z]{1,3}\$?[0-9]{1,7}))?"    r"(?![\w(])")STRING_RE = re.compile(r'"(?:[^"]|"")*"')COORD_RE = re.compile(r"^(\$?)([A-Za-z]{1,3})(\$?)([0-9]+)$")  def shift_point(v, idx, n, delete):    """New 1-based index for a single row/col, or None if deleted."""    if delete:        if v < idx:            return v        if v >= idx + n:            return v - n        return None    return v + n if v >= idx else v  def shift_span(a, b, idx, n, delete):    """New (start, end) for an inclusive span, or None if fully deleted."""    if delete:        na = a if a < idx else (a - n if a >= idx + n else idx)        nb = b if b < idx else (b - n if b >= idx + n else idx - 1)        return None if na > nb else (na, nb)    return (a + n if a >= idx else a, b + n if b >= idx else b)  def shift_range(rng, axis, idx, n, delete):    """Shift an A1 range string (no sheet prefix). None = fully deleted."""    min_col, min_row, max_col, max_row = range_boundaries(rng)    if axis == "rows":        span = shift_span(min_row, max_row, idx, n, delete)        if span is None:            return None        min_row, max_row = span    else:        span = shift_span(min_col, max_col, idx, n, delete)        if span is None:            return None        min_col, max_col = span    start = f"{get_column_letter(min_col)}{min_row}"    end = f"{get_column_letter(max_col)}{max_row}"    return start if start == end and ":" not in rng else f"{start}:{end}"  class RefRewriter:    """Rewrite A1 references in formula-like text for one shift op."""     def __init__(self, target_sheet, axis, idx, n, delete):        self.target = target_sheet.lower()        self.axis, self.idx, self.n, self.delete = axis, idx, n, delete     def _shift_coord(self, coord):        m = COORD_RE.match(coord)        col_abs, col, row_abs, row = m.groups()        ci, ri = column_index_from_string(col.upper()), int(row)        if self.axis == "rows":            ri = shift_point(ri, self.idx, self.n, self.delete)            if ri is None:                return None        else:            ci = shift_point(ci, self.idx, self.n, self.delete)            if ci is None:                return None        return f"{col_abs}{get_column_letter(ci)}{row_abs}{ri}"     def _shift_pair(self, start, end):        """Shift a range preserving $ flags; None = collapsed to #REF!."""        new_start = self._shift_coord(start)        new_end = self._shift_coord(end)        if new_start is None or new_end is None:            # spans may survive partial deletion: clamp via span math            s, e = COORD_RE.match(start), COORD_RE.match(end)            if self.axis == "rows":                span = shift_span(int(s.group(4)), int(e.group(4)),                                  self.idx, self.n, self.delete)                if span is None:                    return None                new_start = f"{s.group(1)}{s.group(2)}{s.group(3)}{span[0]}"                new_end = f"{e.group(1)}{e.group(2)}{e.group(3)}{span[1]}"            else:                span = shift_span(column_index_from_string(s.group(2).upper()),                                  column_index_from_string(e.group(2).upper()),                                  self.idx, self.n, self.delete)                if span is None:                    return None                new_start = (f"{s.group(1)}{get_column_letter(span[0])}"                             f"{s.group(3)}{s.group(4)}")                new_end = (f"{e.group(1)}{get_column_letter(span[1])}"                           f"{e.group(3)}{e.group(4)}")        return new_start, new_end     def _sub(self, match, home_sheet):        prefix = match.group("sheet") or ""        if prefix:            name = prefix[:-1]            if name.startswith("'"):                name = name[1:-1].replace("''", "'")            ref_sheet = name        else:            ref_sheet = home_sheet        if ref_sheet.lower() != self.target:            return match.group(0)        start, end = match.group("start"), match.group("end")        if end is None:            new = self._shift_coord(start)            return prefix + ("#REF!" if new is None else new)        pair = self._shift_pair(start, end)        return (prefix + "#REF!" if pair is None                else f"{prefix}{pair[0]}:{pair[1]}")     def rewrite(self, text, home_sheet):        """Rewrite refs outside quoted string literals. Returns new text."""        out, pos = [], 0        for lit in STRING_RE.finditer(text):            out.append(REF_RE.sub(lambda m: self._sub(m, home_sheet),                                  text[pos:lit.start()]))            out.append(lit.group(0))            pos = lit.end()        out.append(REF_RE.sub(lambda m: self._sub(m, home_sheet), text[pos:]))        return "".join(out)  def shift_dimensions(dims, idx, n, delete, is_row):    """Rebuild a row/column dimensions map with shifted keys."""    items = list(dims.items())    saved = {}    for key, dim in items:        pos = key if is_row else column_index_from_string(key)        new = shift_point(pos, idx, n, delete)        if new is not None and new != pos:            saved[new if is_row else get_column_letter(new)] = dim            del dims[key]    for key, dim in saved.items():        if is_row:            dim.index = key        else:            dim.index = column_index_from_string(key)        dims[key] = dim    return len(saved)  def main(argv=None):    ap = argparse.ArgumentParser(        description="Insert/delete rows or columns AND rewrite formula "                    "references, merges, filters, validations, tables, and "                    "defined names to match.",        epilog="Cannot shift: chart anchors, images, conditional-format "               "rule formulas. See references/restructuring.md.")    ap.add_argument("file", help="path to .xlsx file")    ap.add_argument("--sheet", help="target sheet (default: active)")    ap.add_argument("--out", help="output path (default: edit in place)")    op = ap.add_mutually_exclusive_group(required=True)    op.add_argument("--insert-rows", metavar="IDX[:N]")    op.add_argument("--delete-rows", metavar="IDX[:N]")    op.add_argument("--insert-cols", metavar="COL[:N]",                    help="COL is a letter (B) or 1-based number")    op.add_argument("--delete-cols", metavar="COL[:N]")    args = ap.parse_args(argv)     raw = (args.insert_rows or args.delete_rows           or args.insert_cols or args.delete_cols)    idx_s, _, n_s = raw.partition(":")    n = int(n_s) if n_s else 1    axis = "rows" if (args.insert_rows or args.delete_rows) else "cols"    delete = bool(args.delete_rows or args.delete_cols)    if axis == "cols" and idx_s.isalpha():        idx = column_index_from_string(idx_s.upper())    else:        idx = int(idx_s)     wb = load_workbook(args.file)    ws = wb[args.sheet] if args.sheet else wb.active    rewriter = RefRewriter(ws.title, axis, idx, n, delete)    report = {"ok": True, "sheet": ws.title, "axis": axis,              "op": "delete" if delete else "insert", "index": idx, "count": n,              "formulas": [], "merges": [], "tables": {}, "defined_names": {},              "validations": [], "conditional_formats": [],              "not_shifted": ["chart anchors", "images",                              "conditional-format rule formulas"]}     # 1. capture merge ranges (openpyxl does not move them), then unmerge    old_merges = [str(r) for r in list(ws.merged_cells.ranges)]    for rng in old_merges:        ws.unmerge_cells(rng)     # 2. structural move of cell values/styles/comments    getattr(ws, f"{report['op']}_{axis}")(idx, n)     # 3. formulas everywhere    for sheet in wb.worksheets:        for row in sheet.iter_rows():            for cell in row:                if isinstance(cell.value, str) and cell.value.startswith("="):                    new = rewriter.rewrite(cell.value, sheet.title)                    if new != cell.value:                        report["formulas"].append(                            {"sheet": sheet.title, "cell": cell.coordinate,                             "from": cell.value, "to": new})                        cell.value = new     # 4. merges back, shifted    for rng in old_merges:        new = shift_range(rng, axis, idx, n, delete)        if new is None:            report["merges"].append({"from": rng, "to": None})        else:            ws.merge_cells(new)            if new != rng:                report["merges"].append({"from": rng, "to": new})     # 5. autofilter + freeze panes    if ws.auto_filter.ref:        new = shift_range(ws.auto_filter.ref, axis, idx, n, delete)        if new != ws.auto_filter.ref:            report["autofilter"] = {"from": ws.auto_filter.ref, "to": new}            ws.auto_filter.ref = new    if ws.freeze_panes:        m = COORD_RE.match(ws.freeze_panes)        ci = column_index_from_string(m.group(2).upper())        ri = int(m.group(4))        if axis == "rows":            ri = shift_point(ri, idx, n, delete) or max(idx, 2)        else:            ci = shift_point(ci, idx, n, delete) or max(idx, 2)        new = f"{get_column_letter(ci)}{ri}"        if new != ws.freeze_panes:            report["freeze_panes"] = {"from": ws.freeze_panes, "to": new}            ws.freeze_panes = new     # 6. data validations + conditional formatting applied ranges    for dv in ws.data_validations.dataValidation:        old = str(dv.sqref)        parts = [shift_range(p, axis, idx, n, delete) for p in old.split()]        parts = [p for p in parts if p]        if parts and " ".join(parts) != old:            dv.sqref = " ".join(parts)            report["validations"].append({"from": old, "to": str(dv.sqref)})    new_cf = ConditionalFormattingList()    for cf in ws.conditional_formatting:        old = str(cf.sqref)        parts = [shift_range(p, axis, idx, n, delete) for p in old.split()]        parts = [p for p in parts if p]        if not parts:            report["conditional_formats"].append({"from": old, "to": None})            continue        new = " ".join(parts)        for rule in cf.rules:            new_cf.add(new, rule)        if new != old:            report["conditional_formats"].append({"from": old, "to": new})    ws.conditional_formatting = new_cf     # 7. native tables on the edited sheet    for table in ws.tables.values():        new = shift_range(table.ref, axis, idx, n, delete)        if new and new != table.ref:            report["tables"][table.displayName] = {"from": table.ref,                                                   "to": new}            table.ref = new     # 8. workbook-scope defined names    for name, dn in wb.defined_names.items():        if dn.attr_text and "!" in dn.attr_text:            new = rewriter.rewrite(dn.attr_text, ws.title)            if new != dn.attr_text:                report["defined_names"][name] = {"from": dn.attr_text,                                                 "to": new}                dn.attr_text = new     # 9. row heights / column widths    if axis == "rows":        shift_dimensions(ws.row_dimensions, idx, n, delete, is_row=True)    else:        shift_dimensions(ws.column_dimensions, idx, n, delete, is_row=False)     out = args.out or args.file    wb.save(out)    report["output"] = out    print(json.dumps(report, ensure_ascii=False, indent=2))    return 0  if __name__ == "__main__":    try:        sys.exit(main())    except Exception as exc:  # noqa: BLE001        print(json.dumps({"ok": False, "error": str(exc)}), file=sys.stderr)        sys.exit(1) 
Referenced from SKILL.md