Skip to main content

exhash: Verified Line-Addressed File Editor

exhash combines Can Bölük's very clever line number + hash editing system with the powerful and expressive syntax of the classic ex editor.

Install via pip to get a convenient Python API, an IPython cell magic, and the exhash/lnhashview CLI commands:

pip install exhash

lnhash format

We refer to an lnhash as a tag of the form lineno|hash|, where hash is the lower 16 bits of CRC-32 (IEEE) over the line's UTF-8 content, i.e. Python's zlib.crc32(line) & 0xffff formatted as 4 hex chars.

Address forms:

  • lineno|hash|: hash-verified address
  • $: last line (no hash)
  • %: whole file (1,$, no hashes)

CLI

The exhash and lnhashview commands are Python console scripts over the native Rust extension, installed into your PATH by pip.

View

# Shows every line prefixed with its lnhash
lnhashview path/to/file.txt
# Optional line number range to show
lnhashview path/to/file.txt 10 20

If end is past EOF, lnhashview returns through the last available line instead of failing.

Edit

# Substitute on one line
exhash file.txt '12|abcd|s/foo/bar/g'

# Transliterate characters on one line
exhash file.txt '12|abcd|y/abc/ABC/'

# Change one line with inline text (spaces after c are literal text)
exhash file.txt '12|abcd|c    replacement line'

# Append multiline text (terminated by a single dot)
exhash file.txt '12|abcd|a' <<'EOF'
new line 1
new line 2
.
EOF

# Dry-run
exhash --dry-run file.txt '12|abcd|d'

# Set shift width for < and >
exhash --sw 2 file.txt '12|abcd|>1'

# Last line and whole file shorthands (no hash)
exhash file.txt '$d'
exhash file.txt '%j'

# Move a line to EOF using $ as the destination
exhash file.txt '12|abcd|m$'

# Create a missing file by treating it as empty input
exhash new.txt '0|0000|a' <<'EOF'
first line
.
EOF

Substitute uses Rust regex syntax:

  • Pattern syntax is from regex
  • Replacement syntax is from regex::Replacer, e.g. $1, $0, ${name}
  • \/ escapes the command delimiter in pattern/replacement
  • Custom delimiters: s, y, g, g!, and v all accept any non-alphanumeric char as delimiter instead of /, e.g. s@pat@rep@, g@pat@cmd. Each command in a combo picks its own delimiter independently: g@a/b@s/old/new/
  • For example, s/// accepts newlines in pattern/replacement; replacement newlines split one line into multiple lines.
  • Transliteration uses y/src/dst/ and requires source/destination to have equal character counts
  • A substitute whose pattern matches nothing in its addressed range fails (nothing is written), so a typo cannot silently no-op; substitutes running inside g/g!/v payloads stay lenient, since not every selected line need match

When passing multiple commands, each command's lnhashes are verified immediately before that command runs.

For CLI multiline a/i/c commands, omit inline text and provide the text block on stdin:

printf "new line 1\nnew line 2\n.\n" | exhash file.txt "2|beef|a"

If the file does not exist and the command set is valid on empty input, exhash treats it as an empty file and writes the result. For example, 0|0000|a can create a new file.

Stdin filter mode

cat file.txt | exhash --stdin - '1|abcd|s/foo/bar/'

In --stdin mode, multiline a/i/c text blocks are not available.

Python API

from exhash import exhash, file_exhash, lnhash, lnhashview, lnhashview_file, line_hash

Viewing

text = "foo\nbar\n"
view = lnhashview(text)                        # ["1|a1b2|foo", "2|c3d4|bar"]
view = lnhashview_file("f.py", start=1, end=260) # end past EOF is clamped

lnhashview/lnhashview_file return a list subclass whose repr shows the rows verbatim, one per line, so a bare call in IPython displays a ready-to-copy view.

Editing

exhash(text, cmds, sw=4) takes the text and a required iterable of tuple command specs (use [] for no-op). Raw command strings are rejected by the Python API. sw controls how far < and > shift.

A command is usually (addr, op) or (addr, op, payload). addr is an lnhash address string from lnhash(...)/lnhashview(...); put ranges in that same string, e.g. f"{a1},{a2}". Substitute uses (addr, "s", pattern, replacement[, flags]), so patterns and replacements can contain / without delimiter escaping.

Text fields can contain newlines. That covers multiline a/i/c payloads and substitute pattern/replacement. Commands such as d, m, and sort do not take text.

addr = lnhash(1, "foo")  # "1|a1b2|"
res = exhash(text, [(addr, "s", "foo", "baz")])
print(res["lines"])    # ["baz", "bar"]
print(res["modified"]) # [1]

# Multiple commands
a1, a2 = lnhash(1, "foo"), lnhash(2, "bar")
res = exhash(text, [(a1, "s", "foo", "FOO"), (a2, "s", "bar", "BAR")])

