Skip to main content

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:

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
  2. Command reference at a glance
  3. Line addresses and shell conventions
  4. Complete workflow
  5. read: display numbered content
  6. write: create a new file only
  7. edit: replace original lines
  8. Input, output, paths, and encoding
  9. Safety, preservation, and limitations
  10. Errors, exit codes, and troubleshooting
  11. Development and project layout
  12. License

Requirements and installation

Requirements

  • Python 3.9 or newer
  • 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.

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

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:

rwe --help
read-write-edit-tools --help

Both print the package version with --version:

rwe --version
read-write-edit-tools --version

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

uv tool update-shell

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

uv tool list

Upgrade or remove a uv tool installation

Upgrade to the latest published version:

uv tool upgrade read-write-edit-tools

Remove the persistent tool and its command links:

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:

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:

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:

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:

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

To refresh cached package information when needed:

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

Alternative: install from PyPI with pip

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

Remove that installation using the same Python environment:

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:

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

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

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

A pip virtual environment is another development option:

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:

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

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

# 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:

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:

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:

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

Paths containing spaces follow normal shell quoting rules:

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

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

2. Read it before editing

rwe read example.txt

Output:

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

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

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

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:

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

rwe read notes.txt

Each selected line is printed as:

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:

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

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

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

rwe read notes.txt b7

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

Select an inclusive range

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:

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:

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

Continue with:

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

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:

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:

rwe write empty.txt ''

Existing files are never overwritten

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

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:

rwe write missing/file.txt 'content'

Write from standard input

Use - in place of CONTENT:

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

Or use a quoted here-document:

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:

Created hello.txt (29 bytes)

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

edit: replace original lines

Basic form

rwe edit FILE EDITS

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

HEX_ADDRESS: replacement content

For example:

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

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:

01: First
03: Third
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:

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:

  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:

01: Replacement       # Replacement content is "Replacement"
01:Replacement        # Replacement content is "Replacement"

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

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:

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

this command:

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:

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:

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:

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:

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:

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

On success, rwe prints:

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:

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.

“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:

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

.
├── 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:

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:

python3 -m build --no-isolation

Outputs are written to dist/:

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:

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. 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, 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:

    # pyproject.toml
    [project]
    version = "0.2.0"
    
    # 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:

    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:

    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:

    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. 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.

Metadata

Release files for read-write-edit-tools 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for read-write-edit-tools 0.1.0
File Size Uploaded Provenance Actions
read_write_edit_tools-0.1.0.tar.gz 53.7 kB With provenance Details for read_write_edit_tools-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for read-write-edit-tools 0.1.0
File Interpreter ABI Platform Actions
read_write_edit_tools-0.1.0-py3-none-any.whl Python 3 none any Details for read_write_edit_tools-0.1.0-py3-none-any.whl

Total release size: 78.2 kB

Release files / read_write_edit_tools-0.1.0.tar.gz

Download URL read_write_edit_tools-0.1.0.tar.gz
Size 53.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c143bc195c35169b570ccbdc0960fda32bb6537c12e843192ce8e5283dc4c9b2
BLAKE2b-256 checksum
How to use checksums
9a8345a8f120aa85d356f924c4b484a4c7fdfa4b69d6a0db6ef3a1b4bd27a735
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Release files / read_write_edit_tools-0.1.0-py3-none-any.whl

Download URL read_write_edit_tools-0.1.0-py3-none-any.whl
Size 24.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3458c96469782bf88d4a8ac976e86b3500a527025752a7a38f6640c05a5845a3
BLAKE2b-256 checksum
How to use checksums
8f22edbc36cd41cffd56a387c9b01cba458a5dab22fa36b37e0e9b6ee587b7e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Security for read-write-edit-tools 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for read_write_edit_tools-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page