Skip to content

Commit 698527d

Browse files
committed
Add inline comment support to console
1 parent ee061a8 commit 698527d

3 files changed

Lines changed: 61 additions & 10 deletions

File tree

docs/dev/chs_scripting.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
Chronos executes `.chs` scripts with one command per line. Lines support quoted arguments, key:value properties, and variable expansion.
44

5+
## Comments
6+
7+
- `#` starts a comment.
8+
- Full-line comments are allowed: `# this line is ignored`
9+
- Inline comments are also allowed: `echo Hello # this part is ignored`
10+
- If you need a literal `#`, quote it: `echo "#not-a-comment"`
11+
512
## Variables
613

714
- Set a variable: `set var name:World`

modules/console.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,10 @@ def _load_registry_bundle():
615615

616616
def _split_args_safe(text: str):
617617
try:
618-
return shlex.split(text)
618+
lexer = shlex.shlex(str(text or ""), posix=True)
619+
lexer.whitespace_split = True
620+
lexer.commenters = "#"
621+
return list(lexer)
619622
except Exception:
620623
return [t for t in text.split() if t]
621624

@@ -1494,7 +1497,7 @@ def exec_line(raw_line, line_no=None):
14941497
ln = raw_line.strip()
14951498
if not ln or ln.startswith('#'):
14961499
return
1497-
parts = shlex.split(ln)
1500+
parts = _split_args_safe(ln)
14981501
command, args, properties = parse_input(parts)
14991502
if command:
15001503
# Set context line for single-line 'if' error reporting
@@ -1578,7 +1581,7 @@ def _parse_repeat_count(header_tokens):
15781581
return count_int
15791582

15801583
def _parse_for_header(header_raw):
1581-
raw_tokens = shlex.split(header_raw)
1584+
raw_tokens = _split_args_safe(header_raw)
15821585
if not raw_tokens:
15831586
return None
15841587
var_name = raw_tokens[0]
@@ -1605,7 +1608,7 @@ def _parse_for_header(header_raw):
16051608
return var_name, item_type, props
16061609

16071610
def _parse_while_header(header_raw):
1608-
raw_tokens = shlex.split(header_raw)
1611+
raw_tokens = _split_args_safe(header_raw)
16091612
max_raw = None
16101613
cond_tokens = []
16111614
for tok in raw_tokens:
@@ -1676,7 +1679,7 @@ def collect_commands(start_idx):
16761679
if sl.startswith('repeat ') and sl.endswith(' then'):
16771680
block, i = collect_block(i)
16781681
header = stripped[7:-5].strip()
1679-
header_tokens = _V.expand_list(shlex.split(header))
1682+
header_tokens = _V.expand_list(_split_args_safe(header))
16801683
count = _parse_repeat_count(header_tokens)
16811684
if not count:
16821685
print("❌ Invalid repeat count. Use: repeat count:<n> then")
@@ -1770,7 +1773,7 @@ def collect_commands(start_idx):
17701773

17711774
if sl.startswith('if ') and sl.endswith(' then'):
17721775
header = stripped[3:-5].strip()
1773-
header_parts = shlex.split(header)
1776+
header_parts = _split_args_safe(header)
17741777
cond_tokens = _V.expand_list(header_parts)
17751778
cond_line_no = i + 1
17761779
blocks = [(cond_tokens, [], cond_line_no)]
@@ -1783,7 +1786,7 @@ def collect_commands(start_idx):
17831786
sl2 = s.lower()
17841787
if sl2.startswith('elseif ') and sl2.endswith(' then'):
17851788
elif_header = s[7:-5].strip()
1786-
parts2 = shlex.split(elif_header)
1789+
parts2 = _split_args_safe(elif_header)
17871790
cond2 = _V.expand_list(parts2)
17881791
elif_line_no = i + 1
17891792
i += 1
@@ -1938,7 +1941,7 @@ def collect_commands(start_idx):
19381941
# Join all arguments into a single string to handle quotes correctly
19391942
user_input_str = ' '.join(cli_args)
19401943
# Use shlex.split to parse the string, respecting quotes
1941-
parts = shlex.split(user_input_str)
1944+
parts = _split_args_safe(user_input_str)
19421945
command, args, properties = parse_input(parts)
19431946
if command:
19441947
if command.lower() in {"exit", "quit"}:
@@ -2037,7 +2040,7 @@ def _(event):
20372040
time.sleep(1)
20382041
_play_cli_sound("exit", wait=True)
20392042
break
2040-
parts = shlex.split(user_input)
2043+
parts = _split_args_safe(user_input)
20412044
command, args, properties = parse_input(parts)
20422045
if command:
20432046
invoke_command(command, args, properties.copy())
@@ -2076,7 +2079,7 @@ def _(event):
20762079
break
20772080

20782081
# Use shlex.split for interactive input to handle quotes
2079-
parts = shlex.split(user_input)
2082+
parts = _split_args_safe(user_input)
20802083
command, args, properties = parse_input(parts)
20812084

20822085
if command:

tests/test_console_comments.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
import sys
3+
import unittest
4+
5+
ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
6+
if ROOT_DIR not in sys.path:
7+
sys.path.insert(0, ROOT_DIR)
8+
9+
from modules import console as Console
10+
11+
12+
class TestConsoleComments(unittest.TestCase):
13+
def test_inline_comments_are_ignored(self):
14+
self.assertEqual(
15+
Console._split_args_safe('echo hello # trailing note'),
16+
["echo", "hello"],
17+
)
18+
19+
def test_quoted_hash_is_preserved(self):
20+
self.assertEqual(
21+
Console._split_args_safe('echo "# keep this" # strip this'),
22+
["echo", "# keep this"],
23+
)
24+
25+
def test_cli_parse_ignores_inline_comment_properties(self):
26+
command, args, props = Console.parse_input(
27+
Console._split_args_safe('new task "Test" priority:high # note')
28+
)
29+
self.assertEqual(command, "new")
30+
self.assertEqual(args, ["task", "Test"])
31+
self.assertEqual(props.get("priority"), "high")
32+
33+
def test_block_headers_allow_inline_comments(self):
34+
self.assertEqual(
35+
Console._split_args_safe("repeat count:2 then # run twice"),
36+
["repeat", "count:2", "then"],
37+
)
38+
39+
40+
if __name__ == "__main__":
41+
unittest.main()

0 commit comments

Comments
 (0)