Metadata-Version: 2.4
Name: read-write-edit-tools
Version: 0.1.0
Summary: Fast hex-line-numbered file read, write, and edit CLI
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/llaa33219/read-write-edit-tools
Project-URL: Repository, https://github.com/llaa33219/read-write-edit-tools
Project-URL: Issues, https://github.com/llaa33219/read-write-edit-tools/issues
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# read-write-edit-tools (`rwe`)

`read-write-edit-tools` is a small, fast command-line tool for reading,
creating, and editing UTF-8 text files. It is written in Python, uses only the
Python standard library at runtime, and installs two equivalent commands:

```sh
rwe
read-write-edit-tools
```

Examples below use the shorter `rwe` name. Replace it with
`read-write-edit-tools` anywhere if you prefer the long name. The **PyPI
distribution name is `read-write-edit-tools`**, available at
<https://pypi.org/project/read-write-edit-tools/> after the first automated
release.

The tool deliberately has a narrow scope:

- `read` displays text with stable, 1-based hexadecimal line addresses.
- `write` creates a new file and **refuses to overwrite anything that already
  exists**.
- `edit` replaces one or more lines, including multiple lines in one block,
  using addresses from the **original file**.

It is not a general-purpose editor, binary-file tool, search tool, merge tool,
or interactive terminal UI. It does not delete lines or files, append to
files, create parent directories, or provide pattern-based substitution.

> **The essential `edit` rule:** every address in one `edit` command refers to
> the file as it existed **before that command started**. If one replacement
> adds or removes lines, that does not change any other address in the same
> command. The complete example below shows this explicitly.

## Contents

