Commit 995944ab authored by Roman Alifanov's avatar Roman Alifanov

Add direct shell command calls, remove shell.exec dependency

Unknown function calls now compile directly as shell commands: grep ("pattern") instead of shell.exec ("grep pattern"). Added mixed pipe support (CT func | shell cmd) with fd 3 redirect. Removed ngrep builtin (grep can be called directly now).
parent 9f47c906
......@@ -290,10 +290,13 @@ print (counter.value) # 0
### Pipe-оператор
```
# Shell-like — соединение команд
result = shell.exec ("cmd1") | shell.exec ("cmd2")
# Shell pipe — неизвестные функции компилируются как shell-команды
result = ls ("-la") | grep ("txt") | wc ("-l")
# Функциональный — цепочка функций
# Смешанный pipe — CT-функции и shell-команды вместе
generate_data () | grep ("error") | sort ("-r")
# Функциональный — цепочка CT-функций
result = value | func1 | func2
```
......@@ -1023,7 +1026,25 @@ env.GREETING = "Hello, {name}!"
Чтение генерирует `${VAR}`, установка — `export VAR="value"`. Без fork.
### shell (выполнение команд)
### Shell-команды
Любой вызов неизвестной функции (не определённой в `.ct` файле и не в stdlib) компилируется как shell-команда:
```
# Statement — просто выполняется
echo ("hello world")
# Assignment — вывод захватывается
result = date ("+%s")
name = whoami ()
# Pipe — нативный bash pipe
files = ls ("-la") | grep ("txt") | wc ("-l")
```
Аргументы передаются в кавычках: `grep ("pattern")``grep "pattern"`.
**Legacy:** `shell.exec/capture/source` по-прежнему работают для обратной совместимости:
```
shell.exec ("ls -la")
......@@ -1035,17 +1056,30 @@ shell.source ("config.sh")
Оператор `|` поддерживает два режима работы:
**Shell-like pipe** — соединение shell-команд через stdout:
**Shell pipe** — неизвестные функции компилируются в нативные bash-команды:
```
# Нативный bash pipe - без subshell overhead
files = shell.exec ("ls -la") | shell.exec ("grep txt")
# Генерирует: files=$(ls -la | grep txt)
files = ls ("-la") | grep ("txt")
# Генерирует: files=$(ls "-la" | grep "txt")
# Цепочки из 3+ команд
count = shell.exec ("cat file.txt") | shell.exec ("grep error") | shell.exec ("wc -l")
# Генерирует: count=$(cat file.txt | grep error | wc -l)
count = cat ("file.txt") | grep ("error") | wc ("-l")
# Генерирует: count=$(cat "file.txt" | grep "error" | wc "-l")
```
**Смешанный pipe** — CT-функции и shell-команды вместе:
```
func generate () {
print ("line one")
print ("line two")
}
result = generate () | grep ("one")
# Генерирует: result=$(generate 3>&1 | grep "one")
```
CT-функции автоматически получают `3>&1` для перенаправления `print()` в stdout пайпа.
**Функциональный pipe** — передача результата как первого аргумента:
......@@ -1129,7 +1163,6 @@ keys = config.keys () # ["host", "port"]
exit (code)
is_number (value)
is_empty (value)
ngrep (pattern, text) # grep с номерами строк
```
---
......
......@@ -126,8 +126,11 @@ logger.log ("Changed level")
### Pipe Operator
```
# Shell-like pipe — native bash pipes
files = shell.exec ("ls -la") | shell.exec ("grep txt") | shell.exec ("wc -l")
# Shell pipe — any unknown function compiles to a shell command
files = ls ("-la") | grep ("txt") | wc ("-l")
# Mixed pipe — CT functions and shell commands together
generate_data () | grep ("error") | sort ("-r")
# Functional pipe — chain function calls
result = 5 | double | add_ten # = add_ten(double(5))
......@@ -274,7 +277,7 @@ try {
| **Arrays** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Dicts** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
| **Regex** | `regex.match/extract` |
| **Shell** | `shell.exec/capture/source` |
| **Shell** | Any unknown function call compiles as shell command: `grep ("pattern")`, `sed ("expr")`. Legacy: `shell.exec/capture/source` |
| **Math** | `math.add/sub/mul/div/mod/min/max/abs` |
| **Time** | `time.now/ms` |
| **Random** | `random()`, `random_range()` |
......
......@@ -126,8 +126,11 @@ logger.log ("Уровень изменён")
### Pipe-оператор
```
# Shell-like pipe — нативные bash-пайпы
files = shell.exec ("ls -la") | shell.exec ("grep txt") | shell.exec ("wc -l")
# Shell pipe — любая неизвестная функция компилируется как shell-команда
files = ls ("-la") | grep ("txt") | wc ("-l")
# Смешанный pipe — CT-функции и shell-команды вместе
generate_data () | grep ("error") | sort ("-r")
# Функциональный pipe — цепочка вызовов функций
result = 5 | double | add_ten # = add_ten(double(5))
......@@ -274,7 +277,7 @@ try {
| **Массивы** | `.push()`, `.pop()`, `.shift()`, `.len()`, `.get()`, `.set()`, `.join()`, `.slice()`, `.map()`, `.filter()` |
| **Словари** | `.get()`, `.set()`, `.has()`, `.del()`, `.keys()` |
| **Regex** | `regex.match/extract` |
| **Shell** | `shell.exec/capture/source` |
| **Shell** | Любой неизвестный вызов функции компилируется как shell-команда: `grep ("pattern")`, `sed ("expr")`. Legacy: `shell.exec/capture/source` |
| **Математика** | `math.add/sub/mul/div/mod/min/max/abs` |
| **Время** | `time.now/ms` |
| **Случайные числа** | `random()`, `random_range()` |
......
......@@ -259,9 +259,11 @@ class DispatchMixin:
args_str = self._generate_call_args_str(stmt.value.arguments)
if func_name in self.callback_vars:
self.emit_var_assign(target, f'$("${{{func_name}}}" {args_str})')
else:
elif func_name in self.functions:
self.emit(f'{func_name} {args_str} >/dev/null')
self.emit_var_assign(target, f'${RET_VAR}')
else:
self.emit_var_assign(target, f'$({func_name} {args_str})')
return
if isinstance(stmt.value, ArrayLiteral):
......@@ -689,12 +691,16 @@ class DispatchMixin:
"""Generate pipe expression assignment."""
elements = self._collect_pipe_chain(stmt.value)
if all(self._is_shell_exec(e) for e in elements):
commands = [self._extract_shell_command(e) for e in elements]
if all(self._is_shell_command(e) for e in elements):
commands = [self._extract_shell_command_str(e) for e in elements]
pipe_cmd = " | ".join(commands)
self.emit_var_assign(target, f'$({pipe_cmd})')
return
if any(self._is_shell_command(e) for e in elements[1:]):
self._generate_mixed_pipe_assignment(elements, target)
return
self._generate_functional_pipe_assignment(elements, target)
def _generate_functional_pipe_assignment(self, elements: list, target: str):
......@@ -704,8 +710,8 @@ class DispatchMixin:
first = elements[0]
if isinstance(first, CallExpr):
if self._is_shell_exec(first):
cmd = self._extract_shell_command(first)
if self._is_shell_command(first):
cmd = self._extract_shell_command_str(first)
current_var = self.new_temp()
self.emit_var_assign(current_var, f'$({cmd})')
else:
......@@ -721,8 +727,8 @@ class DispatchMixin:
for elem in elements[1:]:
if isinstance(elem, CallExpr):
if self._is_shell_exec(elem):
cmd = self._extract_shell_command(elem)
if self._is_shell_command(elem):
cmd = self._extract_shell_command_str(elem)
new_var = self.new_temp()
self.emit_var_assign(new_var, f'$(echo "${{{current_var}}}" | {cmd})')
current_var = new_var
......@@ -744,6 +750,40 @@ class DispatchMixin:
self.emit_var_assign(target, f'${{{current_var}}}')
def _generate_mixed_pipe_assignment(self, elements: list, target: str):
"""Generate native bash pipe mixing CT functions and shell commands."""
parts = []
for elem in elements:
if self._is_shell_command(elem):
parts.append(self._extract_shell_command_str(elem))
elif isinstance(elem, CallExpr):
call_code = self.generate_call_statement(elem)
parts.append(f'{call_code} 3>&1')
elif isinstance(elem, Identifier):
parts.append(f'{elem.name} 3>&1')
else:
code = self.generate_expr(elem)
parts.append(code)
pipe_cmd = " | ".join(parts)
self.emit_var_assign(target, f'$({pipe_cmd})')
def _generate_mixed_pipe_stmt(self, elements: list):
"""Generate native bash pipe as statement (no assignment)."""
parts = []
for elem in elements:
if self._is_shell_command(elem):
parts.append(self._extract_shell_command_str(elem))
elif isinstance(elem, CallExpr):
call_code = self.generate_call_statement(elem)
parts.append(f'{call_code} 3>&1')
elif isinstance(elem, Identifier):
parts.append(f'{elem.name} 3>&1')
else:
code = self.generate_expr(elem)
parts.append(code)
pipe_cmd = " | ".join(parts)
self.emit(pipe_cmd)
def _get_call_func_name(self, call_expr: CallExpr) -> str:
"""Get function name from call expression."""
if isinstance(call_expr.callee, Identifier):
......@@ -756,6 +796,16 @@ class DispatchMixin:
def generate_expression_stmt(self, stmt: ExpressionStmt):
expr = stmt.expression
if isinstance(expr, BinaryOp) and expr.operator == "|":
elements = self._collect_pipe_chain(expr)
if all(self._is_shell_command(e) for e in elements):
commands = [self._extract_shell_command_str(e) for e in elements]
self.emit(" | ".join(commands))
return
if any(self._is_shell_command(e) for e in elements[1:]):
self._generate_mixed_pipe_stmt(elements)
return
if isinstance(expr, BaseCall):
if self.current_class:
parent_cls = self.classes.get(self.current_class)
......@@ -967,8 +1017,6 @@ class DispatchMixin:
return f'__ct_print "{args[0]}"' if args else '__ct_print ""'
elif name == "range":
return f'__ct_range {args_str}'
elif name == "ngrep":
return f'__ct_ngrep {args_str}'
elif name == "exit":
return f'__ct_exit {args_str}'
elif name == "len":
......@@ -1004,6 +1052,30 @@ class DispatchMixin:
return True
return False
def _is_shell_command(self, expr) -> bool:
if self._is_shell_exec(expr):
return True
if isinstance(expr, CallExpr) and isinstance(expr.callee, Identifier):
name = expr.callee.name
if (name not in self.functions and
name not in BUILTIN_FUNCS and
name not in self.classes and
not self._is_callback_var(name)):
return True
return False
def _extract_shell_command_str(self, expr) -> str:
if self._is_shell_exec(expr):
return self._extract_shell_command(expr)
if isinstance(expr, CallExpr) and isinstance(expr.callee, Identifier):
name = expr.callee.name
if expr.arguments:
args = [self.generate_expr(arg) for arg in expr.arguments]
args_str = " ".join([f'"{a}"' for a in args])
return f'{name} {args_str}'
return name
return ""
def _generate_member_call(self, callee: MemberAccess, args: list, args_str: str, location=None) -> str:
"""Generate member access call."""
obj = self.generate_expr(callee.object)
......
......@@ -650,12 +650,31 @@ class ExprMixin:
"""Generate pipe expression as inline expression."""
elements = self._collect_pipe_chain(expr)
if all(self._is_shell_exec(e) for e in elements):
commands = [self._extract_shell_command(e) for e in elements]
if all(self._is_shell_command(e) for e in elements):
commands = [self._extract_shell_command_str(e) for e in elements]
return f'$({" | ".join(commands)})'
if any(self._is_shell_command(e) for e in elements[1:]):
return self._generate_mixed_pipe_inline(elements)
return self._generate_functional_pipe_inline(elements)
def _generate_mixed_pipe_inline(self, elements: list) -> str:
"""Generate native bash pipe mixing CT functions and shell commands."""
parts = []
for elem in elements:
if self._is_shell_command(elem):
parts.append(self._extract_shell_command_str(elem))
elif isinstance(elem, CallExpr):
call_code = self.generate_call_statement(elem)
parts.append(f'{call_code} 3>&1')
elif isinstance(elem, Identifier):
parts.append(f'{elem.name} 3>&1')
else:
code = self.generate_expr(elem)
parts.append(code)
return f'$({" | ".join(parts)})'
def _collect_pipe_chain(self, expr: Expression) -> list:
"""Collect all elements in a pipe chain from left to right."""
if isinstance(expr, BinaryOp) and expr.operator == "|":
......@@ -709,8 +728,8 @@ class ExprMixin:
elif isinstance(call_expr.callee, MemberAccess):
call_code = self.generate_call_statement(call_expr)
if self._is_shell_exec(call_expr):
cmd = self._extract_shell_command(call_expr)
if self._is_shell_command(call_expr):
cmd = self._extract_shell_command_str(call_expr)
return f'$(echo "{prev_result}" | {cmd})'
return f'$({call_code} "{prev_result}")'
......
......@@ -43,12 +43,6 @@ class CoreFunctions:
awk_builtin=lambda a: f"(length({a[0]}) == 0)",
min_args=1, max_args=1,
)
ngrep = Method(
name="ngrep",
bash_func="__ct_ngrep",
bash_impl='echo "$2" | grep -n "$1" || true',
min_args=2, max_args=2,
)
random = Method(
name="random",
bash_func="__ct_random",
......
......@@ -278,7 +278,6 @@ class StdlibMixin:
for method_name, method_def in REGEX_METHODS.items():
if method_def.bash_impl:
self.emit(f"{method_def.bash_func} () {{ {method_def.bash_impl}; }}")
self.emit(f"{CORE_FUNCTIONS['ngrep'].bash_func} () {{ {CORE_FUNCTIONS['ngrep'].bash_impl}; }}")
self.emit()
def _emit_utils(self):
......
......@@ -2108,3 +2108,198 @@ print(test())
assert code == 0
assert "15" in stdout
class TestShellCommands:
def test_shell_cmd_statement(self):
code, stdout, stderr = run_ct('echo("hello from shell")')
assert code == 0
assert "hello from shell" in stdout
def test_shell_cmd_assignment(self):
code, stdout, stderr = run_ct('''
result = echo("hello capture")
print(result)
''')
assert code == 0
assert "hello capture" in stdout
def test_shell_cmd_with_flags(self):
code, stdout, stderr = run_ct('''
result = echo("-n", "no newline")
print(result)
''')
assert code == 0
assert "no newline" in stdout
def test_shell_cmd_date(self):
code, stdout, stderr = run_ct('''
ts = date("+%s")
print(ts)
''')
assert code == 0
assert stdout.strip().isdigit()
def test_shell_pipe_two_cmds(self):
code, stdout, stderr = run_ct('''
result = echo("aaa bbb ccc") | wc("-w")
print(result)
''')
assert code == 0
assert "3" in stdout
def test_shell_pipe_three_cmds(self):
code, stdout, stderr = run_ct(r'''
result = printf("alpha\nbeta\ngamma") | sort("-r") | head("-1")
print(result)
''')
assert code == 0
assert "gamma" in stdout
def test_shell_pipe_grep(self):
code, stdout, stderr = run_ct(r'''
result = printf("foo\nbar\nbaz") | grep("ba")
print(result)
''')
assert code == 0
assert "bar" in stdout
assert "baz" in stdout
assert "foo" not in stdout
def test_mixed_pipe_ct_func_to_shell(self):
code, stdout, stderr = run_ct(r'''
func generate() {
print("line one")
print("line two")
print("line three")
}
result = generate() | grep("two")
print(result)
''')
assert code == 0
assert "line two" in stdout
assert "line one" not in stdout
def test_mixed_pipe_ct_func_to_shell_stmt(self):
code, stdout, stderr = run_ct(r'''
func generate() {
print("hello world")
print("goodbye world")
}
generate() | grep("hello")
''')
assert code == 0
assert "hello world" in stdout
assert "goodbye" not in stdout
def test_ct_func_not_treated_as_shell(self):
code, stdout, stderr = run_ct('''
func double(x) {
return x * 2
}
result = double(21)
print(result)
''')
assert code == 0
assert "42" in stdout
def test_callback_not_treated_as_shell(self):
code, stdout, stderr = run_ct('''
func apply(callback, value) {
return callback(value)
}
func triple(x) { return x * 3 }
result = apply(triple, 7)
print(result)
''')
assert code == 0
assert "21" in stdout
def test_legacy_shell_exec(self):
code, stdout, stderr = run_ct('''
result = shell.exec("echo legacy") | shell.exec("tr a-z A-Z")
print(result)
''')
assert code == 0
assert "LEGACY" in stdout
def test_legacy_shell_capture(self):
code, stdout, stderr = run_ct('''
result = shell.capture("echo captured")
print(result)
''')
assert code == 0
assert "captured" in stdout
def test_shell_cmd_in_interpolation(self):
code, stdout, stderr = run_ct('''
name = whoami()
print("user: {name}")
''')
assert code == 0
assert "user: " in stdout
assert len(stdout.strip().split(": ")[1]) > 0
def test_shell_pipe_with_sed(self):
code, stdout, stderr = run_ct('''
result = echo("hello world") | sed("s/world/content/")
print(result)
''')
assert code == 0
assert "hello content" in stdout
def test_shell_cmd_no_args(self):
code, stdout, stderr = run_ct('''
result = pwd()
print(result)
''')
assert code == 0
assert "/" in stdout
def test_mixed_pipe_multiple_ct_and_shell(self):
code, stdout, stderr = run_ct(r'''
func make_lines() {
print("apple")
print("banana")
print("cherry")
}
result = make_lines() | sort("-r") | head("-2")
print(result)
''')
assert code == 0
assert "cherry" in stdout
assert "banana" in stdout
def test_shell_cmd_in_if_condition(self):
code, stdout, stderr = run_ct('''
count = echo("hello") | wc("-c")
if count > 0 {
print("not empty")
}
''')
assert code == 0
assert "not empty" in stdout
def test_shell_cmd_in_loop(self):
code, stdout, stderr = run_ct('''
for i in range(1, 4) {
result = echo("line {i}")
print("got: {result}")
}
''')
assert code == 0
assert "got: line 1" in stdout
assert "got: line 2" in stdout
assert "got: line 3" in stdout
def test_shell_cmd_in_function(self):
code, stdout, stderr = run_ct('''
func get_host() {
return hostname()
}
h = get_host()
print(h)
''')
assert code == 0
assert len(stdout.strip()) > 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment