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:
readdisplays text with stable, 1-based hexadecimal line addresses.writecreates a new file and refuses to overwrite anything that already exists.editreplaces 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
editrule: every address in oneeditcommand 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
- Requirements and installation
- Command reference at a glance
- Line addresses and shell conventions
- Complete workflow
read: display numbered contentwrite: create a new file onlyedit: replace original lines- Input, output, paths, and encoding
- Safety, preservation, and limitations
- Errors, exit codes, and troubleshooting
- Development and project layout
- License
Requirements and installation
Requirements
- Python 3.9 or newer
- uv is recommended for installing and running the CLI in an isolated environment
pipis 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:
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
001through100. - A 4096-line file uses
0001through1000. - Reading only line 1 of a 500-line file prints
001: ..., not01: ....
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:
\nis two characters (backslash andn), 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:
- Replace original line
01with two lines:New first lineandInserted second line. - Replace original line
03(Third line) withNew 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
readagain and use its addresses for the nextedit.
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
01becomes two lines. - Original
02becomes one line. - Original
b7becomes 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
- Read the file to obtain current addresses.
- Build one or more replacement blocks using those addresses.
- Pass all blocks in one
editinvocation. rwevalidates every block before saving anything.- Replacements are applied from the highest original address to the lowest.
- 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 address01, 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 usewriteinstead.
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
01becomes an empty line. - Original
03becomesReplacementfollowed 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
EDITSargument 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
1and01 - 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
readdecodes selected content as UTF-8 and reports an error for invalid selected content.editvalidates the entire original file as UTF-8 before applying changes.- Direct
write CONTENTencodes the argument as UTF-8. write FILE -writes standard-input bytes as supplied.readrecognizes 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
EDITSargument 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:
- Parse all edit blocks.
- Resolve the target and read the original file.
- Validate the file type, UTF-8 content, and every address.
- Build replacements in memory, from bottom to top.
- Create a temporary file in the target's directory.
- Copy the original permission bits and write the complete result.
- Replace the target with
os.replace. - 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:
editreads and writes the complete file. Memory use and I/O grow with file size. - Concurrent writers:
rwedoes not lock files or compare modification times. Another process's changes can be overwritten by a lateredit. - 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
fsyncon the temporary file or parent directory. Atomic replacement is not a guarantee against every power-loss or filesystem-failure scenario. - Parent directories:
writedoes 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 rwesurface
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:
buildchecks 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.publishruns only afterbuildsucceeds. It downloads those exact artifacts and publishes them to PyPI using GitHub's short-lived OIDC identity. Only this job receivesid-token: write; the build job has onlycontents: 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.
-
Use the public repository llaa33219/read-write-edit-tools, including
.github/workflows/publish.yml. No GitHub Environment needs to be created. -
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-toolsOwner llaa33219Repository name read-write-edit-toolsWorkflow name publish.ymlEnvironment 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-toolsbefore the first upload, the pending publisher is invalidated and ownership must be resolved on PyPI. -
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.
-
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.
-
Review the changes and run the local verification commands:
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v uv build
-
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.Zwith numericX,Y, andZ, such asv0.2.0orv1.10.3. Tags such as0.2.0,v0.2,v0.2.0-rc1, and arbitrary release names do not match this release workflow. -
GitHub rejects the release before building if the tag's
X.Y.Zdoes not exactly equal[project].version. Keepsrc/rwe/__init__.pyequal too sorwe --versionreports the same release. -
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.
-
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.pycontains a small hand-written argument dispatcher rather thanargparse, avoiding that startup cost.- Hexadecimal validation and edit-header parsing avoid importing
re. tempfileis imported only wheneditneeds it, keepingreadandwritestartup lighter.readformats at most 1000 selected lines, then counts remaining newlines in 1 MiB binary chunks to determine total width.writeusesO_WRONLY | O_CREAT | O_EXCLfor atomic no-overwrite creation.editparses the entire edit argument, validates against the original, replaces addresses bottom-up, and saves via a sibling temporary file andos.replace.- Expected failures use the internal
RweErrorexception 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.