flyshell 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flyshell/core/auth.py ADDED
@@ -0,0 +1,122 @@
1
+ # src\flyshell\core\auth.py
2
+
3
+ from flyshell.abstract.base_cmd import BaseCmd
4
+ from flyshell.core import data
5
+
6
+ import getpass
7
+ import hashlib
8
+ import re
9
+ import secrets
10
+ import sys
11
+
12
+ def hash_password(password, salt=None):
13
+ if salt is None:
14
+ salt = secrets.token_bytes(16)
15
+ elif isinstance(salt, str):
16
+ salt = bytes.fromhex(salt)
17
+ pwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 600000)
18
+ return pwd_hash.hex(), salt.hex()
19
+
20
+ def verify_password(stored_hash: str, stored_salt: str, password: str) -> bool:
21
+ if not stored_hash or not stored_salt or not password:
22
+ return False
23
+ try:
24
+ salt_bytes = bytes.fromhex(stored_salt)
25
+ attempt_hash, _ = hash_password(password, salt_bytes)
26
+ return secrets.compare_digest(attempt_hash, stored_hash)
27
+ except (ValueError, TypeError):
28
+ return False
29
+
30
+ def setup_acc() -> bool:
31
+ print("\nWelcome to Flyshell, New User!")
32
+ print("Please follow the prompts to create your Flyshell profile.")
33
+ while True:
34
+ username = input("Create Username: ").strip()
35
+ if not username:
36
+ print("\nAccount Error: Username cannot be empty.\n")
37
+ else:
38
+ print(f"\nUsername set as '{username}'\n")
39
+ break
40
+ i = 5
41
+ print("Your password MUST:")
42
+ print("- Contain at least 10 total characters")
43
+ print("- Contain 1 or more upper case letters")
44
+ print("- Contain 1 or more lower case letters")
45
+ print("- Contain 1 or more numbers")
46
+ print("- Contain 1 or more special characters\n")
47
+ while i > 0:
48
+ password = getpass.getpass("Create Password: ")
49
+ confirm = getpass.getpass("Confirm Password: ")
50
+ if password != confirm:
51
+ print("\nAccount Error: Passwords do not match.")
52
+ i -= 1
53
+ if i == 0:
54
+ print("Too many attempts.\n")
55
+ return False
56
+ print(f"You have {i} attempts remaining.\n")
57
+ continue
58
+ has_upper = any(char.isupper() for char in password)
59
+ has_lower = any(char.islower() for char in password)
60
+ has_digit = any(char.isdigit() for char in password)
61
+ has_speci = bool(re.search(r'[^a-zA-Z0-9]', password))
62
+ if len(password) >= 10 and has_upper and has_lower and has_digit and has_speci:
63
+ break
64
+ else:
65
+ print("\nAccount Error: Password does not meet required conditions.\n")
66
+ i -= 1
67
+ if i == 0:
68
+ print("Too many attempts.\n")
69
+ return False
70
+ print(f"You have {i} attempts remaining.\n")
71
+ continue
72
+ pwd_hash, salt = hash_password(password)
73
+ auth_data = {
74
+ "username": username,
75
+ "hash": pwd_hash,
76
+ "salt": salt
77
+ }
78
+ data.set_auth(username, pwd_hash, salt)
79
+ print("\nSUCCESS: Account created successfully!")
80
+ return True
81
+
82
+ def login_flow(is_lock=False) -> bool:
83
+ auth_info = data.get_auth()
84
+ if not auth_info or "hash" not in auth_info:
85
+ if not setup_acc():
86
+ return False
87
+ auth_info = data.get_auth()
88
+ stored_user = auth_info.get("username", "User")
89
+ stored_hash = auth_info.get("hash")
90
+ stored_salt = auth_info.get("salt")
91
+ if is_lock:
92
+ input(f"\nFlyshell is locked. To unlock, press ENTER.")
93
+ print(f"\nWelcome back, {stored_user}!")
94
+ else:
95
+ print(f"\nWelcome, {stored_user}!")
96
+ i = 5
97
+ while i > 0:
98
+ entered_pwd = getpass.getpass("Enter Password: ")
99
+ if verify_password(stored_hash, stored_salt, entered_pwd):
100
+ if is_lock:
101
+ print()
102
+ return True
103
+ i -= 1
104
+ print(f"\nAccount Error: Password does not match.")
105
+ if i == 0:
106
+ print("Too many attempts.\n")
107
+ return False
108
+ print(f"You have {i} attempts remaining.\n")
109
+
110
+ class Lock(BaseCmd):
111
+ name = "lock"
112
+ description = "Lock the Flyshell system"
113
+ min_args = 0
114
+ options = None
115
+ aliases = []
116
+
117
+ def execute(self, args):
118
+ if login_flow(is_lock=True):
119
+ return (0, "")
120
+ else:
121
+ print("\nShutting down...\n")
122
+ sys.exit(0)
@@ -0,0 +1,64 @@
1
+ # src\flyshell\core\console.py
2
+
3
+ from flyshell.core import data
4
+ from flyshell.core.directory import execute_line
5
+
6
+ import os
7
+ import random
8
+ import sys
9
+
10
+ readline = None
11
+
12
+ def check_readline() -> bool:
13
+ global readline
14
+ try:
15
+ import readline
16
+ return True
17
+ except ImportError:
18
+ try:
19
+ import pyreadline3 as readline
20
+ return True
21
+ except ImportError:
22
+ readline = None
23
+ return False
24
+
25
+ def load_history():
26
+ if not readline:
27
+ return
28
+ history_entries = data.get_history(limit=100)
29
+ for entry in history_entries:
30
+ if isinstance(entry, dict) and "command" in entry:
31
+ readline.add_history(entry["command"])
32
+
33
+ def boot():
34
+ load_history()
35
+ print("\033[H\033[2J", end="")
36
+ print(f"\nBoot successful, welcome to Flyshell! (Version {data.VERSION})")
37
+ user_info = data.get_auth() or {}
38
+ username = user_info.get("username")
39
+ GREETINGS = [
40
+ f"Hi {username}, what's it going to be today?",
41
+ f"The mic is yours, {username}, go and blow me away!",
42
+ f"Hey {username}, let's get this party started!",
43
+ f"What's up, {username}? Let's go!",
44
+ f"I'm awaiting your first command, {username}!",
45
+ f"How's it going {username}? I'm here to help.",
46
+ f"{username}, it's your call! Show me what you've got, what are we doing today?",
47
+ f"My wish is your command, {username}! (unless it's not on my command list)",
48
+ f"I have goods for you {username}, it's called a Command Interface.",
49
+ f"I'm your boss! Wait, no... sorry... you, {username}, are my boss!",
50
+ ]
51
+ greeting = random.choice(GREETINGS)
52
+ print(f"\n{greeting}\n")
53
+ while True:
54
+ try:
55
+ folder = os.path.basename(os.getcwd())
56
+ raw_cmd = input(f"Flyshell [v{data.VERSION}] ({folder})>>")
57
+ if not raw_cmd.strip():
58
+ continue
59
+ execute_line(raw_cmd)
60
+ except KeyboardInterrupt:
61
+ print()
62
+ except EOFError:
63
+ print("\n\nFlyshell session closed.\n")
64
+ sys.exit(0)
flyshell/core/data.py ADDED
@@ -0,0 +1,234 @@
1
+ # src\flyshell\core\data.py
2
+
3
+ BUILD = 99
4
+ VERSION = "1.0.0"
5
+
6
+ from pathlib import Path
7
+ import json
8
+ import os
9
+ import platform
10
+ import sqlite3
11
+ import time
12
+
13
+ HOST_OS = platform.system()
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
16
+
17
+ def get_app_dir() -> Path:
18
+ home = Path.home()
19
+ if HOST_OS == "Windows":
20
+ base = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
21
+ elif HOST_OS == "Darwin":
22
+ base = home / "Library" / "Application Support"
23
+ else:
24
+ base = Path(os.environ.get("XDG_DATA_HOME", home / ".local" / "share"))
25
+ app_dir = base / "Flyshell"
26
+ app_dir.mkdir(parents=True, exist_ok=True)
27
+ return app_dir
28
+
29
+ APP_DIR = get_app_dir()
30
+ FILE_PATH = APP_DIR / "flyshell.db"
31
+ USER_PLUGIN_DIR = APP_DIR / "plugins"
32
+
33
+ SESSION_START_TIME = time.time()
34
+ SESSION_CMD_COUNT = 0
35
+ PLUGINS = {}
36
+
37
+ def get_storage_size() -> str:
38
+ if not FILE_PATH.exists():
39
+ return "File not found"
40
+ num_bytes = float(FILE_PATH.stat().st_size)
41
+ for unit in ["B", "KB", "MB", "GB"]:
42
+ if num_bytes < 1024.0:
43
+ return f"{int(num_bytes)} {unit}" if unit == "B" else f"{num_bytes:.2f} {unit}"
44
+ num_bytes /= 1024.0
45
+ return f"{num_bytes:.2f} TB"
46
+
47
+ def _get_connection(filename=FILE_PATH):
48
+ conn = sqlite3.connect(filename)
49
+ conn.execute("PRAGMA foreign_keys = ON;")
50
+ return conn
51
+
52
+ INITIALISED = set()
53
+
54
+ def initialise(filename=FILE_PATH):
55
+ global INITIALISED
56
+ target = str(filename)
57
+ if target in INITIALISED:
58
+ return
59
+ with _get_connection(filename) as conn:
60
+ conn.execute("PRAGMA foreign_keys = ON;")
61
+ current_version = conn.execute("PRAGMA user_version;").fetchone()[0]
62
+ if current_version == 0:
63
+ with conn:
64
+ conn.execute("""
65
+ CREATE TABLE IF NOT EXISTS auth (
66
+ id INTEGER PRIMARY KEY CHECK (id = 1),
67
+ username TEXT NOT NULL,
68
+ hash TEXT NOT NULL,
69
+ salt TEXT NOT NULL,
70
+ created_at TEXT NOT NULL
71
+ );
72
+ """)
73
+ conn.execute("""
74
+ CREATE TABLE IF NOT EXISTS cmd_history (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ command TEXT NOT NULL,
77
+ timestamp TEXT NOT NULL
78
+ );
79
+ """)
80
+ conn.execute("""
81
+ CREATE TABLE IF NOT EXISTS core_config (
82
+ key TEXT PRIMARY KEY,
83
+ value TEXT NOT NULL
84
+ );
85
+ """)
86
+ conn.execute("""
87
+ CREATE TABLE IF NOT EXISTS plugins (
88
+ plugin_name TEXT PRIMARY KEY,
89
+ installed_at TEXT NOT NULL,
90
+ is_enabled INTEGER NOT NULL DEFAULT 1
91
+ );
92
+ """)
93
+ conn.execute("""
94
+ CREATE TABLE IF NOT EXISTS plugin_kv (
95
+ plugin_name TEXT NOT NULL,
96
+ key TEXT NOT NULL,
97
+ value TEXT NOT NULL,
98
+ PRIMARY KEY (plugin_name, key),
99
+ FOREIGN KEY (plugin_name) REFERENCES plugins(plugin_name) ON DELETE CASCADE
100
+ );
101
+ """)
102
+ conn.execute("PRAGMA user_version = 1;")
103
+ INITIALISED.add(target)
104
+
105
+ def get_auth(filename=FILE_PATH) -> dict | None:
106
+ initialise(filename)
107
+ with _get_connection(filename) as conn:
108
+ row = conn.execute("SELECT username, hash, salt, created_at FROM auth WHERE id = 1").fetchone()
109
+ if not row:
110
+ return None
111
+ return {"username": row[0], "hash": row[1], "salt": row[2], "created_at": row[3]}
112
+
113
+ def set_auth(username: str, pwd_hash: str, salt: str, filename=FILE_PATH) -> None:
114
+ initialise(filename)
115
+ now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
116
+ with _get_connection(filename) as conn:
117
+ with conn:
118
+ conn.execute("""
119
+ INSERT OR REPLACE INTO auth (id, username, hash, salt, created_at)
120
+ VALUES (1, ?, ?, ?, ?)
121
+ """, (username, pwd_hash, salt, now_utc))
122
+
123
+ def clear_auth(filename=FILE_PATH) -> bool:
124
+ initialise(filename)
125
+ with _get_connection(filename) as conn:
126
+ with conn:
127
+ cursor = conn.execute("DELETE FROM auth WHERE id = 1")
128
+ return cursor.rowcount > 0
129
+
130
+ def append_history(cmd: str, timestamp: str, filename=FILE_PATH) -> None:
131
+ initialise(filename)
132
+ with _get_connection(filename) as conn:
133
+ with conn:
134
+ conn.execute("INSERT INTO cmd_history (command, timestamp) VALUES (?, ?)", (cmd, timestamp))
135
+
136
+ def get_history(limit: int | None = None, filename=FILE_PATH) -> list[dict]:
137
+ initialise(filename)
138
+ with _get_connection(filename) as conn:
139
+ if limit is not None and limit > 0:
140
+ query = "SELECT command, timestamp FROM cmd_history ORDER BY id DESC LIMIT ?"
141
+ rows = conn.execute(query, (limit,)).fetchall()
142
+ rows.reverse()
143
+ else:
144
+ query = "SELECT command, timestamp FROM cmd_history ORDER BY id ASC"
145
+ rows = conn.execute(query).fetchall()
146
+ return [{"command": r[0], "timestamp": r[1]} for r in rows]
147
+
148
+ def clear_history(filename=FILE_PATH) -> bool:
149
+ initialise(filename)
150
+ with _get_connection(filename) as conn:
151
+ with conn:
152
+ conn.execute("DELETE FROM cmd_history")
153
+ return True
154
+
155
+ def get_config(key: str, default=None, filename=FILE_PATH):
156
+ initialise(filename)
157
+ with _get_connection(filename) as conn:
158
+ row = conn.execute("SELECT value FROM core_config WHERE key = ?", (key,)).fetchone()
159
+ if not row:
160
+ return default
161
+ try:
162
+ return json.loads(row[0])
163
+ except (ValueError, TypeError):
164
+ return row[0]
165
+
166
+ def set_config(key: str, value, filename=FILE_PATH) -> None:
167
+ initialise(filename)
168
+ serialized = json.dumps(value)
169
+ with _get_connection(filename) as conn:
170
+ with conn:
171
+ conn.execute("""
172
+ INSERT OR REPLACE INTO core_config (key, value)
173
+ VALUES (?, ?)
174
+ """, (key, serialized))
175
+
176
+ def delete_config(key: str, filename=FILE_PATH) -> bool:
177
+ initialise(filename)
178
+ with _get_connection(filename) as conn:
179
+ with conn:
180
+ cursor = conn.execute("DELETE FROM core_config WHERE key = ?", (key,))
181
+ return cursor.rowcount > 0
182
+
183
+ def register_plugin(plugin_name: str, filename=FILE_PATH) -> None:
184
+ initialise(filename)
185
+ now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
186
+ with _get_connection(filename) as conn:
187
+ with conn:
188
+ conn.execute("""
189
+ INSERT OR IGNORE INTO plugins (plugin_name, installed_at, is_enabled)
190
+ VALUES (?, ?, 1)
191
+ """, (plugin_name, now_utc))
192
+
193
+ def get_plugin_data(plugin_name: str, key: str, default=None, filename=FILE_PATH):
194
+ initialise(filename)
195
+ with _get_connection(filename) as conn:
196
+ row = conn.execute("""
197
+ SELECT value FROM plugin_kv WHERE plugin_name = ? AND key = ?
198
+ """, (plugin_name, key)).fetchone()
199
+ if not row:
200
+ return default
201
+ try:
202
+ return json.loads(row[0])
203
+ except (ValueError, TypeError):
204
+ return row[0]
205
+
206
+ def set_plugin_data(plugin_name: str, key: str, value, filename=FILE_PATH) -> None:
207
+ initialise(filename)
208
+ register_plugin(plugin_name, filename=filename)
209
+ serialized = json.dumps(value)
210
+ with _get_connection(filename) as conn:
211
+ with conn:
212
+ conn.execute("""
213
+ INSERT OR REPLACE INTO plugin_kv (plugin_name, key, value)
214
+ VALUES (?, ?, ?)
215
+ """, (plugin_name, key, serialized))
216
+
217
+ def get_all_plugin_data(plugin_name: str, filename=FILE_PATH) -> dict:
218
+ initialise(filename)
219
+ with _get_connection(filename) as conn:
220
+ rows = conn.execute("SELECT key, value FROM plugin_kv WHERE plugin_name = ?", (plugin_name,)).fetchall()
221
+ result = {}
222
+ for k, v in rows:
223
+ try:
224
+ result[k] = json.loads(v)
225
+ except (ValueError, TypeError):
226
+ result[k] = v
227
+ return result
228
+
229
+ def purge_plugin(plugin_name: str, filename=FILE_PATH) -> bool:
230
+ initialise(filename)
231
+ with _get_connection(filename) as conn:
232
+ with conn:
233
+ cursor = conn.execute("DELETE FROM plugins WHERE plugin_name = ?", (plugin_name,))
234
+ return cursor.rowcount > 0
@@ -0,0 +1,242 @@
1
+ # src\flyshell\core\directory.py
2
+
3
+ from flyshell.abstract.base_cmd import BaseCmd
4
+ from flyshell.abstract.parent_cmd import ParentCmd
5
+ from flyshell.builtin import fs, system, utils
6
+ from flyshell.core import auth, data
7
+
8
+ from datetime import datetime, timezone
9
+ import shlex
10
+
11
+
12
+ class Cmds(BaseCmd):
13
+ name = "cmds"
14
+ description = "List all available commands and their functions"
15
+ min_args = 0
16
+ options = None
17
+ aliases = ["help"]
18
+
19
+ def execute(self, args):
20
+ lines = ["\nAvailable Commands:"]
21
+ seen_cmds = set()
22
+ for cmd in COMMAND_REGISTRY.values():
23
+ if cmd in seen_cmds:
24
+ continue
25
+ seen_cmds.add(cmd)
26
+ cmd_label = f"{cmd.name} (alias: {', '.join(cmd.aliases)})" if cmd.aliases else cmd.name
27
+ if not cmd.options:
28
+ lines.append(f"\n{cmd_label}: {cmd.description} (Requires {cmd.min_args} parameter(s))")
29
+ else:
30
+ lines.append(f"\n{cmd_label}: {cmd.description} (Requires {cmd.min_args} parameter(s)) [args: {cmd.options}]")
31
+
32
+ lines.append(f"\nTotal available commands: {len(seen_cmds)}")
33
+ lines.append("Use '-h' or '--help' after any command to reveal its specific help list.")
34
+ lines.append("Flyshell also supports ;, && and || command chaining. Give it a go!\n")
35
+ return (0, "\n".join(lines))
36
+
37
+ INSTALLED_COMMANDS: list[BaseCmd] = [
38
+ system.Cd(),
39
+ Cmds(),
40
+ utils.Clear(),
41
+ system.Dir(),
42
+ fs.Fs(),
43
+ utils.History(),
44
+ utils.Kill(),
45
+ auth.Lock(),
46
+ system.OpenFile(),
47
+ utils.Reboot(),
48
+ utils.Sleep(),
49
+ system.SysExe(),
50
+ ]
51
+ COMMAND_REGISTRY: dict[str, BaseCmd] = {}
52
+ for cmd_obj in INSTALLED_COMMANDS:
53
+ COMMAND_REGISTRY[cmd_obj.name] = cmd_obj
54
+ for alias_name in cmd_obj.aliases:
55
+ COMMAND_REGISTRY[alias_name] = cmd_obj
56
+
57
+ def log(cmd):
58
+ utc_now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
59
+ data.append_history(cmd, utc_now)
60
+
61
+ def _execute_single(raw_cmd: str) -> int:
62
+ try:
63
+ posix_mode = False if data.HOST_OS == "Windows" else True
64
+ cmd = shlex.split(raw_cmd, posix=posix_mode)
65
+ except ValueError as e:
66
+ print("\nCommand Error: Invalid syntax.")
67
+ print(f"Details: {e}\n")
68
+ return 1
69
+ if not cmd:
70
+ return 0
71
+ cmd_name = cmd[0]
72
+ args = cmd[1:]
73
+ if "-h" in args or "--help" in args:
74
+ if cmd_name in COMMAND_REGISTRY:
75
+ command = COMMAND_REGISTRY[cmd_name]
76
+ if isinstance(command, ParentCmd):
77
+ return command.execute(args)
78
+ command.help()
79
+ return 0
80
+ elif cmd_name in data.PLUGINS:
81
+ plugin = data.PLUGINS[cmd_name]
82
+ if hasattr(plugin, "help"):
83
+ plugin.help()
84
+ print()
85
+ return 0
86
+ else:
87
+ print(f"\nPlugin Error: Plugin '{cmd_name}' does not provide help details.\n")
88
+ return 1
89
+ if "-d" in args or "--docs" in args:
90
+ if cmd_name in data.PLUGINS:
91
+ plugin = data.PLUGINS[cmd_name]
92
+ if hasattr(plugin, "show_docs") and plugin.show_docs():
93
+ return 0
94
+ print(f"\nPlugin Error: Plugin '{cmd_name}' has not provided documentation.\n")
95
+ return 1
96
+ if cmd_name in COMMAND_REGISTRY:
97
+ command = COMMAND_REGISTRY[cmd_name]
98
+ if len(args) < command.min_args:
99
+ print(f"\nCommand Error: '{cmd_name}' requires at least {command.min_args} parameter(s).\n")
100
+ return 1
101
+ if cmd[0] != "history":
102
+ log(raw_cmd)
103
+ try:
104
+ result = command.execute(args)
105
+ except Exception as e:
106
+ print(f"\nCommand Error: Failed during execution of '{cmd_name}'.")
107
+ print(f"Details: {e}\n")
108
+ return 1
109
+ data.SESSION_CMD_COUNT += 1
110
+ if isinstance(result, tuple):
111
+ code, message = result
112
+ if message:
113
+ print(message)
114
+ return code
115
+ elif isinstance(result, int):
116
+ return result
117
+ elif isinstance(result, str):
118
+ if result:
119
+ print(result)
120
+ return 0
121
+ return 0
122
+ elif cmd_name in data.PLUGINS:
123
+ log(raw_cmd)
124
+ plugin = data.PLUGINS[cmd_name]
125
+ try:
126
+ result = plugin.execute(args)
127
+ return result if isinstance(result, int) else 0
128
+ except NotImplementedError:
129
+ print(f"\nPlugin Error: '{cmd_name}' has not implemented the execute method.\n")
130
+ return 1
131
+ except Exception as e:
132
+ print(f"\nPlugin Error: '{cmd_name}' crashed unexpectedly.")
133
+ print(f"Details: {e}")
134
+ print("Returning to main Flyshell interface...\n")
135
+ return 1
136
+ finally:
137
+ if hasattr(plugin, "on_unload"):
138
+ plugin.on_unload()
139
+ else:
140
+ print(f"\nCommand Error: '{cmd_name}' is not a recognised command. Use 'cmds' for help.\n")
141
+ return 1
142
+
143
+ def _parse_commands(raw_cmd: str) -> list:
144
+ chain = []
145
+ current = []
146
+ in_single_quote = False
147
+ in_double_quote = False
148
+ escape = False
149
+ i = 0
150
+ n = len(raw_cmd)
151
+ pending_op = None
152
+ while i < n:
153
+ char = raw_cmd[i]
154
+ if escape:
155
+ current.append(char)
156
+ escape = False
157
+ i += 1
158
+ continue
159
+ is_windows = (data.HOST_OS == "Windows")
160
+ if char == "\\":
161
+ if not is_windows:
162
+ escape = True
163
+ i += 1
164
+ continue
165
+ else:
166
+ if i + 1 < n and raw_cmd[i + 1] in ('"', "'"):
167
+ escape = True
168
+ i += 1
169
+ continue
170
+ current.append(char)
171
+ i += 1
172
+ continue
173
+ if char == "'" and not in_double_quote:
174
+ in_single_quote = not in_single_quote
175
+ current.append(char)
176
+ i += 1
177
+ continue
178
+ if char == '"' and not in_single_quote:
179
+ in_double_quote = not in_double_quote
180
+ current.append(char)
181
+ i += 1
182
+ continue
183
+ if not in_single_quote and not in_double_quote:
184
+ if raw_cmd[i:i+2] in ("&&", "||"):
185
+ op = raw_cmd[i:i+2]
186
+ cmd_str = "".join(current).strip()
187
+ if cmd_str:
188
+ chain.append((pending_op, cmd_str))
189
+ pending_op = op
190
+ current = []
191
+ i += 2
192
+ continue
193
+ if char == ";":
194
+ cmd_str = "".join(current).strip()
195
+ if cmd_str:
196
+ chain.append((pending_op, cmd_str))
197
+ pending_op = ";"
198
+ current = []
199
+ i += 1
200
+ continue
201
+ current.append(char)
202
+ i += 1
203
+ if in_single_quote or in_double_quote:
204
+ quote_char = "'" if in_single_quote else '"'
205
+ print(f"\nSyntax Error: Unclosed {quote_char} quote detected.\n")
206
+ return []
207
+ if escape:
208
+ print("\nSyntax Error: Dangling '\\' escape character at end of command.\n")
209
+ return []
210
+ final_cmd = "".join(current).strip()
211
+ if final_cmd:
212
+ chain.append((pending_op, final_cmd))
213
+ return chain
214
+
215
+ def execute_line(raw_cmd: str):
216
+ chain = _parse_commands(raw_cmd)
217
+ if not chain:
218
+ return
219
+ last_code = 0
220
+ skipped_last = False
221
+ for i, (prev_op, cmd_str) in enumerate(chain):
222
+ should_run = True
223
+ if prev_op == ";":
224
+ should_run = True
225
+ skipped_last = False
226
+ elif skipped_last:
227
+ if prev_op == "&&":
228
+ should_run = False
229
+ elif prev_op == "||":
230
+ should_run = True
231
+ else:
232
+ if prev_op == "&&" and last_code != 0:
233
+ should_run = False
234
+ elif prev_op == "||" and last_code == 0:
235
+ should_run = False
236
+ if should_run:
237
+ if i > 0:
238
+ print("-" * 40)
239
+ last_code = _execute_single(cmd_str)
240
+ skipped_last = False
241
+ else:
242
+ skipped_last = True