# Hashes are checked just-in-time per command.
# If earlier commands change/shift a later target line, recompute lnhash first.

# Change one line; leading spaces are part of the replacement
res = exhash(text, [(addr, "c", "    replacement line")])

# Append multiline text in one tuple payload (no dot terminator)
res = exhash(text, [(addr, "a", "new line 1\nnew line 2")])

# Wrong for the Python API: the trailing "." would be inserted literally
# res = exhash(text, [(addr, "a", "new line 1\nnew line 2\n.")])

# Also wrong: do not split the inserted text into separate cmds entries
# res = exhash(text, [(addr, "a"), "new line 1", "new line 2"])

# Change shift width for < and >
res = exhash(text, [(addr, ">", "1")], sw=2)

# Literal / needs no delimiter escaping in tuple substitute fields
res = exhash("a/b\n", [(lnhash(1, "a/b"), "s", "a/b", "c/d")])

# Literal newlines in replacement split one line into multiple lines
res = exhash("foo\n", [(lnhash(1, "foo"), "s", "foo", "bar\nbaz")])
print(res["lines"])  # ["bar", "baz"]

# Literal newlines in pattern can match across lines
a1, a2 = lnhash(1, "foo"), lnhash(2, "bar")
res = exhash("foo\nbar\n", [(f"{a1},{a2}", "s", "foo\nbar", "replaced")])

# Global commands take a pattern plus a nested subcommand tuple (no address)
res = exhash("keep\nTODO x\n", [("%", "g", "TODO", ("d",))])

# Transliterate takes source/dest fields (equal character counts)
res = exhash("abc\n", [(lnhash(1, "abc"), "y", "abc", "ABC")])

File helpers

lnhashview_file reads directly from one file path. All file paths, including file-qualified addresses, expand a leading ~ to your home directory. file_exhash(path, *cmds, sw=4, inplace=True) uses path as the default file context for unqualified addresses. Pass each command as its own tuple argument. Put file-qualified source and m/t destination addresses in the address/destination tuple fields:

view = lnhashview_file("file.py")

# By default, writes changed files after every command succeeds
# and returns the combined diff string.
diff = file_exhash("file.py", (addr, "s", "foo", "bar"))

# With inplace=False, files are unchanged and a FileSetEditResult is returned.
res = file_exhash("file.py", (addr, "s", "foo", "bar"), inplace=False)
print(res.changed)          # ["file.py"]
print(res["file.py"].lines)
print(res.format_diff())    # includes --- file.py / +++ file.py headers

# Missing files are treated as empty only when the command is valid on empty input.
diff = file_exhash("new.py", ("0|0000|", "a", "print('hi')"))

# File-qualified addresses can edit or transfer lines across files.
diff = file_exhash("src/a.py",
    ("src/a.py:24|8f12|,38|c0de|", "m", "src/b.py:$"),
    (r"src/a.py:5|91aa|", "s", r"from \.b import old", r"from \.b import helper"))

A file prefix is separated from the address with :. Escape literal colons in filenames as \: and literal backslashes as \\.

file_exhash(..., inplace=False) returns a FileSetEditResult:

  • res.files: dict of path to FileEditResult
  • res.changed: changed paths, in first-touch order
  • res.default_path: the default path passed to file_exhash
  • res[path]: shorthand for res.files[path]
  • res.format_diff(context=1): combined diff with --- path / +++ path headers

Notebook cells

lnhashview_cell(path, cell_id, ...) returns a normal lnhash view for one cell. lnhashview_cells(path, *cell_ids, ...) returns the requested cells in order, using # cell <id> headers before each cell's normal lineno|hash|content lines. cell_exhash(path, cell_id, *cmds, sw=4, inplace=True) edits one cell; pass each command as its own tuple argument. Like file_exhash it writes and returns a diff by default, and inplace=False previews the EditResult without touching the file.

The %%exhash cell magic

Importing exhash.skill under IPython or Jupyter registers the %%exhash cell magic - the standard way to apply a/i/c payload commands interactively. The magic line is %%exhash <path> [<cell_id>] <address> <a|i|c>; the payload is everything below it, taken verbatim. Nothing in the payload is parsed as Python, so there is no quoting or escaping at all:

%%exhash notes.txt 2|beef|a
new line 1
new line 2
  • %%exhash new.py 0|0000| a creates a missing file.
  • %%exhash f.py % c replaces the whole file (% needs no hashes). With a cell id, %%exhash nb.ipynb ab12 % c replaces that notebook cell's source.
  • %%exhash f.py 12|a3f2|,15|b1c3| c replaces just that range, both addresses from one lnhashview_file view.
  • One trailing newline (the cell terminator) is stripped; to end the payload with a blank line, leave one extra blank line at the bottom.
  • Each magic cell applies one command and returns the diff.

Tuple a/i/c payloads (as in the examples above) remain for scripts and tests, where magics don't exist. Interactively, prefer the magic: a Python string layer invites quoting mistakes.

