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 both a convenient Python API, and native CLI binaries:

pip install exhash

Or install just the CLI binaries via cargo:

cargo 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 Rust's DefaultHasher over the line content.

Address forms:

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

CLI

The native Rust binaries are installed into your PATH via 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, exhash_file, 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

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")])

File helpers

lnhashview_file reads directly from one file path. All file paths, including file-qualified addresses, expand a leading ~ to your home directory. exhash_file(path, cmds, sw=4, inplace=True) uses path as the default file context for unqualified addresses. 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 = exhash_file("file.py", [(addr, "s", "foo", "bar")])

# With inplace=False, files are unchanged and a FileSetEditResult is returned.
res = exhash_file("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 = exhash_file("new.py", [("0|0000|", "a", "print('hi')")])

# File-qualified addresses can edit or transfer lines across files.
cmds = [
    ("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"),
]
diff = exhash_file("src/a.py", cmds)

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

exhash_file(..., 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 exhash_file
  • 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. exhash_cell(path, cell_id, cmds, sw=4, inplace=True) edits one cell; like exhash_file it writes and returns a diff by default, and inplace=False previews the EditResult without touching the file.

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

Tests

cargo test && 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.3.tar.gz (37.6 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.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.0 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

exhash-0.4.3-cp313-cp313-macosx_11_0_arm64.whl (900.4 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.0 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

exhash-0.4.3-cp312-cp312-macosx_11_0_arm64.whl (900.5 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.0 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

exhash-0.4.3-cp311-cp311-macosx_11_0_arm64.whl (906.0 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.0 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

exhash-0.4.3-cp310-cp310-macosx_11_0_arm64.whl (906.4 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

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

File hashes

Hashes for exhash-0.4.3.tar.gz
Algorithm Hash digest
SHA256 19eca6ba14e4bbc9d02580e1fb0cbddd0b04812eeddfbbe380971a1311de2db2
MD5 404d445bed807d78dcce575b6b5751b8
BLAKE2b-256 130878148a802f9a2c1112178c94bc14bdf8a06d427c3e342587a5530f46f694

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3.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.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 6678efbe9ef4a2fdd20a0e24622b6308b9acec4409b535b17c1ac9bba548ce4c
MD5 747a979c843ce9ae3b5e6f74b754a356
BLAKE2b-256 7036e6db59610f53fc4d2ecfad29dddb5f90e4480074631e7bd0c2c1324c050f

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 566415c230d2137a752866f5a967ebec868d70374be5c2db6f1665790a8b4d27
MD5 2f411871d8925b74028ff8fac0e6e292
BLAKE2b-256 820d55457b1a7ec9ba55078c02483550c04ee95cd51d954edc2f663153c876d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0d555305a72e5877f89f67125da3980b3ee049832cb0c05d70c74e1368db469d
MD5 8af1a8e5f98e1798c8013f2acff15a5f
BLAKE2b-256 ce14b66ea4b2afa6ac4cecb052b7b133509ad0be7c97a1939c345debe7c54a15

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f8dbf2bfc53409b5696562945edd8be77b09a37abb3b732e39c05450b818a9d2
MD5 d2a434d2b5496b517c6ab750b8666f1a
BLAKE2b-256 c9883367c2123fc54ecb946f002de652ebd674a2027680810ebe0a921f191fb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a4c8ae54c1da97eeb1e9c74df65d63a4a959b50c8ca1b7ca55874cd6a1429a31
MD5 3fb98ca23a7728b4317908f4d67c86ed
BLAKE2b-256 ec22b06ec6f0e9d300f21db35239039fc108d6f45dea9ffdfdd74185fdc9e39e

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 cda7b85b88c9acc0cef7ce216248a5b6218be37f1012e1460e0631eccd6dfcb6
MD5 2b5888c92922dc408424e0d254c48e17
BLAKE2b-256 75f5c574b2592f2b13dc1fc336c67121800c0238520be4568d8fe3466f97e9ad

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f36c38be3b24e4242320fc2218feb945db39fc677677b988023b84a58a514a85
MD5 fc1ff87ce5041e9e38be9ee7d7765bec
BLAKE2b-256 c5ee27764e2d06a89436218ae51efa05e7b011fdf6852e255918369e1646a232

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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.3-cp310-cp310-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for exhash-0.4.3-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e5fb84f64472135f3060647a70f61e765f7e98efc63591850b7368b48511bf36
MD5 9e279224c00e221aac49d86ceb7eb5e3
BLAKE2b-256 98aa25281677262a4cce2175b4760680e958da6a17cc5fcdfc09c6ca730ae4ad

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.4.3-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