1. [Requirements and installation](#requirements-and-installation)
2. [Command reference at a glance](#command-reference-at-a-glance)
3. [Line addresses and shell conventions](#line-addresses-and-shell-conventions)
4. [Complete workflow](#complete-workflow)
5. [`read`: display numbered content](#read-display-numbered-content)
6. [`write`: create a new file only](#write-create-a-new-file-only)
7. [`edit`: replace original lines](#edit-replace-original-lines)
8. [Input, output, paths, and encoding](#input-output-paths-and-encoding)
9. [Safety, preservation, and limitations](#safety-preservation-and-limitations)
10. [Errors, exit codes, and troubleshooting](#errors-exit-codes-and-troubleshooting)
11. [Development and project layout](#development-and-project-layout)
12. [License](#license)

## Requirements and installation

### Requirements

- Python **3.9 or newer**
- **[uv](https://docs.astral.sh/uv/) is recommended** for installing and
  running the CLI in an isolated environment
- `pip` is supported as an alternative
- A UTF-8 locale is recommended so filenames and terminal messages display
  correctly
- No third-party Python runtime dependencies

The package has been tested on Linux with Python 3.14. The code uses standard
Python APIs, but other Python versions and operating systems have not been
verified as part of that test run.

### Recommended: install from PyPI with `uv tool install`

Install the published `read-write-edit-tools` distribution as a persistent,
user-level command-line tool:

```sh
uv tool install read-write-edit-tools
```

`uv tool install` creates an isolated environment for this package and exposes
its commands in uv's tool executable directory. It does not install the
package into your current project's virtual environment, and it does not
require `pip` in that environment.

After installation, both names are available:

```sh
rwe --help
read-write-edit-tools --help
```

Both print the package version with `--version`:

```sh
rwe --version
read-write-edit-tools --version
```

If your shell cannot find `rwe`, follow uv's PATH instructions or run:

```sh
uv tool update-shell
```

Then start a new shell or reload its configuration as instructed by uv. You
can inspect the installed tool with:

```sh
uv tool list
```

#### Upgrade or remove a uv tool installation

Upgrade to the latest published version:

```sh
uv tool upgrade read-write-edit-tools
```

Remove the persistent tool and its command links:

```sh
uv tool uninstall read-write-edit-tools
```

These commands manage the installed CLI environment, not files you created or
edited with `rwe`.

### Ephemeral PyPI use with `uvx`: no persistent tool installation

`uvx` is uv's tool-running interface. It creates an ephemeral tool environment
and does not add `rwe` to your persistent installed-tool commands. By default,
uv keeps that environment in its cache for fast reuse; `uvx` does not
necessarily create and delete a fresh environment for every invocation.

Run the `rwe` command supplied by the full `read-write-edit-tools` PyPI
package:

```sh
uvx --from read-write-edit-tools rwe --help
uvx --from read-write-edit-tools rwe read notes.txt
```

The explicit `--from read-write-edit-tools` is important: it names the PyPI
distribution, while `rwe` is the executable provided by that distribution.
The long executable name works too:

```sh
uvx --from read-write-edit-tools read-write-edit-tools --version
```

“Ephemeral” describes the **CLI installation environment**, not the files on
which the command operates. `read` only reads, but `write` and `edit` still
create or modify their target files exactly as documented:

```sh
uvx --from read-write-edit-tools rwe write ephemeral-demo.txt 'First line
Second line'

uvx --from read-write-edit-tools rwe edit ephemeral-demo.txt '01: Replacement for original line 01
An additional line
02: Replacement for original line 02'

uvx --from read-write-edit-tools rwe read ephemeral-demo.txt
```

The resulting `ephemeral-demo.txt` remains on disk after `uvx` exits. By
default, uv may cache downloaded packages and execution environments to make
later invocations faster. `uvx` therefore does not promise to erase every
cached byte immediately or avoid network access on an uncached first run.

For a fresh temporary cache for one invocation, use:

```sh
uvx --no-cache --from read-write-edit-tools rwe --version
```

To refresh cached package information when needed:

```sh
uvx --refresh --from read-write-edit-tools rwe --version
```

### Alternative: install from PyPI with `pip`

```sh
python3 -m pip install read-write-edit-tools
```

Remove that installation using the same Python environment:

```sh
python3 -m pip uninstall read-write-edit-tools
```

### Install a local checkout for development

For a checkout that is not yet published, or to test local changes, run these
commands from the directory containing `pyproject.toml`:

```sh
uv tool install .
uv tool install --reinstall .  # Reinstall after changing the checkout
```

Use the local package with `uvx` by changing the `--from` source to `.`:

```sh
uvx --from . rwe --help
uvx --from . rwe read README.md 01:0a
```

A pip virtual environment is another development option:

```sh
python3 -m venv .venv
.venv/bin/python -m pip install .
.venv/bin/rwe --help
```

### Run from Python source without building or installing a tool

From the source checkout:

```sh
PYTHONPATH=src python3 -m rwe --help
PYTHONPATH=src python3 -m rwe read README.md 01:0a
```

This bypasses both `uv tool install` and `uvx` and is mainly useful during
development.

## Command reference at a glance

```text
rwe read FILE [RANGE]
rwe write FILE CONTENT
rwe edit FILE EDITS
```

| Command | Purpose | Modifies a file? |
|---|---|---|
| `rwe read FILE` | Print up to 1000 lines with hexadecimal addresses | No |
| `rwe read FILE RANGE` | Print a single line or inclusive range | No |
| `rwe write FILE CONTENT` | Create a file containing exactly `CONTENT` | Yes, only if `FILE` does not exist |
| `rwe edit FILE EDITS` | Replace addressed lines from the original file | Yes |
| `rwe --help` or `rwe -h` | Print a short command reference | No |
| `rwe --version` or `rwe -V` | Print the version | No |

`read`, `write`, and `edit` also accept `--help` or `-h` as their only operand.

### Arguments

| Argument | Meaning |
|---|---|
| `FILE` | Path to the file to read, create, or edit |
| `RANGE` | One hexadecimal address, or `START:END`; see below |
| `CONTENT` | The complete new file content as **one shell argument** |
| `EDITS` | One or more edit blocks as **one shell argument** |

For `write` and `edit`, a final argument of exactly `-` reads `CONTENT` or
`EDITS` from standard input. `read FILE -` is **not** standard-input syntax;
for `read`, `-` is interpreted as a range and is invalid.

### Quick examples

```sh
# Create a multiline file. Use quotes so this is one CONTENT argument.
rwe write notes.txt 'First line
Second line
Third line'

# Read the whole file, subject to the 1000-line limit.
rwe read notes.txt

# Read only original line 03, or an inclusive range.
rwe read notes.txt 03
rwe read notes.txt 01:03

# Replace original lines 01 and 03 in one operation.
rwe edit notes.txt '01: New first line
03: New third line'

# Replace original line 02 with two lines.
rwe edit notes.txt '02: Replacement line
An additional line'

# Re-read after every edit to obtain the new addresses.
rwe read notes.txt
```

## Line addresses and shell conventions

### Hexadecimal, 1-based addresses

Addresses are hexadecimal (`0`–`9`, `a`–`f`), and the first line is `01`, not
`00` or decimal `1` interpreted in base ten. These examples are equivalent:

| Hexadecimal address | Decimal line number |
|---|---|
| `01` | 1 |
| `09` | 9 |
| `0a` | 10 |
| `10` | 16 |
| `64` | 100 |
| `ff` | 255 |
| `100` | 256 |
| `3e8` | 1000 |
| `b7` | 183 |

Output uses lowercase hexadecimal. Input accepts lowercase or uppercase, so
`b7` and `B7` refer to the same line. Leading zeros are allowed on input:
`1`, `01`, and `001` all address line 1. In one `edit` argument, using more
than one spelling of the same address is a duplicate-address error.

`00` is always invalid, including `000` and other all-zero forms.

### Address width

The displayed address width depends on the **total number of lines in the
file**, not on the number of lines selected for output.

| Total file lines | Width | First address | Example last address |
|---|---|---|---|
| 1 through 255 (`ff`) | 2 digits | `01` | `ff` at 255 lines |
| 256 through 4095 (`fff`) | 3 digits | `001` | `fff` at 4095 lines |
| 4096 through 65535 (`ffff`) | 4 digits | `0001` | `ffff` at 65535 lines |

The same rule continues for larger files. For example:

- A 3-line file uses `01`, `02`, `03`.
- A 256-line file uses `001` through `100`.
- A 4096-line file uses `0001` through `1000`.
- Reading only line 1 of a 500-line file prints `001: ...`, not `01: ...`.

### Shell quoting and multiline arguments

The shell, not `rwe`, handles quotes. Use single quotes around multiline
content when the content itself contains no single quote:

```sh
rwe write poem.txt 'Line one
Line two'
```

The opening quote can be at the end of one terminal line and the closing quote
several lines later. The newlines between them are part of the argument.

Within POSIX-style single quotes:

- `\n` is two characters (backslash and `n`), **not** a newline.
- `$HOME`, backticks, and `*` remain literal; the shell does not expand them.
- A literal single quote cannot be embedded directly inside single quotes.

For content containing shell-sensitive text or many quotes, standard input is
usually clearer:

```sh
rwe write literal.txt - <<'EOF'
A literal $HOME, a backslash-n: \n, and 'single quotes'.
EOF
```

The quoted `<<'EOF'` prevents shell expansion in the here-document. The final
newline supplied by the here-document is part of the file content.

### Filenames beginning with `-`

Put `--` immediately after the subcommand:

```sh
rwe read -- -notes.txt
rwe write -- -new.txt 'content'
rwe edit -- -notes.txt '01: replacement'
```

Paths containing spaces follow normal shell quoting rules:

```sh
rwe read 'my notes.txt'
```

## Complete workflow

This example demonstrates the most easily misunderstood behavior: original
addresses remain fixed throughout one `edit`, even when a replacement expands
to multiple lines.

### 1. Create the original file

```sh
rwe write example.txt 'First line
Second line
Third line'
```

### 2. Read it before editing

```sh
rwe read example.txt
```

Output:

```text
01: First line
02: Second line
03: Third line
```

### 3. Expand line 01 and replace line 03 in the same command

```sh
rwe edit example.txt '01: New first line
Inserted second line
03: New third line'
```

The edit argument has two blocks:

1. Replace **original line `01`** with two lines: `New first line` and
   `Inserted second line`.
2. Replace **original line `03`** (`Third line`) with `New third line`.

### 4. Read the result

```text
01: New first line
02: Inserted second line
03: Second line
04: New third line
```

Address `03` in the command did **not** mean “line 3 after inserting a line.”
It still meant original `Third line`. Original `Second line` was not edited,
so it moved from line `02` to line `03` in the result. The replacement for
original line `03` consequently appears at line `04`.

Internally, `rwe` validates all addresses against the original file and applies
replacements from bottom to top. The practical consequences are:

- Every address in one call uses the same original snapshot.
- Blocks may be written in ascending, descending, or mixed address order.
- A replacement with multiple lines never shifts later addresses in that
  same call.
- You cannot address a newly inserted line elsewhere in the same call.
- Addresses are not persistent IDs. After the command finishes, run `read`
  again and use its addresses for the next `edit`.

For example, the following two-block edit is also valid and has the same
addressing semantics:

```sh
rwe edit example.txt '04: Replacement for current original line 04
01: Replacement for current original line 01'
```

Here `04` and `01` refer to the result shown immediately above, because this
is a new invocation with a new original snapshot.

## `read`: display numbered content

### Read the beginning of a file

```sh
rwe read notes.txt
```

Each selected line is printed as:

```text
ADDRESS: CONTENT
```

There is one space after the colon, inserted by the output formatter. It is
not part of the original file content. Content itself is printed without its
line terminator, followed by the output newline.

Example:

```text
01: First line
02: Second line
03: Third line
```

A blank source line appears as an address, colon, and one space:

```text
01: Non-empty line
02:
03: Another non-empty line
```

`read` does not modify the file. An empty file produces no output and exits
successfully. A file containing one empty line produces `01: `.

### Select one line

```sh
rwe read notes.txt b7
```

This prints only line `b7` (decimal 183), with its address.

### Select an inclusive range

```sh
rwe read notes.txt 01:64
```

This prints lines `01` through `64`, including both endpoints.

| Range | Meaning |
|---|---|
| `b7` | Only line `b7` |
| `01:64` | `01` through `64`, inclusive |
| `01:` | `01` through end of file, subject to the output limit |
| `:64` | Beginning through `64`, subject to the output limit |
| `:` | Equivalent to `01:`; the entire file, subject to the output limit |

A range may run past the end of the file as long as its **start exists**. For
example, `02:ff` on a three-line file prints lines `02` and `03`. It does not
create placeholder lines.

These are errors:

- A start address beyond end of file, including any range on an empty file
- A reversed range such as `03:01`
- A non-hexadecimal address such as `xyz`
- Zero as an address
- More than one range colon, such as `01:02:03`

### The 1000-line output limit

Every invocation prints at most **1000 content lines**, even if the selected
range contains more. This applies equally to a full read, a single open-ended
range, and a finite range.

1000 decimal is `3e8` hexadecimal. Therefore, on a file with more than 1000
lines:

```sh
rwe read large.txt
```

prints addresses `001: ...` through `3e8: ...` (three digits because the file
has more than 255 lines), and writes this notice to standard error:

```text
rwe: showing at most 1000 lines; continue at line 3e9
```

Continue with:

```sh
rwe read large.txt 3e9:
```

For a range beginning elsewhere, the continuation address is the range start
plus 1000. For example, `rwe read large.txt 02:` starts at `002`; after 1000
lines the continuation notice names `3ea`.

If a finite range contains exactly 1000 lines, no continuation notice is
printed. If it contains more than 1000 lines, the notice is printed even if
the file ends immediately after the omitted part of that range.

The limit controls output only. It does not truncate or modify the file. `rwe`
still counts the remainder to choose the correct address width, using binary
chunks rather than formatting every unselected line.

### Read behavior summary

| Situation | Behavior |
|---|---|
| Empty file, no range | Success, no output |
| Empty file, explicit range | Error: start is outside the file |
| Range start exists, end is beyond EOF | Print through EOF |
| Selected line has invalid UTF-8 | Error, no numbered content emitted |
| More than 1000 selected lines | Print first 1000 and a continuation notice on stderr |
| CRLF input | Display content without the CRLF terminator |
| Final line has no newline | Display it like any other selected line |

## `write`: create a new file only

### Create a multiline file

```sh
rwe write hello.txt 'First line
Second line
Third line'
```

`CONTENT` becomes the complete file content. `rwe` does not append a newline,
remove a newline, trim spaces, expand variables, or otherwise normalize the
argument. In particular:

```sh
rwe write no-final-newline.txt 'last line'
rwe write with-final-newline.txt 'last line
'
```

These produce different bytes: only the second file ends with a newline.

Create an empty file with:

```sh
rwe write empty.txt ''
```

### Existing files are never overwritten

If `FILE` already exists, `write` fails and leaves it unchanged:

```text
rwe: refusing to overwrite 'hello.txt': it already exists; use 'rwe edit', or remove the file before using 'rwe write'
```

This protection uses `os.open` with `O_EXCL`, so file creation and the
existence check happen atomically. It does not rely on a separate “check,
then create” sequence.

To change part of an existing file, use `edit`. To replace the entire file,
explicitly remove it using your preferred file-management command and then
run `write` again. `rwe` intentionally has no `delete` or force-overwrite
command.

Parent directories are not created. For example, this fails if `missing/`
does not already exist:

```sh
rwe write missing/file.txt 'content'
```

### Write from standard input

Use `-` in place of `CONTENT`:

```sh
printf 'First line\nSecond line\n' | rwe write new.txt -
```

Or use a quoted here-document:

```sh
rwe write new.txt - <<'EOF'
Literal $HOME and 'quotes'
Second line
EOF
```

Standard input is read completely before the exclusive file creation is
attempted. The supplied bytes are written as-is; they are not UTF-8 decoded
and re-encoded on this path.

### Write success output

A successful write prints a message like:

```text
Created hello.txt (29 bytes)
```

The byte count is the encoded content length, not a character count.

## `edit`: replace original lines

### Basic form

```sh
rwe edit FILE EDITS
```

`EDITS` is one argument containing one or more blocks. Each block starts at
the beginning of a line:

```text
HEX_ADDRESS: replacement content
```

For example:

```sh
rwe edit notes.txt '01: Replacement for original line 01
02: Replacement for original line 02'
```

Each block replaces exactly one original line. Its replacement may itself
contain zero characters, one line, or multiple lines.

### Multiple blocks and multiline replacements

```sh
rwe edit notes.txt '01: First replacement line
Second line in the replacement for original 01
02: Replacement for original line 02
b7: Replacement for original line b7'
```

This command addresses three original lines:

- Original `01` becomes two lines.
- Original `02` becomes one line.
- Original `b7` becomes one line.

The second and third addresses are still interpreted against the original
file; the expansion at `01` does not move them.

### Original-address rules

1. Read the file to obtain current addresses.
2. Build one or more replacement blocks using those addresses.
3. Pass all blocks in one `edit` invocation.
4. `rwe` validates every block before saving anything.
5. Replacements are applied from the highest original address to the lowest.
6. Read the resulting file before planning another edit.

Block order in `EDITS` does not determine application order. These are
semantically equivalent:

```text
01: First
03: Third
```

```text
03: Third
01: First
```

Both replace original lines 1 and 3. Duplicate addresses are never merged or
applied sequentially.

### What `edit` does not do

- `02:` makes original line 2 **empty**; it does not delete the line.
- A multiline replacement expands a line; there is no separate insertion-only
  operation.
- There is no “current line 3 after previous block” addressing mode.
- There is no range replacement syntax such as `01:03: text`. The first colon
  ends the address, so that form is content for address `01`, not a range.
- There is no regular-expression or search-and-replace mode.
- An empty file has no addressable lines, so it cannot be populated with
  `edit`; remove it and use `write` instead.

### Header recognition

A new block starts only when a line begins with one or more hexadecimal digits
immediately followed by `:`. Examples recognized as headers:

```text
1: content
01: content
B7: content
dead: content
```

The address need not have the same width shown by `read`. `1` and `01` both
mean original line 1.

These continuation lines are **not** headers:

```text
  02: indented, so this is content
not-hex: content
http://example.com
3.14: content
```

Because only the beginning of a line can start a header, a colon elsewhere in
normal content has no special meaning.

Non-whitespace content before the first recognized header is an error. Leading
blank or whitespace-only lines before the first block are ignored.

### Spaces and tabs after the colon

One ASCII space or one tab immediately after the colon is treated as a
separator and removed. It is optional:

```text
01: Replacement       # Replacement content is "Replacement"
01:Replacement        # Replacement content is "Replacement"
```

If there are multiple spaces, only the first is removed:

```text
01:   indented
```

produces content beginning with two spaces.

On continuation lines, indentation and other whitespace are preserved exactly
as supplied in the edit argument.

### Empty replacements and blank lines

Given an original file:

```text
01: A
02: B
03: C
04: D
```

this command:

```sh
rwe edit notes.txt '01:
03: Replacement

'
```

has the following meaning:

- Original `01` becomes an empty line.
- Original `03` becomes `Replacement` followed by an empty line.

The result is:

```text
01:
02: B
03: Replacement
04:
05: D
```

To understand blank lines, remember these parser rules:

- The newline before the next header terminates the previous block; it is not
  an additional content line.
- One newline at the very end of the `EDITS` argument is treated as the final
  block terminator.
- Extra newlines beyond those terminators become blank replacement lines.
- To make the last block end in a blank line, include an extra blank line
  before the closing shell quote.

### Escaping a content line that looks like a header

Suppose original line `01` should become:

```text
First replacement line
dead: This must remain literal content
```

Because `dead` is valid hexadecimal, an unescaped continuation line beginning
`dead:` would start a new block. Prefix it with one backslash:

```sh
rwe edit notes.txt '01: First replacement line
\dead: This must remain literal content
02: Replacement for original line 02'
```

On continuation lines, a backslash is removed only when the text after it
would otherwise be recognized as an edit header. Other backslashes remain
unchanged. The escape applies to continuation lines, not to the first header
of a block.

In POSIX single quotes the backslash reaches `rwe` literally. When generating
edit strings in another programming language, account for that language's own
backslash escaping as well.

### Edit from standard input

Use `-` as the entire `EDITS` argument:

```sh
rwe edit notes.txt - <<'EOF'
01: Replacement from standard input
03: Another original-line replacement
EOF
```

This is useful for generated edit scripts and content containing quotes. The
edit syntax is otherwise identical, including the rule that one final newline
terminates the last block.

### Validation is all-or-nothing before saving

The file is not intentionally changed if any of these conditions occurs:

- No valid edit block exists
- Non-whitespace content appears before the first block
- An address is zero or malformed as a header
- An address appears more than once, including aliases such as `1` and `01`
- Any address is beyond the original file's last line
- Any original line is not valid UTF-8
- Standard-input edit data is not valid UTF-8
- The target is not a regular file after resolving symbolic links

For example, this rejects the **entire** operation rather than applying the
valid first block:

```sh
rwe edit three-lines.txt '01: Valid replacement
ff: Invalid if the file has only three lines'
```

On success, `rwe` prints:

```text
Edited notes.txt (2 original line addresses)
```

The count is the number of distinct original addresses, not the number of
replacement lines.

## Input, output, paths, and encoding

### Standard output and standard error

| Stream | Used for |
|---|---|
| Standard output | Numbered `read` content, success messages, help, version |
| Standard error | File/validation errors, usage errors, 1000-line continuation notices |

The continuation notice does not make a successful `read` fail; its exit code
is still 0.

The numbered `read` format is designed for humans and for constructing later
`edit` commands. There is no JSON or machine-readable output mode. Programs
parsing it must account for variable address width and the separator space.
Success messages also go to stdout, so redirect or ignore them when a script
needs only another stream's data.

### Encoding and line endings

- `read` decodes selected content as UTF-8 and reports an error for invalid
  selected content.
- `edit` validates the entire original file as UTF-8 before applying changes.
- Direct `write CONTENT` encodes the argument as UTF-8.
- `write FILE -` writes standard-input bytes as supplied.
- `read` recognizes LF (`\n`) and strips a preceding CR for CRLF display.
- A lone carriage return is not treated as a line separator in the original
  file.
- CRLF and CR characters inside the `EDITS` argument itself are normalized to
  LF for parsing; original file line-ending preservation is handled
  separately when replacements are rendered.
- No character-set auto-detection or encoding option is provided.
- Binary files are unsupported.

### Relative and absolute paths

Relative paths are resolved against the process's current working directory.
Absolute paths work normally. There is no project-root discovery, ignore-file
processing, recursive directory traversal, or glob expansion performed by
`rwe` itself. Your shell may expand unquoted wildcards before `rwe` sees them.

For `edit`, symbolic links are resolved and the target file is replaced; the
symbolic link itself remains a link.

## Safety, preservation, and limitations

### What is preserved by `edit`

- Existing file permission bits are copied to the temporary file.
- Each replaced original line keeps its original LF or CRLF ending.
- Unselected lines retain their original bytes after the whole file has
  passed UTF-8 validation.
- A one-line replacement of a final line without a newline remains without a
  final newline.
- When a multiline replacement expands an unterminated final line, the last
  new line remains unterminated; earlier inserted lines use the file's first
  encountered line-ending style, defaulting to LF.
- A symbolic link remains a symbolic link while its target is edited.

### Atomic save model

`edit` follows this sequence:

1. Parse all edit blocks.
2. Resolve the target and read the original file.
3. Validate the file type, UTF-8 content, and every address.
4. Build replacements in memory, from bottom to top.
5. Create a temporary file in the target's directory.
6. Copy the original permission bits and write the complete result.
7. Replace the target with `os.replace`.
8. Remove the temporary file if saving fails before replacement.

This avoids exposing a partially written result through the target path when
an ordinary write failure occurs. It does **not** provide transactions across
multiple files, backups, rollback after a successful replacement, file
locking, or conflict detection.

### Important limitations

- **Whole-file rewrite:** `edit` reads and writes the complete file. Memory
  use and I/O grow with file size.
- **Concurrent writers:** `rwe` does not lock files or compare modification
  times. Another process's changes can be overwritten by a later `edit`.
- **Metadata:** permission bits are preserved, but ownership, ACLs, extended
  attributes, hard-link identity, timestamps, and other filesystem-specific
  metadata are not guaranteed to survive target replacement.
- **Durability:** the implementation does not call `fsync` on the temporary
  file or parent directory. Atomic replacement is not a guarantee against
  every power-loss or filesystem-failure scenario.
- **Parent directories:** `write` does not create them.
- **Line deletion:** unsupported. Emptying a line is not deleting it.
- **Configuration:** there are no configuration files, environment-variable
  options, plugins, or interactive prompts.
- **Read performance:** only selected lines are decoded and formatted, but the
  remainder is still scanned for total line count and address width.

## Errors, exit codes, and troubleshooting

### Exit codes

| Code | Meaning | Examples |
|---|---|---|
| `0` | Success | Read, write, edit, help, version completed |
| `1` | Runtime or validation error | Missing file, existing write target, bad UTF-8, bad range, duplicate edit address |
| `2` | Command-line usage error | Unknown command, missing operand, too many arguments |
| `130` | Interrupted by Ctrl+C | Keyboard interrupt |

When output is piped to a consumer that closes early, such as some uses of
`head`, a broken output pipe is handled as normal termination rather than a
Python traceback.

### Common mistakes

#### “My multiline write failed with a usage error”

Quote the complete content so it is one argument:

```sh
rwe write file.txt 'line one
line two'
```

Do not pass separate unquoted arguments for each word.

#### “`write` refuses to create my file”

That is expected if the path already exists. Use `read` and `edit`, or remove
the file deliberately before recreating it. There is no overwrite flag.

#### “My second edit changed the wrong line”

Use addresses from a fresh `read` before each invocation. Within one command,
addresses use the original snapshot; across commands, earlier edits may have
changed line positions.

#### “Why did original line 03 become output line 04?”

An earlier replacement added a line. The address still correctly targeted
original line 03; its result simply appears later in the new file. See the
[complete workflow](#complete-workflow).

#### “Why is `64` not line 64?”

Addresses are hexadecimal. `64` means decimal 100. Decimal 64 is hexadecimal
`40`.

#### “Why is my first line `001` instead of `01`?”

The file has at least 256 lines, so all displayed addresses use three digits.

#### “My content line `dead: text` became a separate edit”

`dead` is valid hexadecimal. Escape it as `\dead: text` on a continuation
line.

#### “`02:` did not remove line 2”

That syntax replaces line 2 with an empty line. Line deletion is not
implemented.

#### “I only see 1000 lines”

Use the continuation address printed on standard error:

```sh
rwe read large.txt 3e9:
```

or request a smaller range explicitly.

#### “I see a UTF-8 error”

The tool does not support arbitrary encodings or binary files. Convert the
file to UTF-8 with another tool before using `rwe`.

## Development and project layout

```text
.
├── pyproject.toml          # Package metadata, dependencies, console scripts
├── README.md               # Complete user and developer guide
├── LICENSE                 # Apache License 2.0 full text
├── .github/
│   └── workflows/
│       └── publish.yml     # vX.Y.Z tag-triggered PyPI publication
├── src/
│   └── rwe/
│       ├── __init__.py     # Package version
│       ├── __main__.py     # `python -m rwe` entry point
│       └── cli.py          # Parsing, file operations, messages, exit codes
└── tests/
    └── test_cli.py         # Standard-library unittest suite
```

The two console-script names are declared in `pyproject.toml` and both point
to `rwe.cli:main`.

### Run tests

From the project root:

```sh
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
```

The suite uses `unittest`, `tempfile`, mocked streams, and real subprocess
calls. It covers:

- Hexadecimal formatting and width growth
- Single, finite, and open-ended ranges
- Empty files, unterminated final lines, and the 1000-line cap
- Exclusive creation and exact multiline content
- Multiple original-address edits and multiline expansion
- Empty replacements, duplicate and out-of-range rejection
- Escaped headers and standard input
- CRLF and final-newline preservation
- Permissions, symbolic links, invalid UTF-8, and failed replacement cleanup
- Help, version, usage errors, and the real `python -m rwe` surface

The tests require no third-party test runner.

### Build distributions

With the `build` package available:

```sh
python3 -m build --no-isolation
```

Outputs are written to `dist/`:

```text
dist/read_write_edit_tools-0.1.0-py3-none-any.whl
dist/read_write_edit_tools-0.1.0.tar.gz
```

The wheel and source distribution include the Apache-2.0 `LICENSE`. Package
metadata declares `License-Expression: Apache-2.0`.

An equivalent uv build command is:

```sh
uv build
```

### Automated PyPI releases with GitHub Actions

The distribution published to PyPI is named **`read-write-edit-tools`**.
Pushing a version tag runs `.github/workflows/publish.yml`, which builds and
publishes that distribution with
[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/). No
long-lived PyPI API token or password is stored in GitHub Secrets.

The workflow has two jobs:

1. **`build`** checks out the tag, validates its version, runs the full test
   suite, builds the wheel and source distribution, checks metadata and both
   installed entry points, and stores the exact distributions as a workflow
   artifact.
2. **`publish`** runs only after `build` succeeds. It downloads those exact
   artifacts and publishes them to PyPI using GitHub's short-lived OIDC
   identity. Only this job receives `id-token: write`; the build job has only
   `contents: read`.

The publish job does **not** require a GitHub environment. The optional
Environment name is left blank both in the workflow and in the PyPI Trusted
Publisher configuration. The PyPI action generates signed attestations by
default in the Trusted Publishing flow.

#### One-time repository and PyPI setup

Perform these steps before the first tag. They require repository
administration access and a PyPI account; they cannot be completed by editing
this repository alone.

1. Use the public repository
   **[llaa33219/read-write-edit-tools](https://github.com/llaa33219/read-write-edit-tools)**,
   including `.github/workflows/publish.yml`. No GitHub Environment needs to
   be created.

2. Configure PyPI to trust **this exact repository and workflow filename**.
   The optional Environment name must be left **blank**:

   | PyPI Trusted Publisher field | Required value |
   |---|---|
   | PyPI project name | `read-write-edit-tools` |
   | Owner | `llaa33219` |
   | Repository name | `read-write-edit-tools` |
   | Workflow name | `publish.yml` |
   | Environment name | **Leave blank (optional)** |

   For the **first release**, create a **pending publisher** under your PyPI
   account's **Publishing** page. This lets the first successful OIDC upload
   create the project without a manual initial upload. Official instructions:
   <https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/>.

   If the project already exists on PyPI, add the publisher from that
   project's **Publishing** management page instead:
   <https://docs.pypi.org/trusted-publishers/adding-a-publisher/>.

   A pending publisher does not reserve the name. If somebody else registers
   `read-write-edit-tools` before the first upload, the pending publisher is
   invalidated and ownership must be resolved on PyPI.

3. Do **not** add a PyPI username, password, or API token to the workflow. If
   your organization prohibits OIDC, use a separately approved secret-based
   process instead; the supplied workflow intentionally uses Trusted
   Publishing.

#### Release a new version

Suppose the new version is `0.2.0`.

1. Update **both** version declarations to `0.2.0`:

   ```toml
   # pyproject.toml
   [project]
   version = "0.2.0"
   ```

   ```python
   # src/rwe/__init__.py
   __version__ = "0.2.0"
   ```

   Also update documentation examples that intentionally display the current
   version, if any.

2. Review the changes and run the local verification commands:

   ```sh
   PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
   uv build
   ```

3. Commit the release changes using your normal review process, then create
   and push the tag from the intended release commit:

   ```sh
   git tag v0.2.0
   git push origin v0.2.0
   ```

   The workflow accepts only tags matching **`vX.Y.Z`** with numeric `X`, `Y`,
   and `Z`, such as `v0.2.0` or `v1.10.3`. Tags such as `0.2.0`, `v0.2`,
   `v0.2.0-rc1`, and arbitrary release names do not match this release
   workflow.

4. GitHub rejects the release before building if the tag's `X.Y.Z` does not
   exactly equal `[project].version`. Keep `src/rwe/__init__.py` equal too so
   `rwe --version` reports the same release.

5. Watch **GitHub → Actions → Publish to PyPI**. The workflow stops without
   uploading if tests, version checks, the build, or wheel smoke tests fail.
   There is no GitHub Environment approval gate in this configuration.

6. After a successful run, verify the published package and both execution
   modes:

   ```sh
   uv tool install read-write-edit-tools
   rwe --version
   uvx --from read-write-edit-tools rwe --version
   ```

#### Release operational notes

- PyPI filenames are immutable. A version that has already been uploaded
  cannot be replaced by pushing the same tag again. Increment the version and
  create a new tag instead.
- Deleting or recreating a tag does not retract distributions from PyPI. Use
  PyPI's yanking process when a published release must be discouraged.
- The workflow uploads distributions to **real PyPI**, not TestPyPI.
- It does not create a GitHub Release or generate release notes. The GitHub
  Actions artifact is a temporary handoff between jobs, not the public PyPI
  download page.
- Forks and differently named workflows are not trusted by the publisher
  configured for the original repository.
- A first end-to-end publication can only be verified after the external PyPI
  publisher has been configured and a real tag has been pushed.

### Implementation notes

- `src/rwe/cli.py` contains a small hand-written argument dispatcher rather
  than `argparse`, avoiding that startup cost.
- Hexadecimal validation and edit-header parsing avoid importing `re`.
- `tempfile` is imported only when `edit` needs it, keeping `read` and `write`
  startup lighter.
- `read` formats at most 1000 selected lines, then counts remaining newlines
  in 1 MiB binary chunks to determine total width.
- `write` uses `O_WRONLY | O_CREAT | O_EXCL` for atomic no-overwrite creation.
- `edit` parses the entire edit argument, validates against the original,
  replaces addresses bottom-up, and saves via a sibling temporary file and
  `os.replace`.
- Expected failures use the internal `RweError` exception and produce concise
  English messages rather than tracebacks.

## License

This project is licensed under the **Apache License, Version 2.0**.

The complete license text is in [LICENSE](LICENSE). The same license is
included in built distributions. The official license page is:

<https://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an **“AS IS” BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND**, either express or implied. See the License for the
specific language governing permissions and limitations under the License.
