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.5.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.5-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.5-cp313-cp313-macosx_11_0_arm64.whl (900.6 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.4.5-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.5-cp312-cp312-macosx_11_0_arm64.whl (900.6 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.4.5-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.5-cp311-cp311-macosx_11_0_arm64.whl (906.1 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.4.5-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.5-cp310-cp310-macosx_11_0_arm64.whl (906.6 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.4.5.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.5.tar.gz
Algorithm Hash digest
SHA256 318d61d1bedf2c8721c0e9f51edaa2e512cb2b53815b20b6cf2531b208a4893f
MD5 60d43b06cbcfa3516cbc27ba499071bd
BLAKE2b-256 375328024e5921d2a3343555690dab34265f6336b9f1d14e202ecb20ab55683c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 15e2b6a44886fdeab4a59cc1354aacbafdebd7f26a667993310fc42909a360bb
MD5 1a722dd564a6bc4368031d91ae4dad2a
BLAKE2b-256 0bd9ec2c11632434fb80c271d0764c77daf536e79f5c96189ef6176f48e5f5db

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b1ade92b1a587dc2bcd8467fe36be826bb58b98e196ee36dc672aee08acd9a80
MD5 2d2c789cdeae86279131d65dc6d3c533
BLAKE2b-256 5f85bbf1d9bc31b00bf85914f5699083e9618c555df943e517b0db80d781b4d5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 8900408c49c0f4f5a8220d7c5f8c7327b201a56c638221519f844eb8c48dbbdc
MD5 9d3db3770a282063d03a491c4b38321f
BLAKE2b-256 cd90439ef559ccd770fd72d3f37dde8423e9c69d3992835a0280404966749fcb

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 37bd6138ee6cea5832ef22773de2bcb39998ee6639f81e6e079d160af1c2a6e9
MD5 4f05b2e83e2b7225bdc99e1351d4ca92
BLAKE2b-256 eb938ed57ecd6b08b71c8404e7db80b30740e829e95f24bfb62c6a86474a97bc

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 77171fcce91c03ab2e74ea01b9e378a88ff640714aad874f4f02428d4095db68
MD5 20e56c3c3209523d63ec973ce2511a0e
BLAKE2b-256 5224260bc8d85020d2aa099f041e49e303c3b36686646b7b6cf5b7e3f4b915af

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1530e3aecf52d0b81d2b440f5bd5426a7f981c086323ff9608675e303302cdaa
MD5 d990bd5b37fd52d0a0cde7284eac57e8
BLAKE2b-256 799bf442cd62a0b330cc5e3fbdc792f5588648b5da8ceade14d00d7a52aa1b51

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f00baca2a60cb0e7a58142a505f74da5a962381e790baaf1a1fbd0388e6cec02
MD5 e7541b1c06f5ff073c6eb90ce6a6f186
BLAKE2b-256 c5032f79d6b0b7798408e2d2a52198af1b67907268d2bfeca62ad65a402a02b1

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.5-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 72d9425348b5d0f0a9d80536b9cc5e36416c073d376da2e8f325605662a9995d
MD5 9087ef872cf7dd660c27bdded2cbd521
BLAKE2b-256 90157a6078eed5cf3492204bed54fa88d9cea52112a2a5f54fde053b1be8e51b

See more details on using hashes here.

Provenance

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