shortenpath 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. shortenpath-0.2.0/.gitattributes +1 -0
  2. shortenpath-0.2.0/.github/FUNDING.yml +1 -0
  3. shortenpath-0.2.0/.gitignore +16 -0
  4. shortenpath-0.2.0/CHANGELOG.md +47 -0
  5. shortenpath-0.2.0/CONTRIBUTING.md +21 -0
  6. shortenpath-0.2.0/IMPROVEMENTS-round.md +136 -0
  7. shortenpath-0.2.0/LICENSE +21 -0
  8. shortenpath-0.2.0/PKG-INFO +189 -0
  9. shortenpath-0.2.0/README.md +151 -0
  10. shortenpath-0.2.0/pyproject.toml +65 -0
  11. shortenpath-0.2.0/requirements-dev.txt +3 -0
  12. shortenpath-0.2.0/scripts/check.ps1 +16 -0
  13. shortenpath-0.2.0/scripts/fetch_sources.py +33 -0
  14. shortenpath-0.2.0/scripts/release.ps1 +28 -0
  15. shortenpath-0.2.0/src/shortenpath/__init__.py +11 -0
  16. shortenpath-0.2.0/src/shortenpath/__main__.py +10 -0
  17. shortenpath-0.2.0/src/shortenpath/_cli.py +107 -0
  18. shortenpath-0.2.0/src/shortenpath/_name.py +45 -0
  19. shortenpath-0.2.0/src/shortenpath/_parts.py +133 -0
  20. shortenpath-0.2.0/src/shortenpath/_shorten.py +127 -0
  21. shortenpath-0.2.0/src/shortenpath/_width.py +84 -0
  22. shortenpath-0.2.0/src/shortenpath/py.typed +0 -0
  23. shortenpath-0.2.0/tests/sources/MANIFEST.txt +19 -0
  24. shortenpath-0.2.0/tests/test_cli.py +172 -0
  25. shortenpath-0.2.0/tests/test_contract.py +26 -0
  26. shortenpath-0.2.0/tests/test_docs.py +104 -0
  27. shortenpath-0.2.0/tests/test_e2e_wheel.py +108 -0
  28. shortenpath-0.2.0/tests/test_forms.py +137 -0
  29. shortenpath-0.2.0/tests/test_home.py +149 -0
  30. shortenpath-0.2.0/tests/test_oracle.py +121 -0
  31. shortenpath-0.2.0/tests/test_shorten.py +177 -0
  32. shortenpath-0.2.0/tests/test_sources.py +166 -0
  33. shortenpath-0.2.0/tests/test_typing.py +62 -0
  34. shortenpath-0.2.0/tests/test_width.py +172 -0
  35. shortenpath-0.2.0/tests/test_win32.py +95 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1 @@
