Skip to main content

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_length
  • result is 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 \n characters.

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 (clclip, cecell, 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

curtail-2026.7.8.tar.gz (25.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

curtail-2026.7.8-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

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

Hashes for curtail-2026.7.8.tar.gz
Algorithm Hash digest
SHA256 6eb282145b1e23cf43520f217e9fde615a624e6b08e013dd7ac080a34b2db9d6
MD5 e6dfffd64ce0c4e4bb952f0f13499e4d
BLAKE2b-256 593e9394c149d484a9b04734426c282eb9a5b03f2ecc29957b6d0519789c8921

See more details on using hashes here.

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

Hashes for curtail-2026.7.8-py3-none-any.whl
Algorithm Hash digest
SHA256 d287aa280915104f653d0657adad621c279f5aac734df17840b8b481b335e07d
MD5 eb06a09c2240965151656cc0b1ebbfe6
BLAKE2b-256 cc1a575013ea1c2a4ccf2db1b3af6ff762e159dd5e065be1d933dba42e890669

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page