Skip to main content

Snail programming language interpreter

Project description

Snail logo

Snail

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.

AI Slop!

Snail is me learning how to devlop code using LLMs. I think its neat, and maybe useful. I don't think this is high quality. I am going to try and LLM my way into something good, but its certainly not there yet.

Installing Snail

pip install snail-lang
-or-
uv tool install snail-lang

That installs the snail CLI for your user; try it with snail "print('hello')" once the install completes.

✨ What Makes Snail Unique

Curly Braces, Not Indentation

Write Python logic without worrying about whitespace:

def process(items) {
    for item in items {
        if item > 0 { print(item) }
        else { continue }
    }
}

Note, since it is jarring to write python with semicolons everywhere, semicolons are optional. You can separate statements with newlines.

Awk Mode

Process files line-by-line with familiar awk semantics:

BEGIN { print("start") }
/hello/ { print("matched:", $0) }
{ print($1, "->", $2) }
END { print("done") }

Built-in variables:

Variable Description
$0 Current line (with newline stripped)
$1, $2, ... Individual fields (whitespace-split)
$f All fields as a list
$n Global line number (across all files)
$fn Per-file line number
$src Current file path
$m Last regex match object

Begin/end blocks can live in the source file (BEGIN { ... } / END { ... }) or be supplied via CLI flags (-b/--begin, -e/--end) for setup and teardown. CLI BEGIN blocks run before in-file BEGIN blocks; CLI END blocks run after in-file END blocks. BEGIN and END are reserved keywords in all modes. BEGIN/END blocks are regular Snail blocks, so awk/map-only $ variables are not available inside them.

echo -e "5\n4\n3\n2\n1" | snail --awk --begin 'total = 0' --end 'print("Sum:", total)' '/^[0-9]+/ { total = total + int($1) }'

Map Mode

Process files one at a time instead of line-by-line:

BEGIN { print("start") }
print("File:", $src)
print("Size:", len($text), "bytes")
END { print("done") }

Built-in variables:

Variable Description
$src Current file path
$fd Open file handle for the current file
$text Lazy text view of the current file contents

Begin/end blocks can live in the source file (BEGIN { ... } / END { ... }) or be supplied via CLI flags (-b/--begin, -e/--end) for setup and teardown. CLI BEGIN blocks run before in-file BEGIN blocks; CLI END blocks run after in-file END blocks. BEGIN/END blocks are regular Snail blocks, so awk/map-only $ variables are not available inside them. BEGIN and END are reserved keywords in all modes.

snail --map --begin "print('start')" --end "print('done')" "print($src)" *.txt

Built-in Variables (All Modes)

Variable Description
$e Exception object in expr:fallback?
$env Environment map (wrapper around os.environ)

Begin/End Blocks

Regular Snail programs can include BEGIN { ... } and END { ... } blocks for setup and teardown. These blocks can also be supplied via CLI flags (-b/--begin, -e/--end) in all modes. CLI BEGIN blocks run before in-file BEGIN blocks; CLI END blocks run after in-file END blocks. BEGIN/END blocks are regular Snail blocks, so awk/map-only $ variables are not available inside them.

print("running")
BEGIN { print("start") }
END { print("done") }

In regular mode, my main use case for this feature is passing unexported variables

my_bashvar=123
snail -b x=$my_bashvar 'int(x) + 1'

This is roughly the same as using $env to access an exported variable.

my_bashvar=123 snail 'int($env.my_bashvar) + 1'

Compact Error Handling

The ? operator makes error handling terse yet expressive:

# Swallow exception, return None
err = risky()?

# Swallow exception, return exception object
err = risky():$e?

# Provide a fallback value (exception available as $e)
value = js("malformed json"):%{"error": "invalid json"}?
details = fetch_url("foo.com"):"default html"?
exception_info = fetch_url("example.com"):$e.http_response_code?

# Access attributes directly
name = risky("")?.__class__.__name__
args = risky("becomes a list"):[1,2,3]?[0]

Destructuring + if let / while let

Unpack tuples and lists directly, including Python-style rest bindings:

x, *xs = [1, 2, 3]

