shell-lite 0.3.5__py3-none-any.whl → 0.4.1__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.
- shell_lite/ast_nodes.py +24 -1
- shell_lite/compiler.py +14 -2
- shell_lite/interpreter.py +212 -17
- shell_lite/js_compiler.py +17 -3
- shell_lite/lexer.py +4 -1
- shell_lite/main.py +263 -81
- shell_lite/parser.py +227 -23
- shell_lite-0.4.1.dist-info/METADATA +77 -0
- shell_lite-0.4.1.dist-info/RECORD +17 -0
- shell_lite-0.3.5.dist-info/METADATA +0 -40
- shell_lite-0.3.5.dist-info/RECORD +0 -17
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/LICENSE +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/WHEEL +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/entry_points.txt +0 -0
- {shell_lite-0.3.5.dist-info → shell_lite-0.4.1.dist-info}/top_level.txt +0 -0
shell_lite/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from .ast_nodes import *
|
|
|
12
12
|
import json
|
|
13
13
|
def execute_source(source: str, interpreter: Interpreter):
|
|
14
14
|
lines = source.split('\n')
|
|
15
|
+
import difflib
|
|
15
16
|
try:
|
|
16
17
|
lexer = Lexer(source)
|
|
17
18
|
tokens = lexer.tokenize()
|
|
@@ -20,11 +21,33 @@ def execute_source(source: str, interpreter: Interpreter):
|
|
|
20
21
|
for stmt in statements:
|
|
21
22
|
interpreter.visit(stmt)
|
|
22
23
|
except Exception as e:
|
|
24
|
+
# Check if it has a line number
|
|
23
25
|
if hasattr(e, 'line') and e.line > 0:
|
|
24
|
-
print(f"Error on line {e.line}:")
|
|
26
|
+
print(f"\n[ShellLite Error] on line {e.line}:")
|
|
25
27
|
if 0 <= e.line-1 < len(lines):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
print(f" > {lines[e.line-1].strip()}")
|
|
29
|
+
print(f" {'^' * len(lines[e.line-1].strip())}")
|
|
30
|
+
print(f"Message: {e}")
|
|
31
|
+
|
|
32
|
+
# "Did you mean?" for NameErrors
|
|
33
|
+
if "not defined" in str(e):
|
|
34
|
+
# Extract the variable name
|
|
35
|
+
import re
|
|
36
|
+
match = re.search(r"'(.*?)'", str(e))
|
|
37
|
+
if match:
|
|
38
|
+
missing_var = match.group(1)
|
|
39
|
+
# Gather candidates (variables and functions)
|
|
40
|
+
candidates = list(interpreter.global_env.variables.keys()) + list(interpreter.functions.keys())
|
|
41
|
+
suggestions = difflib.get_close_matches(missing_var, candidates, n=1, cutoff=0.6)
|
|
42
|
+
if suggestions:
|
|
43
|
+
print(f"Did you mean: '{suggestions[0]}'?")
|
|
44
|
+
else:
|
|
45
|
+
print(f"\n[ShellLite Error]: {e}")
|
|
46
|
+
|
|
47
|
+
# Optional: Print stack trace only if debug mode
|
|
48
|
+
if os.environ.get("SHL_DEBUG"):
|
|
49
|
+
import traceback
|
|
50
|
+
traceback.print_exc()
|
|
28
51
|
def run_file(filename: str):
|
|
29
52
|
if not os.path.exists(filename):
|
|
30
53
|
print(f"Error: File '{filename}' not found.")
|
|
@@ -38,60 +61,109 @@ def run_repl():
|
|
|
38
61
|
print("\n" + "="*40)
|
|
39
62
|
print(" ShellLite REPL - English Syntax")
|
|
40
63
|
print("="*40)
|
|
41
|
-
print("Version: v0.
|
|
64
|
+
print("Version: v0.04.1 | Made by Shrey Naithani")
|
|
42
65
|
print("Commands: Type 'exit' to quit, 'help' for examples.")
|
|
43
66
|
print("Note: Terminal commands (like 'shl install') must be run in CMD/PowerShell, not here.")
|
|
67
|
+
|
|
68
|
+
# Try importing prompt_toolkit for a better experience
|
|
69
|
+
try:
|
|
70
|
+
from prompt_toolkit import PromptSession
|
|
71
|
+
from prompt_toolkit.lexers import PygmentsLexer
|
|
72
|
+
from pygments.lexers.shell import BashLexer
|
|
73
|
+
from prompt_toolkit.styles import Style
|
|
74
|
+
# Ideally we'd have a ShellLite lexer, but Bash or Python is decent for now.
|
|
75
|
+
|
|
76
|
+
style = Style.from_dict({
|
|
77
|
+
'prompt': '#ansigreen bold',
|
|
78
|
+
})
|
|
79
|
+
session = PromptSession(lexer=PygmentsLexer(BashLexer), style=style)
|
|
80
|
+
has_pt = True
|
|
81
|
+
except ImportError:
|
|
82
|
+
print("[Notice] Install 'prompt_toolkit' for syntax highlighting and history.")
|
|
83
|
+
print(" Run: pip install prompt_toolkit")
|
|
84
|
+
has_pt = False
|
|
85
|
+
buffer = []
|
|
86
|
+
indent_level = 0
|
|
87
|
+
|
|
44
88
|
buffer = []
|
|
45
|
-
|
|
89
|
+
|
|
46
90
|
while True:
|
|
47
91
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
92
|
+
prompt_str = "... " if (buffer and len(buffer) > 0) else ">>> "
|
|
93
|
+
|
|
94
|
+
if has_pt:
|
|
95
|
+
# Use prompt_toolkit
|
|
96
|
+
line = session.prompt(prompt_str)
|
|
97
|
+
else:
|
|
98
|
+
# Fallback
|
|
99
|
+
line = input(prompt_str)
|
|
100
|
+
|
|
50
101
|
if line.strip() == "exit":
|
|
51
102
|
break
|
|
52
|
-
|
|
53
|
-
buffer.append(line[:-1])
|
|
54
|
-
indent_level = 1
|
|
55
|
-
continue
|
|
103
|
+
|
|
56
104
|
if line.strip() == "help":
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
print("\nShellLite Examples:")
|
|
106
|
+
print(' say "Hello World"')
|
|
107
|
+
print(' tasks is a list # Initialize an empty list')
|
|
108
|
+
print(' add "Buy Milk" to tasks # Add items to the list')
|
|
109
|
+
print(' display(tasks) # View the list')
|
|
110
|
+
continue
|
|
111
|
+
|
|
64
112
|
if line.strip().startswith("shl"):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if line.strip().endswith(":"):
|
|
76
|
-
indent_level = 1
|
|
113
|
+
print("! Hint: You are already INSIDE ShellLite.")
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# Multi-line handling logic
|
|
117
|
+
# If line ends with ':' or '\', or we are inside a block (indent > 0)
|
|
118
|
+
# ShellLite uses indentation.
|
|
119
|
+
|
|
120
|
+
# Heuristic: if line ends with ':', expect more.
|
|
121
|
+
# If line is empty and we have buffer, execute.
|
|
122
|
+
|
|
123
|
+
if line.strip().endswith(":") or line.strip().endswith("\\"):
|
|
77
124
|
buffer.append(line)
|
|
78
|
-
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
# If line starts with indent (standard 4 spaces or tab), keep adding to buffer
|
|
128
|
+
if buffer and (line.startswith(" ") or line.startswith("\t")):
|
|
79
129
|
buffer.append(line)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
continue
|
|
131
|
+
elif buffer and not line.strip():
|
|
132
|
+
# Empty line triggers execution of buffer
|
|
133
|
+
source = "\n".join(buffer)
|
|
134
|
+
execute_source(source, interpreter)
|
|
135
|
+
buffer = []
|
|
136
|
+
continue
|
|
137
|
+
elif buffer:
|
|
138
|
+
# Non-empty, non-indented line.
|
|
139
|
+
# This means we left the block? Or it's a new separate command?
|
|
140
|
+
# If we were in a block, usually we need an empty line to signal end in a REPL.
|
|
141
|
+
# But let's assume this ends the block and starts new.
|
|
142
|
+
buffer.append(line)
|
|
143
|
+
# Actually, if it's not indented, it might be the end of the block.
|
|
144
|
+
# Let's try to execute the buffer, then execute this line.
|
|
145
|
+
# But wait, what if it's 'else:'? That is not indented but part of block.
|
|
146
|
+
if line.strip().startswith("else") or line.strip().startswith("elif"):
|
|
147
|
+
buffer.append(line)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Execute accumulated buffer first
|
|
151
|
+
# This mimics Python REPL slightly but Python waits for empty line.
|
|
152
|
+
# Let's force empty line for execution to be safe for now.
|
|
153
|
+
buffer.append(line)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Single line execution if no buffer
|
|
157
|
+
if not buffer:
|
|
158
|
+
if not line.strip(): continue
|
|
159
|
+
execute_source(line, interpreter)
|
|
160
|
+
|
|
88
161
|
except KeyboardInterrupt:
|
|
89
162
|
print("\nExiting...")
|
|
90
163
|
break
|
|
91
164
|
except Exception as e:
|
|
92
165
|
print(f"Error: {e}")
|
|
93
166
|
buffer = []
|
|
94
|
-
indent_level = 0
|
|
95
167
|
def install_globally():
|
|
96
168
|
print("\n" + "="*50)
|
|
97
169
|
print(" ShellLite Global Installer")
|
|
@@ -104,70 +176,164 @@ def install_globally():
|
|
|
104
176
|
is_frozen = getattr(sys, 'frozen', False)
|
|
105
177
|
try:
|
|
106
178
|
if is_frozen:
|
|
107
|
-
|
|
179
|
+
if os.path.abspath(current_path).lower() != os.path.abspath(target_exe).lower():
|
|
180
|
+
try:
|
|
181
|
+
shutil.copy2(current_path, target_exe)
|
|
182
|
+
except Exception as copy_err:
|
|
183
|
+
print(f"Warning: Could not copy executable: {copy_err}")
|
|
184
|
+
print("This is fine if you are running the installed version.")
|
|
185
|
+
else:
|
|
186
|
+
print("Running from install directory, skipping copy.")
|
|
108
187
|
else:
|
|
109
188
|
print("Error: Installation requires the shl.exe file.")
|
|
110
189
|
return
|
|
190
|
+
|
|
191
|
+
# Update PATH user variable
|
|
111
192
|
ps_cmd = f'$oldPath = [Environment]::GetEnvironmentVariable("Path", "User"); if ($oldPath -notlike "*ShellLite*") {{ [Environment]::SetEnvironmentVariable("Path", "$oldPath;{install_dir}", "User") }}'
|
|
112
193
|
subprocess.run(["powershell", "-Command", ps_cmd], capture_output=True)
|
|
113
|
-
|
|
194
|
+
|
|
195
|
+
print(f"\n[SUCCESS] ShellLite (v0.04.1) is installed!")
|
|
114
196
|
print(f"Location: {install_dir}")
|
|
115
|
-
print("\
|
|
116
|
-
print("1. Close
|
|
197
|
+
print("\nIMPORTANT STEP REQUIRED:")
|
|
198
|
+
print("1. Close ALL open terminal windows (CMD, PowerShell, VS Code).")
|
|
117
199
|
print("2. Open a NEW terminal.")
|
|
118
|
-
print("3. Type 'shl' to verify.")
|
|
200
|
+
print("3. Type 'shl' to verify installation.")
|
|
119
201
|
print("="*50 + "\n")
|
|
120
|
-
|
|
202
|
+
|
|
203
|
+
# Optional: Try using setx for immediate effect in future sessions (though usually requires restart of shell)
|
|
204
|
+
# subprocess.run(f'setx PATH "%PATH%;{install_dir}"', shell=True, capture_output=True)
|
|
205
|
+
|
|
206
|
+
input("Press Enter to finish...")
|
|
207
|
+
except Exception as e:
|
|
208
|
+
print(f"Installation failed: {e}")
|
|
121
209
|
except Exception as e:
|
|
122
210
|
print(f"Installation failed: {e}")
|
|
123
|
-
|
|
211
|
+
|
|
212
|
+
def init_project():
|
|
213
|
+
if os.path.exists("shell-lite.toml"):
|
|
214
|
+
print("Error: shell-lite.toml already exists.")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
content = """[project]
|
|
218
|
+
name = "my-shell-lite-app"
|
|
219
|
+
version = "0.1.0"
|
|
220
|
+
description = "A new ShellLite project"
|
|
221
|
+
|
|
222
|
+
[dependencies]
|
|
223
|
+
# Format: "user/repo" = "branch" (default: main)
|
|
224
|
+
# Example: "shrey-n/stdlib" = "main"
|
|
225
|
+
"""
|
|
226
|
+
with open("shell-lite.toml", "w") as f:
|
|
227
|
+
f.write(content)
|
|
228
|
+
print("[SUCCESS] Created shell-lite.toml")
|
|
229
|
+
print("Run 'shl install' to install dependencies listed in it.")
|
|
230
|
+
|
|
231
|
+
def install_all_dependencies():
|
|
232
|
+
if not os.path.exists("shell-lite.toml"):
|
|
233
|
+
print("Error: No shell-lite.toml found. Run 'shl init' first.")
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
print("Reading shell-lite.toml...")
|
|
237
|
+
deps = {}
|
|
238
|
+
in_deps = False
|
|
239
|
+
with open("shell-lite.toml", 'r') as f:
|
|
240
|
+
for line in f:
|
|
241
|
+
line = line.strip()
|
|
242
|
+
if not line or line.startswith('#'): continue
|
|
243
|
+
|
|
244
|
+
if line == "[dependencies]":
|
|
245
|
+
in_deps = True
|
|
246
|
+
continue
|
|
247
|
+
elif line.startswith("["):
|
|
248
|
+
in_deps = False
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
if in_deps and '=' in line:
|
|
252
|
+
# Parse "key" = "value"
|
|
253
|
+
parts = line.split('=', 1)
|
|
254
|
+
key = parts[0].strip().strip('"').strip("'")
|
|
255
|
+
val = parts[1].strip().strip('"').strip("'")
|
|
256
|
+
deps[key] = val
|
|
257
|
+
|
|
258
|
+
if not deps:
|
|
259
|
+
print("No dependencies found.")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
print(f"Found {len(deps)} dependencies.")
|
|
263
|
+
for repo, branch in deps.items():
|
|
264
|
+
install_package(repo, branch=branch)
|
|
265
|
+
|
|
266
|
+
def install_package(package_name: str, branch: str = "main"):
|
|
124
267
|
if '/' not in package_name:
|
|
125
|
-
print("Error: Package must be in format 'user/repo'")
|
|
268
|
+
print(f"Error: Package '{package_name}' must be in format 'user/repo'")
|
|
126
269
|
return
|
|
270
|
+
|
|
127
271
|
user, repo = package_name.split('/')
|
|
128
|
-
print(f"Fetching '{package_name}' from GitHub...")
|
|
272
|
+
print(f"Fetching '{package_name}' ({branch}) from GitHub...")
|
|
273
|
+
|
|
129
274
|
home = os.path.expanduser("~")
|
|
130
275
|
modules_dir = os.path.join(home, ".shell_lite", "modules")
|
|
131
276
|
if not os.path.exists(modules_dir):
|
|
132
277
|
os.makedirs(modules_dir)
|
|
278
|
+
|
|
133
279
|
target_dir = os.path.join(modules_dir, repo)
|
|
280
|
+
|
|
281
|
+
# We always overwrite for now, or maybe warn?
|
|
282
|
+
# Let's remove if exists to ensure fresh install
|
|
134
283
|
if os.path.exists(target_dir):
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
284
|
+
# check if it's the same? simple way: remove and reinstall
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
zip_url = f"https://github.com/{user}/{repo}/archive/refs/heads/{branch}.zip"
|
|
288
|
+
|
|
138
289
|
try:
|
|
290
|
+
# Check if cache/temp dir exists
|
|
291
|
+
import tempfile
|
|
292
|
+
|
|
139
293
|
print(f"Downloading {zip_url}...")
|
|
140
|
-
|
|
141
|
-
zip_data = response.read()
|
|
142
|
-
with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
|
|
143
|
-
z.extractall(modules_dir)
|
|
144
|
-
extracted_name = f"{repo}-main"
|
|
145
|
-
extracted_path = os.path.join(modules_dir, extracted_name)
|
|
146
|
-
if os.path.exists(extracted_path):
|
|
147
|
-
os.rename(extracted_path, target_dir)
|
|
148
|
-
print(f"[SUCCESS] Installed '{repo}' to {target_dir}")
|
|
149
|
-
else:
|
|
150
|
-
print(f"Error: Could not find extracted folder '{extracted_name}'.")
|
|
151
|
-
return
|
|
152
|
-
except urllib.error.HTTPError:
|
|
153
|
-
zip_url = f"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"
|
|
154
|
-
try:
|
|
155
|
-
print(f"Downloading {zip_url}...")
|
|
294
|
+
try:
|
|
156
295
|
with urllib.request.urlopen(zip_url) as response:
|
|
157
296
|
zip_data = response.read()
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
except urllib.error.HTTPError as e:
|
|
298
|
+
if branch == "main" and e.code == 404:
|
|
299
|
+
# Try master fallback if explicit branch wasn't really explicit (default)
|
|
300
|
+
# But here we passed 'main' as default.
|
|
301
|
+
print("Branch 'main' not found, trying 'master'...")
|
|
302
|
+
zip_url = f"https://github.com/{user}/{repo}/archive/refs/heads/master.zip"
|
|
303
|
+
with urllib.request.urlopen(zip_url) as response:
|
|
304
|
+
zip_data = response.read()
|
|
165
305
|
else:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
306
|
+
raise e
|
|
307
|
+
|
|
308
|
+
# Extract to temp first? No, extract to modules_dir then rename
|
|
309
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
|
|
310
|
+
z.extractall(modules_dir)
|
|
311
|
+
|
|
312
|
+
# GitHub zips extract to "repo-branch"
|
|
313
|
+
# We need to find what it extracted to.
|
|
314
|
+
# It usually is repo-branch.
|
|
315
|
+
# Let's handle the rename safely
|
|
316
|
+
|
|
317
|
+
# We don't know exact folder name if branch has slashes or generic.
|
|
318
|
+
# But usually 'repo-branch'.
|
|
319
|
+
|
|
320
|
+
# Heuristic: List dirs in modules_dir, find the new one?
|
|
321
|
+
# A bit racy if running parallel, but we aren't.
|
|
322
|
+
# Better: get the first member of zip
|
|
323
|
+
with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
|
|
324
|
+
root_name = z.namelist()[0].split('/')[0]
|
|
325
|
+
|
|
326
|
+
extracted_path = os.path.join(modules_dir, root_name)
|
|
327
|
+
|
|
328
|
+
if os.path.exists(target_dir):
|
|
329
|
+
shutil.rmtree(target_dir) # Remove old version
|
|
330
|
+
|
|
331
|
+
os.rename(extracted_path, target_dir)
|
|
332
|
+
print(f"[SUCCESS] Installed '{package_name}' to {target_dir}")
|
|
333
|
+
|
|
169
334
|
except Exception as e:
|
|
170
|
-
print(f"Installation failed: {e}")
|
|
335
|
+
print(f"Installation failed for {package_name}: {e}")
|
|
336
|
+
|
|
171
337
|
def compile_file(filename: str, target: str = 'python'):
|
|
172
338
|
if not os.path.exists(filename):
|
|
173
339
|
print(f"Error: File '{filename}' not found.")
|
|
@@ -355,9 +521,20 @@ def main():
|
|
|
355
521
|
package_name = sys.argv[2]
|
|
356
522
|
install_package(package_name)
|
|
357
523
|
else:
|
|
358
|
-
|
|
524
|
+
print("Usage: shl get <user/repo>")
|
|
525
|
+
elif cmd == "init":
|
|
526
|
+
init_project()
|
|
359
527
|
elif cmd == "install":
|
|
360
|
-
|
|
528
|
+
if len(sys.argv) > 2:
|
|
529
|
+
# Install specific package
|
|
530
|
+
package_name = sys.argv[2]
|
|
531
|
+
install_package(package_name)
|
|
532
|
+
# TODO: Add to shell-lite.toml if it exists
|
|
533
|
+
else:
|
|
534
|
+
# Install dependencies from shell-lite.toml
|
|
535
|
+
install_all_dependencies()
|
|
536
|
+
elif cmd == "setup-path": # Renamed from 'install' to avoid confusion, but kept 'install' as verify
|
|
537
|
+
install_globally()
|
|
361
538
|
elif cmd == "fmt" or cmd == "format":
|
|
362
539
|
if len(sys.argv) > 2:
|
|
363
540
|
filename = sys.argv[2]
|
|
@@ -374,6 +551,11 @@ def main():
|
|
|
374
551
|
line = int(sys.argv[3])
|
|
375
552
|
col = int(sys.argv[4])
|
|
376
553
|
resolve_cursor(filename, line, col)
|
|
554
|
+
elif cmd == "run":
|
|
555
|
+
if len(sys.argv) > 2:
|
|
556
|
+
run_file(sys.argv[2])
|
|
557
|
+
else:
|
|
558
|
+
print("Usage: shl run <filename>")
|
|
377
559
|
else:
|
|
378
560
|
run_file(sys.argv[1])
|
|
379
561
|
else:
|