xmyshell 0.2.10__tar.gz → 0.2.12__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.
- {xmyshell-0.2.10/xmyshell.egg-info → xmyshell-0.2.12}/PKG-INFO +25 -1
- {xmyshell-0.2.10 → xmyshell-0.2.12}/README.md +24 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/pyproject.toml +1 -1
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/completer.py +47 -8
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/environment.py +3 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/kernel.py +55 -38
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/keybindings.py +24 -11
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/lexer.py +41 -1
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/main.py +3 -2
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/meta.py +2 -0
- xmyshell-0.2.12/xmyshell/stubs/__init__.py +1 -0
- xmyshell-0.2.12/xmyshell/stubs/__init__.pyi +17 -0
- xmyshell-0.2.12/xmyshell/stubs/py.typed +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12/xmyshell.egg-info}/PKG-INFO +25 -1
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/SOURCES.txt +4 -1
- {xmyshell-0.2.10 → xmyshell-0.2.12}/LICENSE +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/setup.cfg +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/__init__.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/__main__.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/data/commands.json +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/init.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/themes.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/update.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell/utils.py +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/dependency_links.txt +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/entry_points.txt +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/requires.txt +0 -0
- {xmyshell-0.2.10 → xmyshell-0.2.12}/xmyshell.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xmyshell
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -118,6 +118,15 @@ or
|
|
|
118
118
|
❯ unalias ll
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
- Lists, tuples, generators, etc. will be expanded into shell arguments.
|
|
122
|
+
E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
|
|
123
|
+
|
|
124
|
+
Here is an example (though there are other ways to do this, it is just an example )
|
|
125
|
+
```
|
|
126
|
+
❯ pip install {cat("requirements.txt").splitlines()}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
121
130
|
- Syntax highlighting via `prompt_toolkit`
|
|
122
131
|
- Powerful auto-completion, history search, and automatic bracket/quote pairing
|
|
123
132
|
- Startup configuration: `~/.xmyshell/config.py` loaded at launch
|
|
@@ -154,6 +163,12 @@ The exit code of the last command.
|
|
|
154
163
|
- `exec_duration`
|
|
155
164
|
The duration which last command took.
|
|
156
165
|
|
|
166
|
+
- `SHOW_WELCOME`
|
|
167
|
+
Toggle whether to show welcome message when staring.
|
|
168
|
+
|
|
169
|
+
- `SHELL_CMD`
|
|
170
|
+
Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
|
|
171
|
+
|
|
157
172
|
- `HOME_DIR`
|
|
158
173
|
The home directory, similar to `~`.
|
|
159
174
|
|
|
@@ -170,6 +185,15 @@ The path of config file.
|
|
|
170
185
|
- Auto bracket/parenthesis/quote pairing
|
|
171
186
|
|
|
172
187
|
|
|
188
|
+
### Stubs
|
|
189
|
+
|
|
190
|
+
You can write your xmyshell script with these imports:
|
|
191
|
+
```python
|
|
192
|
+
from xmyshell.themes import ... # themes
|
|
193
|
+
from xmyshell.stubs import ... # stubs for built-in names
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
|
|
173
197
|
## Limitations
|
|
174
198
|
|
|
175
199
|
**XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
|
|
@@ -108,6 +108,15 @@ or
|
|
|
108
108
|
❯ unalias ll
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
- Lists, tuples, generators, etc. will be expanded into shell arguments.
|
|
112
|
+
E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
|
|
113
|
+
|
|
114
|
+
Here is an example (though there are other ways to do this, it is just an example )
|
|
115
|
+
```
|
|
116
|
+
❯ pip install {cat("requirements.txt").splitlines()}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
|
|
111
120
|
- Syntax highlighting via `prompt_toolkit`
|
|
112
121
|
- Powerful auto-completion, history search, and automatic bracket/quote pairing
|
|
113
122
|
- Startup configuration: `~/.xmyshell/config.py` loaded at launch
|
|
@@ -144,6 +153,12 @@ The exit code of the last command.
|
|
|
144
153
|
- `exec_duration`
|
|
145
154
|
The duration which last command took.
|
|
146
155
|
|
|
156
|
+
- `SHOW_WELCOME`
|
|
157
|
+
Toggle whether to show welcome message when staring.
|
|
158
|
+
|
|
159
|
+
- `SHELL_CMD`
|
|
160
|
+
Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
|
|
161
|
+
|
|
147
162
|
- `HOME_DIR`
|
|
148
163
|
The home directory, similar to `~`.
|
|
149
164
|
|
|
@@ -160,6 +175,15 @@ The path of config file.
|
|
|
160
175
|
- Auto bracket/parenthesis/quote pairing
|
|
161
176
|
|
|
162
177
|
|
|
178
|
+
### Stubs
|
|
179
|
+
|
|
180
|
+
You can write your xmyshell script with these imports:
|
|
181
|
+
```python
|
|
182
|
+
from xmyshell.themes import ... # themes
|
|
183
|
+
from xmyshell.stubs import ... # stubs for built-in names
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
|
|
163
187
|
## Limitations
|
|
164
188
|
|
|
165
189
|
**XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from collections.abc import Generator, Iterable
|
|
2
2
|
import os
|
|
3
|
+
import shlex
|
|
3
4
|
import sys
|
|
4
5
|
import re
|
|
5
6
|
import time
|
|
@@ -118,16 +119,51 @@ class ShellCompleter(Completer):
|
|
|
118
119
|
others = sorted(c for c in commands if c not in COMMON and c not in BUILTINS)
|
|
119
120
|
return builtins + common + others
|
|
120
121
|
|
|
121
|
-
def _path_completions(self, text: str, max_results: int = 100):
|
|
122
|
+
def _path_completions(self, text: str, max_results: int = 100, in_string: bool=False):
|
|
122
123
|
if text and text[-1] in whitespace: return
|
|
123
|
-
|
|
124
|
+
|
|
125
|
+
def last_token_raw(text: str) -> str:
|
|
126
|
+
quote = None
|
|
127
|
+
start = 0
|
|
128
|
+
i = 0
|
|
129
|
+
while i < len(text):
|
|
130
|
+
ch = text[i]
|
|
131
|
+
if quote:
|
|
132
|
+
if ch == quote:
|
|
133
|
+
quote = None
|
|
134
|
+
elif ch == "\\" and quote == '"':
|
|
135
|
+
i += 1
|
|
136
|
+
else:
|
|
137
|
+
if ch in ("'", '"'):
|
|
138
|
+
quote = ch
|
|
139
|
+
elif ch in " \t":
|
|
140
|
+
start = i + 1
|
|
141
|
+
elif ch == "\\":
|
|
142
|
+
i += 1
|
|
143
|
+
i += 1
|
|
144
|
+
return text[start:]
|
|
145
|
+
|
|
146
|
+
def start_pos(text: str, prefix: str) -> int:
|
|
147
|
+
original_len = len(text)
|
|
148
|
+
if text and text[-1] in ("'", '"') and not text.endswith(prefix):
|
|
149
|
+
text = text[:-1]
|
|
150
|
+
if not prefix or not text.endswith(prefix):
|
|
151
|
+
return -len(prefix)
|
|
152
|
+
start = len(text) - len(prefix)
|
|
153
|
+
if start > 0 and text[start - 1] in ("'", '"'):
|
|
154
|
+
start -= 1
|
|
155
|
+
return start - original_len
|
|
156
|
+
|
|
157
|
+
last = last_token_raw(text)
|
|
158
|
+
in_string = in_string or last.startswith('"') or last.startswith("'")
|
|
159
|
+
last = last.lstrip('"\'')
|
|
124
160
|
if last == '~': return # don't remove this
|
|
125
161
|
path = os.path.expanduser(last)
|
|
126
162
|
dirname = os.path.dirname(path) or "."
|
|
127
|
-
prefix = os.path.basename(path)
|
|
163
|
+
prefix = os.path.basename(path).strip('"\'')
|
|
128
164
|
|
|
129
165
|
try:
|
|
130
|
-
matches = []
|
|
166
|
+
matches: list[str] = []
|
|
131
167
|
for item in os.listdir(dirname):
|
|
132
168
|
if item.startswith(prefix):
|
|
133
169
|
matches.append(item)
|
|
@@ -137,11 +173,14 @@ class ShellCompleter(Completer):
|
|
|
137
173
|
for item in sorted(matches):
|
|
138
174
|
full = os.path.join(dirname, item)
|
|
139
175
|
is_dir = os.path.isdir(full)
|
|
140
|
-
|
|
176
|
+
if ' ' in item and not in_string:
|
|
177
|
+
name = f'"{item}"'
|
|
178
|
+
else:
|
|
179
|
+
name = item
|
|
141
180
|
yield Completion(
|
|
142
181
|
name,
|
|
143
|
-
display=truncate(
|
|
144
|
-
start_position
|
|
182
|
+
display=truncate(item, MAX_DISPLAY_LEN),
|
|
183
|
+
start_position=start_pos(text, prefix),
|
|
145
184
|
display_meta="directory" if is_dir else "file",
|
|
146
185
|
)
|
|
147
186
|
except OSError:
|
|
@@ -207,7 +246,7 @@ class ShellCompleter(Completer):
|
|
|
207
246
|
|
|
208
247
|
string = _in_string(text)
|
|
209
248
|
if string:
|
|
210
|
-
yield from self._path_completions(string)
|
|
249
|
+
yield from self._path_completions(string, in_string=True)
|
|
211
250
|
return
|
|
212
251
|
|
|
213
252
|
if text and text[-1].isspace():
|
|
@@ -10,6 +10,7 @@ from .utils import XMYSHELL_MAGIC, getcwd, getlogin, HOME_DIR, xmyshell_cat
|
|
|
10
10
|
namespace: dict[str, Any] = {}
|
|
11
11
|
aliases: dict[str, str] = {}
|
|
12
12
|
init_environ: dict[str, str] = dict(os.environ)
|
|
13
|
+
SHELL_CMD = ["cmd", "/c"] if os.name == "nt" else ["/bin/sh", "-c"]
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def xmyshell_alias(altname: str, target: str) -> None:
|
|
@@ -34,6 +35,8 @@ _init_namespace: dict[str, Any] = {
|
|
|
34
35
|
"cat": xmyshell_cat,
|
|
35
36
|
"alias": xmyshell_alias,
|
|
36
37
|
"unalias": xmyshell_unalias,
|
|
38
|
+
"SHOW_WELCOME": True,
|
|
39
|
+
"SHELL_CMD": SHELL_CMD,
|
|
37
40
|
"HOME_DIR": HOME_DIR,
|
|
38
41
|
"PROFILE": f"{HOME_DIR}/.xmyshell/config.py",
|
|
39
42
|
"exit_code": 0,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from collections.abc import Generator, Sequence
|
|
2
|
+
from contextlib import contextmanager
|
|
2
3
|
import os
|
|
3
4
|
import sys
|
|
4
5
|
import runpy
|
|
5
6
|
import subprocess
|
|
6
7
|
import signal
|
|
7
8
|
import shlex
|
|
8
|
-
from .environment import namespace, aliases, xmyshell_alias, xmyshell_unalias
|
|
9
|
+
from .environment import namespace, aliases, xmyshell_alias, xmyshell_unalias, SHELL_CMD
|
|
9
10
|
from .utils import pywarning, pyerror, getcwd
|
|
10
11
|
from .meta import HELP_MESSAGE
|
|
11
12
|
|
|
@@ -14,8 +15,20 @@ def _reload() -> None:
|
|
|
14
15
|
from .init import xmyshell_reload
|
|
15
16
|
xmyshell_reload()
|
|
16
17
|
|
|
18
|
+
def _reset_sigint():
|
|
19
|
+
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
_kwargs_preexec = {}
|
|
22
|
+
if os.name == "posix":
|
|
23
|
+
_kwargs_preexec["preexec_fn"] = _reset_sigint
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def _no_interrupt():
|
|
27
|
+
old = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
28
|
+
yield
|
|
29
|
+
signal.signal(signal.SIGINT, old)
|
|
30
|
+
|
|
31
|
+
def _run(cmd_line: list[str] | str, **kwargs) -> subprocess.CompletedProcess:
|
|
19
32
|
# Block SIGINT in the parent while the child runs, so that a Ctrl-C
|
|
20
33
|
# delivered to the console/process group is handled solely by the child.
|
|
21
34
|
#
|
|
@@ -26,17 +39,15 @@ def _run(*args, **kwargs) -> subprocess.CompletedProcess:
|
|
|
26
39
|
# KeyboardInterrupt handler only fires for Ctrl-C at the prompt.
|
|
27
40
|
#
|
|
28
41
|
# Must run on the main thread: signal.signal() is not allowed elsewhere.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
signal.signal(signal.SIGINT, old)
|
|
39
|
-
|
|
42
|
+
with _no_interrupt():
|
|
43
|
+
if isinstance(cmd_line, list):
|
|
44
|
+
return subprocess.run(cmd_line, **_kwargs_preexec, **kwargs)
|
|
45
|
+
else:
|
|
46
|
+
return subprocess.run(
|
|
47
|
+
namespace.get("SHELL_CMD", SHELL_CMD) + [cmd_line],
|
|
48
|
+
**_kwargs_preexec,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
40
51
|
|
|
41
52
|
def xmyshell_source(path: str) -> None:
|
|
42
53
|
_updated = runpy.run_path(
|
|
@@ -176,8 +187,13 @@ def xmyshell_raw_command(cmd_line: str) -> int | None:
|
|
|
176
187
|
try:
|
|
177
188
|
result = eval(expr, dict(os.environ), namespace)
|
|
178
189
|
print(result)
|
|
190
|
+
namespace["last_error"] = None
|
|
179
191
|
return 0
|
|
180
192
|
except Exception as e:
|
|
193
|
+
import traceback
|
|
194
|
+
|
|
195
|
+
namespace["last_error"] = e
|
|
196
|
+
namespace["last_traceback"] = traceback.format_exc()
|
|
181
197
|
pyerror(f"{type(e).__name__}: {e}")
|
|
182
198
|
return -1
|
|
183
199
|
|
|
@@ -245,29 +261,30 @@ def xmyshell(cmd_line: str) -> int:
|
|
|
245
261
|
cmd_line, sh_cmd = cmd_line.split("|>", 1)
|
|
246
262
|
|
|
247
263
|
if sh_cmd:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
264
|
+
with _no_interrupt():
|
|
265
|
+
proc = subprocess.Popen(
|
|
266
|
+
namespace.get("SHELL_CMD", SHELL_CMD) + [sh_cmd],
|
|
267
|
+
env=os.environ,
|
|
268
|
+
stdin=subprocess.PIPE,
|
|
269
|
+
stdout=subprocess.PIPE,
|
|
270
|
+
text=True,
|
|
271
|
+
**_kwargs_preexec,
|
|
272
|
+
)
|
|
273
|
+
old_stdout = sys.stdout
|
|
274
|
+
sys.stdout = proc.stdin
|
|
275
|
+
return_code = xmyshell_raw_command(cmd_line)
|
|
276
|
+
if return_code is None:
|
|
277
|
+
pyerror(f"'{cmd_line}' cannot be used in a pipeline with '|>'")
|
|
278
|
+
return -1
|
|
279
|
+
assert proc.stdin
|
|
280
|
+
proc.stdin.close()
|
|
281
|
+
sys.stdout = old_stdout
|
|
282
|
+
if target_var:
|
|
283
|
+
result, _ = proc.communicate()
|
|
284
|
+
namespace[target_var] = result
|
|
285
|
+
return proc.returncode
|
|
286
|
+
else:
|
|
287
|
+
return proc.wait()
|
|
271
288
|
|
|
272
289
|
if target_var:
|
|
273
290
|
from io import StringIO
|
|
@@ -337,10 +354,10 @@ def xmyshell(cmd_line: str) -> int:
|
|
|
337
354
|
cmd_line = _fix_first_word(cmd_line)
|
|
338
355
|
if target_var:
|
|
339
356
|
result = _run(
|
|
340
|
-
cmd_line,
|
|
357
|
+
cmd_line, env=os.environ, capture_output=True, text=True
|
|
341
358
|
)
|
|
342
359
|
namespace[target_var] = result.stdout
|
|
343
360
|
else:
|
|
344
|
-
result = _run(cmd_line,
|
|
361
|
+
result = _run(cmd_line, env=os.environ)
|
|
345
362
|
|
|
346
363
|
return result.returncode
|
|
@@ -3,7 +3,6 @@ from prompt_toolkit.buffer import Buffer
|
|
|
3
3
|
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
|
4
4
|
from prompt_toolkit.selection import SelectionState
|
|
5
5
|
from prompt_toolkit.keys import Keys
|
|
6
|
-
from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
|
|
9
8
|
@dataclass
|
|
@@ -16,7 +15,7 @@ class BufferInfo:
|
|
|
16
15
|
return BufferInfo(buffer.text, buffer.cursor_position)
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
MAX_UNDO_LIST_LEN = 100
|
|
20
19
|
UNDO_MERGE_TIMEOUT = 0.5
|
|
21
20
|
bindings = KeyBindings()
|
|
22
21
|
_auto_editing: bool = False
|
|
@@ -35,6 +34,8 @@ def on_text_changed(buffer: Buffer) -> None:
|
|
|
35
34
|
_redo_list.clear()
|
|
36
35
|
if not _should_merge(buffer):
|
|
37
36
|
_undo_list.append(_last_info)
|
|
37
|
+
while len(_undo_list) > MAX_UNDO_LIST_LEN:
|
|
38
|
+
del _undo_list[0]
|
|
38
39
|
_last_info = BufferInfo.from_buffer(buffer)
|
|
39
40
|
|
|
40
41
|
|
|
@@ -198,15 +199,12 @@ def _(event: KeyPressEvent) -> None:
|
|
|
198
199
|
stack.pop()
|
|
199
200
|
_auto_editing = False
|
|
200
201
|
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)
|
|
208
|
-
or buffer.cursor_position == 0
|
|
209
|
-
):
|
|
202
|
+
pos = buffer.cursor_position
|
|
203
|
+
text = buffer.text
|
|
204
|
+
before_ok = pos == 0 or (pos > 0 and (text[pos - 1].isspace()
|
|
205
|
+
or text[pos - 1] in set(r"!@#$%^&*:;,./?|+-=~()[]{}<>_")))
|
|
206
|
+
not_on_quote = pos >= len(text) or text[pos] != '"'
|
|
207
|
+
if should_completion(buffer) and before_ok and not_on_quote:
|
|
210
208
|
_auto_editing = True
|
|
211
209
|
buffer.insert_text('""')
|
|
212
210
|
buffer.cursor_left()
|
|
@@ -357,3 +355,18 @@ def _(event: KeyPressEvent) -> None:
|
|
|
357
355
|
_auto_editing = False
|
|
358
356
|
_last_info = BufferInfo.from_buffer(buffer)
|
|
359
357
|
_reset_undo_merge()
|
|
358
|
+
|
|
359
|
+
@bindings.add(Keys.Tab)
|
|
360
|
+
def _(event: KeyPressEvent) -> None:
|
|
361
|
+
buffer = event.current_buffer
|
|
362
|
+
if buffer.complete_state:
|
|
363
|
+
if buffer.complete_state.current_completion is None:
|
|
364
|
+
buffer.complete_next()
|
|
365
|
+
if buffer.complete_state.current_completion:
|
|
366
|
+
# Trailing space bypasses prompt_toolkit's `completion_does_nothing`
|
|
367
|
+
# filter (which hides completions identical to the replaced text) and
|
|
368
|
+
# doubles as the shell argument separator. Invisible characters would
|
|
369
|
+
# also bypass the filter, but would corrupt command parsing.
|
|
370
|
+
text = buffer.complete_state.current_completion.text
|
|
371
|
+
buffer.complete_state.current_completion.text = text.rstrip()
|
|
372
|
+
buffer.apply_completion(buffer.complete_state.current_completion)
|
|
@@ -14,6 +14,46 @@ def _is_number_literal(s: str) -> bool:
|
|
|
14
14
|
except (ValueError, SyntaxError):
|
|
15
15
|
return False
|
|
16
16
|
|
|
17
|
+
def _shell_split_keep_sep(text: str) -> list[str]:
|
|
18
|
+
parts: list[str] = []
|
|
19
|
+
quote: str | None = None
|
|
20
|
+
i = 0
|
|
21
|
+
n = len(text)
|
|
22
|
+
seg_start = 0
|
|
23
|
+
|
|
24
|
+
def flush(end: int) -> None:
|
|
25
|
+
nonlocal seg_start
|
|
26
|
+
if end > seg_start:
|
|
27
|
+
parts.append(text[seg_start:end])
|
|
28
|
+
seg_start = end
|
|
29
|
+
|
|
30
|
+
while i < n:
|
|
31
|
+
ch = text[i]
|
|
32
|
+
if quote is None:
|
|
33
|
+
if ch in " \t":
|
|
34
|
+
flush(i)
|
|
35
|
+
j = i
|
|
36
|
+
while j < n and text[j] in " \t":
|
|
37
|
+
j += 1
|
|
38
|
+
parts.append(text[i:j])
|
|
39
|
+
seg_start = j
|
|
40
|
+
i = j
|
|
41
|
+
continue
|
|
42
|
+
elif ch == "\\" and i + 1 < n:
|
|
43
|
+
i += 2
|
|
44
|
+
continue
|
|
45
|
+
elif ch in ("'", '"'):
|
|
46
|
+
quote = ch
|
|
47
|
+
else:
|
|
48
|
+
if ch == quote:
|
|
49
|
+
quote = None
|
|
50
|
+
elif ch == "\\" and quote == '"' and i + 1 < n:
|
|
51
|
+
i += 2
|
|
52
|
+
continue
|
|
53
|
+
i += 1
|
|
54
|
+
flush(n)
|
|
55
|
+
return parts
|
|
56
|
+
|
|
17
57
|
def _split_with_braces_and_pipes(line: str) -> list[str]:
|
|
18
58
|
parts = []
|
|
19
59
|
outer_buf = []
|
|
@@ -86,7 +126,7 @@ def _split_with_braces_and_pipes(line: str) -> list[str]:
|
|
|
86
126
|
return parts
|
|
87
127
|
|
|
88
128
|
def _lex_cmd(cmd: str, first_word: bool = True) -> list[tuple[str, str]]:
|
|
89
|
-
parts: list[str] =
|
|
129
|
+
parts: list[str] = _shell_split_keep_sep(cmd)
|
|
90
130
|
result: list[tuple[str, str]] = []
|
|
91
131
|
for part in parts:
|
|
92
132
|
if part == "":
|
|
@@ -17,8 +17,9 @@ from .lexer import XmyShellLexer
|
|
|
17
17
|
def xmyshell_main() -> None:
|
|
18
18
|
xmyshell_init()
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
if namespace.get("SHOW_WELCOME"):
|
|
21
|
+
print(WELCOME_MESSAGE)
|
|
22
|
+
print(f"Initialized with {(time_ns() - _start_time) // 1_000_000}ms.")
|
|
22
23
|
|
|
23
24
|
session = PromptSession(
|
|
24
25
|
history=FileHistory(os.path.expanduser("~/.xmyshell/history.txt")),
|
|
@@ -59,6 +59,8 @@ Built-in variables:
|
|
|
59
59
|
exec_duration - The duration which last command took.
|
|
60
60
|
HOME_DIR - The home directory, similar to "~".
|
|
61
61
|
PROFILE - The path of config file.
|
|
62
|
+
SHOW_WELCOME - Toggle whether to show welcome message.
|
|
63
|
+
SHELL_CMD - Shell command line prefix, e.g. ["cmd", "/c"]
|
|
62
64
|
|
|
63
65
|
Keyboard Shortcuts:
|
|
64
66
|
↑↓ - Navigate through command history.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Nothing here
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# XmyShell built-in names
|
|
2
|
+
|
|
3
|
+
def getlogin() -> str: ...
|
|
4
|
+
def getcwd() -> str: ...
|
|
5
|
+
def dirname(p: str) -> str: ...
|
|
6
|
+
def basename(p: str) -> str: ...
|
|
7
|
+
def cat(file_path: str) -> str: ...
|
|
8
|
+
def alias(altname: str, target: str) -> None: ...
|
|
9
|
+
def unalias(altname: str) -> None: ...
|
|
10
|
+
SHOW_WELCOME: bool
|
|
11
|
+
SHELL_CMD: list[str]
|
|
12
|
+
HOME_DIR: str
|
|
13
|
+
PROFILE: str
|
|
14
|
+
exit_code: int
|
|
15
|
+
exec_duration: int
|
|
16
|
+
last_error: Exception | None
|
|
17
|
+
last_traceback: str | None
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xmyshell
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.12
|
|
4
4
|
Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -118,6 +118,15 @@ or
|
|
|
118
118
|
❯ unalias ll
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
+
- Lists, tuples, generators, etc. will be expanded into shell arguments.
|
|
122
|
+
E.g. `{["arg1", "arg2 xxx"]}` will be expanded into `arg1 'arg2 xxx'`
|
|
123
|
+
|
|
124
|
+
Here is an example (though there are other ways to do this, it is just an example )
|
|
125
|
+
```
|
|
126
|
+
❯ pip install {cat("requirements.txt").splitlines()}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
|
|
121
130
|
- Syntax highlighting via `prompt_toolkit`
|
|
122
131
|
- Powerful auto-completion, history search, and automatic bracket/quote pairing
|
|
123
132
|
- Startup configuration: `~/.xmyshell/config.py` loaded at launch
|
|
@@ -154,6 +163,12 @@ The exit code of the last command.
|
|
|
154
163
|
- `exec_duration`
|
|
155
164
|
The duration which last command took.
|
|
156
165
|
|
|
166
|
+
- `SHOW_WELCOME`
|
|
167
|
+
Toggle whether to show welcome message when staring.
|
|
168
|
+
|
|
169
|
+
- `SHELL_CMD`
|
|
170
|
+
Shell command line prefix, defaults to `["cmd", "/c"]` on Windows and `["bin/sh", "-c"]` on other OS.
|
|
171
|
+
|
|
157
172
|
- `HOME_DIR`
|
|
158
173
|
The home directory, similar to `~`.
|
|
159
174
|
|
|
@@ -170,6 +185,15 @@ The path of config file.
|
|
|
170
185
|
- Auto bracket/parenthesis/quote pairing
|
|
171
186
|
|
|
172
187
|
|
|
188
|
+
### Stubs
|
|
189
|
+
|
|
190
|
+
You can write your xmyshell script with these imports:
|
|
191
|
+
```python
|
|
192
|
+
from xmyshell.themes import ... # themes
|
|
193
|
+
from xmyshell.stubs import ... # stubs for built-in names
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
|
|
173
197
|
## Limitations
|
|
174
198
|
|
|
175
199
|
**XmyShell** is Python‑first. This brings power, but also intentional trade‑offs.
|
|
@@ -20,4 +20,7 @@ xmyshell.egg-info/dependency_links.txt
|
|
|
20
20
|
xmyshell.egg-info/entry_points.txt
|
|
21
21
|
xmyshell.egg-info/requires.txt
|
|
22
22
|
xmyshell.egg-info/top_level.txt
|
|
23
|
-
xmyshell/data/commands.json
|
|
23
|
+
xmyshell/data/commands.json
|
|
24
|
+
xmyshell/stubs/__init__.py
|
|
25
|
+
xmyshell/stubs/__init__.pyi
|
|
26
|
+
xmyshell/stubs/py.typed
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|