xmyshell 0.1.0__py3-none-any.whl
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.
- xmyshell/__init__.py +7 -0
- xmyshell/__main__.py +12 -0
- xmyshell/completer.py +405 -0
- xmyshell/data/commands.json +4417 -0
- xmyshell/environment.py +59 -0
- xmyshell/init.py +34 -0
- xmyshell/kernel.py +261 -0
- xmyshell/keybindings.py +259 -0
- xmyshell/lexer.py +219 -0
- xmyshell/main.py +56 -0
- xmyshell/meta.py +55 -0
- xmyshell/themes.py +197 -0
- xmyshell/utils.py +35 -0
- xmyshell-0.1.0.dist-info/METADATA +142 -0
- xmyshell-0.1.0.dist-info/RECORD +19 -0
- xmyshell-0.1.0.dist-info/WHEEL +5 -0
- xmyshell-0.1.0.dist-info/entry_points.txt +2 -0
- xmyshell-0.1.0.dist-info/licenses/LICENCE +0 -0
- xmyshell-0.1.0.dist-info/top_level.txt +1 -0
xmyshell/__init__.py
ADDED
xmyshell/__main__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
def main() -> None:
|
|
4
|
+
if "--version" in sys.argv:
|
|
5
|
+
from .meta import WELCOME_MESSAGE
|
|
6
|
+
print(WELCOME_MESSAGE)
|
|
7
|
+
elif "--help" in sys.argv:
|
|
8
|
+
from .meta import HELP_MESSAGE
|
|
9
|
+
print(HELP_MESSAGE)
|
|
10
|
+
else:
|
|
11
|
+
from .main import xmyshell_main
|
|
12
|
+
xmyshell_main()
|
xmyshell/completer.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import keyword
|
|
6
|
+
import pkgutil
|
|
7
|
+
from string import whitespace
|
|
8
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
9
|
+
from prompt_toolkit.document import Document
|
|
10
|
+
from .environment import namespace
|
|
11
|
+
from .utils import truncate
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
MAX_DISPLAY_LEN = 24
|
|
15
|
+
MAX_META_LEN = 12
|
|
16
|
+
|
|
17
|
+
BUILTINS = ("cd", "cp", "cat", "echo", "exit", "import", "ls", "print", "pyexec", "pwd", "source",
|
|
18
|
+
"clear", "grep", "find", "from", "mkdir", "rm", "reload", "mv", "help")
|
|
19
|
+
|
|
20
|
+
COMMON = (
|
|
21
|
+
"python", "python3", "pip", "pip3", "git", "conda", "uv",
|
|
22
|
+
"ssh", "scp", "curl", "wget", "winget", "docker",
|
|
23
|
+
"vim", "nvim", "nano", "code",
|
|
24
|
+
"sudo", "apt", "yum", "brew", "chmod", "chown",
|
|
25
|
+
"ps", "top", "htop", "kill", "pkill", "killall",
|
|
26
|
+
"systemctl", "journalctl", "service", "watch", "screen", "tmux", "nohup",
|
|
27
|
+
"ping", "dig", "nslookup", "traceroute", "nc", "route", "arp", "iptables", "ufw",
|
|
28
|
+
"ip", "ifconfig", "netstat", "ss",
|
|
29
|
+
"tar", "gzip", "gunzip", "bzip2", "xz", "zip", "unzip",
|
|
30
|
+
"node", "npm", "npx", "yarn", "pnpm",
|
|
31
|
+
"java", "javac", "mvn", "gradle", "ant",
|
|
32
|
+
"gcc", "g++", "make", "cmake",
|
|
33
|
+
"go", "rustc", "cargo", "gdb",
|
|
34
|
+
"sqlite3", "mysql", "psql", "mongosh", "redis-cli",
|
|
35
|
+
"free", "ffmpeg", "df", "du", "iostat", "vmstat",
|
|
36
|
+
"awk", "sed", "xargs"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
KEYWORDS = list(keyword.kwlist)
|
|
40
|
+
|
|
41
|
+
COMMAND_TREE = {}
|
|
42
|
+
json_path = Path(__file__).parent / "data" / "commands.json"
|
|
43
|
+
try:
|
|
44
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
45
|
+
COMMAND_TREE = json.load(f)
|
|
46
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
class ShellCompleter(Completer):
|
|
50
|
+
|
|
51
|
+
def __init__(self, cache_duration: int = 3) -> None:
|
|
52
|
+
self._cache: list[str] = []
|
|
53
|
+
self._cache_time: float = 0
|
|
54
|
+
self._cache_duration = cache_duration
|
|
55
|
+
self._get_commands()
|
|
56
|
+
|
|
57
|
+
def _strip_ext(self, name: str) -> str:
|
|
58
|
+
if os.name == "nt":
|
|
59
|
+
ext = os.path.splitext(name)[1].lower()
|
|
60
|
+
if ext in {".exe", ".bat", ".cmd", ".ps1", ".py", ".com"}:
|
|
61
|
+
return os.path.splitext(name)[0]
|
|
62
|
+
return name
|
|
63
|
+
|
|
64
|
+
def _get_commands(self) -> list[str]:
|
|
65
|
+
now = time.time()
|
|
66
|
+
if self._cache and now - self._cache_time < self._cache_duration:
|
|
67
|
+
return self._cache
|
|
68
|
+
|
|
69
|
+
commands: set[str] = set(BUILTINS)
|
|
70
|
+
for path in os.environ.get("PATH", "").split(os.pathsep):
|
|
71
|
+
if not path or not os.path.isdir(path) or path.startswith("/mnt/"):
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
for name in os.listdir(path):
|
|
75
|
+
if os.name == "nt":
|
|
76
|
+
name = name.lower()
|
|
77
|
+
full = os.path.join(path, name)
|
|
78
|
+
if os.path.isfile(full) and self._is_executable(name, full):
|
|
79
|
+
commands.add(self._strip_ext(name))
|
|
80
|
+
except (OSError, PermissionError):
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
self._cache = self._sort_commands(commands)
|
|
84
|
+
self._cache_time = now
|
|
85
|
+
return self._cache
|
|
86
|
+
|
|
87
|
+
def _is_executable(self, name: str, full_path: str) -> bool:
|
|
88
|
+
if name.endswith((".dll", ".pyd", ".pyc", ".sys", ".msi", ".drv")):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
if os.name == "nt":
|
|
92
|
+
ext = os.path.splitext(name)[1].lower()
|
|
93
|
+
return ext in {".exe", ".bat", ".cmd", ".ps1", ".py", ".com"}
|
|
94
|
+
|
|
95
|
+
return os.access(full_path, os.X_OK) and not name.endswith((".so", ".a", ".o"))
|
|
96
|
+
|
|
97
|
+
def _inside_braces(self, text: str) -> bool:
|
|
98
|
+
count = 0
|
|
99
|
+
i = 0
|
|
100
|
+
while i < len(text):
|
|
101
|
+
ch = text[i]
|
|
102
|
+
if ch == '{' and (i == 0 or text[i-1] != '{'):
|
|
103
|
+
count += 1
|
|
104
|
+
i += 1
|
|
105
|
+
elif ch == '}' and (i == 0 or text[i-1] != '}'):
|
|
106
|
+
count -= 1
|
|
107
|
+
i += 1
|
|
108
|
+
else:
|
|
109
|
+
i += 1
|
|
110
|
+
return count > 0
|
|
111
|
+
|
|
112
|
+
def _sort_commands(self, commands: set[str]) -> list[str]:
|
|
113
|
+
builtins = [c for c in BUILTINS if c in commands]
|
|
114
|
+
common = [c for c in COMMON if c in commands]
|
|
115
|
+
others = sorted(c for c in commands if c not in COMMON and c not in BUILTINS)
|
|
116
|
+
return builtins + common + others
|
|
117
|
+
|
|
118
|
+
def _path_completions(self, text: str, max_results: int = 100):
|
|
119
|
+
if text and text[-1] in whitespace: return
|
|
120
|
+
last = text.split()[-1] if text.split() else ""
|
|
121
|
+
if last == '~': return # don't remove this
|
|
122
|
+
path = os.path.expanduser(last)
|
|
123
|
+
dirname = os.path.dirname(path) or "."
|
|
124
|
+
prefix = os.path.basename(path)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
matches = []
|
|
128
|
+
for item in os.listdir(dirname):
|
|
129
|
+
if item.startswith(prefix):
|
|
130
|
+
matches.append(item)
|
|
131
|
+
if len(matches) >= max_results:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
for item in matches:
|
|
135
|
+
full = os.path.join(dirname, item)
|
|
136
|
+
is_dir = os.path.isdir(full)
|
|
137
|
+
name = item + ("/" if is_dir else "")
|
|
138
|
+
yield Completion(
|
|
139
|
+
name,
|
|
140
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
141
|
+
start_position=-len(prefix),
|
|
142
|
+
display_meta="directory" if is_dir else "file",
|
|
143
|
+
)
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _last_identifier(text: str) -> str:
|
|
149
|
+
i = len(text) - 1
|
|
150
|
+
while i >= 0 and (text[i].isalnum() or text[i] == '_'):
|
|
151
|
+
i -= 1
|
|
152
|
+
start = i + 1
|
|
153
|
+
identifier = text[start:]
|
|
154
|
+
return identifier
|
|
155
|
+
|
|
156
|
+
def _environment_completion(self, text: str):
|
|
157
|
+
last_word = self._last_identifier(text)
|
|
158
|
+
if not last_word:
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
candidates = set(os.environ)
|
|
162
|
+
for name in sorted(candidates):
|
|
163
|
+
if name.startswith(last_word):
|
|
164
|
+
yield Completion(
|
|
165
|
+
name,
|
|
166
|
+
start_position=-len(last_word),
|
|
167
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
168
|
+
display_meta="environment"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _python_completion(self, text: str):
|
|
172
|
+
if text and text[-1].isspace():
|
|
173
|
+
return
|
|
174
|
+
last_token = text.split()[-1] if text.split() else ''
|
|
175
|
+
if not last_token:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if '.' in last_token:
|
|
179
|
+
last_dot = last_token.rfind('.')
|
|
180
|
+
after_dot = last_token[last_dot+1:]
|
|
181
|
+
if after_dot == "" or after_dot.isidentifier():
|
|
182
|
+
parts = last_token.split('.')
|
|
183
|
+
|
|
184
|
+
# find the start point of identifiers chain
|
|
185
|
+
start = 0
|
|
186
|
+
for idx in range(len(parts)-2, -1, -1):
|
|
187
|
+
if not parts[idx].isidentifier():
|
|
188
|
+
start = idx
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
obj = None
|
|
192
|
+
root = self._last_identifier(parts[start])
|
|
193
|
+
if root in namespace:
|
|
194
|
+
obj = namespace[root]
|
|
195
|
+
elif hasattr(__builtins__, root):
|
|
196
|
+
obj = getattr(__builtins__, root)
|
|
197
|
+
elif isinstance(__builtins__, dict) and root in __builtins__:
|
|
198
|
+
obj = __builtins__[root]
|
|
199
|
+
|
|
200
|
+
if obj is None:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
for part in parts[start+1:-1]:
|
|
204
|
+
try:
|
|
205
|
+
obj = getattr(obj, part)
|
|
206
|
+
except AttributeError:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
prefix = parts[-1]
|
|
210
|
+
attrs = dir(obj)
|
|
211
|
+
attrs.sort(key=lambda x: (x.startswith('_'), x))
|
|
212
|
+
for attr in attrs:
|
|
213
|
+
if attr.startswith(prefix):
|
|
214
|
+
try:
|
|
215
|
+
value = getattr(obj, attr)
|
|
216
|
+
meta = (
|
|
217
|
+
"method"
|
|
218
|
+
if callable(value)
|
|
219
|
+
else truncate(type(value).__name__, MAX_META_LEN)
|
|
220
|
+
)
|
|
221
|
+
except Exception:
|
|
222
|
+
meta = "attr"
|
|
223
|
+
yield Completion(
|
|
224
|
+
attr,
|
|
225
|
+
start_position=-len(prefix),
|
|
226
|
+
display=attr,
|
|
227
|
+
display_meta=meta,
|
|
228
|
+
)
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
last_word = self._last_identifier(last_token)
|
|
232
|
+
if not last_word:
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
builtins_candidates = []
|
|
236
|
+
if isinstance(__builtins__, dict):
|
|
237
|
+
builtins_candidates = list(__builtins__.keys())
|
|
238
|
+
elif hasattr(__builtins__, '__dict__'):
|
|
239
|
+
builtins_candidates = dir(__builtins__)
|
|
240
|
+
else:
|
|
241
|
+
builtins_candidates = []
|
|
242
|
+
|
|
243
|
+
candidates = set(namespace.keys()) | set(builtins_candidates)
|
|
244
|
+
for name in sorted(candidates) + KEYWORDS:
|
|
245
|
+
if name in namespace:
|
|
246
|
+
meta = truncate(type(namespace[name]).__name__, MAX_META_LEN)
|
|
247
|
+
elif name in __builtins__:
|
|
248
|
+
meta = truncate(type(__builtins__[name]).__name__, MAX_META_LEN)
|
|
249
|
+
else:
|
|
250
|
+
meta = "python"
|
|
251
|
+
if name.startswith(last_word):
|
|
252
|
+
yield Completion(
|
|
253
|
+
name,
|
|
254
|
+
start_position=-len(last_word),
|
|
255
|
+
display=name,
|
|
256
|
+
display_meta=meta
|
|
257
|
+
)
|
|
258
|
+
yield from self._environment_completion(text)
|
|
259
|
+
|
|
260
|
+
def _subcommand_completion(self, text: str):
|
|
261
|
+
parts = text.split()
|
|
262
|
+
if not parts: return
|
|
263
|
+
sub_cmd = COMMAND_TREE.get(parts[0])
|
|
264
|
+
if not sub_cmd: return
|
|
265
|
+
global_flags: set[str] = set()
|
|
266
|
+
if sub_cmd.get("_global"):
|
|
267
|
+
global_flags.update(sub_cmd["_global"])
|
|
268
|
+
if not text.endswith(parts[-1]):
|
|
269
|
+
parts.append("")
|
|
270
|
+
last_word = parts[-1]
|
|
271
|
+
|
|
272
|
+
for part in parts[1:-1]:
|
|
273
|
+
if part.startswith("-"):
|
|
274
|
+
continue
|
|
275
|
+
if sub_cmd.get("_global"):
|
|
276
|
+
global_flags.update(sub_cmd["_global"])
|
|
277
|
+
if sub_cmd.get(part):
|
|
278
|
+
sub_cmd = sub_cmd[part]
|
|
279
|
+
else:
|
|
280
|
+
sub_cmd = {}
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
for name in sorted(sub_cmd.keys()):
|
|
284
|
+
if name == "_global" or name == "_flags": continue
|
|
285
|
+
if name in parts: continue
|
|
286
|
+
if name.startswith(last_word):
|
|
287
|
+
yield Completion(
|
|
288
|
+
name,
|
|
289
|
+
start_position=-len(last_word),
|
|
290
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
291
|
+
display_meta="subcommand"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
for name in sorted(sub_cmd.get("_flags") or set()):
|
|
295
|
+
if name in parts: continue
|
|
296
|
+
if name.startswith(last_word):
|
|
297
|
+
yield Completion(
|
|
298
|
+
name,
|
|
299
|
+
start_position=-len(last_word),
|
|
300
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
301
|
+
display_meta="flag",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
for name in sorted(global_flags):
|
|
305
|
+
if name in parts: continue
|
|
306
|
+
if name.startswith(last_word):
|
|
307
|
+
yield Completion(
|
|
308
|
+
name,
|
|
309
|
+
start_position=-len(last_word),
|
|
310
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
311
|
+
display_meta="flag",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def get_completions(self, document: Document, complete_event):
|
|
315
|
+
text = document.text_before_cursor
|
|
316
|
+
if "=>" in text: return
|
|
317
|
+
segments = re.split(r'\|>|\|', text)
|
|
318
|
+
current_segment = segments[-1] if segments else text
|
|
319
|
+
lstripped = current_segment.lstrip()
|
|
320
|
+
if lstripped.startswith("sudo"):
|
|
321
|
+
lstripped = lstripped[4:].lstrip()
|
|
322
|
+
|
|
323
|
+
if self._inside_braces(lstripped):
|
|
324
|
+
yield from self._python_completion(lstripped)
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
last_token = lstripped.split()[-1] if lstripped.split() else ''
|
|
328
|
+
if '.' in last_token and not last_token.startswith('.'):
|
|
329
|
+
yield from self._python_completion(lstripped)
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
if " " not in lstripped:
|
|
333
|
+
if len(lstripped) < 1:
|
|
334
|
+
return
|
|
335
|
+
for cmd in self._get_commands():
|
|
336
|
+
if cmd.startswith(lstripped):
|
|
337
|
+
meta = "command" if cmd in BUILTINS else "program"
|
|
338
|
+
yield Completion(
|
|
339
|
+
cmd,
|
|
340
|
+
start_position=-len(lstripped),
|
|
341
|
+
display=truncate(cmd, MAX_DISPLAY_LEN),
|
|
342
|
+
display_meta=meta,
|
|
343
|
+
)
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# special built-ins
|
|
347
|
+
parts = lstripped.split()
|
|
348
|
+
if len(parts) >= 1:
|
|
349
|
+
cmd = parts[0]
|
|
350
|
+
if cmd in ("print", "pyexec"):
|
|
351
|
+
param_text = lstripped[len(cmd):].lstrip()
|
|
352
|
+
yield from self._python_completion(param_text)
|
|
353
|
+
return
|
|
354
|
+
if cmd == "from":
|
|
355
|
+
if not lstripped.endswith(parts[-1]):
|
|
356
|
+
parts.append("")
|
|
357
|
+
elif len(parts) == 2:
|
|
358
|
+
for module in pkgutil.iter_modules():
|
|
359
|
+
name: str = module.name
|
|
360
|
+
if not name.startswith(parts[-1]):
|
|
361
|
+
continue
|
|
362
|
+
if name.startswith("_"):
|
|
363
|
+
continue
|
|
364
|
+
yield Completion(
|
|
365
|
+
name,
|
|
366
|
+
start_position=-len(parts[-1]),
|
|
367
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
368
|
+
display_meta="module",
|
|
369
|
+
)
|
|
370
|
+
elif len(parts) == 3:
|
|
371
|
+
yield Completion(
|
|
372
|
+
"import",
|
|
373
|
+
start_position=-len(parts[-1]),
|
|
374
|
+
display_meta="keyword",
|
|
375
|
+
)
|
|
376
|
+
return
|
|
377
|
+
if cmd == "import":
|
|
378
|
+
if not lstripped.endswith(parts[-1]):
|
|
379
|
+
parts.append("")
|
|
380
|
+
elif len(parts) == 2:
|
|
381
|
+
for module in pkgutil.iter_modules():
|
|
382
|
+
name: str = module.name
|
|
383
|
+
if not name.startswith(parts[-1]):
|
|
384
|
+
continue
|
|
385
|
+
if name.startswith("_"):
|
|
386
|
+
continue
|
|
387
|
+
yield Completion(
|
|
388
|
+
name,
|
|
389
|
+
start_position=-len(parts[-1]),
|
|
390
|
+
display=truncate(name, MAX_DISPLAY_LEN),
|
|
391
|
+
display_meta="module",
|
|
392
|
+
)
|
|
393
|
+
elif len(parts) == 3:
|
|
394
|
+
yield Completion(
|
|
395
|
+
"as",
|
|
396
|
+
start_position=-len(parts[-1]),
|
|
397
|
+
display_meta="keyword",
|
|
398
|
+
)
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
yield from self._subcommand_completion(lstripped)
|
|
402
|
+
yield from self._path_completions(lstripped)
|
|
403
|
+
|
|
404
|
+
def completer_init() -> None:
|
|
405
|
+
namespace["shell_completer"] = ShellCompleter()
|