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

Uploaded CPython 3.13macOS 11.0+ ARM64

exhash-0.3.13-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.13-cp312-cp312-macosx_11_0_arm64.whl (900.2 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

exhash-0.3.13-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.13-cp311-cp311-macosx_11_0_arm64.whl (906.8 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

exhash-0.3.13-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.13-cp310-cp310-macosx_11_0_arm64.whl (907.1 kB view details)

Uploaded CPython 3.10macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: exhash-0.3.13.tar.gz
  • Upload date:
  • Size: 33.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.13.tar.gz
Algorithm Hash digest
SHA256 c6ffa1ab70021a6082cd3f1d0a1ae0f6861f23db3c42010b17471df0e81df8db
MD5 9c91cd11c36e9053a656637c006bd725
BLAKE2b-256 4360ee751baccd26ec7f2e07db37bf77fd0d8d4c6a449839925f4824328bbdd5

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 369b3cf6e21d435424cb454c25e72e1bce5b7464780de5c024807eabf93a8892
MD5 cf85e98777a350f23f854773822bfcef
BLAKE2b-256 86e83deff746dc6c3553337f37ca0003fd35f8cc37256c46561ad0ac4f8acf52

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 0776e72763a7369be548ded794a81486070aab23d29d93d58b38e4854da1a6ef
MD5 d2c19ded6ed3f56065b1919dfb83e21c
BLAKE2b-256 01486d92ec4792cb4968da8100b05fbf9769985414d7968832d4553f24ca3cbb

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 206d57f4fcfaa1a4efb59e56c65a760a2728e13fd8325c13408a844c47c4cfd3
MD5 fd181aed36b90c25e3b934e19093f117
BLAKE2b-256 d5238e78b479b427e7efbc7bd05c83d6301df1d9490e71cffd3b8a828dd59b32

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 aa836f8bdac1388381a8482198680a0cc37f6e2c20da3b6b97c02c8749dbb66f
MD5 d9e8fc666abf6991dd4521355bcfbf51
BLAKE2b-256 092c6b4454a8dda8c839c4735546e5751ee5d0b06c0fc4b4d360eb4c3ca5699c

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 06a0f24b643df568ed56efd5fc84ce56d332d25a6054ace7e9a963b35828c02d
MD5 6f0260e900423a620e8edb2079520eb3
BLAKE2b-256 b6f5e371ae85a3415ccf653f26e608a73cf54fefaa0bba6f83738a3577f5e290

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 782e3d2f10760dbce9e7845395c381b5fc3f06b6f2fa1a0bb4afc37b3989fac9
MD5 f014b996bcd92b14d7d843b00aa746f3
BLAKE2b-256 14f02afa02102487a27fdc90b6338c0ae543df9173ff4aba20841ee594786f6b

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 fb4e71dda8e07eebe89ce06b1b9aa629a6e4907ae9c6286a5c29897641af0b1e
MD5 67950d5d6d6bcc8b04ab081bf0e0f9b3
BLAKE2b-256 ea5357fa350ea02223bcc03cb6ac6ad8b871aeca57bd654ae642a25099974e12

See more details on using hashes here.

Provenance

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

File metadata

File hashes

Hashes for exhash-0.3.13-cp310-cp310-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 dac979f168297cc26500f578ee7b673c6e991f7bf4a3982e636f53c22e774b9d
MD5 4fc98f243ec9613690117ac3fbfafe3e
BLAKE2b-256 84671832c7c2b2e531ef687ce9f4303667d19c566ad35dec423e534a395bed28

See more details on using hashes here.

Provenance

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