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/
  • Literal newlines in pattern/replacement are supported (joins/splits lines as needed)
  • 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 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 command strings (use [] for no-op). sw controls how far < and > shift. For single-line a/i/c, text after the command character is literal inserted text, including leading spaces, e.g. ["12|abcd|c return x"].

For multiline a/i/c commands, include the inserted text in the same command string using newline characters. Text after the command character is the first inserted line, so f"{addr}cfirst line\nsecond line" and f"{addr}c\nfirst line\nsecond line" are both valid. Do not use . terminators, and do not split the text block into separate cmds entries. If you include a final . line, it is inserted literally and exhash emits a warning.

addr = lnhash(1, "foo")  # "1|a1b2|"
res = exhash(text, [f"{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, [f"{a1}s/foo/FOO/", f"{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 with inline text; spaces after c are part of the replacement
res = exhash(text, [f"{addr}c    replacement line"])

# Append multiline text in the same command string (no dot terminator)
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])

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

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

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

# Custom delimiters (useful when pattern/replacement contains /)
res = exhash(text, [f"{addr}s|foo|bar|"])

# Literal newlines in pattern/replacement (joins/splits 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=False) uses path as the default file context for unqualified addresses, and also accepts file-qualified source and m/t destination addresses:

view = lnhashview_file("file.py")

# Returns FileSetEditResult, files unchanged
res = exhash_file("file.py", [f"{addr}s/foo/bar/"])
print(res.changed)          # ["file.py"]
print(res["file.py"].lines)
print(res.format_diff())    # includes --- file.py / +++ file.py headers

# With inplace=True, writes changed files after every command succeeds
# and returns the combined diff string.
diff = exhash_file("file.py", [f"{addr}s/foo/bar/"], inplace=True)

# Missing files are treated as empty only when the command is valid on empty input.
diff = exhash_file("new.py", ["0|0000|a\nprint('hi')"], inplace=True)

# 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/from \.b import old/from \.b import helper/",
]
diff = exhash_file("src/a.py", cmds, inplace=True)

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

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, [f"{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.3.15.tar.gz (34.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.3.15-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.3.15-cp313-cp313-macosx_11_0_arm64.whl (900.5 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.3.15-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.3.15-cp312-cp312-macosx_11_0_arm64.whl (900.7 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.3.15-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.3.15-cp311-cp311-macosx_11_0_arm64.whl (907.5 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.3.15-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.3.15-cp310-cp310-macosx_11_0_arm64.whl (907.4 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.3.15.tar.gz
  • Upload date:
  • Size: 34.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.3.15.tar.gz
Algorithm Hash digest
SHA256 a689bf7d792b65297073268fc5291a138243885f4f37aa7563d37a215cc2a99b
MD5 69aa86c02e78b7859a386703cad54646
BLAKE2b-256 01f99f679eeb621bb679a321b93add7e93405556bbaf0031cf63eeccc12d5b84

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 57dfdbdbfd8bb206633ddb80493a9f5d2f208e56bd4b3d5116e01f16acd59b77
MD5 145b601ec9322f2887d51876fa6e6538
BLAKE2b-256 5252065c077a228c9734c6387f231c5195c4b9ebd0bc840f61a8a35ddbdfb1f8

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 99da7cb941a450f3a40faddd0e88bccdf6afbc4d35f7e4672cef373b8cea3759
MD5 e7a5ffaeb4553cd81fe07f74eb95a95e
BLAKE2b-256 bf37550a41d8fe8c0da8269142d8c6f11bf1a91f102fcb87b494956625d99af2

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 70034cf7b57a1a3f8555be71e488551e075e29068e6ccc80c962bad515d6125c
MD5 ead7a6e88b4ac5e75678de17c476c4bc
BLAKE2b-256 37fd87a0c16f8e467f915e33031d733b7e5680511ef16992369066c7e316b134

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5b48bb03af5c386fcaa45a818c2842cd680207bb86c3f66f24bda491d35e0fb4
MD5 355ee33e4cf43434afb39f73738bffc0
BLAKE2b-256 6be5d96f373e36718e5fb6d222889202ba55ccb9b7997007dc37bcbe56ddcdff

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 ddad069f6c6f1c62df241177977754e3933362e756827acda2926c1b1de1f2de
MD5 af7013d70c4b56681d50618de57d70a0
BLAKE2b-256 059e5490424c85e72cfefed2b8e90cd1a1572c9bb55dade175b3595191f39b6f

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 112a0ed8ca1b9b57cc894d404adffcf8b8cb84ed7d1b8d937f7e60dc99cecd95
MD5 d978d7599defadea4281e2aedea7cd51
BLAKE2b-256 36ed9cd60e43252f3a5803051d97a8b2a820744a6bef5e3f8a7c09a25dc31e8d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 4d25364cafe6ea7aa4d320e4f4447007f62b877da294407c655e380173aed6a9
MD5 74f2ce74d94ee52050af0154c4fa2302
BLAKE2b-256 1ff972ead3d177051ff54847ad3d06dd34a956b071125834de5a01b141e25880

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.15-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 55150d10171c3b9b9b6a0efebdd58cbf09e199b7fe22e7c8997dcd203c18ee68
MD5 d1bd0267e918dc8256f3544e89b53c85
BLAKE2b-256 125b87759ffe40301cc99aad95adf2c9a5ee923aa9eacab13ea9213f78be72cd

See more details on using hashes here.

Provenance

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