forked from cool-RR/python_toolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathposhing.py
More file actions
278 lines (214 loc) · 8.91 KB
/
poshing.py
File metadata and controls
278 lines (214 loc) · 8.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
from __future__ import annotations
import pathlib
import os
import socket
import sys
import re
import urllib.parse
import json
from typing import Iterable
# Constants for CLI
QUOTE_AUTO = 'auto'
QUOTE_NEVER = 'never'
QUOTE_ALWAYS = 'always'
SEPARATOR_NEWLINE = 'newline'
SEPARATOR_SPACE = 'space'
unc_drive_pattern = re.compile(r'^\\\\(?P<host>[^\\]+)\\(?P<share>[^\\])$')
def format_envvar(x: str) -> str:
return '~' if x == 'HOME' else f'${x}'
def expand_envvars(s: str) -> str:
"""Expand environment variables in a string. Supports $VAR and ${VAR} syntax."""
result = s
# Handle ${VAR} syntax
result = re.sub(
r'\$\{([^}]+)\}',
lambda m: os.environ.get(m.group(1), m.group(0)),
result
)
# Handle $VAR syntax (variable name ends at non-alphanumeric/underscore)
result = re.sub(
r'\$([A-Za-z_][A-Za-z0-9_]*)',
lambda m: os.environ.get(m.group(1), m.group(0)),
result
)
return result
def ensure_windows_path_string(path_string: str) -> str:
"""Convert Linux/POSIX-style paths to Windows format."""
# Handle file:/// URLs
if path_string.startswith('file:///'):
return urllib.parse.unquote(path_string[8:])
# Return other URLs (like http://) unaltered
if re.match(r'^[a-zA-Z]+://', path_string):
return path_string
path = pathlib.Path(path_string)
posix_path = path.as_posix()
if re.match('^/[a-zA-Z]/.*$', posix_path):
# Handle local drive paths like /c/Users/...
return '%s:%s' % (
posix_path[1],
re.sub('(?<=[^\\\\])\\\\ ', ' ', posix_path).replace('/', '\\')[2:]
)
elif re.match('^//[^/]+/.*$', posix_path):
# Handle UNC network paths like //server/share/...
return posix_path.replace('/', '\\')
else:
return path_string
def normalize_path_separators(s: str) -> str:
"""Normalize path separators for the current OS."""
if sys.platform == 'win32':
return ensure_windows_path_string(s)
else:
return ensure_linux_path_string(s)
def ensure_linux_path_string(path_string: str) -> str:
"""Convert Windows-style paths to Linux/POSIX format."""
# Handle file:/// URLs
if path_string.startswith('file:///'):
return urllib.parse.unquote(path_string[8:])
# Return other URLs (like http://) unaltered
if re.match(r'^[a-zA-Z]+://', path_string):
return path_string
# Convert backslashes to forward slashes
result = path_string.replace('\\', '/')
# Handle Windows drive paths like C:/Users/... -> /c/Users/...
if re.match(r'^[a-zA-Z]:/', result):
result = '/' + result[0].lower() + result[2:]
# Handle UNC paths like //server/share -> //server/share (already correct)
return result
def load_config() -> tuple[dict, int]:
"""
Load configuration from ~/.posh/config.json.
Returns (envvar_paths_dict, shawty_length_threshold).
Expected format:
{
"envvars": {
"ENVVAR_NAME": ["path1", "path2", ...],
...
},
"shawty_length_threshold": 30
}
"""
config_path = pathlib.Path.home() / '.posh' / 'config.json'
if not config_path.exists():
return {}, 30
try:
with open(config_path, 'r') as f:
data = json.load(f)
if not isinstance(data, dict):
return {}, 30
envvars = data.get('envvars', {})
threshold = data.get('shawty_length_threshold', 30)
return (envvars if isinstance(envvars, dict) else {}), threshold
except (json.JSONDecodeError, IOError):
return {}, 30
def _posh(path_string: str = None, allow_cwd: bool = True) -> str:
# Return URLs (like http://) unaltered
if re.match(r'^[a-zA-Z]+://', path_string):
return path_string
path = pathlib.Path(path_string)
if not path.is_absolute():
if allow_cwd:
path = pathlib.Path.cwd() / path
else:
return pathlib.Path(os.path.normpath(path)).as_posix()
path = pathlib.Path(os.path.normpath(path))
if ((sys.platform == 'win32') and
(unc_drive_match := unc_drive_pattern.fullmatch(str(path.drive))) and
(unc_drive_match.group('host').lower() == socket.gethostname().lower())):
share = unc_drive_match.group('share')
path = pathlib.Path(f'{share}:\\', *path.parts[1:])
# Load envvar paths from config file
envvar_paths, _ = load_config()
# Convert string paths to pathlib.Path objects (with envvar expansion and separator normalization)
for envvar_name in list(envvar_paths.keys()):
if not isinstance(envvar_paths[envvar_name], list):
envvar_paths[envvar_name] = []
envvar_paths[envvar_name] = [
pathlib.Path(normalize_path_separators(expand_envvars(p)))
for p in envvar_paths[envvar_name]
]
# Add environment values if they exist
for envvar_name in envvar_paths:
try:
envvar_value = os.environ[envvar_name]
envvar_paths[envvar_name].append(pathlib.Path(envvar_value))
except KeyError:
pass
# Try each envvar and its paths
for envvar_name, paths in envvar_paths.items():
for envvar_path in paths:
if path == envvar_path:
return f'{format_envvar(envvar_name)}'
try:
relative_path = path.relative_to(envvar_path)
return f'{format_envvar(envvar_name)}/{relative_path.as_posix()}'
except ValueError:
continue
return path.as_posix()
def apply_shawty(path_string: str, shawty_length_threshold: int = 30) -> str:
"""Apply shawty abbreviation to a path string."""
starts_with_slash = path_string.startswith('/')
slash_count = path_string.count('/')
# Adjust count if path starts with slash (leading slash doesn't count)
adjusted_count = slash_count - 1 if starts_with_slash else slash_count
if adjusted_count < 2:
return path_string
# Find first and last slash positions
if starts_with_slash:
# Skip the leading slash, find the second slash
first_slash = path_string.index('/', 1)
else:
first_slash = path_string.index('/')
last_slash = path_string.rindex('/')
# Build abbreviated path (without slashes around ellipsis)
abbreviated = path_string[:first_slash] + '…' + path_string[last_slash + 1:]
# If still over threshold, delete everything before the ellipsis
if len(abbreviated) > shawty_length_threshold:
abbreviated = '…' + path_string[last_slash + 1:]
return abbreviated
def posh(path_strings: Iterable[str] | str | None = None,
quote_mode: str = QUOTE_AUTO,
separator: str = SEPARATOR_NEWLINE,
allow_cwd: bool = True,
shawty: bool = False,
shawty_length_threshold: int | None = None) -> str:
"""
Convert paths to a more readable format using environment variables.
Args:
paths: A single path or list of paths to process
quote_mode: Whether to quote paths (QUOTE_AUTO, QUOTE_NEVER, or QUOTE_ALWAYS)
separator: Separator to use between multiple paths (SEPARATOR_NEWLINE or SEPARATOR_SPACE)
allow_cwd: When False, don't resolve relative paths against current working directory
shawty: Abbreviate paths with 2+ slashes: replace middle sections with ellipsis
shawty_length_threshold: If abbreviated path still exceeds this length, trim further (defaults to config.json value)
Returns:
Formatted path string(s)
"""
if path_strings is None:
return ""
if not isinstance(path_strings, (list, tuple)):
path_strings = [path_strings]
# Load config to get default threshold if not provided
_, config_threshold = load_config()
threshold = shawty_length_threshold if shawty_length_threshold is not None else config_threshold
results = [_posh(path_string, allow_cwd=allow_cwd) for path_string in path_strings]
if shawty:
results = [apply_shawty(result, threshold) for result in results]
if quote_mode == QUOTE_ALWAYS:
quoted_results = [f'"{result}"' for result in results]
elif quote_mode == QUOTE_AUTO:
if separator == SEPARATOR_SPACE and len(results) > 1:
# If using space separator with multiple paths, quote all paths in auto mode
quoted_results = [f'"{result}"' for result in results]
else:
quoted_results = [f'"{result}"' if ' ' in result else result for result in results]
else:
assert quote_mode == QUOTE_NEVER
quoted_results = results
sep = '\n' if separator == SEPARATOR_NEWLINE else ' '
return sep.join(quoted_results)
def posh_path(path: pathlib.Path | str, allow_cwd: bool = True) -> str:
"""Process a path using the posh function directly."""
path_str = str(path)
if sys.platform == 'win32':
path_str = ensure_windows_path_string(path_str)
return _posh(path_str, allow_cwd=allow_cwd)