Pyskill

The package registers exhash.skill as a pyskill exposing the primary Python APIs with LLM-oriented workflow docs. Use doc(exhash.skill) after importing it through a pyskills host.

EditResult

exhash() returns an EditResult with attributes (also accessible via res["key"]):

  • lines: list of output lines
  • hashes: lnhash for each output line
  • modified: 1-based line numbers of modified/added lines
  • deleted: 1-based line numbers of removed lines (in original)
  • origins: for each output line, the 1-based original line number (None if inserted)

res.format_diff(context=1) returns a unified-diff-style summary showing only changed lines with context:

res = exhash(text, [(addr, "s", "foo", "baz")])
print(res.format_diff())
# --- original
# +++ modified
# -1|a1b2|foo
# +1|c3d4|baz
#  2|e5f6|bar

All diff strings returned by format_diff, file_exhash, and cell_exhash are fastcore PrettyStrings, and the result objects' reprs show the diff too - so in IPython, ending a cell with the bare call displays the diff verbatim, no print needed.

Tests

pytest -q

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

exhash-0.4.8.tar.gz (45.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

exhash-0.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

exhash-0.4.8-cp313-cp313-macosx_11_0_arm64.whl (931.2 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

exhash-0.4.8-cp312-cp312-macosx_11_0_arm64.whl (931.1 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

exhash-0.4.8-cp311-cp311-macosx_11_0_arm64.whl (935.1 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.1 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

exhash-0.4.8-cp310-cp310-macosx_11_0_arm64.whl (935.3 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

Details for the file exhash-0.4.8.tar.gz.

File metadata

  • Download URL: exhash-0.4.8.tar.gz
  • Upload date:
  • Size: 45.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for exhash-0.4.8.tar.gz
Algorithm Hash digest
SHA256 2a36cef725acafdd61ae4d8411c7cb40f5dd59942b9990deb5a7332887b14e56
MD5 6216832075415b801aa1378c36ee1458
BLAKE2b-256 431082f496eccae24a10b61ddc8b6246572b7da4dcd07d87fa49ae9a9e135186

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8.tar.gz:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4aa2b04eb0b0114756879f3386a5244778dbe220282026f1e8cff183f74bd8e4
MD5 af2a82488345d91372df6ce4105c0005
BLAKE2b-256 d9e507d0f818bd98dcae6aed020df9acd711fd92f3eb70cec16d7aa552e1a669

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 57fae092c51de051045ad2c450dcee00be808bf7e3c7cfaa82525d9fdb06f9d0
MD5 98a0cd862e06f2f0f36011f89a6255c6
BLAKE2b-256 67d03c93faae24c0db47e9f87f0a157121b1ab5ce26249b344a1ded94993b531

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp313-cp313-macosx_11_0_arm64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 7d69ff5b9a99b6cde19e0cf43ed54b6f258327cff5ed7b9d7f3b230d700b85f5
MD5 0caf437bc8de4f26e42365f073e0c2de
BLAKE2b-256 57369508913cc70a2349f2688180823dd4124c75b1b9d7c160171ce6822ca88a

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1a4e3f284cefb2c8d3ada556eb75ad4abca7e19473ee94ac89816a2d9f3a80c7
MD5 92f58ad9d1bc1adf7a3ae0eec3a8059f
BLAKE2b-256 02641880e25ae9f99964cecfa492d6e86862210571f55092ab61c4362a5866d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp312-cp312-macosx_11_0_arm64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 53516a2b0c22e84fe1778733383a97d90d88b8b76d73faf252cebaf30d2a5f42
MD5 d1e1c2c52a46e22c60c5ca6bfe638d09
BLAKE2b-256 fc4503f8b316522232728fd9cd6efea52d233e64976b17cf998638113c7969e1

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ebb9b81455000a3a4c18981b7dde619c6e8f8224fbd224e42328b1c47ac2733c
MD5 7b00a854ad6b4078ee27cd112db2f17b
BLAKE2b-256 cd4f73753f4d867b542c1586aaa145104c0a761a32410dfefaa68bd9bd9b6f78

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp311-cp311-macosx_11_0_arm64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6a0857eec15cbd52cb69664306bac4a6d7d53d222d7688a82b394f3b3348e186
MD5 fefc09e1c6420c1da34d161caa3bbcca
BLAKE2b-256 e22ebb905997d8f8614e2295cc4df1de3e6d9b21e3255dc38a325d88ecc4f199

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file exhash-0.4.8-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.8-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 adfd512e6b5a4070b0671df51c5235c3e08da03755e373f6a1244fe0a815018a
MD5 dc93375e019459f68cd64789f3b58a0e
BLAKE2b-256 7113c738330c3e3ce1f1a328bc66aa972ee031f86298750733372b0b3cd4924f

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.8-cp310-cp310-macosx_11_0_arm64.whl:

Publisher: ci.yml on AnswerDotAI/exhash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page