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/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
- print(f" {lines[e.line-1].strip()}")
27
- print(f"Exception: {e}")
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.03.3 | Made by Shrey Naithani")
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
- indent_level = 0
89
+
46
90
  while True:
47
91
  try:
48
- prompt = "... " if indent_level > 0 else ">>> "
49
- line = input(prompt)
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
- if line.endswith("\\"):
53
- buffer.append(line[:-1])
54
- indent_level = 1
55
- continue
103
+
56
104
  if line.strip() == "help":
57
- print("\nShellLite Examples:")
58
- print(' say "Hello World"')
59
- print(' tasks is a list # Initialize an empty list')
60
- print(' add "Buy Milk" to tasks # Add items to the list')
61
- print(' display(tasks) # View the list')
62
- print(' \\ # Tip: Use \\ at the end of a line for multi-line typing')
63
- continue
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
- print("! Hint: You are already INSIDE ShellLite. You don't need to type 'shl' here.")
66
- print("! To run terminal commands, exit this REPL first.")
67
- continue
68
- if not line:
69
- if indent_level > 0:
70
- source = "\n".join(buffer)
71
- execute_source(source, interpreter)
72
- buffer = []
73
- indent_level = 0
74
- continue
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
- elif indent_level > 0 and (line.startswith(" ") or line.startswith("\t")):
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
- else:
81
- if indent_level > 0:
82
- source = "\n".join(buffer)
83
- execute_source(source, interpreter)
84
- buffer = []
85
- indent_level = 0
86
- if line.strip():
87
- execute_source(line, interpreter)
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
- shutil.copy2(current_path, target_exe)
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
- print(f"\n[SUCCESS] ShellLite is now installed!")
194
+
195
+ print(f"\n[SUCCESS] ShellLite (v0.04.1) is installed!")
114
196
  print(f"Location: {install_dir}")
115
- print("\nACTION REQUIRED:")
116
- print("1. Close this terminal window.")
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
- input("Press Enter to continue...")
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
- def install_package(package_name: str):
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
- print(f"Removing existing '{repo}'...")
136
- shutil.rmtree(target_dir)
137
- zip_url = f"https://github.com/{user}/{repo}/archive/refs/heads/main.zip"
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
- with urllib.request.urlopen(zip_url) as response:
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
- with zipfile.ZipFile(io.BytesIO(zip_data)) as z:
159
- z.extractall(modules_dir)
160
- extracted_name = f"{repo}-master"
161
- extracted_path = os.path.join(modules_dir, extracted_name)
162
- if os.path.exists(extracted_path):
163
- os.rename(extracted_path, target_dir)
164
- print(f"[SUCCESS] Installed '{repo}' to {target_dir}")
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
- print(f"Error: Could not find extracted folder '{extracted_name}'.")
167
- except Exception as e:
168
- print(f"Installation failed: {e}")
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
- print("Usage: shl get <user/repo>")
524
+ print("Usage: shl get <user/repo>")
525
+ elif cmd == "init":
526
+ init_project()
359
527
  elif cmd == "install":
360
- install_globally()
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: