Skip to main content

Verified line-addressed file editor using lnhash addresses

Project description

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

Project details


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.6.tar.gz (38.2 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.6-cp313-cp313-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.34+ x86-64

exhash-0.3.6-cp313-cp313-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.3.6-cp312-cp312-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.34+ x86-64

exhash-0.3.6-cp312-cp312-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.3.6-cp311-cp311-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.34+ x86-64

exhash-0.3.6-cp311-cp311-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.3.6-cp310-cp310-manylinux_2_34_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.34+ x86-64

exhash-0.3.6-cp310-cp310-macosx_11_0_arm64.whl (1.9 MB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.3.6.tar.gz
  • Upload date:
  • Size: 38.2 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.6.tar.gz
Algorithm Hash digest
SHA256 c86edef1dfa47a2133fe8f01e5b4ef69da94600d313c633a03718cf09b7f3c5f
MD5 da51c7d000fe676a7e7a3eacdff3aa5b
BLAKE2b-256 a13f4c78e78b4e39c450706f34e442eb4c9985c0b55aef435e28c178fb65ec1b

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.3.6.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.6-cp313-cp313-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.3.6-cp313-cp313-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 c356b64768d7056a47058dca8f74fcf9fc47932392bbc066dba06cf63932d1e2
MD5 24c50092834218cff499193b487e9a64
BLAKE2b-256 f3db788e06fc2809aabb8318f2d302487775caa15fb9ccf937901d406bb05ee8

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.6-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 da6190cd5fb398b1de99fbde01c4273c586c63835ca91e58064a00dccdb4fe51
MD5 368fc5bc6beb7bffc0c14af5b7b826cf
BLAKE2b-256 44b77f1cd4176054dbdf7bbf7e32c3baee183c2f81e78efe2e40741c10dacb6a

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.3.6-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.6-cp312-cp312-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.3.6-cp312-cp312-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 9536d70cede752c0dab2233521f0af4dfb42398b31981bf8750ed357caca6db7
MD5 ba198cdf0af854969fddf8ee5dc361a6
BLAKE2b-256 38686c298b2695106f0bcbd6c5c8873435cf8fad690008dadc943b0683bb2080

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.6-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7c6456f4fd211673d25806d69d48b1272800df80441e983f39cc2b459d640326
MD5 f2855353c0df01458d665101deec6e80
BLAKE2b-256 e463f6519bd022851f748c1ec7879717934e784cf360abaa65c138967f66e68c

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.3.6-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.6-cp311-cp311-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.3.6-cp311-cp311-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 2a1a94d4a8941ade8498ce2648fc351edbd4c095cbd1975f7b07901eace9f47e
MD5 d6cc535a74e939d0bcf26422e063a200
BLAKE2b-256 c9e53fca467878a128af07ce2e60d74abe9758b7f2213c563763ef68c52c206d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.6-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 73dd27f5858e6690b7eb1c33c0d8a87b2c5fb6d62e2beef98b8f6cc6af9b1ffd
MD5 4c5a5c5048166a35e3e87e3099b72662
BLAKE2b-256 071d3a0873c4f9ec810776eeb34ca6b2ce3cd98bd39a4065ced8fcbf6cb5ec83

See more details on using hashes here.

Provenance

The following attestation bundles were made for exhash-0.3.6-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.6-cp310-cp310-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for exhash-0.3.6-cp310-cp310-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 add716b1d64ee515adb303cafa02ed24b3dbd0143f1af1726d4fbedac277aa98
MD5 81ed33aedb18bdfa1a0b18ee73dcc553
BLAKE2b-256 166360ecbafa0a920f58962641496f2d4c0ba41c6de9c7608139282d6693865b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.6-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 ddb720a85b19610264eec9581ace58fb4bf58a62c1f8b3d6da11e23b710a9b86
MD5 3a3ba01fda46f5ad899d16bc138c010d
BLAKE2b-256 f0538b5f4d2e62b3a3e7a21edab13ab41a809e1ad978b1845d0670e99a8d8881

See more details on using hashes here.

Provenance

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