if let [head, *tail] = [1, 2, 3]; head > 0 {
    print(head, tail)
}

if let/while let only enter the block when the destructuring succeeds. A guard after ; lets you add a boolean check that runs after the bindings are created.

Note that this syntax is more powerful than the walrus operator as that does not allow for destructuring.

Pipeline Operator

The | operator enables data pipelining as syntactic sugar for nested function calls. x | y | z becomes z(y(x)). This lets you stay in a shell mindset.

# Pipe data to subprocess stdin
result = "hello\nworld" | $(grep hello)

# Chain multiple transformations
output = "foo\nbar" | $(grep foo) | $(wc -l)

# Custom pipeline handlers
class Doubler {
    def __call__(self, x) { return x * 2 }
}
doubled = 21 | Doubler()  # yields 42

Arbitrary callables make up pipelines, even if they have multiple parameters. Snail supports this via placeholders.

greeting = "World" | greet("Hello ", _)  # greet("Hello ", "World")
excited = "World" | greet(_, "!")        # greet("World", "!")
formal = "World" | greet("Hello ", suffix=_)  # greet("Hello ", "World")

When a pipeline targets a call expression, the left-hand value is passed to the resulting callable. If the call includes a single _ placeholder, Snail substitutes the piped value at that position (including keyword arguments). Only one placeholder is allowed in a piped call. Outside of pipeline calls, _ remains a normal identifier.

Built-in Subprocess

Shell commands are first-class citizens with capturing and non-capturing forms.

# Capture command output with interpolation
greeting = $(echo hello {name})

# Pipe data through commands
result = "foo\nbar\nbaz" | $(grep bar) | $(cat -n)

# Check command status
@(make build)?  # returns exit code on failure instead of raising

Regex Literals

Snail supports first class patterns. Think of them as an infinte set.

if bad_email in /^[\w.]+@[\w.]+$/ {
    print("Valid email")
}

# Compiled regex for reuse
pattern = /\d{3}-\d{4}/
match = pattern.search(phone)
match2 = "555-1212" in pattern

Snail regexes don't return a match object, rather they return a tuple containing all of the match groups, including group 0. Both search and in return the same tuple (or () when there is no match).

JSON Queries with JMESPath

Parse and query JSON data with the js() function and structured pipeline accessor:

# Parse JSON and query with $[jmespath]

