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
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)The native Rust binaries are installed into your PATH via pip.
# 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
# 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/'
# 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$'
Substitute uses Rust regex syntax:
regexregex::Replacer, e.g. $1, $0, ${name}\/ escapes the command delimiter in pattern/replacementy/src/dst/ and requires source/destination to have equal character countsWhen passing multiple commands, each command’s lnhashes are verified immediately before that command runs.
For a/i/c commands, provide the text block on stdin:
printf "new line 1\nnew line 2\n.\n" | exhash file.txt "2|beef|a"
cat file.txt | exhash --stdin - '1|abcd|s/foo/bar/'
In --stdin mode, multiline a/i/c text blocks are not available.
from exhash import exhash, exhash_result, lnhash, lnhashview, line_hash
text = "foo\nbar\n"
view = lnhashview(text) # ["1|a1b2| foo", "2|c3d4| bar"]
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 a/i/c commands, lines after the command are the text block (no . terminator needed):
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.
# Append multiline text (no dot terminator)
res = exhash(text, [f"{addr}a\nnew line 1\nnew line 2"])
# Change shift width for < and >
res = exhash(text, [f"{addr}>1"], sw=2)
lines — list of output lineshashes — lnhash for each output linemodified — 1-based line numbers of modified/added linesdeleted — 1-based line numbers of removed lines (in original)exhash_result([res1, res2, ...]) renders modified lines in lnhash format, matching the old repr(EditResult) style.
cargo test && pytest -q