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.
- shortenpath-0.2.0/.gitattributes +1 -0
- shortenpath-0.2.0/.github/FUNDING.yml +1 -0
- shortenpath-0.2.0/.gitignore +16 -0
- shortenpath-0.2.0/CHANGELOG.md +47 -0
- shortenpath-0.2.0/CONTRIBUTING.md +21 -0
- shortenpath-0.2.0/IMPROVEMENTS-round.md +136 -0
- shortenpath-0.2.0/LICENSE +21 -0
- shortenpath-0.2.0/PKG-INFO +189 -0
- shortenpath-0.2.0/README.md +151 -0
- shortenpath-0.2.0/pyproject.toml +65 -0
- shortenpath-0.2.0/requirements-dev.txt +3 -0
- shortenpath-0.2.0/scripts/check.ps1 +16 -0
- shortenpath-0.2.0/scripts/fetch_sources.py +33 -0
- shortenpath-0.2.0/scripts/release.ps1 +28 -0
- shortenpath-0.2.0/src/shortenpath/__init__.py +11 -0
- shortenpath-0.2.0/src/shortenpath/__main__.py +10 -0
- shortenpath-0.2.0/src/shortenpath/_cli.py +107 -0
- shortenpath-0.2.0/src/shortenpath/_name.py +45 -0
- shortenpath-0.2.0/src/shortenpath/_parts.py +133 -0
- shortenpath-0.2.0/src/shortenpath/_shorten.py +127 -0
- shortenpath-0.2.0/src/shortenpath/_width.py +84 -0
- shortenpath-0.2.0/src/shortenpath/py.typed +0 -0
- shortenpath-0.2.0/tests/sources/MANIFEST.txt +19 -0
- shortenpath-0.2.0/tests/test_cli.py +172 -0
- shortenpath-0.2.0/tests/test_contract.py +26 -0
- shortenpath-0.2.0/tests/test_docs.py +104 -0
- shortenpath-0.2.0/tests/test_e2e_wheel.py +108 -0
- shortenpath-0.2.0/tests/test_forms.py +137 -0
- shortenpath-0.2.0/tests/test_home.py +149 -0
- shortenpath-0.2.0/tests/test_oracle.py +121 -0
- shortenpath-0.2.0/tests/test_shorten.py +177 -0
- shortenpath-0.2.0/tests/test_sources.py +166 -0
- shortenpath-0.2.0/tests/test_typing.py +62 -0
- shortenpath-0.2.0/tests/test_width.py +172 -0
- 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"
|