# JSON query with JMESPath
data = js($(curl -s https://api.github.com/repos/sudonym1/snail))
counts = data | $[stargazers_count]

# Inline parsing and querying
result = js('{{"foo": 12}}') | $[foo]

# JSONL parsing returns a list
names = js('{{"name": "Ada"}}\n{{"name": "Lin"}}') | $[[*].name]

Snail rewrites JMESPath queries in $[query] so that double-quoted segments are treated as string literals. This lets you write $[items[?ifname=="eth0"].ifname] inside a single-quoted shell command. If you need JMESPath quoted identifiers (for keys like "foo-bar"), escape the quotes in the query (for example, $[\"foo-bar\"]). JSON literal backticks (`...`) are left unchanged.

Full Python Interoperability

Snail compiles to Python AST—import any Python module, use any library, in any environment. Assuming that you are using Python 3.8 or later.

🚀 Quick Start

# One-liner: arithmetic + interpolation
snail 'name="Snail"; print("{name} says: {6 * 7}")'

# JSON query with JMESPath
snail 'js($(curl -s https://api.github.com/repos/sudonym1/snail)) | $[stargazers_count]'

# Compact error handling with fallback
snail 'result = int("oops"):"bad int {$e}"?; print(result)'

# Regex match and capture
snail 'if let [_, user, domain] = "user@example.com" in /^[\w.]+@([\w.]+)$/ { print(domain) }'

# Awk mode: print line numbers for matches
rg -n "TODO" README.md | snail --awk '/TODO/ { print("{$n}: {$0}") }'

# Environment variables
snail 'print($env.PATH)'

📚 Documentation

Documentation is WIP

🔌 Editor Support

Vim/Neovim plugin with Tree-sitter-based highlighting (Neovim), formatting, and run commands:

Plug 'sudonym1/snail', { 'rtp': 'extras/vim' }

See extras/vim/README.md for details. Tree-sitter grammar available in extras/tree-sitter-snail/.

Performance

Section is WIP

Startup performance is benchmarked with ./benchmarks/startup.py. On my machine snail adds 5 ms of overhead above the regular python3 interpreter.

🛠️ Building from Source

Prerequisites

Python 3.8+ (required at runtime)

Snail runs in-process via a Pyo3 extension module, so it uses the active Python environment.

Installation per platform:

  • Ubuntu/Debian: sudo apt install python3 python3-dev
  • Fedora/RHEL: sudo dnf install python3 python3-devel
  • macOS: brew install python@3.12 (or use the system Python 3)
  • Windows: Download from python.org

Build, Test, and Install

# Clone the repository
git clone https://github.com/sudonym1/snail.git
cd snail

make test
make install

Arch Linux (PKGBUILD)

An Arch package build file is available at extras/arch/PKGBUILD.

mkdir -p /tmp/snail-pkg
cp extras/arch/PKGBUILD /tmp/snail-pkg/
cd /tmp/snail-pkg

# Update pkgver and sha256sums as needed, then build and install
makepkg -si

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

snail_lang-0.7.10.tar.gz (86.3 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

snail_lang-0.7.10-cp38-abi3-win_amd64.whl (690.4 kB view details)

Uploaded CPython 3.8+Windows x86-64

snail_lang-0.7.10-cp38-abi3-manylinux_2_34_x86_64.whl (5.1 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

snail_lang-0.7.10-cp38-abi3-macosx_11_0_arm64.whl (698.2 kB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file snail_lang-0.7.10.tar.gz.

File metadata

  • Download URL: snail_lang-0.7.10.tar.gz
  • Upload date:
  • Size: 86.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for snail_lang-0.7.10.tar.gz
Algorithm Hash digest
SHA256 fc718c901388b77f1afc0b7df6679f9e0b6765b6acf21dd61949e0de89dfc3a6
MD5 cf52a5f0db5f38ff3b0526d17794688e
BLAKE2b-256 bb26085aa8c14da056edac3c4cead2d9301316d2106e49fe33ebe45455ceb30b

See more details on using hashes here.

Provenance

The following attestation bundles were made for snail_lang-0.7.10.tar.gz:

Publisher: release.yml on sudonym1/snail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file snail_lang-0.7.10-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: snail_lang-0.7.10-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 690.4 kB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for snail_lang-0.7.10-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 e8ac4a23ff319ef9a1a0c1ccc743b710987f42ff3dc1f5b9753252a090f640eb
MD5 cc2fbf24950ac50a80846f4fa59ebfe0
BLAKE2b-256 5ea4d4e5781519973af3fa961031b0cc4f546c72345332d12edae8bd0cc9f751

See more details on using hashes here.

Provenance

The following attestation bundles were made for snail_lang-0.7.10-cp38-abi3-win_amd64.whl:

Publisher: release.yml on sudonym1/snail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file snail_lang-0.7.10-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for snail_lang-0.7.10-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 1a9d6af5cde721002f82a61b63db8dd5121825a086b47f003debdf46f7fdb5d4
MD5 40d923a458ef6b4a0e8494ce8f5fe159
BLAKE2b-256 a0b90c31e510fee30e3fb23c0b7627bde5645d7e5e07d021c7d4dfc7e0c63b9d

See more details on using hashes here.

Provenance

The following attestation bundles were made for snail_lang-0.7.10-cp38-abi3-manylinux_2_34_x86_64.whl:

Publisher: release.yml on sudonym1/snail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file snail_lang-0.7.10-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for snail_lang-0.7.10-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 c7198c6bc324897d8073a5191a121f4128d39104aa7d41435db73cf50ad018ec
MD5 7eba2be03cdb9ea223134284af69f2ca
BLAKE2b-256 87391da1d7439acab6db0af05ef7b5d55342acfb5f99ab28f4a682c1fe9326b0

See more details on using hashes here.

Provenance

The following attestation bundles were made for snail_lang-0.7.10-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: release.yml on sudonym1/snail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page