snail-lang 0.7.2__cp38-abi3-win_amd64.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.
- snail/__init__.py +35 -0
- snail/_native.pyd +0 -0
- snail/cli.py +423 -0
- snail/runtime/__init__.py +261 -0
- snail/runtime/augmented.py +49 -0
- snail/runtime/compact_try.py +13 -0
- snail/runtime/env.py +26 -0
- snail/runtime/lazy_file.py +41 -0
- snail/runtime/lazy_text.py +50 -0
- snail/runtime/regex.py +37 -0
- snail/runtime/structured_accessor.py +74 -0
- snail/runtime/subprocess.py +66 -0
- snail_lang-0.7.2.dist-info/METADATA +329 -0
- snail_lang-0.7.2.dist-info/RECORD +17 -0
- snail_lang-0.7.2.dist-info/WHEEL +4 -0
- snail_lang-0.7.2.dist-info/entry_points.txt +2 -0
- snail_lang-0.7.2.dist-info/licenses/LICENSE +20 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import operator as _op
|
|
4
|
+
|
|
5
|
+
_OPS = {
|
|
6
|
+
"+": _op.add,
|
|
7
|
+
"-": _op.sub,
|
|
8
|
+
"*": _op.mul,
|
|
9
|
+
"/": _op.truediv,
|
|
10
|
+
"//": _op.floordiv,
|
|
11
|
+
"%": _op.mod,
|
|
12
|
+
"**": _op.pow,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _apply_op(left, right, op: str):
|
|
17
|
+
try:
|
|
18
|
+
func = _OPS[op]
|
|
19
|
+
except KeyError as exc:
|
|
20
|
+
raise ValueError(f"unknown augmented op: {op}") from exc
|
|
21
|
+
return func(left, right)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __snail_incr_attr(obj, attr: str, delta: int, pre: bool):
|
|
25
|
+
old = getattr(obj, attr)
|
|
26
|
+
new = old + delta
|
|
27
|
+
setattr(obj, attr, new)
|
|
28
|
+
return new if pre else old
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __snail_incr_index(obj, index, delta: int, pre: bool):
|
|
32
|
+
old = obj[index]
|
|
33
|
+
new = old + delta
|
|
34
|
+
obj[index] = new
|
|
35
|
+
return new if pre else old
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __snail_aug_attr(obj, attr: str, value, op: str):
|
|
39
|
+
old = getattr(obj, attr)
|
|
40
|
+
new = _apply_op(old, value, op)
|
|
41
|
+
setattr(obj, attr, new)
|
|
42
|
+
return new
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def __snail_aug_index(obj, index, value, op: str):
|
|
46
|
+
old = obj[index]
|
|
47
|
+
new = _apply_op(old, value, op)
|
|
48
|
+
obj[index] = new
|
|
49
|
+
return new
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def compact_try(expr_fn, fallback_fn=None):
|
|
5
|
+
try:
|
|
6
|
+
return expr_fn()
|
|
7
|
+
except Exception as exc:
|
|
8
|
+
if fallback_fn is None:
|
|
9
|
+
fallback_member = getattr(exc, "__fallback__", None)
|
|
10
|
+
if callable(fallback_member):
|
|
11
|
+
return fallback_member()
|
|
12
|
+
return None
|
|
13
|
+
return fallback_fn(exc)
|
snail/runtime/env.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class EnvMap:
|
|
7
|
+
__slots__ = ("_env",)
|
|
8
|
+
|
|
9
|
+
def __init__(self, env=None) -> None:
|
|
10
|
+
self._env = os.environ if env is None else env
|
|
11
|
+
|
|
12
|
+
def __fallback__(self) -> str:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
def _lookup(self, key):
|
|
16
|
+
try:
|
|
17
|
+
return self._env[key]
|
|
18
|
+
except KeyError as exc:
|
|
19
|
+
exc.__fallback__ = self.__fallback__
|
|
20
|
+
raise
|
|
21
|
+
|
|
22
|
+
def __getitem__(self, key):
|
|
23
|
+
return self._lookup(key)
|
|
24
|
+
|
|
25
|
+
def __getattr__(self, name):
|
|
26
|
+
return self._lookup(name)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Lazy file opener for map mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LazyFile:
|
|
7
|
+
"""Context manager that opens the file on first access."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("_path", "_mode", "_kwargs", "_fd", "_closed")
|
|
10
|
+
|
|
11
|
+
def __init__(self, path, mode="r", **kwargs):
|
|
12
|
+
self._path = path
|
|
13
|
+
self._mode = mode
|
|
14
|
+
self._kwargs = kwargs
|
|
15
|
+
self._fd = None
|
|
16
|
+
self._closed = False
|
|
17
|
+
|
|
18
|
+
def _ensure_open(self):
|
|
19
|
+
if self._closed:
|
|
20
|
+
raise ValueError("I/O operation on closed file.")
|
|
21
|
+
if self._fd is None:
|
|
22
|
+
self._fd = open(self._path, self._mode, **self._kwargs)
|
|
23
|
+
return self._fd
|
|
24
|
+
|
|
25
|
+
def __enter__(self):
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc, tb):
|
|
29
|
+
self._closed = True
|
|
30
|
+
if self._fd is not None:
|
|
31
|
+
self._fd.close()
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def __getattr__(self, name):
|
|
35
|
+
return getattr(self._ensure_open(), name)
|
|
36
|
+
|
|
37
|
+
def __iter__(self):
|
|
38
|
+
return iter(self._ensure_open())
|
|
39
|
+
|
|
40
|
+
def __next__(self):
|
|
41
|
+
return next(self._ensure_open())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Lazy text content reader for map mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LazyText:
|
|
7
|
+
"""Lazily reads file content on first access."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("_fd", "_text")
|
|
10
|
+
|
|
11
|
+
def __init__(self, fd):
|
|
12
|
+
self._fd = fd
|
|
13
|
+
self._text = None
|
|
14
|
+
|
|
15
|
+
def _ensure_loaded(self):
|
|
16
|
+
if self._text is None:
|
|
17
|
+
self._text = self._fd.read()
|
|
18
|
+
return self._text
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return self._ensure_loaded()
|
|
22
|
+
|
|
23
|
+
def __repr__(self):
|
|
24
|
+
return repr(str(self))
|
|
25
|
+
|
|
26
|
+
def __eq__(self, other):
|
|
27
|
+
if isinstance(other, LazyText):
|
|
28
|
+
return str(self) == str(other)
|
|
29
|
+
return str(self) == other
|
|
30
|
+
|
|
31
|
+
def __hash__(self):
|
|
32
|
+
return hash(str(self))
|
|
33
|
+
|
|
34
|
+
def __len__(self):
|
|
35
|
+
return len(str(self))
|
|
36
|
+
|
|
37
|
+
def __iter__(self):
|
|
38
|
+
return iter(str(self))
|
|
39
|
+
|
|
40
|
+
def __contains__(self, item):
|
|
41
|
+
return item in str(self)
|
|
42
|
+
|
|
43
|
+
def __add__(self, other):
|
|
44
|
+
return str(self) + other
|
|
45
|
+
|
|
46
|
+
def __radd__(self, other):
|
|
47
|
+
return other + str(self)
|
|
48
|
+
|
|
49
|
+
def __getattr__(self, name):
|
|
50
|
+
return getattr(str(self), name)
|
snail/runtime/regex.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SnailRegex:
|
|
7
|
+
def __init__(self, pattern: str) -> None:
|
|
8
|
+
self.pattern = pattern
|
|
9
|
+
self._regex = re.compile(pattern)
|
|
10
|
+
|
|
11
|
+
def search(self, value):
|
|
12
|
+
return regex_search(value, self._regex)
|
|
13
|
+
|
|
14
|
+
def __contains__(self, value):
|
|
15
|
+
return bool(self.__snail_contains__(value))
|
|
16
|
+
|
|
17
|
+
def __snail_contains__(self, value):
|
|
18
|
+
return self.search(value)
|
|
19
|
+
|
|
20
|
+
def __repr__(self) -> str:
|
|
21
|
+
return f"/{self.pattern}/"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def regex_search(value, pattern):
|
|
25
|
+
if isinstance(pattern, SnailRegex):
|
|
26
|
+
return pattern.search(value)
|
|
27
|
+
if hasattr(pattern, "search"):
|
|
28
|
+
match = pattern.search(value)
|
|
29
|
+
else:
|
|
30
|
+
match = re.search(pattern, value)
|
|
31
|
+
if match is None:
|
|
32
|
+
return ()
|
|
33
|
+
return (match.group(0),) + match.groups()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def regex_compile(pattern):
|
|
37
|
+
return SnailRegex(pattern)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json as _json
|
|
4
|
+
import os as _os
|
|
5
|
+
import sys as _sys
|
|
6
|
+
|
|
7
|
+
def __snail_jmespath_query(query: str):
|
|
8
|
+
"""Create a callable that applies JMESPath query.
|
|
9
|
+
|
|
10
|
+
Used by the $[query] syntax which lowers to __snail_jmespath_query(query).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import jmespath as _jmespath
|
|
14
|
+
|
|
15
|
+
def apply(data):
|
|
16
|
+
return _jmespath.search(query, data)
|
|
17
|
+
|
|
18
|
+
return apply
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_jsonl(content: str):
|
|
22
|
+
lines = [line for line in content.splitlines() if line.strip()]
|
|
23
|
+
if not lines:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
items = []
|
|
27
|
+
for line in lines:
|
|
28
|
+
try:
|
|
29
|
+
items.append(_json.loads(line))
|
|
30
|
+
except _json.JSONDecodeError as exc:
|
|
31
|
+
raise _json.JSONDecodeError(
|
|
32
|
+
f"Invalid JSONL line: {exc.msg}",
|
|
33
|
+
line,
|
|
34
|
+
exc.pos,
|
|
35
|
+
) from exc
|
|
36
|
+
return items
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def js(input_data=None):
|
|
40
|
+
"""Parse JSON from various input sources.
|
|
41
|
+
|
|
42
|
+
Returns the parsed Python object (dict, list, etc.) directly.
|
|
43
|
+
If called with no arguments, reads from stdin.
|
|
44
|
+
"""
|
|
45
|
+
if input_data is None:
|
|
46
|
+
input_data = _sys.stdin
|
|
47
|
+
|
|
48
|
+
if isinstance(input_data, str):
|
|
49
|
+
try:
|
|
50
|
+
return _json.loads(input_data)
|
|
51
|
+
except _json.JSONDecodeError:
|
|
52
|
+
if _os.path.exists(input_data):
|
|
53
|
+
with open(input_data, "r", encoding="utf-8") as handle:
|
|
54
|
+
content = handle.read()
|
|
55
|
+
try:
|
|
56
|
+
return _json.loads(content)
|
|
57
|
+
except _json.JSONDecodeError:
|
|
58
|
+
return _parse_jsonl(content)
|
|
59
|
+
else:
|
|
60
|
+
return _parse_jsonl(input_data)
|
|
61
|
+
elif hasattr(input_data, "read"):
|
|
62
|
+
content = input_data.read()
|
|
63
|
+
if isinstance(content, bytes):
|
|
64
|
+
content = content.decode("utf-8")
|
|
65
|
+
try:
|
|
66
|
+
return _json.loads(content)
|
|
67
|
+
except _json.JSONDecodeError:
|
|
68
|
+
return _parse_jsonl(content)
|
|
69
|
+
elif isinstance(input_data, (dict, list, int, float, bool)) or input_data is None:
|
|
70
|
+
return input_data
|
|
71
|
+
else:
|
|
72
|
+
raise TypeError(
|
|
73
|
+
f"js() input must be JSON-compatible, got {type(input_data).__name__}"
|
|
74
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SubprocessCapture:
|
|
7
|
+
def __init__(self, cmd: str) -> None:
|
|
8
|
+
self.cmd = cmd
|
|
9
|
+
|
|
10
|
+
def __call__(self, input_data=None):
|
|
11
|
+
try:
|
|
12
|
+
if input_data is None:
|
|
13
|
+
completed = subprocess.run(
|
|
14
|
+
self.cmd,
|
|
15
|
+
shell=True,
|
|
16
|
+
check=True,
|
|
17
|
+
text=True,
|
|
18
|
+
stdout=subprocess.PIPE,
|
|
19
|
+
)
|
|
20
|
+
else:
|
|
21
|
+
if not isinstance(input_data, (str, bytes)):
|
|
22
|
+
input_data = str(input_data)
|
|
23
|
+
completed = subprocess.run(
|
|
24
|
+
self.cmd,
|
|
25
|
+
shell=True,
|
|
26
|
+
check=True,
|
|
27
|
+
text=True,
|
|
28
|
+
input=input_data,
|
|
29
|
+
stdout=subprocess.PIPE,
|
|
30
|
+
)
|
|
31
|
+
return completed.stdout.rstrip("\n")
|
|
32
|
+
except subprocess.CalledProcessError as exc:
|
|
33
|
+
|
|
34
|
+
def __fallback(exc=exc):
|
|
35
|
+
raise exc
|
|
36
|
+
|
|
37
|
+
exc.__fallback__ = __fallback
|
|
38
|
+
raise
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SubprocessStatus:
|
|
42
|
+
def __init__(self, cmd: str) -> None:
|
|
43
|
+
self.cmd = cmd
|
|
44
|
+
|
|
45
|
+
def __call__(self, input_data=None):
|
|
46
|
+
try:
|
|
47
|
+
if input_data is None:
|
|
48
|
+
subprocess.run(self.cmd, shell=True, check=True)
|
|
49
|
+
else:
|
|
50
|
+
if not isinstance(input_data, (str, bytes)):
|
|
51
|
+
input_data = str(input_data)
|
|
52
|
+
subprocess.run(
|
|
53
|
+
self.cmd,
|
|
54
|
+
shell=True,
|
|
55
|
+
check=True,
|
|
56
|
+
text=True,
|
|
57
|
+
input=input_data,
|
|
58
|
+
)
|
|
59
|
+
return 0
|
|
60
|
+
except subprocess.CalledProcessError as exc:
|
|
61
|
+
|
|
62
|
+
def __fallback(exc=exc):
|
|
63
|
+
return exc.returncode
|
|
64
|
+
|
|
65
|
+
exc.__fallback__ = __fallback
|
|
66
|
+
raise
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: snail-lang
|
|
3
|
+
Version: 0.7.2
|
|
4
|
+
Requires-Dist: astunparse>=1.6.3 ; python_full_version < '3.9'
|
|
5
|
+
Requires-Dist: jmespath>=1.0.1
|
|
6
|
+
Requires-Dist: maturin>=1.5 ; extra == 'dev'
|
|
7
|
+
Requires-Dist: pytest ; extra == 'dev'
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Summary: Snail programming language interpreter
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="logo.png" alt="Snail logo" width="200">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<h1 align="center">Snail</h1>
|
|
19
|
+
|
|
20
|
+
**Snail** is a programming language that compiles to Python, combining Python's familiarity and extensive libraries with Perl/awk-inspired syntax for quick scripts and one-liners. Its what you get when you shove a snake in a shell.
|
|
21
|
+
|
|
22
|
+
## AI Slop!
|
|
23
|
+
|
|
24
|
+
Snail is me learning how to devlop code using LLMs. I think its neat, and
|
|
25
|
+
maybe useful. I don't think this is high quality. I am going to try and LLM my
|
|
26
|
+
way into something good, but its certainly not there yet.
|
|
27
|
+
|
|
28
|
+
## Installing Snail
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install snail-lang
|
|
32
|
+
-or-
|
|
33
|
+
uv tool install snail-lang
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That installs the `snail` CLI for your user; try it with `snail "print('hello')"` once the install completes.
|
|
37
|
+
|
|
38
|
+
## β¨ What Makes Snail Unique
|
|
39
|
+
|
|
40
|
+
### Curly Braces, Not Indentation
|
|
41
|
+
|
|
42
|
+
Write Python logic without worrying about whitespace:
|
|
43
|
+
|
|
44
|
+
```snail
|
|
45
|
+
def process(items) {
|
|
46
|
+
for item in items {
|
|
47
|
+
if item > 0 { print(item) }
|
|
48
|
+
else { continue }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Note, since it is jarring to write python with semicolons everywhere,
|
|
54
|
+
semicolons are optional. You can separate statements with newlines.
|
|
55
|
+
|
|
56
|
+
### Awk Mode
|
|
57
|
+
|
|
58
|
+
Process files line-by-line with familiar awk semantics:
|
|
59
|
+
|
|
60
|
+
```snail-awk("hello world\nfoo bar\n")
|
|
61
|
+
BEGIN { print("start") }
|
|
62
|
+
/hello/ { print("matched:", $0) }
|
|
63
|
+
{ print($1, "->", $2) }
|
|
64
|
+
END { print("done") }
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Built-in variables:**
|
|
68
|
+
|
|
69
|
+
| Variable | Description |
|
|
70
|
+
|----------|-------------|
|
|
71
|
+
| `$0` | Current line (with newline stripped) |
|
|
72
|
+
| `$1`, `$2`, ... | Individual fields (whitespace-split) |
|
|
73
|
+
| `$f` | All fields as a list |
|
|
74
|
+
| `$n` | Global line number (across all files) |
|
|
75
|
+
| `$fn` | Per-file line number |
|
|
76
|
+
| `$p` | Current file path |
|
|
77
|
+
| `$m` | Last regex match object |
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
|
|
81
|
+
via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
|
|
82
|
+
before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
|
|
83
|
+
`BEGIN` and `END` are reserved keywords in all modes.
|
|
84
|
+
BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
|
|
85
|
+
```bash
|
|
86
|
+
echo -e "5\n4\n3\n2\n1" | snail --awk --begin 'total = 0' --end 'print("Sum:", total)' '/^[0-9]+/ { total = total + int($1) }'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Map Mode
|
|
90
|
+
|
|
91
|
+
Process files one at a time instead of line-by-line:
|
|
92
|
+
|
|
93
|
+
```snail-map
|
|
94
|
+
BEGIN { print("start") }
|
|
95
|
+
print("File:", $src)
|
|
96
|
+
print("Size:", len($text), "bytes")
|
|
97
|
+
END { print("done") }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Built-in variables:**
|
|
101
|
+
|
|
102
|
+
| Variable | Description |
|
|
103
|
+
|----------|-------------|
|
|
104
|
+
| `$src` | Current file path |
|
|
105
|
+
| `$fd` | Open file handle for the current file |
|
|
106
|
+
| `$text` | Lazy text view of the current file contents |
|
|
107
|
+
|
|
108
|
+
Begin/end blocks can live in the source file (`BEGIN { ... }` / `END { ... }`) or be supplied
|
|
109
|
+
via CLI flags (`-b`/`--begin`, `-e`/`--end`) for setup and teardown. CLI BEGIN blocks run
|
|
110
|
+
before in-file BEGIN blocks; CLI END blocks run after in-file END blocks.
|
|
111
|
+
BEGIN/END blocks are regular Snail blocks, so awk/map-only `$` variables are not available inside them.
|
|
112
|
+
`BEGIN` and `END` are reserved keywords in all modes.
|
|
113
|
+
```bash
|
|
114
|
+
snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Built-in Variables (All Modes)
|
|
118
|
+
|
|
119
|
+
| Variable | Description |
|
|
120
|
+
|----------|-------------|
|
|
121
|
+
| `$e` | Exception object in `expr:fallback?` |
|
|
122
|
+
| `$env` | Environment map (wrapper around `os.environ`) |
|
|
123
|
+
|
|
124
|
+
### Compact Error Handling
|
|
125
|
+
|
|
126
|
+
The `?` operator makes error handling terse yet expressive:
|
|
127
|
+
|
|
128
|
+
```snail
|
|
129
|
+
# Swallow exception, return None
|
|
130
|
+
err = risky()?
|
|
131
|
+
|
|
132
|
+
# Swallow exception, return exception object
|
|
133
|
+
err = risky():$e?
|
|
134
|
+
|
|
135
|
+
# Provide a fallback value (exception available as $e)
|
|
136
|
+
value = js("malformed json"):%{"error": "invalid json"}?
|
|
137
|
+
details = fetch_url("foo.com"):"default html"?
|
|
138
|
+
exception_info = fetch_url("example.com"):$e.http_response_code?
|
|
139
|
+
|
|
140
|
+
# Access attributes directly
|
|
141
|
+
name = risky("")?.__class__.__name__
|
|
142
|
+
args = risky("becomes a list"):[1,2,3]?[0]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Destructuring + `if let` / `while let`
|
|
146
|
+
|
|
147
|
+
Unpack tuples and lists directly, including Python-style rest bindings:
|
|
148
|
+
|
|
149
|
+
```snail
|
|
150
|
+
x, *xs = [1, 2, 3]
|
|
151
|
+
|
|
152
|
+
if let [head, *tail] = [1, 2, 3]; head > 0 {
|
|
153
|
+
print(head, tail)
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`if let`/`while let` only enter the block when the destructuring succeeds. A guard
|
|
158
|
+
after `;` lets you add a boolean check that runs after the bindings are created.
|
|
159
|
+
|
|
160
|
+
Note that this syntax is more powerful than the walrus operator as that does
|
|
161
|
+
not allow for destructuring.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
### Pipeline Operator
|
|
165
|
+
|
|
166
|
+
The `|` operator enables data pipelining as syntactic sugar for nested
|
|
167
|
+
function calls. `x | y | z` becomes `z(y(x))`. This lets you stay in a
|
|
168
|
+
shell mindset.
|
|
169
|
+
|
|
170
|
+
```snail
|
|
171
|
+
# Pipe data to subprocess stdin
|
|
172
|
+
result = "hello\nworld" | $(grep hello)
|
|
173
|
+
|
|
174
|
+
# Chain multiple transformations
|
|
175
|
+
output = "foo\nbar" | $(grep foo) | $(wc -l)
|
|
176
|
+
|
|
177
|
+
# Custom pipeline handlers
|
|
178
|
+
class Doubler {
|
|
179
|
+
def __call__(self, x) { return x * 2 }
|
|
180
|
+
}
|
|
181
|
+
doubled = 21 | Doubler() # yields 42
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Arbitrary callables make up pipelines, even if they have multiple parameters.
|
|
185
|
+
Snail supports this via placeholders.
|
|
186
|
+
```snail
|
|
187
|
+
greeting = "World" | greet("Hello ", _) # greet("Hello ", "World")
|
|
188
|
+
excited = "World" | greet(_, "!") # greet("World", "!")
|
|
189
|
+
formal = "World" | greet("Hello ", suffix=_) # greet("Hello ", "World")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
When a pipeline targets a call expression, the left-hand value is passed to the
|
|
193
|
+
resulting callable. If the call includes a single `_` placeholder, Snail substitutes
|
|
194
|
+
the piped value at that position (including keyword arguments). Only one
|
|
195
|
+
placeholder is allowed in a piped call. Outside of pipeline calls, `_` remains a
|
|
196
|
+
normal identifier.
|
|
197
|
+
|
|
198
|
+
### Built-in Subprocess
|
|
199
|
+
|
|
200
|
+
Shell commands are first-class citizens with capturing and non-capturing
|
|
201
|
+
forms.
|
|
202
|
+
|
|
203
|
+
```snail
|
|
204
|
+
# Capture command output with interpolation
|
|
205
|
+
greeting = $(echo hello {name})
|
|
206
|
+
|
|
207
|
+
# Pipe data through commands
|
|
208
|
+
result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)
|
|
209
|
+
|
|
210
|
+
# Check command status
|
|
211
|
+
@(make build)? # returns exit code on failure instead of raising
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
### Regex Literals
|
|
216
|
+
|
|
217
|
+
Snail supports first class patterns. Think of them as an infinte set.
|
|
218
|
+
|
|
219
|
+
```snail
|
|
220
|
+
if bad_email in /^[\w.]+@[\w.]+$/ {
|
|
221
|
+
print("Valid email")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Compiled regex for reuse
|
|
225
|
+
pattern = /\d{3}-\d{4}/
|
|
226
|
+
match = pattern.search(phone)
|
|
227
|
+
match2 = "555-1212" in pattern
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Snail regexes don't return a match object, rather they return a tuple
|
|
231
|
+
containing all of the match groups, including group 0. Both `search` and `in`
|
|
232
|
+
return the same tuple (or `()` when there is no match).
|
|
233
|
+
|
|
234
|
+
### JSON Queries with JMESPath
|
|
235
|
+
|
|
236
|
+
Parse and query JSON data with the `js()` function and structured pipeline accessor:
|
|
237
|
+
|
|
238
|
+
```snail
|
|
239
|
+
# Parse JSON and query with $[jmespath]
|
|
240
|
+
|
|
241
|
+
# JSON query with JMESPath
|
|
242
|
+
data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
|
|
243
|
+
counts = data | $[stargazers_count]
|
|
244
|
+
|
|
245
|
+
# Inline parsing and querying
|
|
246
|
+
result = js('{{"foo": 12}}') | $[foo]
|
|
247
|
+
|
|
248
|
+
# JSONL parsing returns a list
|
|
249
|
+
names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Full Python Interoperability
|
|
253
|
+
|
|
254
|
+
Snail compiles to Python ASTβimport any Python module, use any library, in any
|
|
255
|
+
environment. Assuming that you are using Python 3.8 or later.
|
|
256
|
+
|
|
257
|
+
## π Quick Start
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
# One-liner: arithmetic + interpolation
|
|
261
|
+
snail 'name="Snail"; print("{name} says: {6 * 7}")'
|
|
262
|
+
|
|
263
|
+
# JSON query with JMESPath
|
|
264
|
+
snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'
|
|
265
|
+
|
|
266
|
+
# Compact error handling with fallback
|
|
267
|
+
snail 'result = int("oops"):"bad int {$e}"?; print(result)'
|
|
268
|
+
|
|
269
|
+
# Regex match and capture
|
|
270
|
+
snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'
|
|
271
|
+
|
|
272
|
+
# Awk mode: print line numbers for matches
|
|
273
|
+
rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'
|
|
274
|
+
|
|
275
|
+
# Environment variables
|
|
276
|
+
snail 'print($env.PATH)'
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## π Documentation
|
|
280
|
+
|
|
281
|
+
Documentation is WIP
|
|
282
|
+
|
|
283
|
+
- **[Language Reference](docs/REFERENCE.md)** β Complete syntax and semantics
|
|
284
|
+
- **[examples/all_syntax.snail](examples/all_syntax.snail)** β Every feature in one file
|
|
285
|
+
- **[examples/awk.snail](examples/awk.snail)** β Awk mode examples
|
|
286
|
+
- **[examples/map.snail](examples/map.snail)** β Map mode examples
|
|
287
|
+
|
|
288
|
+
## π Editor Support
|
|
289
|
+
|
|
290
|
+
Vim/Neovim plugin with syntax highlighting, formatting, and run commands:
|
|
291
|
+
|
|
292
|
+
```vim
|
|
293
|
+
Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
See [extras/vim/README.md](extras/vim/README.md) for details. Tree-sitter grammar available in `extras/tree-sitter-snail/`.
|
|
297
|
+
|
|
298
|
+
## Performance
|
|
299
|
+
|
|
300
|
+
Section is WIP
|
|
301
|
+
|
|
302
|
+
Startup performance is benchmarked with `./benchmarks/startup.py`. On my
|
|
303
|
+
machine snail adds 5 ms of overhead above the regular python3 interpreter.
|
|
304
|
+
|
|
305
|
+
## π οΈ Building from Source
|
|
306
|
+
|
|
307
|
+
### Prerequisites
|
|
308
|
+
|
|
309
|
+
**Python 3.8+** (required at runtime)
|
|
310
|
+
|
|
311
|
+
Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.
|
|
312
|
+
|
|
313
|
+
Installation per platform:
|
|
314
|
+
- **Ubuntu/Debian**: `sudo apt install python3 python3-dev`
|
|
315
|
+
- **Fedora/RHEL**: `sudo dnf install python3 python3-devel`
|
|
316
|
+
- **macOS**: `brew install python@3.12` (or use the system Python 3)
|
|
317
|
+
- **Windows**: Download from [python.org](https://www.python.org/downloads/)
|
|
318
|
+
|
|
319
|
+
### Build, Test, and Install
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Clone the repository
|
|
323
|
+
git clone https://github.com/sudonym1/snail.git
|
|
324
|
+
cd snail
|
|
325
|
+
|
|
326
|
+
make test
|
|
327
|
+
make install
|
|
328
|
+
```
|
|
329
|
+
|