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

Uploaded CPython 3.13macOS 11.0+ ARM64

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

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.4.4-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.4-cp311-cp311-macosx_11_0_arm64.whl (906.2 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.4.4-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.4-cp310-cp310-macosx_11_0_arm64.whl (906.5 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.4.4.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.4.tar.gz
Algorithm Hash digest
SHA256 7e1e3e2d87922d9c2f7069a086f48095357fb9aa255b4c9fd30c220e5d8ccbf2
MD5 0ede4a636f8a6507766b569d8c9ee2e9
BLAKE2b-256 84db5a91f879709bb42ed2f5bed5f7baeb1b09c12d7d718325d6ecf64d96492c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 44a96d4ab03261141c4b17bd5aafdf8c6d5e6ee2dcee019e154d40f9a9425f89
MD5 7ea505e4799b2defc2e7003a74462a29
BLAKE2b-256 207cba1ee737fd409faa5f105fa1cd525eab59a1fef728799b4dc75cc386e097

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 2a8632488c25485515fe9a3413d5781fef60ff0bfb384dc3bac5a1455af66120
MD5 ae003bc7005c7477c650e1a37852c063
BLAKE2b-256 f7c7b07afe319dc610c1506a3d3891e857ba849a69025e40adb184e290efedf2

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 a47c2849e94ad91854eb2c5307b496b4a72f776f432d8660c7c87d3164726f91
MD5 23c648fea231002fb6ec464a14f0ae3a
BLAKE2b-256 bf391398043c3e730cc3337a07aaac7328c80c813e55540dd6c991bdb52698d6

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9a8eee27f60cd76106eb61a34e0b229db32baa472cb67b5ce87a1aeb29295887
MD5 351a5dff16694d896d4205ab68ea8772
BLAKE2b-256 16c5144e9fdb08699f87b2e623ae8111dcbd4820f533eecba34694c5860b7454

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 d303075eb59879c401927387adc81313d3abf6633b789cb4ae4cceec8ddc83b9
MD5 8273f6901e6ab6c468c2165ab155c7d7
BLAKE2b-256 a1d582c545ffafa3f93718ba7f82efcc81c2ba9c63c4369b52f8658c5a427468

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 4694ca7d61e35da46d0eb62ae8ae1ae566cff3b9df3248bfa99f5b59d3adb692
MD5 c7a4e7d1ec06b1c0a8a97e5573d3df05
BLAKE2b-256 f55debcd657e3d737935372b9c7fbed1045e56a8ac4c35ae7f062a2381ecb6f7

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 138e6fc52bf3010e0fadd9a388289d8779508f634941479a627e290aba1284f0
MD5 3a70e572cb3904dc76cec2450b3f8e7f
BLAKE2b-256 07c69dfda960214df6c9d96d45eac65a8b959293f8dec8f7c661fb0bbd49b14c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.4.4-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b2a8d523754044b1ed104b84272890f107242e1823f677a15160e48442eab96f
MD5 ee98e08ee5bba8b381e88bd761ccbd69
BLAKE2b-256 7b2cc9e38e00b73156bfeb6185d12c64e31843c89bcd1e1c36ff07f0d07d03b7

See more details on using hashes here.

Provenance

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