NeoShell 2.2__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.
- neoshell-2.2.dist-info/METADATA +12 -0
- neoshell-2.2.dist-info/RECORD +6 -0
- neoshell-2.2.dist-info/WHEEL +5 -0
- neoshell-2.2.dist-info/entry_points.txt +2 -0
- neoshell-2.2.dist-info/top_level.txt +1 -0
- neoshell.py +1403 -0
neoshell.py
ADDED
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""neoshell 2 - a CMD-style shell written in Python, with live syntax highlighting.
|
|
3
|
+
|
|
4
|
+
pip install prompt_toolkit psutil
|
|
5
|
+
python neoshell.py interactive shell
|
|
6
|
+
python neoshell.py -c "dir /b & ver" run one line and exit
|
|
7
|
+
neoshell --install / --uninstall add / remove the Windows Terminal profile
|
|
8
|
+
neoshell --no-install start without auto-registering the profile
|
|
9
|
+
|
|
10
|
+
When built into neoshell.exe (see build_exe.bat) it registers its own Windows
|
|
11
|
+
Terminal profile, with icon and color scheme, the first time it runs.
|
|
12
|
+
|
|
13
|
+
Uses CMD command names and syntax (dir, copy, del, tasklist, findstr, %VAR%,
|
|
14
|
+
&, &&, ||, if exist ...). On Windows, any CMD command not built in here
|
|
15
|
+
(assoc, mklink, vol, ...) is passed through to the real cmd.exe.
|
|
16
|
+
"""
|
|
17
|
+
import ast
|
|
18
|
+
import contextlib
|
|
19
|
+
import datetime
|
|
20
|
+
import fnmatch
|
|
21
|
+
import getpass
|
|
22
|
+
import glob
|
|
23
|
+
import io
|
|
24
|
+
import json
|
|
25
|
+
import operator
|
|
26
|
+
import os
|
|
27
|
+
import platform
|
|
28
|
+
import random
|
|
29
|
+
import re
|
|
30
|
+
import shutil
|
|
31
|
+
import socket
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
IS_WIN = os.name == "nt"
|
|
37
|
+
if IS_WIN:
|
|
38
|
+
os.system("") # turn on ANSI escape support in the console
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from prompt_toolkit import PromptSession
|
|
42
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
43
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
44
|
+
from prompt_toolkit.history import FileHistory
|
|
45
|
+
from prompt_toolkit.lexers import Lexer
|
|
46
|
+
HAVE_PT = True
|
|
47
|
+
except ImportError:
|
|
48
|
+
HAVE_PT = False
|
|
49
|
+
Lexer = Completer = object
|
|
50
|
+
try:
|
|
51
|
+
import psutil
|
|
52
|
+
except ImportError:
|
|
53
|
+
psutil = None
|
|
54
|
+
|
|
55
|
+
VERSION = "2.2"
|
|
56
|
+
HIST = os.path.expanduser("~/.neoshell_history")
|
|
57
|
+
aliases = {} # doskey macros
|
|
58
|
+
dir_stack = [] # pushd / popd
|
|
59
|
+
last_status = 0 # %ERRORLEVEL%
|
|
60
|
+
last_dur = 0.0
|
|
61
|
+
old_dir = None
|
|
62
|
+
session = None
|
|
63
|
+
_which_cache = {}
|
|
64
|
+
_prog_cache = None
|
|
65
|
+
|
|
66
|
+
WIN_INTERNAL = {"assoc", "attrib", "chcp", "cmd", "ftype", "label", "mklink", "vol",
|
|
67
|
+
"verify", "for", "call", "ipconfig", "ping", "netstat", "systeminfo"}
|
|
68
|
+
GLOB_CMDS = {"type", "copy", "xcopy", "move", "del", "erase", "ren", "rename"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Op(str):
|
|
72
|
+
"""An unquoted operator token such as | & && || > >> <"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Q(str):
|
|
76
|
+
"""A token that was written in double quotes (never glob-expanded)."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------- helpers
|
|
80
|
+
def paint(s, code):
|
|
81
|
+
return f"\033[{code}m{s}\033[0m" if sys.stdout.isatty() else s
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
EXT_COLORS = {".exe": "32", ".bat": "32", ".cmd": "32", ".com": "32", ".sh": "32",
|
|
85
|
+
".py": "33", ".js": "33", ".zip": "31", ".7z": "31", ".rar": "31",
|
|
86
|
+
".gz": "31", ".png": "35", ".jpg": "35", ".jpeg": "35", ".gif": "35",
|
|
87
|
+
".mp3": "35", ".mp4": "35"}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def colorize_name(path, name):
|
|
91
|
+
if os.path.isdir(path):
|
|
92
|
+
return paint(name, "1;34")
|
|
93
|
+
code = EXT_COLORS.get(os.path.splitext(name)[1].lower())
|
|
94
|
+
return paint(name, code) if code else name
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def env_get(name):
|
|
98
|
+
if name in os.environ:
|
|
99
|
+
return os.environ[name]
|
|
100
|
+
up = name.upper()
|
|
101
|
+
return next((v for k, v in os.environ.items() if k.upper() == up), None)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def expand_vars(line):
|
|
105
|
+
def sub(m):
|
|
106
|
+
name = m.group(1)
|
|
107
|
+
u = name.upper()
|
|
108
|
+
if u == "ERRORLEVEL": return str(last_status)
|
|
109
|
+
if u == "CD": return os.getcwd()
|
|
110
|
+
if u == "DATE": return datetime.date.today().strftime("%m/%d/%Y")
|
|
111
|
+
if u == "TIME": return datetime.datetime.now().strftime("%H:%M:%S.%f")[:-4]
|
|
112
|
+
if u == "RANDOM": return str(random.randint(0, 32767))
|
|
113
|
+
v = env_get(name)
|
|
114
|
+
return v if v is not None else m.group(0)
|
|
115
|
+
return re.sub(r"%([^%\s]+)%", sub, line)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def tokenize(line):
|
|
119
|
+
"""Split a CMD line. Only double quotes quote; backslashes are literal."""
|
|
120
|
+
toks, cur, quoted, inq, i = [], [], False, False, 0
|
|
121
|
+
|
|
122
|
+
def flush():
|
|
123
|
+
nonlocal cur, quoted
|
|
124
|
+
if cur or quoted:
|
|
125
|
+
s = "".join(cur)
|
|
126
|
+
toks.append(Q(s) if quoted else s)
|
|
127
|
+
cur, quoted = [], False
|
|
128
|
+
|
|
129
|
+
while i < len(line):
|
|
130
|
+
ch = line[i]
|
|
131
|
+
if ch == '"':
|
|
132
|
+
inq, quoted = not inq, True
|
|
133
|
+
elif inq:
|
|
134
|
+
cur.append(ch)
|
|
135
|
+
elif ch.isspace():
|
|
136
|
+
flush()
|
|
137
|
+
elif line[i:i + 2] in ("&&", "||", ">>"):
|
|
138
|
+
flush(); toks.append(Op(line[i:i + 2])); i += 1
|
|
139
|
+
elif ch in "&|<>":
|
|
140
|
+
flush(); toks.append(Op(ch))
|
|
141
|
+
else:
|
|
142
|
+
cur.append(ch)
|
|
143
|
+
i += 1
|
|
144
|
+
flush()
|
|
145
|
+
return toks
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def expand_aliases(tokens):
|
|
149
|
+
out, at_cmd = [], True
|
|
150
|
+
for t in tokens:
|
|
151
|
+
if at_cmd and not isinstance(t, Op) and str(t).lower() in aliases:
|
|
152
|
+
out.extend(tokenize(aliases[str(t).lower()]))
|
|
153
|
+
else:
|
|
154
|
+
out.append(t)
|
|
155
|
+
at_cmd = isinstance(t, Op) and t in ("&&", "||", "&", "|")
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def split_flags(args):
|
|
160
|
+
"""Separate /flags from other arguments."""
|
|
161
|
+
flags, rest = [], []
|
|
162
|
+
for a in args:
|
|
163
|
+
if re.fullmatch(r"/[A-Za-z?][A-Za-z0-9:,\-]*", a) and not (len(a) > 3 and os.path.exists(a)):
|
|
164
|
+
flags.append(a.lower())
|
|
165
|
+
else:
|
|
166
|
+
rest.append(a)
|
|
167
|
+
return flags, rest
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def human(n):
|
|
171
|
+
for u in ("B", "K", "M", "G", "T"):
|
|
172
|
+
if n < 1024:
|
|
173
|
+
return f"{n:.0f}{u}"
|
|
174
|
+
n /= 1024
|
|
175
|
+
return f"{n:.1f}P"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def git_branch():
|
|
179
|
+
d = os.getcwd()
|
|
180
|
+
while True:
|
|
181
|
+
g = os.path.join(d, ".git")
|
|
182
|
+
if os.path.isdir(g):
|
|
183
|
+
try:
|
|
184
|
+
with open(os.path.join(g, "HEAD")) as f:
|
|
185
|
+
head = f.read().strip()
|
|
186
|
+
return head[16:] if head.startswith("ref: refs/heads/") else head[:7]
|
|
187
|
+
except OSError:
|
|
188
|
+
return ""
|
|
189
|
+
parent = os.path.dirname(d)
|
|
190
|
+
if parent == d:
|
|
191
|
+
return ""
|
|
192
|
+
d = parent
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# --- tiny safe arithmetic for `set /a`
|
|
196
|
+
def _shl(a, b):
|
|
197
|
+
if not 0 <= b < 64:
|
|
198
|
+
raise ValueError
|
|
199
|
+
return a << b
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
_BIN = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
|
|
203
|
+
ast.Div: lambda a, b: int(a / b), ast.Mod: operator.mod, ast.LShift: _shl,
|
|
204
|
+
ast.RShift: operator.rshift, ast.BitAnd: operator.and_,
|
|
205
|
+
ast.BitOr: operator.or_, ast.BitXor: operator.xor}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def calc(expr):
|
|
209
|
+
def ev(n):
|
|
210
|
+
if isinstance(n, ast.Expression): return ev(n.body)
|
|
211
|
+
if isinstance(n, ast.Constant) and isinstance(n.value, int): return n.value
|
|
212
|
+
if isinstance(n, ast.Name):
|
|
213
|
+
v = env_get(n.id)
|
|
214
|
+
return int(v) if v and re.fullmatch(r"-?\d+", v) else 0
|
|
215
|
+
if isinstance(n, ast.BinOp) and type(n.op) in _BIN:
|
|
216
|
+
return _BIN[type(n.op)](ev(n.left), ev(n.right))
|
|
217
|
+
if isinstance(n, ast.UnaryOp):
|
|
218
|
+
v = ev(n.operand)
|
|
219
|
+
if isinstance(n.op, ast.USub): return -v
|
|
220
|
+
if isinstance(n.op, ast.UAdd): return v
|
|
221
|
+
if isinstance(n.op, ast.Invert): return ~v
|
|
222
|
+
raise ValueError("bad expression")
|
|
223
|
+
return ev(ast.parse(expr.strip(), mode="eval"))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------- builtins
|
|
227
|
+
BUILTINS, HELP = {}, {}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def builtin(*names, doc=""):
|
|
231
|
+
def deco(f):
|
|
232
|
+
for n in names:
|
|
233
|
+
BUILTINS[n] = f
|
|
234
|
+
HELP[names[0]] = doc
|
|
235
|
+
return f
|
|
236
|
+
return deco
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@builtin("cd", "chdir", doc="Show or change directory. cd - goes back")
|
|
240
|
+
def b_cd(a):
|
|
241
|
+
global old_dir
|
|
242
|
+
_, rest = split_flags(a)
|
|
243
|
+
if not rest:
|
|
244
|
+
print(os.getcwd()); return 0
|
|
245
|
+
target = " ".join(rest)
|
|
246
|
+
if target == "-" and old_dir:
|
|
247
|
+
target = old_dir
|
|
248
|
+
here = os.getcwd()
|
|
249
|
+
try:
|
|
250
|
+
os.chdir(target)
|
|
251
|
+
old_dir = here
|
|
252
|
+
return 0
|
|
253
|
+
except OSError:
|
|
254
|
+
print("The system cannot find the path specified."); return 1
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@builtin("pushd", doc="Save the current directory and change to another")
|
|
258
|
+
def b_pushd(a):
|
|
259
|
+
here = os.getcwd()
|
|
260
|
+
rc = b_cd(a)
|
|
261
|
+
if rc == 0 and a:
|
|
262
|
+
dir_stack.append(here)
|
|
263
|
+
return rc
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@builtin("popd", doc="Return to the last pushd directory")
|
|
267
|
+
def b_popd(a):
|
|
268
|
+
if not dir_stack:
|
|
269
|
+
return 0
|
|
270
|
+
return b_cd([dir_stack.pop()])
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@builtin("cls", doc="Clear the screen")
|
|
274
|
+
def b_cls(a):
|
|
275
|
+
print("\033[2J\033[3J\033[H", end="", flush=True); return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@builtin("echo", doc="Print text")
|
|
279
|
+
def b_echo(a):
|
|
280
|
+
print("ECHO is on." if not a else " ".join(a)); return 0
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@builtin("title", doc="Set the window title")
|
|
284
|
+
def b_title(a):
|
|
285
|
+
sys.stdout.write(f"\033]0;{' '.join(a)}\007"); sys.stdout.flush(); return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@builtin("exit", doc="Quit")
|
|
289
|
+
def b_exit(a):
|
|
290
|
+
_, rest = split_flags(a)
|
|
291
|
+
sys.exit(int(rest[0]) if rest and rest[0].isdigit() else 0)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@builtin("ver", doc="Show version")
|
|
295
|
+
def b_ver(a):
|
|
296
|
+
if IS_WIN:
|
|
297
|
+
print(f"\nMicrosoft Windows [Version {platform.version()}]")
|
|
298
|
+
print(f"neoshell {VERSION} on {platform.system()} {platform.release()}"); return 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@builtin("hostname", doc="Show computer name")
|
|
302
|
+
def b_hostname(a): print(socket.gethostname()); return 0
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@builtin("whoami", doc="Show current user")
|
|
306
|
+
def b_whoami(a):
|
|
307
|
+
print(f"{socket.gethostname()}\\{getpass.getuser()}" if IS_WIN else getpass.getuser()); return 0
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@builtin("date", doc="Show the date")
|
|
311
|
+
def b_date(a):
|
|
312
|
+
d = datetime.datetime.now().strftime("%a %m/%d/%Y")
|
|
313
|
+
print(d if "/t" in [x.lower() for x in a] else f"The current date is: {d}"); return 0
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@builtin("time", doc="Show the time")
|
|
317
|
+
def b_time(a):
|
|
318
|
+
now = datetime.datetime.now()
|
|
319
|
+
if "/t" in [x.lower() for x in a]:
|
|
320
|
+
print(now.strftime("%I:%M %p"))
|
|
321
|
+
else:
|
|
322
|
+
print("The current time is: " + now.strftime("%H:%M:%S.%f")[:-4])
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@builtin("dir", doc="List files. /b bare /w wide /s recurse /ad dirs only /o:n|s|d|e sort (-x reverses)")
|
|
327
|
+
def b_dir(a):
|
|
328
|
+
fl, specs = split_flags(a)
|
|
329
|
+
bare, wide, rec, only_dirs = "/b" in fl, "/w" in fl, "/s" in fl, "/ad" in fl
|
|
330
|
+
order = next((f[3:] for f in fl if f.startswith("/o:")), "gn")
|
|
331
|
+
mode = order.lstrip("-g")[:1] or "n"
|
|
332
|
+
rev = order.startswith("-")
|
|
333
|
+
|
|
334
|
+
def key(p):
|
|
335
|
+
try:
|
|
336
|
+
st = os.stat(p)
|
|
337
|
+
except OSError:
|
|
338
|
+
return (False, "")
|
|
339
|
+
k = {"n": os.path.basename(p).lower(), "s": st.st_size, "d": st.st_mtime,
|
|
340
|
+
"e": os.path.splitext(p)[1].lower()}.get(mode, "")
|
|
341
|
+
return (not os.path.isdir(p), k)
|
|
342
|
+
|
|
343
|
+
def show(d, entries):
|
|
344
|
+
if only_dirs:
|
|
345
|
+
entries = [e for e in entries if os.path.isdir(e)]
|
|
346
|
+
entries.sort(key=key, reverse=rev)
|
|
347
|
+
if not bare:
|
|
348
|
+
print(f"\n Directory of {os.path.abspath(d)}\n")
|
|
349
|
+
nf = nd = size = 0
|
|
350
|
+
cells = []
|
|
351
|
+
for e in entries:
|
|
352
|
+
try:
|
|
353
|
+
st = os.stat(e)
|
|
354
|
+
except OSError:
|
|
355
|
+
continue
|
|
356
|
+
name, isdir = os.path.basename(e), os.path.isdir(e)
|
|
357
|
+
if isdir: nd += 1
|
|
358
|
+
else: nf += 1; size += st.st_size
|
|
359
|
+
if bare:
|
|
360
|
+
print(colorize_name(e, e if rec else name))
|
|
361
|
+
elif wide:
|
|
362
|
+
cells.append((e, f"[{name}]" if isdir else name))
|
|
363
|
+
else:
|
|
364
|
+
t = datetime.datetime.fromtimestamp(st.st_mtime).strftime("%m/%d/%Y %I:%M %p")
|
|
365
|
+
col = "<DIR>".ljust(14) if isdir else f"{st.st_size:>14,}"
|
|
366
|
+
print(f"{t} {col} {colorize_name(e, name)}")
|
|
367
|
+
if wide and cells:
|
|
368
|
+
w = max(len(l) for _, l in cells) + 2
|
|
369
|
+
per = max(1, shutil.get_terminal_size().columns // w)
|
|
370
|
+
for i in range(0, len(cells), per):
|
|
371
|
+
print("".join(colorize_name(e, l.ljust(w)) for e, l in cells[i:i + per]))
|
|
372
|
+
if not bare:
|
|
373
|
+
print(f"{nf:>16} File(s) {size:>14,} bytes")
|
|
374
|
+
try:
|
|
375
|
+
free = shutil.disk_usage(d).free
|
|
376
|
+
except OSError:
|
|
377
|
+
free = 0
|
|
378
|
+
print(f"{nd:>16} Dir(s) {free:>14,} bytes free")
|
|
379
|
+
|
|
380
|
+
found = False
|
|
381
|
+
for spec in specs or ["."]:
|
|
382
|
+
if rec:
|
|
383
|
+
base, pat = (spec, "*") if os.path.isdir(spec) else (os.path.dirname(spec) or ".", os.path.basename(spec))
|
|
384
|
+
for root, _, _ in os.walk(base):
|
|
385
|
+
ents = [os.path.join(root, n) for n in os.listdir(root) if fnmatch.fnmatch(n, pat)]
|
|
386
|
+
if ents:
|
|
387
|
+
show(root, ents); found = True
|
|
388
|
+
elif os.path.isdir(spec):
|
|
389
|
+
show(spec, [os.path.join(spec, n) for n in os.listdir(spec)]); found = True
|
|
390
|
+
else:
|
|
391
|
+
hits = glob.glob(spec)
|
|
392
|
+
if hits:
|
|
393
|
+
show(os.path.dirname(spec) or ".", hits); found = True
|
|
394
|
+
if not found:
|
|
395
|
+
print("File Not Found"); return 1
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@builtin("tree", doc="Draw a directory tree. /f includes files")
|
|
400
|
+
def b_tree(a):
|
|
401
|
+
fl, rest = split_flags(a)
|
|
402
|
+
root, files = (rest[0] if rest else "."), "/f" in fl
|
|
403
|
+
print(os.path.abspath(root))
|
|
404
|
+
|
|
405
|
+
def walk(d, prefix):
|
|
406
|
+
try:
|
|
407
|
+
names = sorted(os.listdir(d), key=str.lower)
|
|
408
|
+
except OSError:
|
|
409
|
+
return
|
|
410
|
+
items = [n for n in names if files or os.path.isdir(os.path.join(d, n))]
|
|
411
|
+
for i, n in enumerate(items):
|
|
412
|
+
last, p = i == len(items) - 1, os.path.join(d, n)
|
|
413
|
+
print(prefix + ("└── " if last else "├── ") + colorize_name(p, n))
|
|
414
|
+
if os.path.isdir(p) and not os.path.islink(p):
|
|
415
|
+
walk(p, prefix + (" " if last else "│ "))
|
|
416
|
+
walk(root, "")
|
|
417
|
+
return 0
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@builtin("type", doc="Print file contents")
|
|
421
|
+
def b_type(a):
|
|
422
|
+
_, files = split_flags(a)
|
|
423
|
+
rc = 0
|
|
424
|
+
for f in files:
|
|
425
|
+
try:
|
|
426
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
427
|
+
sys.stdout.write(fh.read())
|
|
428
|
+
except OSError:
|
|
429
|
+
print("The system cannot find the file specified."); rc = 1
|
|
430
|
+
return rc
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@builtin("more", doc="Page through a file or piped text")
|
|
434
|
+
def b_more(a):
|
|
435
|
+
_, files = split_flags(a)
|
|
436
|
+
lines = []
|
|
437
|
+
for f in files:
|
|
438
|
+
try:
|
|
439
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
440
|
+
lines += fh.read().splitlines()
|
|
441
|
+
except OSError:
|
|
442
|
+
print("Cannot access file " + f); return 1
|
|
443
|
+
if not files:
|
|
444
|
+
lines = sys.stdin.read().splitlines()
|
|
445
|
+
page = max(5, shutil.get_terminal_size().lines - 1)
|
|
446
|
+
for i, ln in enumerate(lines):
|
|
447
|
+
print(ln)
|
|
448
|
+
if sys.stdout.isatty() and (i + 1) % page == 0 and i + 1 < len(lines):
|
|
449
|
+
try:
|
|
450
|
+
if sys.__stdin__.readline().strip().lower() == "q":
|
|
451
|
+
break
|
|
452
|
+
except Exception:
|
|
453
|
+
break
|
|
454
|
+
return 0
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@builtin("copy", doc="Copy files")
|
|
458
|
+
def b_copy(a):
|
|
459
|
+
_, args = split_flags(a)
|
|
460
|
+
if len(args) < 2:
|
|
461
|
+
print("The syntax of the command is incorrect."); return 1
|
|
462
|
+
*srcs, dst = args
|
|
463
|
+
if len(srcs) > 1 and not os.path.isdir(dst):
|
|
464
|
+
print("The syntax of the command is incorrect."); return 1
|
|
465
|
+
n, rc = 0, 0
|
|
466
|
+
for s in srcs:
|
|
467
|
+
try:
|
|
468
|
+
shutil.copy2(s, dst); n += 1
|
|
469
|
+
except OSError:
|
|
470
|
+
print("The system cannot find the file specified."); rc = 1
|
|
471
|
+
print(f" {n} file(s) copied.")
|
|
472
|
+
return rc
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@builtin("xcopy", doc="Copy files and folders. /e includes subfolders")
|
|
476
|
+
def b_xcopy(a):
|
|
477
|
+
_, args = split_flags(a)
|
|
478
|
+
if len(args) < 2:
|
|
479
|
+
print("Invalid number of parameters"); return 4
|
|
480
|
+
src, dst = args[0], args[1]
|
|
481
|
+
try:
|
|
482
|
+
if os.path.isdir(src):
|
|
483
|
+
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
484
|
+
else:
|
|
485
|
+
shutil.copy2(src, dst)
|
|
486
|
+
print("File(s) copied"); return 0
|
|
487
|
+
except OSError:
|
|
488
|
+
print(f"File not found - {src}"); return 4
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@builtin("move", doc="Move files or folders")
|
|
492
|
+
def b_move(a):
|
|
493
|
+
_, args = split_flags(a)
|
|
494
|
+
if len(args) < 2:
|
|
495
|
+
print("The syntax of the command is incorrect."); return 1
|
|
496
|
+
*srcs, dst = args
|
|
497
|
+
for s in srcs:
|
|
498
|
+
try:
|
|
499
|
+
shutil.move(s, dst)
|
|
500
|
+
except (OSError, shutil.Error):
|
|
501
|
+
print("The system cannot find the file specified."); return 1
|
|
502
|
+
print(f" {len(srcs)} file(s) moved.")
|
|
503
|
+
return 0
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@builtin("ren", "rename", doc="Rename a file or folder")
|
|
507
|
+
def b_ren(a):
|
|
508
|
+
_, args = split_flags(a)
|
|
509
|
+
if len(args) != 2:
|
|
510
|
+
print("The syntax of the command is incorrect."); return 1
|
|
511
|
+
try:
|
|
512
|
+
os.rename(args[0], os.path.join(os.path.dirname(args[0]), args[1])); return 0
|
|
513
|
+
except OSError:
|
|
514
|
+
print("The system cannot find the file specified."); return 1
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@builtin("del", "erase", doc="Delete files. /q quiet /s include subfolders")
|
|
518
|
+
def b_del(a):
|
|
519
|
+
fl, pats = split_flags(a)
|
|
520
|
+
targets = []
|
|
521
|
+
for p in pats:
|
|
522
|
+
targets += glob.glob(p)
|
|
523
|
+
if "/s" in fl:
|
|
524
|
+
targets += glob.glob(os.path.join("**", os.path.basename(p)), recursive=True)
|
|
525
|
+
rc = 0
|
|
526
|
+
if not targets:
|
|
527
|
+
print("Could Not Find " + (os.path.abspath(pats[0]) if pats else "")); return 1
|
|
528
|
+
for t in dict.fromkeys(targets):
|
|
529
|
+
try:
|
|
530
|
+
if os.path.isdir(t):
|
|
531
|
+
for n in os.listdir(t):
|
|
532
|
+
f = os.path.join(t, n)
|
|
533
|
+
if os.path.isfile(f): os.remove(f)
|
|
534
|
+
else:
|
|
535
|
+
os.remove(t)
|
|
536
|
+
except OSError:
|
|
537
|
+
print(f"Access is denied: {t}"); rc = 1
|
|
538
|
+
return rc
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@builtin("md", "mkdir", doc="Make directories")
|
|
542
|
+
def b_md(a):
|
|
543
|
+
_, dirs = split_flags(a)
|
|
544
|
+
rc = 0
|
|
545
|
+
for d in dirs:
|
|
546
|
+
try:
|
|
547
|
+
os.makedirs(d)
|
|
548
|
+
except FileExistsError:
|
|
549
|
+
print(f"A subdirectory or file {d} already exists."); rc = 1
|
|
550
|
+
except OSError:
|
|
551
|
+
print("The system cannot find the path specified."); rc = 1
|
|
552
|
+
return rc
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@builtin("rd", "rmdir", doc="Remove directories. /s whole tree /q no prompt")
|
|
556
|
+
def b_rd(a):
|
|
557
|
+
fl, dirs = split_flags(a)
|
|
558
|
+
for d in dirs:
|
|
559
|
+
try:
|
|
560
|
+
if "/s" in fl:
|
|
561
|
+
if "/q" not in fl and input(f"{d}, Are you sure (Y/N)? ").strip().lower() != "y":
|
|
562
|
+
continue
|
|
563
|
+
shutil.rmtree(d)
|
|
564
|
+
else:
|
|
565
|
+
os.rmdir(d)
|
|
566
|
+
except FileNotFoundError:
|
|
567
|
+
print("The system cannot find the file specified."); return 1
|
|
568
|
+
except OSError:
|
|
569
|
+
print("The directory is not empty."); return 145
|
|
570
|
+
return 0
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@builtin("set", doc="Show/set variables. set X=1 set /a X=X+2 set /p X=Prompt")
|
|
574
|
+
def b_set(a):
|
|
575
|
+
fl, rest = split_flags(a)
|
|
576
|
+
text = " ".join(rest)
|
|
577
|
+
if "/a" in fl:
|
|
578
|
+
res = None
|
|
579
|
+
for part in text.split(","):
|
|
580
|
+
m = re.fullmatch(r"\s*(\w+)\s*(<<|>>|[-+*/%&|^])?=\s*(.+)", part)
|
|
581
|
+
try:
|
|
582
|
+
if m:
|
|
583
|
+
name, op, rhs = m.groups()
|
|
584
|
+
val = calc(rhs)
|
|
585
|
+
if op:
|
|
586
|
+
val = calc(f"{env_get(name) or 0}{op}({val})")
|
|
587
|
+
os.environ[name] = str(val); res = val
|
|
588
|
+
else:
|
|
589
|
+
res = calc(part)
|
|
590
|
+
except (ValueError, SyntaxError, ZeroDivisionError):
|
|
591
|
+
print("Invalid number. Numeric constants are either decimal."); return 1
|
|
592
|
+
print(res); return 0
|
|
593
|
+
if "/p" in fl:
|
|
594
|
+
name, _, prompt = text.partition("=")
|
|
595
|
+
os.environ[name.strip()] = input(prompt)
|
|
596
|
+
return 0
|
|
597
|
+
if not text:
|
|
598
|
+
for k, v in sorted(os.environ.items(), key=lambda kv: kv[0].upper()):
|
|
599
|
+
print(f"{k}={v}")
|
|
600
|
+
return 0
|
|
601
|
+
if "=" in text:
|
|
602
|
+
k, v = text.split("=", 1)
|
|
603
|
+
if v: os.environ[k.strip()] = v
|
|
604
|
+
else: os.environ.pop(k.strip(), None)
|
|
605
|
+
return 0
|
|
606
|
+
hits = [(k, v) for k, v in os.environ.items() if k.upper().startswith(text.upper())]
|
|
607
|
+
for k, v in sorted(hits):
|
|
608
|
+
print(f"{k}={v}")
|
|
609
|
+
if not hits:
|
|
610
|
+
print(f"Environment variable {text} not defined"); return 1
|
|
611
|
+
return 0
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@builtin("path", doc="Show or set PATH")
|
|
615
|
+
def b_path(a):
|
|
616
|
+
if not a:
|
|
617
|
+
print("PATH=" + os.environ.get("PATH", ""))
|
|
618
|
+
else:
|
|
619
|
+
text = " ".join(a).lstrip("=")
|
|
620
|
+
os.environ["PATH"] = "" if text == ";" else text
|
|
621
|
+
_which_cache.clear()
|
|
622
|
+
return 0
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
@builtin("doskey", doc="Define macros. doskey ll=dir /w doskey /macros")
|
|
626
|
+
def b_doskey(a):
|
|
627
|
+
text = " ".join(a)
|
|
628
|
+
if not text or text.lower() == "/macros":
|
|
629
|
+
for k, v in sorted(aliases.items()): print(f"{k}={v}")
|
|
630
|
+
elif "=" in text:
|
|
631
|
+
k, v = text.split("=", 1)
|
|
632
|
+
if v.strip(): aliases[k.strip().lower()] = v.strip()
|
|
633
|
+
else: aliases.pop(k.strip().lower(), None)
|
|
634
|
+
return 0
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@builtin("where", doc="Locate programs on PATH")
|
|
638
|
+
def b_where(a):
|
|
639
|
+
_, names = split_flags(a)
|
|
640
|
+
dirs = [os.getcwd()] + [d for d in os.environ.get("PATH", "").split(os.pathsep) if d]
|
|
641
|
+
exts = [""] + (os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") if IS_WIN else [])
|
|
642
|
+
seen = set()
|
|
643
|
+
for n in names:
|
|
644
|
+
for d in dirs:
|
|
645
|
+
for e in exts:
|
|
646
|
+
for hit in glob.glob(os.path.join(glob.escape(d), n + e)):
|
|
647
|
+
if os.path.isfile(hit) and (IS_WIN or os.access(hit, os.X_OK)) and hit not in seen:
|
|
648
|
+
seen.add(hit); print(hit)
|
|
649
|
+
if not seen:
|
|
650
|
+
print("INFO: Could not find files for the given pattern(s)."); return 1
|
|
651
|
+
return 0
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@builtin("find", doc='Search text: find [/i] [/v] [/c] [/n] "text" [files]')
|
|
655
|
+
def b_find(a):
|
|
656
|
+
fl, rest = split_flags(a)
|
|
657
|
+
if not rest:
|
|
658
|
+
print("FIND: Parameter format not correct"); return 2
|
|
659
|
+
needle, files = rest[0], rest[1:]
|
|
660
|
+
ci, inv, cnt, nums = "/i" in fl, "/v" in fl, "/c" in fl, "/n" in fl
|
|
661
|
+
total = 0
|
|
662
|
+
|
|
663
|
+
def scan(lines, label=None):
|
|
664
|
+
n = hits = 0
|
|
665
|
+
if label and not cnt:
|
|
666
|
+
print(f"\n---------- {label.upper()}")
|
|
667
|
+
for ln in lines:
|
|
668
|
+
n += 1
|
|
669
|
+
s = ln.rstrip("\n")
|
|
670
|
+
ok = (needle.lower() in s.lower()) if ci else (needle in s)
|
|
671
|
+
if ok != inv:
|
|
672
|
+
hits += 1
|
|
673
|
+
if not cnt:
|
|
674
|
+
print((f"[{n}]" if nums else "") + s)
|
|
675
|
+
if cnt:
|
|
676
|
+
print(f"---------- {label.upper()}: {hits}" if label else hits)
|
|
677
|
+
return hits
|
|
678
|
+
|
|
679
|
+
if not files:
|
|
680
|
+
total = scan(sys.stdin)
|
|
681
|
+
for f in files:
|
|
682
|
+
try:
|
|
683
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
684
|
+
total += scan(fh, f)
|
|
685
|
+
except OSError:
|
|
686
|
+
print(f"File not found - {f.upper()}")
|
|
687
|
+
return 0 if total else 1
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
@builtin("findstr", doc="Search with /i /n /v /r /s /c:\"phrase\" (space = OR of words)")
|
|
691
|
+
def b_findstr(a):
|
|
692
|
+
ci = nums = inv = regex = sub = False
|
|
693
|
+
lits, rest = [], []
|
|
694
|
+
for x in a:
|
|
695
|
+
xl = x.lower()
|
|
696
|
+
if xl.startswith("/c:"): lits.append(x[3:])
|
|
697
|
+
elif xl == "/i": ci = True
|
|
698
|
+
elif xl == "/n": nums = True
|
|
699
|
+
elif xl == "/v": inv = True
|
|
700
|
+
elif xl == "/r": regex = True
|
|
701
|
+
elif xl == "/s": sub = True
|
|
702
|
+
elif re.fullmatch(r"/[A-Za-z]", x): pass
|
|
703
|
+
else: rest.append(x)
|
|
704
|
+
if not lits:
|
|
705
|
+
if not rest:
|
|
706
|
+
print("FINDSTR: Bad command line"); return 2
|
|
707
|
+
pat = rest.pop(0)
|
|
708
|
+
lits = [pat] if regex else pat.split()
|
|
709
|
+
try:
|
|
710
|
+
rx = re.compile("|".join(lits if regex else map(re.escape, lits)), re.I if ci else 0)
|
|
711
|
+
except re.error:
|
|
712
|
+
print("FINDSTR: Bad regular expression"); return 2
|
|
713
|
+
|
|
714
|
+
files = []
|
|
715
|
+
for spec in rest:
|
|
716
|
+
if sub:
|
|
717
|
+
base, pat2 = os.path.dirname(spec) or ".", os.path.basename(spec)
|
|
718
|
+
for root, _, names in os.walk(base):
|
|
719
|
+
files += [os.path.join(root, n) for n in names if fnmatch.fnmatch(n, pat2)]
|
|
720
|
+
else:
|
|
721
|
+
files += glob.glob(spec) or [spec]
|
|
722
|
+
prefix = len(files) > 1 or sub
|
|
723
|
+
tty = sys.stdout.isatty()
|
|
724
|
+
|
|
725
|
+
def scan(lines, label):
|
|
726
|
+
found = 0
|
|
727
|
+
for n, ln in enumerate(lines, 1):
|
|
728
|
+
s = ln.rstrip("\n")
|
|
729
|
+
if bool(rx.search(s)) != inv:
|
|
730
|
+
found += 1
|
|
731
|
+
if tty and not inv:
|
|
732
|
+
s = rx.sub(lambda m: f"\033[1;31m{m.group()}\033[0m", s)
|
|
733
|
+
head = (f"{label}:" if label and prefix else "") + (f"{n}:" if nums else "")
|
|
734
|
+
print(head + s)
|
|
735
|
+
return found
|
|
736
|
+
|
|
737
|
+
total = 0
|
|
738
|
+
if not files:
|
|
739
|
+
total = scan(sys.stdin, None)
|
|
740
|
+
for f in files:
|
|
741
|
+
try:
|
|
742
|
+
with open(f, encoding="utf-8", errors="replace") as fh:
|
|
743
|
+
total += scan(fh, f)
|
|
744
|
+
except OSError:
|
|
745
|
+
print(f"FINDSTR: Cannot open {f}")
|
|
746
|
+
return 0 if total else 1
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
@builtin("tasklist", doc='List processes. /fi "imagename eq x" or "pid eq n" to filter')
|
|
750
|
+
def b_tasklist(a):
|
|
751
|
+
if not psutil:
|
|
752
|
+
print("Install psutil first: pip install psutil"); return 1
|
|
753
|
+
fl, rest = split_flags(a)
|
|
754
|
+
flt = None
|
|
755
|
+
if "/fi" in fl and rest:
|
|
756
|
+
m = re.fullmatch(r"\s*(imagename|pid)\s+(eq|ne)\s+(.+?)\s*", rest[0], re.I)
|
|
757
|
+
if m: flt = (m.group(1).lower(), m.group(2).lower(), m.group(3).lower())
|
|
758
|
+
rows = []
|
|
759
|
+
for p in psutil.process_iter(["pid", "name", "memory_info"]):
|
|
760
|
+
i = p.info
|
|
761
|
+
name = i["name"] or "?"
|
|
762
|
+
if flt:
|
|
763
|
+
v = name.lower() if flt[0] == "imagename" else str(i["pid"])
|
|
764
|
+
if (v == flt[2]) != (flt[1] == "eq"):
|
|
765
|
+
continue
|
|
766
|
+
rows.append((name, i["pid"], i["memory_info"].rss // 1024 if i["memory_info"] else 0))
|
|
767
|
+
if not rows:
|
|
768
|
+
print("INFO: No tasks are running which match the specified criteria."); return 0
|
|
769
|
+
rows.sort(key=lambda r: r[0].lower())
|
|
770
|
+
print(f"{'Image Name':<28}{'PID':>8}{'Mem Usage':>16}")
|
|
771
|
+
print("=" * 27 + " " + "=" * 8 + " " + "=" * 15)
|
|
772
|
+
for n, pid, mem in rows:
|
|
773
|
+
print(f"{n[:27]:<28}{pid:>8}{mem:>13,} K")
|
|
774
|
+
return 0
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
@builtin("taskkill", doc="Kill a process: taskkill /pid N or /im name [/f]")
|
|
778
|
+
def b_taskkill(a):
|
|
779
|
+
if not psutil:
|
|
780
|
+
print("Install psutil first: pip install psutil"); return 1
|
|
781
|
+
pid = im = None
|
|
782
|
+
force = False
|
|
783
|
+
it = iter(a)
|
|
784
|
+
for x in it:
|
|
785
|
+
xl = x.lower()
|
|
786
|
+
if xl == "/pid": pid = next(it, None)
|
|
787
|
+
elif xl == "/im": im = next(it, None)
|
|
788
|
+
elif xl == "/f": force = True
|
|
789
|
+
victims = []
|
|
790
|
+
for p in psutil.process_iter(["pid", "name"]):
|
|
791
|
+
if (pid and str(p.info["pid"]) == pid) or (im and (p.info["name"] or "").lower() == im.lower()):
|
|
792
|
+
victims.append(p)
|
|
793
|
+
if not victims:
|
|
794
|
+
print(f'ERROR: The process "{pid or im}" not found.'); return 128
|
|
795
|
+
rc = 0
|
|
796
|
+
for p in victims:
|
|
797
|
+
try:
|
|
798
|
+
p.kill() if force else p.terminate()
|
|
799
|
+
print(f'SUCCESS: Sent termination signal to the process "{p.info["name"]}" with PID {p.info["pid"]}.')
|
|
800
|
+
except Exception as e:
|
|
801
|
+
print(f"ERROR: {e}"); rc = 1
|
|
802
|
+
return rc
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
@builtin("sysinfo", doc="CPU, memory and disk summary")
|
|
806
|
+
def b_sysinfo(a):
|
|
807
|
+
if not psutil:
|
|
808
|
+
print("Install psutil first: pip install psutil"); return 1
|
|
809
|
+
vm, d = psutil.virtual_memory(), psutil.disk_usage(os.path.abspath(os.sep))
|
|
810
|
+
print(f"CPU {psutil.cpu_percent(interval=0.3)}% ({psutil.cpu_count()} logical cores)")
|
|
811
|
+
print(f"RAM {human(vm.used)} / {human(vm.total)} ({vm.percent}%)")
|
|
812
|
+
print(f"Disk {human(d.used)} / {human(d.total)} ({d.percent}%)")
|
|
813
|
+
return 0
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@builtin("history", doc="Show command history")
|
|
817
|
+
def b_history(a):
|
|
818
|
+
if session:
|
|
819
|
+
for i, s in enumerate(session.history.get_strings(), 1):
|
|
820
|
+
print(f"{i:>5} {s}")
|
|
821
|
+
return 0
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@builtin("start", doc="Open a file/URL with its default app, or launch a program")
|
|
825
|
+
def b_start(a):
|
|
826
|
+
a = list(a)
|
|
827
|
+
if a and a[0] == "":
|
|
828
|
+
a.pop(0)
|
|
829
|
+
if not a:
|
|
830
|
+
return 0
|
|
831
|
+
try:
|
|
832
|
+
if len(a) == 1 and hasattr(os, "startfile"):
|
|
833
|
+
os.startfile(a[0])
|
|
834
|
+
elif len(a) == 1:
|
|
835
|
+
subprocess.Popen(["open" if sys.platform == "darwin" else "xdg-open", a[0]])
|
|
836
|
+
else:
|
|
837
|
+
subprocess.Popen(a)
|
|
838
|
+
return 0
|
|
839
|
+
except OSError as e:
|
|
840
|
+
print(f"Cannot start: {e}"); return 1
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
@builtin("timeout", doc="Wait N seconds: timeout /t 5")
|
|
844
|
+
def b_timeout(a):
|
|
845
|
+
nums = [x for x in a if x.lstrip("-").isdigit()]
|
|
846
|
+
n = int(nums[0]) if nums else 5
|
|
847
|
+
try:
|
|
848
|
+
for i in range(n, 0, -1):
|
|
849
|
+
print(f"\rWaiting for {i} seconds, press Ctrl+C to quit ...", end="", flush=True)
|
|
850
|
+
time.sleep(1)
|
|
851
|
+
except KeyboardInterrupt:
|
|
852
|
+
pass
|
|
853
|
+
print()
|
|
854
|
+
return 0
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
@builtin("pause", doc="Wait for Enter")
|
|
858
|
+
def b_pause(a):
|
|
859
|
+
try:
|
|
860
|
+
input("Press Enter to continue . . . ")
|
|
861
|
+
except EOFError:
|
|
862
|
+
pass
|
|
863
|
+
return 0
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
@builtin("help", doc="List commands, or help <command>")
|
|
867
|
+
def b_help(a):
|
|
868
|
+
if a:
|
|
869
|
+
n = a[0].lower()
|
|
870
|
+
if n in HELP:
|
|
871
|
+
print(f"{n}: {HELP[n]}"); return 0
|
|
872
|
+
if IS_WIN:
|
|
873
|
+
return subprocess.run(["cmd", "/c", "help", n]).returncode
|
|
874
|
+
print("No help for that command."); return 1
|
|
875
|
+
print(f"neoshell {VERSION} built-in commands:\n")
|
|
876
|
+
for n in sorted(HELP):
|
|
877
|
+
print(f" {paint(n.ljust(10), '1;32')} {HELP[n]}")
|
|
878
|
+
print("\nSyntax: a & b a && b a || b a | b > >> < %VAR% %ERRORLEVEL%")
|
|
879
|
+
print(" if [not] exist X cmd if [not] errorlevel N cmd if a==b cmd rem comment")
|
|
880
|
+
if IS_WIN:
|
|
881
|
+
print("Anything else CMD knows (assoc, mklink, vol, ...) is passed to cmd.exe.")
|
|
882
|
+
return 0
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
# ---------------------------------------------------------------- execution
|
|
886
|
+
def run_command(argv, data, last, redir):
|
|
887
|
+
"""Run one command. Returns (stdout_bytes_or_None, status)."""
|
|
888
|
+
name = str(argv[0]).lower()
|
|
889
|
+
m = re.fullmatch(r"(cd|chdir)(\.\.+|[\\/])", name) # cd.. and cd\
|
|
890
|
+
if m:
|
|
891
|
+
argv, name = [m.group(1), m.group(2)] + list(argv[1:]), m.group(1)
|
|
892
|
+
elif name.startswith(("echo.", "echo/")): # echo. prints a blank line
|
|
893
|
+
argv, name = ["echo", str(argv[0])[5:]] + list(argv[1:]), "echo"
|
|
894
|
+
|
|
895
|
+
capture = (not last) or redir
|
|
896
|
+
fn = BUILTINS.get(name)
|
|
897
|
+
if fn:
|
|
898
|
+
args = []
|
|
899
|
+
for x in argv[1:]:
|
|
900
|
+
if name in GLOB_CMDS and not isinstance(x, Q) and any(c in x for c in "*?["):
|
|
901
|
+
args += sorted(glob.glob(x)) or [str(x)]
|
|
902
|
+
else:
|
|
903
|
+
args.append(str(x))
|
|
904
|
+
buf, old_in = io.StringIO(), sys.stdin
|
|
905
|
+
if data is not None:
|
|
906
|
+
sys.stdin = io.StringIO(data.decode("utf-8", "replace"))
|
|
907
|
+
try:
|
|
908
|
+
with (contextlib.redirect_stdout(buf) if capture else contextlib.nullcontext()):
|
|
909
|
+
st = fn(args)
|
|
910
|
+
except KeyboardInterrupt:
|
|
911
|
+
st = 130
|
|
912
|
+
finally:
|
|
913
|
+
sys.stdin = old_in
|
|
914
|
+
return (buf.getvalue().encode() if capture else None), (st or 0)
|
|
915
|
+
|
|
916
|
+
argv = [str(x) for x in argv]
|
|
917
|
+
pipe = subprocess.PIPE if capture else None
|
|
918
|
+
try:
|
|
919
|
+
p = subprocess.run(argv, input=data, stdout=pipe)
|
|
920
|
+
except OSError:
|
|
921
|
+
if IS_WIN: # let the real cmd.exe handle its own internal commands
|
|
922
|
+
try:
|
|
923
|
+
p = subprocess.run(["cmd", "/c"] + argv, input=data, stdout=pipe)
|
|
924
|
+
except KeyboardInterrupt:
|
|
925
|
+
return None, 130
|
|
926
|
+
else:
|
|
927
|
+
print(paint(f"'{argv[0]}' is not recognized as an internal or external command,\n"
|
|
928
|
+
"operable program or batch file.", "31"))
|
|
929
|
+
return None, 9009
|
|
930
|
+
except KeyboardInterrupt:
|
|
931
|
+
return None, 130
|
|
932
|
+
return (p.stdout if capture else None), p.returncode
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def devnull(path):
|
|
936
|
+
return os.devnull if path.lower() == "nul" else os.path.expanduser(path)
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def run_if(t):
|
|
940
|
+
i = 1
|
|
941
|
+
ci = False
|
|
942
|
+
if i < len(t) and str(t[i]).lower() == "/i":
|
|
943
|
+
ci = True; i += 1
|
|
944
|
+
neg = False
|
|
945
|
+
if i < len(t) and str(t[i]).lower() == "not":
|
|
946
|
+
neg = True; i += 1
|
|
947
|
+
kind = str(t[i]).lower() if i < len(t) else ""
|
|
948
|
+
try:
|
|
949
|
+
if kind == "exist":
|
|
950
|
+
cond, i = os.path.exists(str(t[i + 1])), i + 2
|
|
951
|
+
elif kind == "errorlevel":
|
|
952
|
+
cond, i = last_status >= int(t[i + 1]), i + 2
|
|
953
|
+
elif kind == "defined":
|
|
954
|
+
cond, i = env_get(str(t[i + 1])) is not None, i + 2
|
|
955
|
+
else:
|
|
956
|
+
s = str(t[i])
|
|
957
|
+
if "==" in s and not (s == "==" and not isinstance(t[i], Q)):
|
|
958
|
+
x, y = s.split("==", 1); i += 1 # a==b or "a"=="b"
|
|
959
|
+
else:
|
|
960
|
+
x, y = s, str(t[i + 2]); i += 3 # a == b
|
|
961
|
+
cond = (x.lower() == y.lower()) if ci else (x == y)
|
|
962
|
+
except (IndexError, ValueError):
|
|
963
|
+
print("The syntax of the command is incorrect."); return 1
|
|
964
|
+
rest = t[i:]
|
|
965
|
+
if cond != neg and rest:
|
|
966
|
+
return run_tokens(rest)
|
|
967
|
+
return last_status
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def run_pipeline(toks):
|
|
971
|
+
if not isinstance(toks[0], Q) and str(toks[0]).lower() == "if":
|
|
972
|
+
return run_if(toks)
|
|
973
|
+
cmds, cur = [], []
|
|
974
|
+
for t in toks:
|
|
975
|
+
if isinstance(t, Op) and t == "|":
|
|
976
|
+
cmds.append(cur); cur = []
|
|
977
|
+
else:
|
|
978
|
+
cur.append(t)
|
|
979
|
+
cmds.append(cur)
|
|
980
|
+
data, st = None, 0
|
|
981
|
+
for n, c in enumerate(cmds):
|
|
982
|
+
argv, out, inp, j = [], None, None, 0
|
|
983
|
+
while j < len(c):
|
|
984
|
+
t = c[j]
|
|
985
|
+
if isinstance(t, Op) and t in (">", ">>") and j + 1 < len(c):
|
|
986
|
+
out = (str(t), str(c[j + 1])); j += 2
|
|
987
|
+
elif isinstance(t, Op) and t == "<" and j + 1 < len(c):
|
|
988
|
+
inp = str(c[j + 1]); j += 2
|
|
989
|
+
else:
|
|
990
|
+
argv.append(t); j += 1
|
|
991
|
+
if not argv:
|
|
992
|
+
continue
|
|
993
|
+
if inp:
|
|
994
|
+
try:
|
|
995
|
+
with open(devnull(inp), "rb") as f:
|
|
996
|
+
data = f.read()
|
|
997
|
+
except OSError:
|
|
998
|
+
print("The system cannot find the file specified."); return 1
|
|
999
|
+
data, st = run_command(argv, data, n == len(cmds) - 1, out is not None)
|
|
1000
|
+
if out and data is not None:
|
|
1001
|
+
try:
|
|
1002
|
+
with open(devnull(out[1]), "ab" if out[0] == ">>" else "wb") as f:
|
|
1003
|
+
f.write(data)
|
|
1004
|
+
except OSError:
|
|
1005
|
+
print("Access is denied."); st = 1
|
|
1006
|
+
data = None
|
|
1007
|
+
return st
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
def run_tokens(tokens):
|
|
1011
|
+
global last_status
|
|
1012
|
+
groups, cur, op = [], [], "&"
|
|
1013
|
+
for t in tokens:
|
|
1014
|
+
if isinstance(t, Op) and t in ("&", "&&", "||"):
|
|
1015
|
+
groups.append((op, cur)); cur, op = [], str(t)
|
|
1016
|
+
else:
|
|
1017
|
+
cur.append(t)
|
|
1018
|
+
groups.append((op, cur))
|
|
1019
|
+
for op, toks in groups:
|
|
1020
|
+
if (op == "&&" and last_status != 0) or (op == "||" and last_status == 0):
|
|
1021
|
+
continue
|
|
1022
|
+
if toks:
|
|
1023
|
+
last_status = run_pipeline(toks)
|
|
1024
|
+
return last_status
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def run_line(line):
|
|
1028
|
+
line = line.strip()
|
|
1029
|
+
if not line or line.startswith("::") or re.match(r"(?i)rem(\s|$)", line):
|
|
1030
|
+
return
|
|
1031
|
+
toks = expand_aliases(tokenize(expand_vars(line)))
|
|
1032
|
+
try:
|
|
1033
|
+
run_tokens(toks)
|
|
1034
|
+
finally:
|
|
1035
|
+
_which_cache.clear()
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
# ---------------------------------------------------------------- highlighting
|
|
1039
|
+
def is_command(name):
|
|
1040
|
+
low = name.lower()
|
|
1041
|
+
if low in BUILTINS or low in aliases or (IS_WIN and low in WIN_INTERNAL):
|
|
1042
|
+
return True
|
|
1043
|
+
if low not in _which_cache:
|
|
1044
|
+
_which_cache[low] = bool(shutil.which(name)) or os.path.isfile(name)
|
|
1045
|
+
return _which_cache[low]
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def arg_style(t):
|
|
1049
|
+
if t.startswith('"'): return "fg:ansiyellow"
|
|
1050
|
+
if t.startswith("%"): return "fg:ansiblue bold"
|
|
1051
|
+
if re.fullmatch(r"[/-][A-Za-z?][\w:,-]*", t): return "fg:ansicyan"
|
|
1052
|
+
if re.fullmatch(r"\d+", t): return "fg:ansimagenta"
|
|
1053
|
+
if os.path.exists(os.path.expandvars(t)): return "underline"
|
|
1054
|
+
return ""
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
class CmdLexer(Lexer):
|
|
1058
|
+
TOK = re.compile(r'"[^"]*"?|%[^%\s"]*%?|&&|\|\||>>|[&|<>]|\s+|[^\s"%&|<>]+')
|
|
1059
|
+
CHAIN = ("&&", "||", "&", "|")
|
|
1060
|
+
|
|
1061
|
+
def lex_document(self, document):
|
|
1062
|
+
lines = document.lines
|
|
1063
|
+
|
|
1064
|
+
def get_line(n):
|
|
1065
|
+
line = lines[n]
|
|
1066
|
+
if re.match(r"\s*(::|rem(\s|$))", line, re.I):
|
|
1067
|
+
return [("fg:ansibrightblack italic", line)]
|
|
1068
|
+
out, state, skip = [], "cmd", 0
|
|
1069
|
+
for m in self.TOK.finditer(line):
|
|
1070
|
+
t = m.group()
|
|
1071
|
+
if t.isspace():
|
|
1072
|
+
out.append(("", t)); continue
|
|
1073
|
+
if t in ("&&", "||", "&", "|", ">", ">>", "<"):
|
|
1074
|
+
out.append(("fg:ansimagenta bold", t))
|
|
1075
|
+
state = "cmd" if t in self.CHAIN else "arg"
|
|
1076
|
+
continue
|
|
1077
|
+
bare = t.strip('"')
|
|
1078
|
+
low = bare.lower()
|
|
1079
|
+
style = ""
|
|
1080
|
+
if state == "cmd":
|
|
1081
|
+
if low == "if":
|
|
1082
|
+
style, state = "fg:ansimagenta bold", "if"
|
|
1083
|
+
else:
|
|
1084
|
+
style = "fg:ansigreen bold" if is_command(bare) else "fg:ansired"
|
|
1085
|
+
state = "arg"
|
|
1086
|
+
elif state == "if":
|
|
1087
|
+
if low in ("not", "/i"):
|
|
1088
|
+
style = "fg:ansimagenta"
|
|
1089
|
+
elif low in ("exist", "errorlevel", "defined"):
|
|
1090
|
+
style, state, skip = "fg:ansimagenta", "skip", 1
|
|
1091
|
+
elif "==" in bare and bare != "==":
|
|
1092
|
+
state = "cmd"
|
|
1093
|
+
else:
|
|
1094
|
+
state, skip = "skip", 2
|
|
1095
|
+
elif state == "skip":
|
|
1096
|
+
skip -= 1
|
|
1097
|
+
if skip <= 0:
|
|
1098
|
+
state = "cmd"
|
|
1099
|
+
out.append((style or arg_style(t), t))
|
|
1100
|
+
return out
|
|
1101
|
+
return get_line
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
def programs():
|
|
1105
|
+
global _prog_cache
|
|
1106
|
+
if _prog_cache is None:
|
|
1107
|
+
exts = tuple(e.lower() for e in os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";")) if IS_WIN else ()
|
|
1108
|
+
found = set()
|
|
1109
|
+
for d in os.environ.get("PATH", "").split(os.pathsep):
|
|
1110
|
+
try:
|
|
1111
|
+
for n in os.listdir(d):
|
|
1112
|
+
if IS_WIN:
|
|
1113
|
+
base, ext = os.path.splitext(n)
|
|
1114
|
+
if ext.lower() in exts: found.add(base.lower())
|
|
1115
|
+
elif os.access(os.path.join(d, n), os.X_OK):
|
|
1116
|
+
found.add(n)
|
|
1117
|
+
except OSError:
|
|
1118
|
+
pass
|
|
1119
|
+
_prog_cache = found
|
|
1120
|
+
return _prog_cache
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
class CmdCompleter(Completer):
|
|
1124
|
+
DIR_CMDS = {"cd", "chdir", "rd", "rmdir", "pushd", "tree"}
|
|
1125
|
+
|
|
1126
|
+
def get_completions(self, document, complete_event):
|
|
1127
|
+
text = document.text_before_cursor
|
|
1128
|
+
m = re.search(r'("[^"]*|[^\s&|<>"]*)$', text)
|
|
1129
|
+
raw, before = m.group(1), text[:m.start()]
|
|
1130
|
+
word, quote, start = raw.lstrip('"'), raw.startswith('"'), -len(raw)
|
|
1131
|
+
seg = re.split(r"&&|\|\||[&|]", before)[-1].split()
|
|
1132
|
+
at_cmd = not seg and not re.search(r"[<>]\s*$", before)
|
|
1133
|
+
|
|
1134
|
+
if word.startswith("%"):
|
|
1135
|
+
pre = word[1:].upper()
|
|
1136
|
+
for k in sorted(os.environ):
|
|
1137
|
+
if k.upper().startswith(pre):
|
|
1138
|
+
yield Completion(f"%{k}%", start_position=start, display_meta="variable")
|
|
1139
|
+
return
|
|
1140
|
+
if at_cmd and not re.search(r"[\\/]", word):
|
|
1141
|
+
pool = {n: "builtin" for n in BUILTINS}
|
|
1142
|
+
pool.update({n: "macro" for n in aliases})
|
|
1143
|
+
for n in programs():
|
|
1144
|
+
pool.setdefault(n, "program")
|
|
1145
|
+
for n in sorted(pool):
|
|
1146
|
+
if n.lower().startswith(word.lower()):
|
|
1147
|
+
yield Completion(n, start_position=start, display_meta=pool[n])
|
|
1148
|
+
return
|
|
1149
|
+
dirs_only = bool(seg) and seg[0].lower() in self.DIR_CMDS
|
|
1150
|
+
d, base = os.path.split(word)
|
|
1151
|
+
try:
|
|
1152
|
+
names = os.listdir(os.path.expanduser(d) if d else ".")
|
|
1153
|
+
except OSError:
|
|
1154
|
+
return
|
|
1155
|
+
for n in sorted(names, key=str.lower):
|
|
1156
|
+
if not n.lower().startswith(base.lower()):
|
|
1157
|
+
continue
|
|
1158
|
+
full = os.path.join(d, n) if d else n
|
|
1159
|
+
isdir = os.path.isdir(full)
|
|
1160
|
+
if dirs_only and not isdir:
|
|
1161
|
+
continue
|
|
1162
|
+
out = full + (os.sep if isdir else "")
|
|
1163
|
+
if " " in out or quote:
|
|
1164
|
+
out = '"' + out + ("" if isdir else '"')
|
|
1165
|
+
yield Completion(out, start_position=start, display=n + (os.sep if isdir else ""))
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
# ---------------------------------------------------------------- Windows Terminal profile
|
|
1169
|
+
PROFILE_GUID = "{9d4b7e32-6a1c-4c8f-b5d0-3e8a2f1c7b64}"
|
|
1170
|
+
FROZEN = getattr(sys, "frozen", False) # True when running as neoshell.exe
|
|
1171
|
+
TERM_SCHEME = {
|
|
1172
|
+
"name": "neoshell",
|
|
1173
|
+
"background": "#0F1420", "foreground": "#D7DEEA",
|
|
1174
|
+
"cursorColor": "#4DE1FF", "selectionBackground": "#25324D",
|
|
1175
|
+
"black": "#1B2233", "red": "#FF6B81", "green": "#5BEAA0", "yellow": "#FFD479",
|
|
1176
|
+
"blue": "#6EA8FF", "purple": "#C792FF", "cyan": "#4DE1FF", "white": "#D7DEEA",
|
|
1177
|
+
"brightBlack": "#5C6B8A", "brightRed": "#FF8FA1", "brightGreen": "#8BF5BE",
|
|
1178
|
+
"brightYellow": "#FFE3A0", "brightBlue": "#9CC4FF", "brightPurple": "#DDB8FF",
|
|
1179
|
+
"brightCyan": "#8CEBFF", "brightWhite": "#FFFFFF",
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _profile_paths():
|
|
1184
|
+
local = os.environ.get("LOCALAPPDATA") # only set on Windows
|
|
1185
|
+
if not local:
|
|
1186
|
+
return None
|
|
1187
|
+
return (os.path.join(local, "neoshell"),
|
|
1188
|
+
os.path.join(local, "Microsoft", "Windows Terminal", "Fragments", "neoshell"))
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def _resource(name):
|
|
1192
|
+
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
|
1193
|
+
return os.path.join(base, name)
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _self_path():
|
|
1197
|
+
return os.path.abspath(sys.executable if FROZEN else __file__)
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
def _profile_command(inst):
|
|
1201
|
+
if FROZEN:
|
|
1202
|
+
return f'"{os.path.join(inst, "neoshell.exe")}"'
|
|
1203
|
+
return f'"{sys.executable}" "{os.path.join(inst, "neoshell.py")}"'
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def _remove_legacy():
|
|
1207
|
+
"""Remove the profile + install folder from when this program was called pyshell."""
|
|
1208
|
+
local = os.environ.get("LOCALAPPDATA")
|
|
1209
|
+
if not local:
|
|
1210
|
+
return False
|
|
1211
|
+
old_frag = os.path.join(local, "Microsoft", "Windows Terminal", "Fragments", "pyshell")
|
|
1212
|
+
found = os.path.isdir(old_frag) or os.path.isdir(os.path.join(local, "pyshell"))
|
|
1213
|
+
shutil.rmtree(old_frag, ignore_errors=True)
|
|
1214
|
+
shutil.rmtree(os.path.join(local, "pyshell"), ignore_errors=True)
|
|
1215
|
+
return found
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def install_profile():
|
|
1219
|
+
"""Copy neoshell to %LOCALAPPDATA%\\neoshell and register a Windows Terminal profile.
|
|
1220
|
+
|
|
1221
|
+
Uses a Windows Terminal "fragment" file, so the user's settings.json is never edited."""
|
|
1222
|
+
paths = _profile_paths()
|
|
1223
|
+
if not paths:
|
|
1224
|
+
print("Windows Terminal profiles need Windows."); return False
|
|
1225
|
+
inst, frag_dir = paths
|
|
1226
|
+
target = os.path.join(inst, "neoshell.exe" if FROZEN else "neoshell.py")
|
|
1227
|
+
try:
|
|
1228
|
+
os.makedirs(inst, exist_ok=True)
|
|
1229
|
+
if _self_path().lower() != os.path.abspath(target).lower():
|
|
1230
|
+
shutil.copy2(_self_path(), target)
|
|
1231
|
+
icon = os.path.join(inst, "neoshell.ico")
|
|
1232
|
+
if os.path.isfile(_resource("neoshell.ico")):
|
|
1233
|
+
shutil.copy2(_resource("neoshell.ico"), icon)
|
|
1234
|
+
profile = {"guid": PROFILE_GUID, "name": "neoshell", "commandline": _profile_command(inst),
|
|
1235
|
+
"startingDirectory": "%USERPROFILE%", "colorScheme": "neoshell",
|
|
1236
|
+
"tabTitle": "neoshell", "padding": "10", "cursorShape": "bar"}
|
|
1237
|
+
if os.path.isfile(icon):
|
|
1238
|
+
profile["icon"] = icon
|
|
1239
|
+
os.makedirs(frag_dir, exist_ok=True)
|
|
1240
|
+
with open(os.path.join(frag_dir, "neoshell.json"), "w", encoding="utf-8") as f:
|
|
1241
|
+
json.dump({"profiles": [profile], "schemes": [TERM_SCHEME]}, f, indent=2)
|
|
1242
|
+
except OSError as e:
|
|
1243
|
+
print(paint(f"Could not install the profile: {e}", "31")); return False
|
|
1244
|
+
if _remove_legacy():
|
|
1245
|
+
print(" (removed the old 'pyshell' profile)")
|
|
1246
|
+
print(paint("Windows Terminal profile installed.", "32"))
|
|
1247
|
+
print(f" Restart Windows Terminal, then pick 'neoshell' from the dropdown (the arrow next to +).")
|
|
1248
|
+
print(f" Make it the default: Settings > Startup > Default profile. Installed in {inst}")
|
|
1249
|
+
return True
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def uninstall_profile():
|
|
1253
|
+
paths = _profile_paths()
|
|
1254
|
+
if not paths:
|
|
1255
|
+
print("Nothing to remove (not Windows)."); return False
|
|
1256
|
+
inst, frag_dir = paths
|
|
1257
|
+
shutil.rmtree(frag_dir, ignore_errors=True)
|
|
1258
|
+
shutil.rmtree(inst, ignore_errors=True)
|
|
1259
|
+
_remove_legacy()
|
|
1260
|
+
print("Profile removed. Restart Windows Terminal." +
|
|
1261
|
+
(f"\nIf {inst} is still there, close neoshell and delete it." if os.path.isdir(inst) else ""))
|
|
1262
|
+
return True
|
|
1263
|
+
|
|
1264
|
+
|
|
1265
|
+
def profile_current():
|
|
1266
|
+
"""True if the registered profile already points at an up-to-date install."""
|
|
1267
|
+
paths = _profile_paths()
|
|
1268
|
+
if not paths:
|
|
1269
|
+
return True
|
|
1270
|
+
inst, frag_dir = paths
|
|
1271
|
+
if os.path.isdir(os.path.join(os.path.dirname(frag_dir), "pyshell")):
|
|
1272
|
+
return False # old pyshell profile still registered
|
|
1273
|
+
if os.path.dirname(_self_path()).lower() == os.path.abspath(inst).lower():
|
|
1274
|
+
return True # we ARE the installed copy
|
|
1275
|
+
try:
|
|
1276
|
+
with open(os.path.join(frag_dir, "neoshell.json"), encoding="utf-8") as f:
|
|
1277
|
+
if json.load(f)["profiles"][0]["commandline"] != _profile_command(inst):
|
|
1278
|
+
return False
|
|
1279
|
+
if FROZEN: # newer exe than the installed one -> update it
|
|
1280
|
+
return os.path.getsize(os.path.join(inst, "neoshell.exe")) == os.path.getsize(sys.executable)
|
|
1281
|
+
except (OSError, ValueError, KeyError, IndexError):
|
|
1282
|
+
return False
|
|
1283
|
+
return True
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def start_in_home():
|
|
1287
|
+
"""Don't leave the user in System32 / Program Files / neoshell's own folder.
|
|
1288
|
+
|
|
1289
|
+
Double-clicking the exe starts it in an awkward place. In those cases move to the
|
|
1290
|
+
logged-in user's folder. If the user opened neoshell in a folder on purpose (for
|
|
1291
|
+
example "Open in Terminal" from Explorer), leave it alone."""
|
|
1292
|
+
home = os.path.expanduser("~")
|
|
1293
|
+
if not os.path.isdir(home):
|
|
1294
|
+
return
|
|
1295
|
+
norm = lambda x: os.path.abspath(x).lower().rstrip("\\/")
|
|
1296
|
+
here = norm(os.getcwd())
|
|
1297
|
+
exact = {norm(os.path.dirname(_self_path()))}
|
|
1298
|
+
if _profile_paths():
|
|
1299
|
+
exact.add(norm(_profile_paths()[0]))
|
|
1300
|
+
prefixes = [norm(os.environ[v]) for v in ("SystemRoot", "ProgramFiles", "ProgramFiles(x86)")
|
|
1301
|
+
if os.environ.get(v)]
|
|
1302
|
+
if here in exact or any(here == p or here.startswith(p + os.sep) for p in prefixes):
|
|
1303
|
+
os.chdir(home)
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
# ---------------------------------------------------------------- main loop
|
|
1307
|
+
def prompt_text():
|
|
1308
|
+
parts = [("fg:ansiblue bold", os.getcwd())]
|
|
1309
|
+
br = git_branch()
|
|
1310
|
+
if br:
|
|
1311
|
+
parts.append(("fg:ansimagenta", f" ({br})"))
|
|
1312
|
+
parts.append(("fg:ansigreen bold" if last_status == 0 else "fg:ansired bold", " > "))
|
|
1313
|
+
return parts
|
|
1314
|
+
|
|
1315
|
+
|
|
1316
|
+
def rprompt_text():
|
|
1317
|
+
bits = []
|
|
1318
|
+
if last_status:
|
|
1319
|
+
bits.append(("fg:ansired", f"x{last_status} "))
|
|
1320
|
+
if last_dur >= 1:
|
|
1321
|
+
bits.append(("fg:ansibrightblack", f"{last_dur:.1f}s "))
|
|
1322
|
+
bits.append(("fg:ansibrightblack", time.strftime("%H:%M")))
|
|
1323
|
+
return bits
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
LOGO = ["┏┓╻┏━╸┏━┓┏━┓╻ ╻┏━╸╻ ╻ ",
|
|
1327
|
+
"┃┗┫┣╸ ┃ ┃┗━┓┣━┫┣╸ ┃ ┃ ",
|
|
1328
|
+
"╹ ╹┗━╸┗━┛┗━┛╹ ╹┗━╸┗━╸┗━╸"]
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def banner():
|
|
1332
|
+
"""Gradient ASCII logo (cyan -> violet, same colors as the app icon)."""
|
|
1333
|
+
if not sys.stdout.isatty():
|
|
1334
|
+
print(f"neoshell {VERSION}"); return
|
|
1335
|
+
c1, c2 = (77, 225, 255), (199, 146, 255)
|
|
1336
|
+
width = max(len(r) for r in LOGO)
|
|
1337
|
+
print()
|
|
1338
|
+
for row in LOGO:
|
|
1339
|
+
out = ""
|
|
1340
|
+
for x, ch in enumerate(row):
|
|
1341
|
+
t = x / max(1, width - 1)
|
|
1342
|
+
r, g, b = (int(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
|
|
1343
|
+
out += f"\033[38;2;{r};{g};{b}m{ch}"
|
|
1344
|
+
print(" " + out + "\033[0m")
|
|
1345
|
+
print(f" \033[90mCMD-style shell · v{VERSION} · type \033[0m\033[1mhelp\033[0m\033[90m for commands\033[0m\n")
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def main():
|
|
1349
|
+
global session, last_dur
|
|
1350
|
+
argv = sys.argv[1:]
|
|
1351
|
+
if "--uninstall" in argv:
|
|
1352
|
+
return 0 if uninstall_profile() else 1
|
|
1353
|
+
if "--install" in argv:
|
|
1354
|
+
return 0 if install_profile() else 1
|
|
1355
|
+
no_install = "--no-install" in argv
|
|
1356
|
+
if argv[:1] == ["-c"]:
|
|
1357
|
+
try:
|
|
1358
|
+
run_line(" ".join(argv[1:]))
|
|
1359
|
+
except SystemExit as e:
|
|
1360
|
+
return e.code or 0
|
|
1361
|
+
return last_status
|
|
1362
|
+
|
|
1363
|
+
old_hist = os.path.expanduser("~/.pyshell_history")
|
|
1364
|
+
if os.path.isfile(old_hist) and not os.path.isfile(HIST):
|
|
1365
|
+
with contextlib.suppress(OSError):
|
|
1366
|
+
shutil.copy2(old_hist, HIST)
|
|
1367
|
+
if HAVE_PT:
|
|
1368
|
+
session = PromptSession(history=FileHistory(HIST), auto_suggest=AutoSuggestFromHistory(),
|
|
1369
|
+
lexer=CmdLexer(), completer=CmdCompleter(),
|
|
1370
|
+
complete_while_typing=False)
|
|
1371
|
+
read = lambda: session.prompt(prompt_text, rprompt=rprompt_text)
|
|
1372
|
+
else:
|
|
1373
|
+
print("(tip: pip install prompt_toolkit for syntax highlighting and tab completion)")
|
|
1374
|
+
read = lambda: input(f"{os.getcwd()}> ")
|
|
1375
|
+
|
|
1376
|
+
start_in_home()
|
|
1377
|
+
b_title(["neoshell"])
|
|
1378
|
+
banner()
|
|
1379
|
+
if FROZEN and not no_install and _profile_paths() and not profile_current():
|
|
1380
|
+
install_profile()
|
|
1381
|
+
print()
|
|
1382
|
+
while True:
|
|
1383
|
+
try:
|
|
1384
|
+
line = read()
|
|
1385
|
+
except KeyboardInterrupt:
|
|
1386
|
+
continue
|
|
1387
|
+
except EOFError:
|
|
1388
|
+
print(); break
|
|
1389
|
+
t0 = time.time()
|
|
1390
|
+
try:
|
|
1391
|
+
run_line(line)
|
|
1392
|
+
except SystemExit as e:
|
|
1393
|
+
return e.code or 0
|
|
1394
|
+
except KeyboardInterrupt:
|
|
1395
|
+
print("^C")
|
|
1396
|
+
except Exception as e:
|
|
1397
|
+
print(paint(f"neoshell error: {e}", "31"))
|
|
1398
|
+
last_dur = time.time() - t0
|
|
1399
|
+
return 0
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
if __name__ == "__main__":
|
|
1403
|
+
sys.exit(main())
|