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.12.tar.gz (37.9 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.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.3.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.3.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.17+ x86-64

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

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.3.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.17+ x86-64

exhash-0.3.12-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.12.tar.gz.

File metadata

  • Download URL: exhash-0.3.12.tar.gz
  • Upload date:
  • Size: 37.9 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.12.tar.gz
Algorithm Hash digest
SHA256 27b025e7ee1b21ad35273137d686873de51b71245acbce2e647f88aea7f66bfe
MD5 16f030815c07b4f1d50a6fe633115469
BLAKE2b-256 f5240b69671dddd3367766d8bf913e8d7e6a82618c8a0b9aa420114a7618119d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 099f97aabf70edb5c4d012e78e929a32035e6833988eeddd7343eff2cc9dd474
MD5 516089db56040637eb8a4f8ac4c9772d
BLAKE2b-256 d2fab0e937c05f3d4f5b7292279763226c71db8e7788742ec568b54f5b49c2ea

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 369d1eb99625c3be114ab4830ca03cc207f695dc28609a80e9e1021835fc2d59
MD5 1f08def6338b6270e1b27a82cbca404c
BLAKE2b-256 9dca6cdcdebc1376ab5e04d0262e4e5f316065fc41f8e71ceac1bfa831e5161c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 22e5444187a66f85bb2cdaba97cd42ceb42c22d97a1a5e526d2751c05c417b66
MD5 84310b008645df0912c180a33be544a3
BLAKE2b-256 0e51b42e4bd819dbe82ca6411e775a50bbe88af5fff61c9fc95f0b732d0a8b3a

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 de033330bff3be236607212e8dc01c41cfbe8f8ef2936ec3809927dc09d7e1c5
MD5 f5309596b6396ebd6a423da7af9ea1f0
BLAKE2b-256 1a1ab385fc7bb498fde5b447a6e3cf5cfdef91ac395b194a4e6be4fc745cc35d

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 f9c35ff481ebb09aab9eed93dabef76175ac1f306f88cfed8a3797bd264ecfca
MD5 08deda39b459f8cb9fdc9193575e66ed
BLAKE2b-256 0e4d6324f71530635431a85a2d728e7c7bc022f1026c5d64d7debf78fd4625aa

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 90312b1ac951dfb684c9cdda3bd6e46eb7dcb267205e61a4882e3d93ade2b26b
MD5 e28d00d2a52fcf1de7746afacf5cec93
BLAKE2b-256 8c099e8abd9f2399c0bc013b9d6b12d21dbecb82e3deae782836e585c6ec3863

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 69be0ef40c91a1a29cd2e2f55b15c75da1e5d1b044ffae9431cdfc579e51e1dc
MD5 4419f0a4375eb1d74953bdaec6299c9a
BLAKE2b-256 deaa9ff3ca01cbd4dbe8b9604dcf71c65f7dfa89b2f8bcd918845c12c40c1ed2

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.12-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 494b9395770d44f8f91444c0189113f5e06ec94fe3dfdf61a82e8778a7e7ce28
MD5 65ea4fa7d93d2e6328c72c89e9e4576d
BLAKE2b-256 ceb65a145d57efedb68cbcbe6af8d57eed00f4fd44f689a8ba87f4b23233f025

See more details on using hashes here.

Provenance

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