Curtail (cut the tail) - reduce arbitrary text to a special string by cutting short.
Project description
curtail
Curtail (cut the tail) - reduce arbitrary text to a special string by cutting short.
Feed it any string — unbalanced emphasis markers, mid-word cuts, mixed code spans, raw user content — and it always returns a string that is both short enough and well-formed inline markdown.
from curtail import clip
clip('**bold text** and more _italic_', 20)
# → '**bold** (cont.)'
clip('`code with *stars*', 12, '…')
# → '`code with`…'
No runtime dependencies. Pure Python ≥ 3.11.
Install
pip install curtail
Manual
The man page provides the CLI reference (man curtail after placing the file on your MANPATH):
mkdir -p ~/.local/share/man/man1
cp docs/curtail.1 ~/.local/share/man/man1/
Quickstart
Run curtail with no arguments for a usage summary.
For a dedicated feature walkthrough you can follow in minutes visit quickstart.
A step-by-step guided build of a document pipeline is provided in the tutorial.
Library
clip(text, max_length, indicator=' (cont.)', *, word_boundary=True)
Returns a string of at most max_length characters that is valid inline
markdown.
from curtail import clip
# Truncation with default indicator
clip('**bold text** and more _italic_', 20)
# → '**bold** (cont.)'
# Custom indicator
clip('**bold text** and more _italic_', 16, '…')
# → '**bold text**…'
# Hard cut (no word-boundary snapping)
clip('hello world', 8, '...', word_boundary=False)
# → 'hello...'
# Unbalanced input is closed automatically
clip('**unfinished bold', 30)
# → '**unfinished bold**'
Guarantees — both hold unconditionally for every input:
len(result) ≤ max_lengthresultis well-formed inline markdown (all opened spans are closed)
patch(text, *, replace=None, stop_at=None)
Pre-process text before passing it to clip. Apply character
replacements, truncate at a sentinel, or both.
from curtail import patch, clip
# Strip everything from the first newline
patch('first line\nsecond line', stop_at='\n')
# → 'first line'
# Normalise whitespace variants
patch('line\r\nend', replace={'\r': '', '\n': ' '})
# → 'line end'
# Escape pipe characters for a Markdown table cell, then truncate
cell = patch(raw, replace={'|': r'\|'}, stop_at='\n')
preview = clip(cell, 60, '…')
stop_at truncates before replace applies, so stop characters are never
transformed — they simply end the string. Replacements inside backtick code
spans are skipped so that literal content is never altered.
clip_cell(text, max_length, indicator='…', *, word_boundary=True)
Convenience wrapper for Markdown table cells. Combines patch and
clip in one call: trims to the first line, escapes |, then truncates.
from curtail import clip_cell
clip_cell('**bold** value with a|pipe\nand more', 40)
# → '**bold** value with a\|pipe…'
Guarantees — in addition to the two clip guarantees:
- Result contains no unescaped
|characters. - Result contains no
\ncharacters.
code(text, connector='-', marker=chr(30), gremlins=GREMLINS, policy='lower')
Derive a kebab-style identifier from arbitrary text.
from curtail import code, DASH, RS, GREMLINS
code('Hello, World!') # → 'hello-world'
code('foo-bar baz') # → 'foo-bar-baz' (connector preserved)
code('__init__') # → 'init'
code('hello world', policy='upper') # → 'HELLO-WORLD'
code('hello world', connector='_') # → 'hello_world'
Connector characters already present in the input are preserved through a
sandwich transform: they are encoded as marker before gremlin replacement
and restored afterwards, so 'foo-bar' and 'foo bar' produce different identifiers.
Three module-level constants expose the defaults:
from curtail import DASH, RS, GREMLINS
DASH # '-'
RS # chr(30) — ASCII Record Separator (sandwich-transform marker)
GREMLINS # ' .,;?!_()[]{}<>\\/$:"\'`´'
CLI
Calling curtail with no arguments prints the help screen and exits 0.
Unique prefix matching is supported for all subcommands (cl → clip, ce → cell, etc.).
curtail [-V] clip -n N [-i STR] [--hard-cut] [--stop-at CHARS] [--replace FROM TO] [text]
curtail [-V] cell -n N [-i STR] [--hard-cut] [text]
curtail [-V] code [--connector STR] [--gremlins STR] [--policy METHOD] [--marker STR] [text]
curtail [-V] patch [--stop-at CHARS] [--replace FROM TO] [text]
curtail [-V] version
When text is omitted the input is read from stdin.
# Clip: basic truncation
echo "**bold text** and more _italic_" | curtail clip -n 20
# → **bold** (cont.)
# Cell: table-cell pipeline (pipes escaped, newlines dropped)
curtail cell -n 60 -i '…' "$(cat cell.txt)"
# Clip with pre-processing: stop at newline, escape pipe
curtail clip -n 60 -i '…' --stop-at $'\n' --replace '|' '\|' "$(cat cell.txt)"
# Code: identifier from a heading
curtail code 'Hello, World!'
# → hello-world
# Code with custom connector and upper-case policy
curtail code --connector '_' --policy upper 'hello world'
# → HELLO_WORLD
# Patch: pre-process without truncating
curtail patch --stop-at $'\n' 'first line\nsecond line'
# → first line
| Subcommand | Flag | Default | Meaning |
|---|---|---|---|
clip |
-n N |
(required) | Maximum output length |
clip |
-i STR |
' (cont.)' |
Continuation indicator |
clip |
--hard-cut |
off | Character-level cut instead of word boundary |
clip |
--stop-at CHARS |
— | Truncate at first occurrence of any of these characters |
clip |
--replace FROM TO |
— | Replace character FROM with string TO (repeatable) |
cell |
-n N |
(required) | Maximum output length |
cell |
-i STR |
'…' |
Continuation indicator |
cell |
--hard-cut |
off | Character-level cut instead of word boundary |
code |
--connector STR |
'-' |
Separator between identifier parts |
code |
--gremlins STR |
GREMLINS | Characters replaced by the connector |
code |
--policy METHOD |
lower |
str method applied to the final identifier |
code |
--marker STR |
chr(30) | Sandwich-transform placeholder (rarely needed) |
patch |
--stop-at CHARS |
— | Truncate at first occurrence of any of these characters |
patch |
--replace FROM TO |
— | Replace character FROM with string TO (repeatable) |
| all | -V, --version |
— | Print version and exit |
Tracked inline constructs
| Construct | Opener | Closer |
|---|---|---|
| Code span | `, ``, … |
matching run of backticks |
| Emphasis | * or _ |
matching * or _ |
| Strong | ** or __ |
matching ** or __ |
Underscore emphasis follows CommonMark flanking rules: _ surrounded by
alphanumeric characters on both sides (intra-word) is treated as literal text.
Links, images, block-level elements, HTML tags, and entity references are
intentionally left unchanged — they either render safely as literal text
(unclosed [) or are the caller's responsibility.
Development
# Tests (238 cases including Hypothesis property tests)
make test
# Branch coverage (100 % on both implementation modules)
make coverage
# Format, lint, type-check, security scan
make quality
# Coverage-guided fuzzing (requires python-afl and AFL++)
make fuzz-scan fuzz_time=300
make corpus-update # merge → minimise → promote findings
Design and requirements
| Document | Identifier | File |
|---|---|---|
| Software Requirements Specification | CRT-SRS-001 | requirements/srs/ |
| Software Design Description | CRT-SDD-001 | design/sdd/ |
Both documents follow the MIL-STD-498 DID structure and are rendered into the documentation site alongside the quickstart and tutorial.
Bug Tracker
Any feature requests or bug reports shall go to the todos of curtail.
Primary Source repository
The main source of curtail is on a mountain in Central Switzerland under
configuration control (fossil).
Contributions
If you like to share small changes under the repositories license please kindly do so by sending a patchset. You can send such a patchset per email using git send-email.
Support
Please kindly submit issues at https://todo.sr.ht/~sthagen/curtail or write plain text email to ~sthagen/curtail@lists.sr.ht to support. Thanks.
Changes
See docs/changes.md for the release history.
Coverage
The test suite maintains 100% branch coverage on both implementation modules.
The HTML report (if generated) is in site/coverage/.
SBOM
Runtime dependency information is published in docs/sbom/ in SPDX 2.3 (JSON)
and CycloneDX 1.5 (JSON) formats.
See docs/sbom/README.md for the component inventory and validation guide.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file curtail-2026.7.8.tar.gz.
File metadata
- Download URL: curtail-2026.7.8.tar.gz
- Upload date:
- Size: 25.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6eb282145b1e23cf43520f217e9fde615a624e6b08e013dd7ac080a34b2db9d6
|
|
| MD5 |
e6dfffd64ce0c4e4bb952f0f13499e4d
|
|
| BLAKE2b-256 |
593e9394c149d484a9b04734426c282eb9a5b03f2ecc29957b6d0519789c8921
|
File details
Details for the file curtail-2026.7.8-py3-none-any.whl.
File metadata
- Download URL: curtail-2026.7.8-py3-none-any.whl
- Upload date:
- Size: 13.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d287aa280915104f653d0657adad621c279f5aac734df17840b8b481b335e07d
|
|
| MD5 |
eb06a09c2240965151656cc0b1ebbfe6
|
|
| BLAKE2b-256 |
cc1a575013ea1c2a4ccf2db1b3af6ff762e159dd5e065be1d933dba42e890669
|