{"id":45041133,"url":"https://github.com/answerdotai/exhash","last_synced_at":"2026-04-02T00:49:24.373Z","repository":{"id":339324703,"uuid":"1161436036","full_name":"AnswerDotAI/exhash","owner":"AnswerDotAI","description":"Verified Line-Addressed File Editor","archived":false,"fork":false,"pushed_at":"2026-02-23T01:57:36.000Z","size":42,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-24T09:23:11.849Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://answerdotai.github.io/exhash/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AnswerDotAI.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-19T05:17:09.000Z","updated_at":"2026-02-23T10:43:15.000Z","dependencies_parsed_at":"2026-02-24T05:00:47.577Z","dependency_job_id":null,"html_url":"https://github.com/AnswerDotAI/exhash","commit_stats":null,"previous_names":["answerdotai/exhash"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/AnswerDotAI/exhash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Fexhash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Fexhash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Fexhash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Fexhash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AnswerDotAI","download_url":"https://codeload.github.com/AnswerDotAI/exhash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AnswerDotAI%2Fexhash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30246872,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T05:41:50.788Z","status":"ssl_error","status_checked_at":"2026-03-08T05:41:39.075Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-19T07:26:50.289Z","updated_at":"2026-04-02T00:49:24.363Z","avatar_url":"https://github.com/AnswerDotAI.png","language":"Rust","readme":"# exhash — Verified Line-Addressed File Editor\n\nexhash combines Can Bölük's very clever [line number + hash editing system](https://blog.can.ac/2026/02/12/the-harness-problem/) with the powerful and expressive syntax of the classic [ex editor](https://en.wikipedia.org/wiki/Ex_(text_editor)).\n\nInstall via pip to get both a convenient Python API, and native CLI binaries:\n\n```bash\npip install exhash\n```\n\nOr install just the CLI binaries via cargo:\n\n```bash\ncargo install exhash\n```\n\n## lnhash format\n\nWe 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.\n\nAddress forms:\n\n- `lineno|hash|` — hash-verified address\n- `$` — last line (no hash)\n- `%` — whole file (`1,$`, no hashes)\n\n## CLI\n\nThe native Rust binaries are installed into your PATH via pip.\n\n### View\n\n```bash\n# Shows every line prefixed with its lnhash\nlnhashview path/to/file.txt\n# Optional line number range to show\nlnhashview path/to/file.txt 10 20\n```\n\n### Edit\n\n```bash\n# Substitute on one line\nexhash file.txt '12|abcd|s/foo/bar/g'\n\n# Transliterate characters on one line\nexhash file.txt '12|abcd|y/abc/ABC/'\n\n# Append multiline text (terminated by a single dot)\nexhash file.txt '12|abcd|a' \u003c\u003c'EOF'\nnew line 1\nnew line 2\n.\nEOF\n\n# Dry-run\nexhash --dry-run file.txt '12|abcd|d'\n\n# Set shift width for \u003c and \u003e\nexhash --sw 2 file.txt '12|abcd|\u003e1'\n\n# Last line and whole file shorthands (no hash)\nexhash file.txt '$d'\nexhash file.txt '%j'\n\n# Move a line to EOF using $ as the destination\nexhash file.txt '12|abcd|m$'\n```\n\nSubstitute uses Rust regex syntax:\n\n- Pattern syntax is from [`regex`](https://docs.rs/regex/latest/regex/)\n- Replacement syntax is from [`regex::Replacer`](https://docs.rs/regex/latest/regex/struct.Regex.html#method.replace), e.g. `$1`, `$0`, `${name}`\n- `\\/` escapes the command delimiter in pattern/replacement\n- 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/`\n- Literal newlines in pattern/replacement are supported (joins/splits lines as needed)\n- Transliteration uses `y/src/dst/` and requires source/destination to have equal character counts\n\nWhen passing multiple commands, each command's lnhashes are verified immediately before that command runs.\n\nFor `a/i/c` commands, provide the text block on stdin:\n\n```bash\nprintf \"new line 1\\nnew line 2\\n.\\n\" | exhash file.txt \"2|beef|a\"\n```\n\n### Stdin filter mode\n\n```bash\ncat file.txt | exhash --stdin - '1|abcd|s/foo/bar/'\n```\n\nIn `--stdin` mode, multiline `a/i/c` text blocks are not available.\n\n## Python API\n\n```py\nfrom exhash import exhash, exhash_file, lnhash, lnhashview, lnhashview_file, line_hash\n```\n\n### Viewing\n\n```py\ntext = \"foo\\nbar\\n\"\nview = lnhashview(text)       # [\"1|a1b2|  foo\", \"2|c3d4|  bar\"]\nview = lnhashview_file(\"f.py\") # same but reads from file\n```\n\n### Editing\n\n`exhash(text, cmds, sw=4)` takes the text and a required iterable of command strings (use `[]` for no-op). `sw` controls how far `\u003c` and `\u003e` shift. For `a`/`i`/`c` commands, lines after the command are the text block (no `.` terminator needed):\n\n```py\naddr = lnhash(1, \"foo\")  # \"1|a1b2|\"\nres = exhash(text, [f\"{addr}s/foo/baz/\"])\nprint(res[\"lines\"])    # [\"baz\", \"bar\"]\nprint(res[\"modified\"]) # [1]\n\n# Multiple commands\na1, a2 = lnhash(1, \"foo\"), lnhash(2, \"bar\")\nres = exhash(text, [f\"{a1}s/foo/FOO/\", f\"{a2}s/bar/BAR/\"])\n\n# Hashes are checked just-in-time per command.\n# If earlier commands change/shift a later target line, recompute lnhash first.\n\n# Append multiline text (no dot terminator)\nres = exhash(text, [f\"{addr}a\\nnew line 1\\nnew line 2\"])\n\n# Change shift width for \u003c and \u003e\nres = exhash(text, [f\"{addr}\u003e1\"], sw=2)\n\n# Custom delimiters (useful when pattern/replacement contains /)\nres = exhash(text, [f\"{addr}s|foo|bar|\"])\n\n# Literal newlines in pattern/replacement (joins/splits lines)\na1, a2 = lnhash(1, \"foo\"), lnhash(2, \"bar\")\nres = exhash(\"foo\\nbar\\n\", [f\"{a1},{a2}s/foo\\nbar/replaced/\"])\n```\n\n### File helpers\n\n`exhash_file` and `lnhashview_file` read directly from a file path:\n\n```py\nview = lnhashview_file(\"file.py\")\n\n# Returns EditResult, file unchanged\nres = exhash_file(\"file.py\", [f\"{addr}s/foo/bar/\"])\n\n# With inplace=True, writes back on success and returns diff string\ndiff = exhash_file(\"file.py\", [f\"{addr}s/foo/bar/\"], inplace=True)\n```\n\n### EditResult\n\n`exhash()` returns an `EditResult` with attributes (also accessible via `res[\"key\"]`):\n\n- `lines` — list of output lines\n- `hashes` — lnhash for each output line\n- `modified` — 1-based line numbers of modified/added lines\n- `deleted` — 1-based line numbers of removed lines (in original)\n- `origins` — for each output line, the 1-based original line number (None if inserted)\n\n`res.format_diff(context=1)` returns a unified-diff-style summary showing only changed lines with context:\n\n```py\nres = exhash(text, [f\"{addr}s/foo/baz/\"])\nprint(res.format_diff())\n# -1|a1b2|  foo\n# +1|c3d4|  baz\n#  2|e5f6|  bar\n```\n\n## Tests\n\n```bash\ncargo test \u0026\u0026 pytest -q\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Fexhash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fanswerdotai%2Fexhash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fanswerdotai%2Fexhash/lists"}