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

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. 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.2.tar.gz (36.5 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.2-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.2-cp313-cp313-macosx_11_0_arm64.whl (900.0 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.4.2-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.2-cp312-cp312-macosx_11_0_arm64.whl (900.0 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.4.2-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.2-cp311-cp311-macosx_11_0_arm64.whl (905.7 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.4.2-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.2-cp310-cp310-macosx_11_0_arm64.whl (905.8 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.4.2.tar.gz
  • Upload date:
  • Size: 36.5 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.2.tar.gz
Algorithm Hash digest
SHA256 56b3d2ca939d8207dd3eb58e68e54309e57b9711e77ba9cb7f8d77f38f074d7a
MD5 26308e83e831864e65240c6a232a7e3b
BLAKE2b-256 5db0e0981f13d6cb6a09f703cc976927a9ee2e634d4e1faf324115c6d94b6ff6

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0a7c4f9dece51df7c80482420d1d220005366a660eb8c04c27df3164603dbd24
MD5 4ed74c33d48001c9105089c458472640
BLAKE2b-256 fcf7d5ae5ab86cb4cac123a6731b8e812ccf7cb75fab5dcd2cab4e930f9db65b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7af1f2e29c801b96f746b2b10c935714a7bca39916ea3d17409e4f3916ef7e3a
MD5 f2f648b394a7fbedbe911d9c35e59fb0
BLAKE2b-256 d59ae66d2eae353be7d6c7014722cdf541c1192268eed65f45969a22614d41ba

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 dd4d82f30cf269e19683b1e670d04a476f22efb6b3e7a1bef5bdaec08097cb14
MD5 20123965ceb30ad676a483a921d4230c
BLAKE2b-256 68ac6b02dc0cc0410d9e8d96cea8f90bd0ce9b55d00074dec0bf221c183c923c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 1808a33fcf427f37696706428fbab12062a6b3e85496e2ed59c8ee072340c879
MD5 1a28a4568c5c7dd461966fd323c4de03
BLAKE2b-256 ad5fd9c87ef7ee9d3d7c549c392775999fd9aa0608fffe75655a10c571b16bee

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4bde399c0d8d30fe08d08a0b4884bcac1e4944b6fccb1d11edbe54bfa94eb5e8
MD5 64600329864eb4f6414855cf602027fa
BLAKE2b-256 6a8b6df0523629cd8b7cf501f2f208636d6651e2884c65b30cbc6cd2f72e3356

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 e99381494ec5688b760a1773ae69c3ff901934a77d63e19dc92d97bf972c1710
MD5 4f196de2a4445500a37ccf527a98c2f1
BLAKE2b-256 7cb7f2cace69e2cd89a748572d11c781e7151aa2d167a4c167365819f1b8a37a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 626e2fb817526e18666ade1bf424aef64ceb4080d33bf92740e511f3b16c3a17
MD5 5c442dba786be8fd6932acdda2d0b6d4
BLAKE2b-256 39d78a856764fa53ee8ed7ba9d53e4b2bc05a5c2079638e2eaeb9323a3474ad0

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.2-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2eef8e011deb534fd633bf2fe651d259657de2284785012a6d44c89d7597eb35
MD5 a5b62981bf1cc5cde5998c7b7f126ff1
BLAKE2b-256 df5d38f5bac666c9709f339f95347985f8fdf82bdaa66fc3eb1fe499f5966864

See more details on using hashes here.

Provenance

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