1
+ buy_me_a_coffee: jlianesglrs
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Third-party sources, fetched by scripts/fetch_sources.py (see its MANIFEST.txt)
15
+ tests/sources/*
16
+ !tests/sources/MANIFEST.txt
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0]
10
+
11
+ ### Added
12
+
13
+ - `flavor` (`"posix"`, `"windows"`, `"native"`) and the `Flavor` type. Windows paths keep
14
+ their drive, UNC share or device prefix (`\\.\`, `\\?\`, `\\?\UNC\`, `\\.\UNC\`) whole and
15
+ take both `\` and `/` as separators, except after an exact `\\?\`.
16
+ - `display_width(text)`: terminal columns, with East Asian wide and fullwidth characters as
17
+ two, combining marks as none and a character with the emoji presentation selector as two.
18
+ The budget of `shorten` is now in columns.
19
+ - Command line tool `shortenpath` (also `python -m shortenpath`), streaming paths read from
20
+ standard input line by line.
21
+ - `os.PathLike` paths and homes.
22
+
23
+ ### Changed
24
+
25
+ - The second argument is now `width`, in terminal columns.
26
+ - Elision keeps the most whole names around the placeholder, then the most at the start,
27
+ and keeps the whole last name before the anchor when only one of them fits, also for a
28
+ path with a single name.
29
+ - A name that cannot fit is cut in the middle, keeping its start and its extension, and
30
+ never between a letter and its combining marks.
31
+ - Separator runs are kept as written.
32
+ - `sep` is now optional and, when given, replaces the flavor's rules; the default home is
33
+ only used for paths of the running system.
34
+ - The home folder folds only on whole names, never when it is relative or a bare root or
35
+ drive, and without regard to case or separator kind on Windows.
36
+
37
+ ### Fixed
38
+
39
+ - A UNC share or device prefix was cut into a lone separator.
40
+ - Wrong argument types failed deep inside; they now raise `TypeError`, and an empty
41
+ `placeholder` raises `ValueError`.
42
+
43
+ ## [0.1.0]
44
+
45
+ - First release: `shorten` collapses the home directory to `~` and middle-elides a
46
+ path to fit a width budget, with a basename-truncation fallback and a configurable
47
+ separator, home and placeholder.
@@ -0,0 +1,21 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project keeps a small, predictable shape.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ pip install -r requirements-dev.txt
9
+ ```
10
+
11
+ ## Before you open a pull request
12
+
13
+ - Keep public API in `src/shortenpath/__init__.py`; internals live in `_`-prefixed modules.
14
+ - Add a test for the behaviour you change. Tests inject time, disk and network; they
15
+ never touch the real thing.
16
+ - Run `python scripts/fetch_sources.py` once so the tests can check the rules against the
17
+ Microsoft, POSIX and Unicode texts, then `scripts/check.ps1` (format, lint, types, tests)
18
+ and make sure it passes. `SHORTENPATH_E2E=1` adds the installed-wheel test.
19
+ - One logical change per commit, imperative subject line.
20
+
21
+ Open an issue first if the change is large or shifts the public API.
@@ -0,0 +1,136 @@
1
+ # shortenpath improvement round
2
+
3
+ Progress:
4
+ - [x] Brainstorm ranked improvements (this file)
5
+ - [x] Path forms: POSIX roots, Windows drives, UNC, `\\?\` and `\\.\` device paths, mixed separators
6
+ - [x] Display width: wide and fullwidth characters count two columns, combining marks none
7
+ - [x] Home folding to `~` that respects component boundaries and Windows case rules
8
+ - [x] File name kept whole when it can be, extension kept when the name is cut
9
+ - [x] Deterministic elision search (most segments kept, then most head segments)
10
+ - [x] Typed API: `Flavor` alias, `display_width`, clean `TypeError` and `ValueError`
11
+ - [x] Command line tool (`shortenpath`, `python -m shortenpath`)
12
+ - [x] Contracts: fetched sources, stdlib and Win32 oracles, properties, typing, README, wheel e2e
13
+ - [x] Mutation run (guards changed one at a time, each must fail the suite)
14
+ - [x] Docs: README, CHANGELOG, docstrings, packaging metadata
15
+ - [x] Gates: ruff format, ruff check, mypy --strict, pytest
16
+ - [x] Independent review findings fixed with regression tests
17
+ - [x] Pull request opened, guard clean
18
+
19
+ ## Starting point
20
+
21
+ `0.1.0` (not on PyPI) ships `shorten(path, max_len, *, home_tilde, home, sep, placeholder)`.
22
+ Read as a user who puts paths in prompts, status bars, tables and logs:
23
+
24
+ - One separator only, `os.sep` by default. On Windows `C:/Users/x` is one segment; on Linux a
25
+ Windows path from a log cannot be shortened unless the caller picks `sep="\\"`, and then
26
+ `/` inside it is not a separator.
27
+ - The only root it knows is a leading separator. `\\server\share\dir\f` is elided into
28
+ `\...\f`, losing the server and share; `\\?\C:\x\y\f` loses the device prefix and the drive.
29
+ - Length is `len()`. A path with CJK names or fullwidth characters takes twice the columns it
30
+ is budgeted for; a name with combining accents is budgeted for columns it never uses, and a
31
+ cut can separate a letter from its accent.
32
+ - Home folding compares raw strings: `C:\Users\Javi` does not fold `c:\users\javi\x` on
33
+ Windows, and `home="C:\\"` folds every path on the drive into `~\...`.
34
+ - When the name does not fit it keeps only its end; the start of the name is lost first.
35
+ - No command line tool, README examples not executed, no wheel test, no ground truth in the
36
+ tests.
37
+
38
+ ## Sources (fetched 2026-10-13)
39
+
40
+ - File path formats on Windows systems, dotnet/docs at 3ad903a7:
41
+ traditional DOS paths (`C:\`, `\dir` relative to the current drive, `C:dir` relative to the
42
+ current directory of drive C), UNC paths (`\\server\share` is the volume), DOS device paths
43
+ (`\\.\` and `\\?\`, the first segment after the prefix names the volume, `UNC\server\share`
44
+ for network shares), forward slashes are separators except in a path that starts exactly
45
+ with `\\?\`, which skips normalization.
46
+ - Naming Files, Paths, and Namespaces, MicrosoftDocs/win32 at ca8274de: backslash and forward
47
+ slash are both reserved; the prefix of a path decides its namespace.
48
+ - PathCompactPathExW, MicrosoftDocs/sdk-api at 656e9656: replaces path components with an
49
+ ellipsis to fit a character count, keeps `/` if the input used it, truncates a bare file
50
+ name at the end. Probed live through `shlwapi.dll` on Windows 11: it counts characters, not
51
+ columns, cuts inside the UNC server name (`\\serv...\file.txt`), cuts inside directory names (`C:\path1...\sample.txt`)
52
+ and drops the extension when the name is cut (`...\verylon...`).
53
+ - POSIX.1-2024 base definitions 3.2, 3.42, 3.146, 3.170, 3.254, 3.256 and 4.16: a filename
54
+ never contains a slash, runs of slashes equal one, exactly two leading slashes are
55
+ implementation defined, three or more equal one; the home directory is `HOME`.
56
+ - `EastAsianWidth.txt` for Unicode 13.0.0 to 17.0.0: the W and F classes, read as two
57
+ columns. Zero columns for combining marks, format characters and Hangul medial and final
58
+ jamo, and two for a character with the emoji presentation selector, are this library's own
59
+ convention (the one most terminals follow), not a fetched rule.
60
+ - The texts are third-party and not committed: `scripts/fetch_sources.py` fetches them into a
61
+ gitignored folder checked against a tracked MANIFEST; the tests that read them skip when the
62
+ folder is absent.
63
+
64
+ ## Ranked improvements
65
+
66
+ 1. **Path forms.** A `flavor` (`"posix"`, `"windows"`, `"native"`) decides the separators and
67
+ the anchor. The anchor (drive, root, UNC server and share, device prefix and volume) is
68
+ never cut. Windows takes `\` and `/` everywhere except after an exact `\\?\`. Runs of
69
+ separators and the original separator characters are kept as written. The anchor split
70
+ agrees with `ntpath.splitroot` and `posixpath.splitroot` where they exist.
71
+ 2. **Display width.** `display_width(text)` counts W and F as two columns, combining marks,
72
+ format characters and Hangul medial and final jamo as zero, everything else as one. The
73
+ budget is in columns, cuts never split a base from its marks or a zero width joiner from
74
+ the next character, and the result never exceeds the budget.
75
+ 3. **Home folding.** Only on a component boundary, never for a home that is a bare anchor,
76
+ case-insensitive and separator-insensitive on Windows, keeping the separator written in the
77
+ path after `~`.
78
+ 4. **Name and extension.** The last segment is kept whole whenever `.../name` fits; a name that
79
+ cannot fit is cut in the middle, keeping its start and its extension.
80
+ 5. **Deterministic elision.** Among all forms `anchor head/.../tail` that fit, keep the most
81
+ segments, then the most head segments. One rule, `O(n log n)`, documented with examples.
82
+ 6. **Typed API and failures.** `Flavor` literal alias, `width` in columns, `os.PathLike`
83
+ accepted, `TypeError` for bytes and non-strings, `ValueError` for a width below one or an
84
+ empty separator or placeholder.
85
+ 7. **Command line tool.** `shortenpath [-w COLS] [--flavor] [--no-tilde] [--home] PATH...`,
86
+ lines from stdin without paths, terminal width by default, exit 2 on bad input, quiet on a
87
+ closed pipe.
88
+ 8. **Contracts that bite.** Examples from the fetched Microsoft and POSIX texts, the width table
89
+ of the running Unicode version, `splitroot` oracles over fuzzed paths, the live Win32
90
+ function on Windows as a reference, properties over fuzzed paths (fits, deterministic,
91
+ anchor and name kept when they fit), executed README, installed-wheel e2e, mutation run.
92
+ 9. **Docs and packaging.** README with the rules and a table of forms, CHANGELOG 0.2.0,
93
+ classifiers and keywords, console script.
94
+
95
+ Left out on purpose: resolving `.` and `..` or symlinks (display only, never touches disk),
96
+ grapheme clusters beyond marks and joiners (emoji sequences differ between terminals),
97
+ ambiguous width characters as two columns (terminal setting, not data), URL schemes.
98
+
99
+ ## Verification
100
+
101
+ - Gates: `ruff format --check .`, `ruff check .`, `mypy --strict src` (also `src tests`) and
102
+ `pytest` exit 0 on a second machine with Python 3.14, a fresh clone, the sources in place and
103
+ `SHORTENPATH_E2E=1`: 222 passed, nothing skipped. The wheel test builds the wheel and the
104
+ sdist, checks that no fetched source or test ships in them, installs the wheel in a fresh
105
+ venv, drives the API from an empty folder, and runs the installed `shortenpath` script and
106
+ `python -m shortenpath` (stdin included).
107
+ - Ground truth: every example path of the two Microsoft pages is read from the fetched text
108
+ and split with the anchor its description gives; the rules relied on (forward slash as a
109
+ separator except after an exact `\\?\`, server and share as the volume of a UNC device, both
110
+ slashes reserved) and the POSIX leading slash, filename and home definitions are checked in
111
+ the fetched texts; every assigned code point of `EastAsianWidth.txt` for the running Unicode
112
+ version gets two columns for W and F and one otherwise.
113
+ - Oracles: `ntpath.splitroot` and `ntpath.splitdrive` over 40000 fuzzed Windows paths (only
114
+ the two documented differences excluded), `posixpath.splitroot`, `os.path.splitext` for the
115
+ extension, a brute force reading of the elision rule, and on Windows the live
116
+ `PathCompactPathExW`: both keep a path that fits, and whenever it keeps the file name whole,
117
+ so does this library. It compacts a path exactly as long as its budget, which this one does
118
+ not. Properties over fuzzed paths: never wider than the budget, a fitting result is stable,
119
+ anchor and last name kept whole when they fit, a last name that fits alone never cut.
120
+ - Mutations: 96 mutants (width classes and ranges, emoji presentation, clusters and cuts,
121
+ extension and squeeze rules, every anchor form and the verbatim prefix, separator runs,
122
+ home folding guards and keys, every validation, the search and its tie break, every
123
+ fallback, and the command line rules: width, stdin, streaming flush, option check before
124
+ input, closed pipe, code page), applied one at a time with the bytecode cache disabled and
125
+ purged before each run, baseline green before and after. Earlier runs left survivors (range
126
+ ends, separator runs, a home check that depended on the machine's environment, equivalent
127
+ branches that were then simplified); each got a test or a simpler code path. The final run
128
+ killed all 96.
129
+ - Independent review: 11 findings. Fixed with regression tests: a single name that fits
130
+ alone was cut to keep the anchor; a separator run after a UNC share blocked the whole name;
131
+ a wide last stem character broke the extension rule and the start could be empty; a
132
+ relative home folded relative paths; the emoji presentation selector and Hangul Jamo
133
+ Extended-B were miscounted; the command line read all of stdin before writing; README rules
134
+ were incomplete; the checklist was stale; the UAX #11 line claimed an unfetched source;
135
+ weak Win32 and property tests were tightened. Control characters still count one column and
136
+ are documented.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: shortenpath
3
+ Version: 0.2.0
4
+ Summary: Shorten a filesystem path to fit a terminal column budget: home folds to ~, the root and file name stay, the middle goes
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/shortenpath
7
+ Project-URL: Issues, https://github.com/JaviLianes8/shortenpath/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/shortenpath/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ Keywords: cli,display,elide,ellipsis,path,shorten,terminal,tilde,unc,width
15
+ Classifier: Development Status :: 4 - Beta
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3.14
26
+ Classifier: Topic :: Software Development :: Libraries
27
+ Classifier: Topic :: System :: Filesystems
28
+ Classifier: Topic :: Terminals
29
+ Classifier: Typing :: Typed
30
+ Requires-Python: >=3.10
31
+ Provides-Extra: dev
32
+ Requires-Dist: build; extra == 'dev'
33
+ Requires-Dist: mypy; extra == 'dev'
34
+ Requires-Dist: pytest; extra == 'dev'
35
+ Requires-Dist: ruff; extra == 'dev'
36
+ Requires-Dist: twine; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # shortenpath
40
+
41
+ Shorten a filesystem path to fit a terminal column budget for prompts, status bars, tables
42
+ and logs. The home folder folds to `~`, the root and the file name stay, and whole folders
43
+ in the middle give way to `...`. Display only: the filesystem is never read.
44
+
45
+ ## Install
46
+
47
+ ```
48
+ pip install shortenpath
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ```python
54
+ from shortenpath import shorten
55
+
56
+ path = "/home/javi/projects/shop/src/cart/checkout.py"
57
+ print(shorten(path, 60, flavor="posix", home="/home/javi"))
58
+ print(shorten(path, 30, flavor="posix", home="/home/javi"))
59
+ print(shorten(path, 22, flavor="posix", home="/home/javi"))
60
+ print(shorten(path, 14, flavor="posix", home="/home/javi"))
61
+ print(shorten(path, 9, flavor="posix", home="/home/javi"))
62
+ # ~/projects/shop/src/cart/checkout.py
63
+ # ~/.../src/cart/checkout.py
64
+ # ~/.../cart/checkout.py
65
+ # checkout.py
66
+ # ch...t.py
67
+ ```
68
+
69
+ Windows paths keep their drive, UNC share or device prefix whole, and both `\` and `/`
70
+ separate folders (each kept as written). The home folder matches regardless of case.
71
+
72
+ ```python
73
+ from shortenpath import shorten
74
+
75
+ print(shorten(r"C:\Users\javi\AppData\Local\Temp\report.pdf", 24, flavor="windows"))
76
+ print(shorten(r"\\fileserver\finance\2026\q3\budget.xlsx", 36, flavor="windows"))
77
+ print(shorten(r"\\?\C:\very\deep\tree\of\folders\notes.txt", 26, flavor="windows"))
78
+ print(
79
+ shorten("C:/Users/javi/Downloads/tools/setup.exe", 26, flavor="windows", home=r"c:\users\javi")
80
+ )
81
+ # C:\Users\...\report.pdf
82
+ # \\fileserver\finance\...\budget.xlsx
83
+ # \\?\C:\very\...\notes.txt
84
+ # ~/Downloads/.../setup.exe
85
+ ```
86
+
87
+ The budget is in terminal columns: East Asian wide characters take two, combining accents
88
+ none, and a cut never splits a letter from its accent.
89
+
90
+ ```python
91
+ from shortenpath import display_width, shorten
92
+
93
+ path = "/srv/写真/2026/夏休み/海辺の夕焼け.jpg"
94
+ print(display_width(path), len(path))
95
+ print(shorten(path, 30, flavor="posix"))
96
+ print(shorten(path, 24, flavor="posix"))
97
+ # 38 27
98
+ # /srv/写真/.../海辺の夕焼け.jpg
99
+ # /.../海辺の夕焼け.jpg
100
+ ```
101
+
102
+ Any separator and any placeholder:
103
+
104
+ ```python
105
+ from shortenpath import shorten
106
+
107
+ print(shorten("docs::api::v2::internal::client", 20, sep="::"))
108
+ print(shorten("/var/log/nginx/access.log", 16, flavor="posix", placeholder="…"))
109
+ # docs::...::client
110
+ # /…/access.log
111
+ ```
112
+
113
+ ## The rules
114
+
115
+ `shorten(path, width, *, flavor="native", home_tilde=True, home=None, sep=None,
116
+ placeholder="...")` returns `path` itself when it fits, otherwise the first of these that
117
+ fits `width` columns:
118
+
119
+ 1. The anchor, then as many whole folders as fit around the placeholder, then the last
120
+ name: the form keeping the most names wins, and between equals the one keeping more at
121
+ the start. The anchor is a POSIX root (`/`, `//`), a drive (`C:\`, `C:`), a UNC share
122
+ (`\\server\share\`), a device path (`\\.\C:\`, `\\?\Volume{...}\`,
123
+ `\\?\UNC\server\share\`) or `~`.
124
+ 2. The placeholder, the separator and the last name: the whole name is worth more than the
125
+ anchor. A run of separators that does not fit shrinks to one.
126
+ 3. The last name alone, then the anchor with the name cut in the middle (a path with a
127
+ single name and room for both).
128
+ 4. The last name cut in the middle: its start, the placeholder, and its end, which keeps
129
+ the extension and the stem character before it when there is room for them. With no
130
+ room for the placeholder only the start of the name is kept, and when not even its first
131
+ character fits the result is empty.
132
+
133
+ | Option | Meaning |
134
+ | --- | --- |
135
+ | `flavor` | `"posix"`, `"windows"` or `"native"` (the running system). `Flavor` is its type. |
136
+ | `home_tilde`, `home` | Fold the home folder to `~`. `home` defaults to the current user's for native paths. A relative home, or one that is only a root, never folds. |
137
+ | `sep` | A custom separator instead of the flavor's rules, for URLs or keys. |
138
+ | `placeholder` | What stands for the removed folders, `...` by default. |
139
+
140
+ Separator runs are kept as written. On Windows a path that starts exactly with `\\?\`
141
+ skips normalization, so there only `\` separates and `/` is part of a name.
142
+ `display_width(text)` gives the columns of any text: East Asian wide and fullwidth
143
+ characters and characters with the emoji presentation selector take two, combining marks,
144
+ format characters and Hangul medial and final jamo none, everything else (control
145
+ characters included) one. Emoji joined into one picture are counted as their parts, which
146
+ can only overestimate. Wrong types raise `TypeError`; a
147
+ width below one or an empty separator or placeholder raises `ValueError`.
148
+
149
+ Compared with the Win32 `PathCompactPathEx` function, this never cuts inside the
150
+ anchor or a folder name, keeps the extension of a cut name, counts columns instead of
151
+ characters, and returns a path exactly as long as the budget unchanged.
152
+
153
+ ## Command line
154
+
155
+ ```
156
+ $ shortenpath -w 24 -f posix --home /home/javi /home/javi/projects/shop/src/cart/checkout.py
157
+ ~/.../cart/checkout.py
158
+ $ shortenpath -w 24 -f windows 'C:\Program Files\Common Files\system\ado\msado15.dll'
159
+ C:\...\ado\msado15.dll
160
+ ```
161
+
162
+ Paths come from the arguments, or one per line from standard input, each written as soon
163
+ as it is read. `-w` defaults to the terminal width. Bad input exits with status 2.
164
+ `python -m shortenpath` works too.
165
+
166
+ ## Why
167
+
168
+ Folding a path to `~` and trimming its middle to fit a column budget is a common need,
169
+ yet on PyPI the pieces live apart: `os.path.expanduser` only expands, and
170
+ `textwrap.shorten` cuts words from the end, not folders from the middle. Most snippets
171
+ that do it count characters, split on one separator and cut UNC shares in half. This one
172
+ reads Windows and POSIX path forms, counts columns, and has no dependencies.
173
+
174
+ ## Development
175
+
176
+ `python scripts/fetch_sources.py` downloads the Microsoft, POSIX and Unicode texts the
177
+ tests check the rules against (listed with their hashes in `tests/sources/MANIFEST.txt`);
178
+ without them those tests skip. `SHORTENPATH_E2E=1 pytest` also builds the wheel and tests
179
+ the installed package. `scripts/check.ps1` runs the whole gate.
180
+
181
+ ## License
182
+
183
+ MIT. See [LICENSE](LICENSE).
184
+
185
+ ## Links
186
+
187
+ - Website: https://lianes8studio.com
188
+ - GitHub: https://github.com/JaviLianes8
189
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,151 @@
1
+ # shortenpath
2
+
3
+ Shorten a filesystem path to fit a terminal column budget for prompts, status bars, tables
4
+ and logs. The home folder folds to `~`, the root and the file name stay, and whole folders
5
+ in the middle give way to `...`. Display only: the filesystem is never read.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install shortenpath
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from shortenpath import shorten
17
+
18
+ path = "/home/javi/projects/shop/src/cart/checkout.py"
19
+ print(shorten(path, 60, flavor="posix", home="/home/javi"))
20
+ print(shorten(path, 30, flavor="posix", home="/home/javi"))
21
+ print(shorten(path, 22, flavor="posix", home="/home/javi"))
22
+ print(shorten(path, 14, flavor="posix", home="/home/javi"))
23
+ print(shorten(path, 9, flavor="posix", home="/home/javi"))
24
+ # ~/projects/shop/src/cart/checkout.py
25
+ # ~/.../src/cart/checkout.py
26
+ # ~/.../cart/checkout.py
27
+ # checkout.py
28
+ # ch...t.py
29
+ ```
30
+
31
+ Windows paths keep their drive, UNC share or device prefix whole, and both `\` and `/`
32
+ separate folders (each kept as written). The home folder matches regardless of case.
33
+
34
+ ```python
35
+ from shortenpath import shorten
36
+
37
+ print(shorten(r"C:\Users\javi\AppData\Local\Temp\report.pdf", 24, flavor="windows"))
38
+ print(shorten(r"\\fileserver\finance\2026\q3\budget.xlsx", 36, flavor="windows"))
39
+ print(shorten(r"\\?\C:\very\deep\tree\of\folders\notes.txt", 26, flavor="windows"))
40
+ print(
41
+ shorten("C:/Users/javi/Downloads/tools/setup.exe", 26, flavor="windows", home=r"c:\users\javi")
42
+ )
43
+ # C:\Users\...\report.pdf
44
+ # \\fileserver\finance\...\budget.xlsx
45
+ # \\?\C:\very\...\notes.txt
46
+ # ~/Downloads/.../setup.exe
47
+ ```
48
+
49
+ The budget is in terminal columns: East Asian wide characters take two, combining accents
50
+ none, and a cut never splits a letter from its accent.
51
+
52
+ ```python
53
+ from shortenpath import display_width, shorten
54
+
55
+ path = "/srv/写真/2026/夏休み/海辺の夕焼け.jpg"
56
+ print(display_width(path), len(path))
57
+ print(shorten(path, 30, flavor="posix"))
58
+ print(shorten(path, 24, flavor="posix"))
59
+ # 38 27
60
+ # /srv/写真/.../海辺の夕焼け.jpg
61
+ # /.../海辺の夕焼け.jpg
62
+ ```
63
+
64
+ Any separator and any placeholder:
65
+
66
+ ```python
67
+ from shortenpath import shorten
68
+
69
+ print(shorten("docs::api::v2::internal::client", 20, sep="::"))
70
+ print(shorten("/var/log/nginx/access.log", 16, flavor="posix", placeholder="…"))
71
+ # docs::...::client
72
+ # /…/access.log
73
+ ```
74
+
75
+ ## The rules
76
+
77
+ `shorten(path, width, *, flavor="native", home_tilde=True, home=None, sep=None,
78
+ placeholder="...")` returns `path` itself when it fits, otherwise the first of these that
79
+ fits `width` columns:
80
+
81
+ 1. The anchor, then as many whole folders as fit around the placeholder, then the last
82
+ name: the form keeping the most names wins, and between equals the one keeping more at
83
+ the start. The anchor is a POSIX root (`/`, `//`), a drive (`C:\`, `C:`), a UNC share
84
+ (`\\server\share\`), a device path (`\\.\C:\`, `\\?\Volume{...}\`,
85
+ `\\?\UNC\server\share\`) or `~`.
86
+ 2. The placeholder, the separator and the last name: the whole name is worth more than the
87
+ anchor. A run of separators that does not fit shrinks to one.
88
+ 3. The last name alone, then the anchor with the name cut in the middle (a path with a
89
+ single name and room for both).
90
+ 4. The last name cut in the middle: its start, the placeholder, and its end, which keeps
91
+ the extension and the stem character before it when there is room for them. With no
92
+ room for the placeholder only the start of the name is kept, and when not even its first
93
+ character fits the result is empty.
94
+
95
+ | Option | Meaning |
96
+ | --- | --- |
97
+ | `flavor` | `"posix"`, `"windows"` or `"native"` (the running system). `Flavor` is its type. |
98
+ | `home_tilde`, `home` | Fold the home folder to `~`. `home` defaults to the current user's for native paths. A relative home, or one that is only a root, never folds. |
99
+ | `sep` | A custom separator instead of the flavor's rules, for URLs or keys. |
100
+ | `placeholder` | What stands for the removed folders, `...` by default. |
101
+
102
+ Separator runs are kept as written. On Windows a path that starts exactly with `\\?\`
103
+ skips normalization, so there only `\` separates and `/` is part of a name.
104
+ `display_width(text)` gives the columns of any text: East Asian wide and fullwidth
105
+ characters and characters with the emoji presentation selector take two, combining marks,
106
+ format characters and Hangul medial and final jamo none, everything else (control
107
+ characters included) one. Emoji joined into one picture are counted as their parts, which
108
+ can only overestimate. Wrong types raise `TypeError`; a
109
+ width below one or an empty separator or placeholder raises `ValueError`.
110
+
111
+ Compared with the Win32 `PathCompactPathEx` function, this never cuts inside the
112
+ anchor or a folder name, keeps the extension of a cut name, counts columns instead of
113
+ characters, and returns a path exactly as long as the budget unchanged.
114
+
115
+ ## Command line
116
+
117
+ ```
118
+ $ shortenpath -w 24 -f posix --home /home/javi /home/javi/projects/shop/src/cart/checkout.py
119
+ ~/.../cart/checkout.py
120
+ $ shortenpath -w 24 -f windows 'C:\Program Files\Common Files\system\ado\msado15.dll'
121
+ C:\...\ado\msado15.dll
122
+ ```
123
+
124
+ Paths come from the arguments, or one per line from standard input, each written as soon
125
+ as it is read. `-w` defaults to the terminal width. Bad input exits with status 2.
126
+ `python -m shortenpath` works too.
127
+
128
+ ## Why
129
+
130
+ Folding a path to `~` and trimming its middle to fit a column budget is a common need,
131
+ yet on PyPI the pieces live apart: `os.path.expanduser` only expands, and
132
+ `textwrap.shorten` cuts words from the end, not folders from the middle. Most snippets
133
+ that do it count characters, split on one separator and cut UNC shares in half. This one
134
+ reads Windows and POSIX path forms, counts columns, and has no dependencies.
135
+
136
+ ## Development
137
+
138
+ `python scripts/fetch_sources.py` downloads the Microsoft, POSIX and Unicode texts the
139
+ tests check the rules against (listed with their hashes in `tests/sources/MANIFEST.txt`);
140
+ without them those tests skip. `SHORTENPATH_E2E=1 pytest` also builds the wheel and tests
141
+ the installed package. `scripts/check.ps1` runs the whole gate.
142
+
143
+ ## License
144
+
145
+ MIT. See [LICENSE](LICENSE).
146
+
147
+ ## Links
148
+
149
+ - Website: https://lianes8studio.com
150
+ - GitHub: https://github.com/JaviLianes8
151
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,65 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "shortenpath"
7
+ version = "0.2.0"
8
+ description = "Shorten a filesystem path to fit a terminal column budget: home folds to ~, the root and file name stay, the middle goes"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Javier Lianes", email = "javier@lianes8studio.com" }]
14
+ keywords = ["path", "shorten", "ellipsis", "elide", "display", "terminal", "width", "tilde", "unc", "cli"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development :: Libraries",
28
+ "Topic :: System :: Filesystems",
29
+ "Topic :: Terminals",
30
+ "Typing :: Typed",
31
+ ]
32
+ dependencies = []
33
+
34
+ [project.optional-dependencies]
35
+ dev = ["ruff", "mypy", "pytest", "build", "twine"]
36
+
37
+ [project.scripts]
38
+ shortenpath = "shortenpath._cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://lianes8studio.com"
42
+ Repository = "https://github.com/JaviLianes8/shortenpath"
43
+ Issues = "https://github.com/JaviLianes8/shortenpath/issues"
44
+ Changelog = "https://github.com/JaviLianes8/shortenpath/blob/main/CHANGELOG.md"
45
+ LinkedIn = "https://www.linkedin.com/in/jlianes/"
46
+ Funding = "https://buymeacoffee.com/jlianesglrs"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/shortenpath"]
50
+
51
+ [tool.ruff]
52
+ line-length = 100
53
+ target-version = "py310"
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
57
+
58
+ [tool.mypy]
59
+ python_version = "3.10"
60
+ strict = true
61
+
62
+ [tool.pytest.ini_options]
63
+ testpaths = ["tests"]
64
+ pythonpath = ["src"]
65
+ addopts = "-q"
@@ -0,0 +1,3 @@
1
+ # Development dependencies: the package in editable mode plus its dev extras
2
+ # (ruff, mypy, pytest, build, twine). Install with: pip install -r requirements-dev.txt
3
+ -e .[dev]