pycli-dsl 0.1.0__tar.gz
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.
- pycli_dsl-0.1.0/PKG-INFO +601 -0
- pycli_dsl-0.1.0/README.md +578 -0
- pycli_dsl-0.1.0/pyproject.toml +44 -0
- pycli_dsl-0.1.0/src/pycli/__init__.py +340 -0
- pycli_dsl-0.1.0/src/pycli/highlighter.py +122 -0
- pycli_dsl-0.1.0/src/pycli/importer.py +214 -0
- pycli_dsl-0.1.0/src/pycli/lexer.py +321 -0
- pycli_dsl-0.1.0/src/pycli/parser.py +499 -0
- pycli_dsl-0.1.0/src/pycli/repl.py +75 -0
- pycli_dsl-0.1.0/src/pycli/runtime.py +1647 -0
- pycli_dsl-0.1.0/src/pycli/transformer.py +812 -0
pycli_dsl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pycli-dsl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python-compatible DevOps DSL transpiler extending Python with first-class shell command execution
|
|
5
|
+
Keywords: devops,dsl,transpiler,shell,bash,cli
|
|
6
|
+
Author: Vito Vessia
|
|
7
|
+
Author-email: Vito Vessia <vito.vessia@wolterskluwer.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: System :: Systems Administration
|
|
16
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Project-URL: Homepage, https://github.com/vitovex/pycli
|
|
19
|
+
Project-URL: Repository, https://github.com/vitovex/pycli
|
|
20
|
+
Project-URL: Issues, https://github.com/vitovex/pycli/issues
|
|
21
|
+
Project-URL: Documentation, https://github.com/vitovex/pycli#readme
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# pycli
|
|
25
|
+
|
|
26
|
+
[](https://github.com/vitovex/pycli/actions/workflows/ci.yml)
|
|
27
|
+
[](https://www.python.org/)
|
|
28
|
+
[](https://github.com/vitovex/pycli/actions)
|
|
29
|
+
|
|
30
|
+
A lightweight Python-compatible DevOps DSL that extends Python with first-class shell command execution.
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
`pycli` transpiles `.spy` files into pure standard Python (`.py`), providing:
|
|
35
|
+
- **Python Readability**: Natural Python syntax and standard ecosystem compatibility.
|
|
36
|
+
- **PowerShell-like Command Invocation**: Run shell commands directly with `$(...)`.
|
|
37
|
+
- **Bash-like Command Composition**: Pipelines (`|`), redirections (`>`, `>>`, `<`), and subcommands.
|
|
38
|
+
- **Native Object Handling**: Seamless integration with Python objects, string interpolation `{var}`, and list expansion (`{*files}`).
|
|
39
|
+
- **Cross-Platform Compatibility**: Tested and verified across Linux (`ubuntu-latest`), macOS (`macos-latest`), and Windows (`windows-latest`).
|
|
40
|
+
- **Zero Custom VM**: Transpiled directly to standard Python and executed on standard CPython.
|
|
41
|
+
|
|
42
|
+
See the full specification in [docs/pycli-grammar.md](docs/pycli-grammar.md).
|
|
43
|
+
|
|
44
|
+
## Example
|
|
45
|
+
|
|
46
|
+
### Source DSL (`deploy.spy`)
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
target_env = "production"
|
|
50
|
+
branch = $(git branch --show-current).text
|
|
51
|
+
|
|
52
|
+
# 1. Pipelines (|) and line streaming
|
|
53
|
+
live_pods = $(kubectl get pods -n {target_env} | grep -E 'Running|Pending').lines
|
|
54
|
+
for pod in live_pods:
|
|
55
|
+
print(f"Active pod: {pod}")
|
|
56
|
+
|
|
57
|
+
# 2. Context managers: temporary directory (cd) and environment variables (env)
|
|
58
|
+
with cd("frontend"), env(NODE_ENV=target_env):
|
|
59
|
+
$(npm ci)!
|
|
60
|
+
build_log = $(npm run build).tee
|
|
61
|
+
|
|
62
|
+
# 3. List expansion (splat), structured JSON output, and safe probing (?)
|
|
63
|
+
artifacts = ["dist/app.js", "dist/app.css"]
|
|
64
|
+
$(gzip -k {*artifacts})
|
|
65
|
+
|
|
66
|
+
cluster = $(az aks show --name prod-cluster --resource-group {target_env}).json
|
|
67
|
+
print(f"Cluster FQDN: {cluster.fqdn}")
|
|
68
|
+
|
|
69
|
+
status = $(curl -sSf http://localhost:8080/health)?
|
|
70
|
+
if status:
|
|
71
|
+
print("Health check passed successfully!")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Transpiled Python (`deploy.py`)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from pycli.runtime import cd, env, run, run_expanded
|
|
78
|
+
|
|
79
|
+
target_env = "production"
|
|
80
|
+
branch = run("git branch --show-current").text
|
|
81
|
+
|
|
82
|
+
# 1. Pipelines (|) and line streaming
|
|
83
|
+
live_pods = run(f"""kubectl get pods -n {target_env} | grep -E 'Running|Pending'""").lines
|
|
84
|
+
for pod in live_pods:
|
|
85
|
+
print(f"Active pod: {pod}")
|
|
86
|
+
|
|
87
|
+
# 2. Context managers: temporary directory (cd) and environment variables (env)
|
|
88
|
+
with cd("frontend"), env(NODE_ENV=target_env):
|
|
89
|
+
run("npm ci", capture=False, check=True)
|
|
90
|
+
build_log = run("npm run build", tee=True)
|
|
91
|
+
|
|
92
|
+
# 3. List expansion (splat), structured JSON output, and safe probing (?)
|
|
93
|
+
artifacts = ["dist/app.js", "dist/app.css"]
|
|
94
|
+
run_expanded("gzip", "-k", *artifacts, capture=False)
|
|
95
|
+
|
|
96
|
+
cluster = run(f"az aks show --name prod-cluster --resource-group {target_env}").json
|
|
97
|
+
print(f"Cluster FQDN: {cluster.fqdn}")
|
|
98
|
+
|
|
99
|
+
status = run("curl -sSf http://localhost:8080/health", suppress_errors=True)
|
|
100
|
+
if status:
|
|
101
|
+
print("Health check passed successfully!")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## CLI Usage
|
|
105
|
+
|
|
106
|
+
### Global Installation (Centralized Command)
|
|
107
|
+
|
|
108
|
+
You can install `pycli` globally into your system `PATH` using `uv tool`:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
uv tool install --editable . --force
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This registers two commands globally on your machine:
|
|
115
|
+
- **`spy`**: Ultra-concise runner for `.spy` scripts.
|
|
116
|
+
- **`pycli`**: The full CLI tool with subcommands.
|
|
117
|
+
|
|
118
|
+
Once installed, you can run `.spy` scripts from **any folder or terminal**:
|
|
119
|
+
```powershell
|
|
120
|
+
spy script.spy
|
|
121
|
+
# or
|
|
122
|
+
pycli script.spy
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Direct Script Execution without Global Install
|
|
126
|
+
|
|
127
|
+
If working inside this repository with `uv`:
|
|
128
|
+
```powershell
|
|
129
|
+
uv run pycli script.spy
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Transpile `.spy` to `.py`
|
|
133
|
+
|
|
134
|
+
```powershell
|
|
135
|
+
# Output to stdout with syntax coloring
|
|
136
|
+
spy transpile script.spy
|
|
137
|
+
|
|
138
|
+
# Output to a file (clean Python without ANSI codes)
|
|
139
|
+
spy transpile script.spy -o script.py
|
|
140
|
+
|
|
141
|
+
# Force / disable color
|
|
142
|
+
spy transpile script.spy --color
|
|
143
|
+
spy transpile script.spy --no-color
|
|
144
|
+
|
|
145
|
+
# Validate generated Python code with ast.parse
|
|
146
|
+
spy transpile script.spy --validate
|
|
147
|
+
|
|
148
|
+
# Disable automatic shell_quote() sanitization on interpolations
|
|
149
|
+
spy transpile script.spy --unsafe-interpolation
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Run `.spy` Scripts
|
|
153
|
+
|
|
154
|
+
```powershell
|
|
155
|
+
# Run a script directly
|
|
156
|
+
spy run script.spy
|
|
157
|
+
# or simply
|
|
158
|
+
spy script.spy
|
|
159
|
+
|
|
160
|
+
# Run with generated Python syntax validation
|
|
161
|
+
spy run --validate script.spy
|
|
162
|
+
|
|
163
|
+
# Run with unsafe interpolation (disables shell_quote)
|
|
164
|
+
spy run --unsafe-interpolation script.spy
|
|
165
|
+
|
|
166
|
+
# Run with warning on untrusted external scripts
|
|
167
|
+
spy run --warn-external script.spy
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Security & Robustness
|
|
173
|
+
|
|
174
|
+
### Automatic Shell Interpolation Sanitization
|
|
175
|
+
By default, all variable interpolations `{var}` and dynamic redirection targets are wrapped with `shell_quote(var)` (`shlex.quote`) during transpilation. This protects against shell injection attacks if variables contain metacharacters (`;`, `&&`, `|`, etc.).
|
|
176
|
+
If raw, unquoted shell syntax expansion is explicitly needed, pass `--unsafe-interpolation` or use `transpile(..., unsafe_interpolation=True)`.
|
|
177
|
+
|
|
178
|
+
### Execution Privilege Model (Not a Sandbox)
|
|
179
|
+
`pycli` executes `.spy` files on standard CPython runtimes with the full privileges and environment of the user running the process. It is **not** a sandbox. When executing `.spy` scripts from external or untrusted sources, use `--warn-external` and verify the script contents.
|
|
180
|
+
|
|
181
|
+
### Command Execution Controls
|
|
182
|
+
The runtime functions `run()`, `run_expanded()`, and `async_run()` support robust controls:
|
|
183
|
+
- **`timeout`**: Terminate hanging processes and raise `CommandTimeoutError`.
|
|
184
|
+
- **`encoding`**: Custom text decoding (default `"utf-8"`, configurable to CP1252, Latin-1, etc.).
|
|
185
|
+
- **`max_output_bytes`**: Cap memory consumption by truncating stdout/stderr beyond a threshold (`res.truncated = True`).
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
## Supported Language Features
|
|
191
|
+
|
|
192
|
+
| Feature | Syntax Example | Target Python Equivalent |
|
|
193
|
+
|---|---|---|
|
|
194
|
+
| **Statement Form** | `$(git status)` | `run("git status", capture=False)` (streams output to console) |
|
|
195
|
+
| **Expression Form** | `res = $(git status)` | `res = run("git status")` (captures stdout/stderr) |
|
|
196
|
+
| **Strict Mode** | `$(git status)!` | `run("git status", capture=False, check=True)` |
|
|
197
|
+
| **Safe Mode** | `$(curl http://...) ?` | `run(..., suppress_errors=True)` (no exception on non-zero exit code) |
|
|
198
|
+
| **Background / Async** | `job = $(docker build .) &` | `run_bg(...)` returning `BackgroundJob` (`.wait()`, `.poll()`, `.kill()`) |
|
|
199
|
+
| **Async / Await** | `res = await $(git pull)` | `await async_run(...)` for `asyncio` workflows |
|
|
200
|
+
| **Live Stream & Capture** | `res = $(npm test).tee` | `run(..., tee=True)` (live streaming to console + captured in `res`) |
|
|
201
|
+
| **Stdin Piping** | `$(kubectl apply -f -).input(yaml)` | `run(..., input=yaml)` |
|
|
202
|
+
| **Output Line Iteration** | `for line in $(git log): ...` | Direct iteration over `res`, or `res.lines` and `res.text` |
|
|
203
|
+
| **Context Managers** | `with cd(dir):`, `with env(K="V"):` | Temporary directory and environment variable scoping |
|
|
204
|
+
| **Quote Interpolation** | `$(echo "{name}" '{raw}')` | Double quotes interpolate `{expr}`; single quotes stay strictly literal |
|
|
205
|
+
| **List Expansion (Splat)** | `$(rm {*files})` | `run_expanded("rm", *files, capture=False)` |
|
|
206
|
+
| **Pipelines** | `$(kubectl get pods \| grep api)` | `run("kubectl get pods \| grep api", capture=False)` |
|
|
207
|
+
| **Redirection** | `$(git status > status.txt)` | `run("git status > status.txt", capture=False)` |
|
|
208
|
+
| **Subcommands** | `$(echo $(git branch --show-current))` | `run("echo $(git branch --show-current)", capture=False)` |
|
|
209
|
+
| **Truthiness** | `if $(git diff --quiet): ...` | `if run("git diff --quiet"): ...` (truthy if `exit_code == 0`) |
|
|
210
|
+
| **Structured Output (JSON)** | `vms = $(az vm list).json` | Navigable `DynamicObj` via `vm.name` or `vm["name"]` |
|
|
211
|
+
| **Interactive REPL** | `spy repl` or `spy` | Interactive shell with on-the-fly transpilation |
|
|
212
|
+
| **CommandResult Properties** | `res = $(git status)` | `res.stdout`, `res.stderr`, `res.exit_code`, `res.lines`, `res.text` |
|
|
213
|
+
| **Modular .spy Imports** | `import devops_utils` | Seamlessly import `.spy` files and packages via Python `importlib` hook |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Complete Language Syntax Reference
|
|
218
|
+
|
|
219
|
+
`spy` is a superset of standard Python. Everything that is valid in Python 3.12+ is fully valid in `.spy`. `spy` introduces the **Command Expression** `$(...)` for seamless command-line execution and shell orchestration.
|
|
220
|
+
|
|
221
|
+
### 1. Command Invocation Forms
|
|
222
|
+
|
|
223
|
+
#### Statement Form (Unassigned)
|
|
224
|
+
When a command expression appears as a standalone line or single-line statement:
|
|
225
|
+
```python
|
|
226
|
+
$(terraform init)
|
|
227
|
+
if should_apply: $(terraform apply -auto-approve)
|
|
228
|
+
```
|
|
229
|
+
- **Execution**: The command is executed and its output (`stdout` and `stderr`) is streamed live to the console in real-time.
|
|
230
|
+
- **Return value**: Discarded (`capture=False`).
|
|
231
|
+
|
|
232
|
+
#### Expression Form (Assigned / Inline)
|
|
233
|
+
When a command expression is assigned to a variable, passed as a function argument, or used in an expression:
|
|
234
|
+
```python
|
|
235
|
+
current_branch = $(git branch --show-current)
|
|
236
|
+
log_output = $(git log -n 10).text
|
|
237
|
+
```
|
|
238
|
+
- **Execution**: The output is captured silently and returned as a `CommandResult` instance.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
### 2. Execution Modifiers
|
|
243
|
+
|
|
244
|
+
Modifiers are placed immediately after the closing parenthesis `)` of a command:
|
|
245
|
+
|
|
246
|
+
| Modifier | Syntax | Behavior | Python Equivalent |
|
|
247
|
+
|---|---|---|---|
|
|
248
|
+
| **Strict (`!`)** | `$(cmd)!` | Raises `CommandError` if exit code != 0 | `run(..., check=True)` |
|
|
249
|
+
| **Safe (`?`)** | `$(cmd)?` | Suppresses errors; never raises exceptions on failure | `run(..., suppress_errors=True)` |
|
|
250
|
+
| **Background (`&`)** | `job = $(cmd) &` | Spawns in background non-blockingly; returns `BackgroundJob` | `run_bg(...)` |
|
|
251
|
+
|
|
252
|
+
#### Examples:
|
|
253
|
+
```python
|
|
254
|
+
# 1. Strict mode: abort pipeline if build fails
|
|
255
|
+
try:
|
|
256
|
+
$(docker build -t app:latest .)!
|
|
257
|
+
except Exception as err:
|
|
258
|
+
print(f"Build failed with exit code: {err.result.exit_code}")
|
|
259
|
+
|
|
260
|
+
# 2. Safe mode: probe an endpoint or optional service without try/except
|
|
261
|
+
probe = $(curl -sSf http://localhost:8080/health)?
|
|
262
|
+
if probe:
|
|
263
|
+
print("Service is healthy!")
|
|
264
|
+
else:
|
|
265
|
+
print(f"Service offline (exit code: {probe.exit_code})")
|
|
266
|
+
|
|
267
|
+
# 3. Background jobs: run long-running tasks concurrently (&)
|
|
268
|
+
job = $(mvn clean package) &
|
|
269
|
+
print("Maven build started in background...")
|
|
270
|
+
result = job.wait()
|
|
271
|
+
print(f"Build finished with code: {result.exit_code}")
|
|
272
|
+
|
|
273
|
+
# 3.1 Waiting for multiple parallel background jobs with wait_all(...)
|
|
274
|
+
j_api = $(deploy-service api) &
|
|
275
|
+
j_web = $(deploy-service web) &
|
|
276
|
+
j_db = $(deploy-service db) &
|
|
277
|
+
|
|
278
|
+
# Accepts variable arguments wait_all(j1, j2, ...) or a list wait_all([j1, j2, ...]):
|
|
279
|
+
all_results = wait_all(j_api, j_web, j_db)
|
|
280
|
+
for res in all_results:
|
|
281
|
+
print(f"Completed: {res.command} (exit code: {res.exit_code})")
|
|
282
|
+
|
|
283
|
+
# 4. Async / Await inside coroutines
|
|
284
|
+
async def pull_repo(name: str):
|
|
285
|
+
res = await $(git -C {name} pull origin main)
|
|
286
|
+
return res.stdout
|
|
287
|
+
|
|
288
|
+
# 4.1 Running multiple async commands in parallel with asyncio.gather
|
|
289
|
+
async def update_all_microservices():
|
|
290
|
+
repos = ["frontend", "backend", "worker"]
|
|
291
|
+
# All pull commands execute concurrently:
|
|
292
|
+
results = await asyncio.gather(*(pull_repo(r) for r in repos))
|
|
293
|
+
print(f"Updated {len(results)} repositories simultaneously.")
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### 3. Chaining Methods & Modifiers
|
|
299
|
+
|
|
300
|
+
You can chain properties and helper methods directly onto command expressions:
|
|
301
|
+
|
|
302
|
+
#### `.json` — Structured JSON Output
|
|
303
|
+
Automatically parses JSON standard output into a navigable `DynamicObj`:
|
|
304
|
+
```python
|
|
305
|
+
pods = $(kubectl get pods -o json).json
|
|
306
|
+
for item in pods.items:
|
|
307
|
+
print(f"Pod: {item.metadata.name} | Status: {item.status.phase}")
|
|
308
|
+
# Supports both dot access and dict access:
|
|
309
|
+
print(f"Namespace: {item['metadata']['namespace']}")
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### `.tee` — Live Console Streaming + Output Capture
|
|
313
|
+
Streams stdout/stderr in real-time to the console while simultaneously capturing the complete result in the variable:
|
|
314
|
+
```python
|
|
315
|
+
# Output is displayed immediately on screen AND stored in 'test_run'
|
|
316
|
+
test_run = $(pytest tests/ -v).tee
|
|
317
|
+
if test_run.exit_code != 0:
|
|
318
|
+
print("Failed test log:", test_run.stderr)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `.input(...)` — Feeding Stdin Data
|
|
322
|
+
Pipes string or binary data directly into the standard input of the subprocess:
|
|
323
|
+
```python
|
|
324
|
+
manifest = """
|
|
325
|
+
apiVersion: v1
|
|
326
|
+
kind: ConfigMap
|
|
327
|
+
metadata:
|
|
328
|
+
name: app-config
|
|
329
|
+
"""
|
|
330
|
+
$(kubectl apply -f -).input(manifest)
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
#### `.lines` & `.text`
|
|
334
|
+
- `.lines`: Returns a `list[str]` of non-empty lines from `stdout` (stripped of trailing newlines).
|
|
335
|
+
- `.text`: Returns the trimmed `stdout` string (`stdout.strip()`).
|
|
336
|
+
```python
|
|
337
|
+
branches = $(git branch --list).lines
|
|
338
|
+
first_line = $(head -n 1 file.txt).text
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### 4. Interpolation & Quoting Semantics
|
|
344
|
+
|
|
345
|
+
`spy` provides precise rules for parameter interpolation to keep shell scripts intuitive:
|
|
346
|
+
|
|
347
|
+
#### Unquoted Variable / Expression Interpolation
|
|
348
|
+
Any Python expression enclosed in `{...}` is evaluated and inserted into the command string:
|
|
349
|
+
```python
|
|
350
|
+
target_cluster = "prod-us-east-1"
|
|
351
|
+
$(kubectl config use-context {target_cluster})
|
|
352
|
+
$(az vm list --resource-group {config.resource_group})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
#### Double Quotes (`"..."`) — Interpolation Enabled
|
|
356
|
+
Double-quoted command strings expand `{expression}`:
|
|
357
|
+
```python
|
|
358
|
+
name = "World"
|
|
359
|
+
$(echo "Hello, {name}!")
|
|
360
|
+
# Evaluates to: echo "Hello, World!"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### Single Quotes (`'...'`) — Strictly Literal
|
|
364
|
+
Single-quoted command strings preserve braces literally. No interpolation occurs inside single quotes. This is critical for shell tools like `awk`, regex patterns, or inline sub-scripts:
|
|
365
|
+
```python
|
|
366
|
+
# Braces remain literal {print $1}:
|
|
367
|
+
$(awk '{print $1}' access.log)
|
|
368
|
+
|
|
369
|
+
# Regex stays literal:
|
|
370
|
+
$(grep -E '^[0-9]{4}-[0-9]{2}' server.log)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### List Expansion / Splat (`{*iterable}`)
|
|
374
|
+
Expands a Python list, tuple, or iterable into space-separated command-line arguments:
|
|
375
|
+
```python
|
|
376
|
+
files = ["service.py", "models.py", "utils.py"]
|
|
377
|
+
$(ruff check {*files})
|
|
378
|
+
# Transpiles to: run_expanded("ruff", "check", *files, capture=False)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
### 5. Preserved Shell Semantics
|
|
384
|
+
|
|
385
|
+
`spy` passes command strings to the underlying shell without interfering with native shell operators.
|
|
386
|
+
|
|
387
|
+
#### Pipelines (`|`)
|
|
388
|
+
Connect the standard output of one command directly to the standard input of the next:
|
|
389
|
+
```python
|
|
390
|
+
# 1. Pipeline in statement form (streaming output directly to terminal)
|
|
391
|
+
$(kubectl get pods -n prod | grep -v Completed | sort)
|
|
392
|
+
|
|
393
|
+
# 2. Pipeline in expression form (captured and iterated)
|
|
394
|
+
failed_jobs = $(docker ps -a | grep "Exited (" | awk '{print $1}').lines
|
|
395
|
+
for container_id in failed_jobs:
|
|
396
|
+
print(f"Removing dead container: {container_id}")
|
|
397
|
+
$(docker rm {container_id})
|
|
398
|
+
|
|
399
|
+
# 3. Chaining with Python processing
|
|
400
|
+
build_errors = $(cargo check 2>&1 | grep "error\[E").lines
|
|
401
|
+
if build_errors:
|
|
402
|
+
print(f"Found {len(build_errors)} compile errors:")
|
|
403
|
+
for err in build_errors:
|
|
404
|
+
print(" -", err)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Redirections (`>`, `>>`, `<`)
|
|
408
|
+
Direct process outputs or inputs to and from filesystem files:
|
|
409
|
+
```python
|
|
410
|
+
# Overwrite file with stdout (>)
|
|
411
|
+
$(terraform output -json > tf_outputs.json)
|
|
412
|
+
|
|
413
|
+
# Append to log file (>>)
|
|
414
|
+
$(date >> deployment.log)
|
|
415
|
+
$(echo "Deployed by {user} on {branch}" >> deployment.log)
|
|
416
|
+
|
|
417
|
+
# Read input from file (<)
|
|
418
|
+
$(mysql -u root -p{db_pass} my_database < migration.sql)!
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Subcommands (`$(...)`)
|
|
422
|
+
Inner shell command substitutions are handled directly by the shell runtime:
|
|
423
|
+
```python
|
|
424
|
+
# Create timestamped tarball using subshell date command:
|
|
425
|
+
$(tar -czf backup-$(date +%Y%m%d).tar.gz /var/data)
|
|
426
|
+
|
|
427
|
+
# Create git release tag from file content:
|
|
428
|
+
$(git tag release-$(cat VERSION))
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
### 6. The `CommandResult` Object
|
|
434
|
+
|
|
435
|
+
Captured command expressions return a `CommandResult` instance with rich inspection capabilities:
|
|
436
|
+
|
|
437
|
+
| Attribute / Method | Type | Description |
|
|
438
|
+
|---|---|---|
|
|
439
|
+
| `res.stdout` | `str` | Full standard output |
|
|
440
|
+
| `res.stderr` | `str` | Full standard error |
|
|
441
|
+
| `res.exit_code` | `int` | Process exit status code (`0` = success) |
|
|
442
|
+
| `res.duration` | `float` | Command execution time in seconds |
|
|
443
|
+
| `res.command` | `str` | Exact command string executed |
|
|
444
|
+
| `res.lines` | `list[str]` | List of non-empty stdout lines |
|
|
445
|
+
| `res.text` | `str` | Trimmed standard output (`stdout.strip()`) |
|
|
446
|
+
| `res.json` | `DynamicObj` | Parsed JSON object / list |
|
|
447
|
+
| `for line in res:` | `Iterator[str]` | Iterate directly over lines in stdout |
|
|
448
|
+
| `res[index]` | `str` | Access a specific line by index |
|
|
449
|
+
| `bool(res)` | `bool` | **Truthiness**: `True` if `exit_code == 0`, else `False` |
|
|
450
|
+
|
|
451
|
+
#### Truthiness Example:
|
|
452
|
+
```python
|
|
453
|
+
if $(git diff --quiet):
|
|
454
|
+
print("Working tree clean")
|
|
455
|
+
else:
|
|
456
|
+
print("Uncommitted changes detected")
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### 7. Built-in Context Managers
|
|
462
|
+
|
|
463
|
+
Every `.spy` script and module automatically has access to `cd()` and `env()` as first-class primitives without requiring any manual `import` statement.
|
|
464
|
+
|
|
465
|
+
#### `with cd(path)` — Directory Navigation
|
|
466
|
+
Changes current working directory for the duration of the `with` block and **guarantees** restoration to the previous directory upon exiting, even if an exception occurs:
|
|
467
|
+
```python
|
|
468
|
+
# 1. Work in a specific subproject directory
|
|
469
|
+
with cd("services/billing"):
|
|
470
|
+
$(cargo build --release)!
|
|
471
|
+
$(cargo test)
|
|
472
|
+
|
|
473
|
+
# 2. Nested directory navigation
|
|
474
|
+
with cd("packages"):
|
|
475
|
+
with cd("frontend"):
|
|
476
|
+
$(npm test)
|
|
477
|
+
# Automatically back in "packages"
|
|
478
|
+
# Automatically back in the root directory
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### `with env(**kwargs)` — Temporary Environment Variables
|
|
482
|
+
Sets or overrides environment variables for the duration of the `with` block and safely restores the original environment afterwards:
|
|
483
|
+
```python
|
|
484
|
+
with env(AWS_DEFAULT_REGION="eu-west-1", STAGE="staging"):
|
|
485
|
+
$(aws s3 ls)
|
|
486
|
+
$(serverless deploy)
|
|
487
|
+
# AWS_DEFAULT_REGION and STAGE are restored to their original values
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
#### Combining `cd()` and `env()`
|
|
491
|
+
You can combine multiple context managers cleanly on a single line:
|
|
492
|
+
```python
|
|
493
|
+
with cd("apps/backend"), env(DATABASE_URL="postgres://test:5432/db", LOG_LEVEL="DEBUG"):
|
|
494
|
+
$(alembic upgrade head)!
|
|
495
|
+
$(pytest -v)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
---
|
|
499
|
+
|
|
500
|
+
### 8. Modular Architecture (`.spy` Imports)
|
|
501
|
+
|
|
502
|
+
You can structure large DevOps and infrastructure projects into modular files. `.spy` scripts can import other `.spy` scripts or packages natively:
|
|
503
|
+
|
|
504
|
+
```python
|
|
505
|
+
# main.spy
|
|
506
|
+
import devops_utils
|
|
507
|
+
from infrastructure.cloud import deploy_cluster
|
|
508
|
+
|
|
509
|
+
status = devops_utils.get_git_status()
|
|
510
|
+
deploy_cluster("production")
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The underlying import hook compiles `.spy` files into standard Python bytecode on the fly with zero disk pollution.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
### 9. Interactive REPL
|
|
518
|
+
|
|
519
|
+
`spy` includes a dedicated interactive read-eval-print loop with instant transpilation:
|
|
520
|
+
|
|
521
|
+
```powershell
|
|
522
|
+
# Launch interactive shell
|
|
523
|
+
spy repl
|
|
524
|
+
# or simply
|
|
525
|
+
spy
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
```text
|
|
529
|
+
>>> branch = $(git branch --show-current).text
|
|
530
|
+
>>> branch
|
|
531
|
+
'main'
|
|
532
|
+
>>> for file in $(git ls-files):
|
|
533
|
+
... if file.endswith(".spy"):
|
|
534
|
+
... print("Found spy script:", file)
|
|
535
|
+
...
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Examples
|
|
541
|
+
|
|
542
|
+
The repository includes runnable `.spy` examples in the `examples/` directory:
|
|
543
|
+
|
|
544
|
+
- [examples/demo.spy](examples/demo.spy): Basic overview demonstrating variable interpolation, list expansion, and status checking.
|
|
545
|
+
- [examples/syntax_reference.spy](examples/syntax_reference.spy): Comprehensive, executable reference covering every syntax construct and execution mode.
|
|
546
|
+
- [examples/advanced_features.spy](examples/advanced_features.spy): Practical demonstration of the 8 advanced productivity features (streaming, `.tee`, `.input(...)`, `cd()`/`env()`, background jobs `&`, safe mode `?`, quote semantics).
|
|
547
|
+
- [examples/parallel_async_jobs.spy](examples/parallel_async_jobs.spy): Concurrent process orchestration showing how to launch multiple background jobs with `&`, await them all with `wait_all(...)`, and coordinate async coroutines with `asyncio.gather(...)`.
|
|
548
|
+
- [examples/complex_devops.spy](examples/complex_devops.spy): Advanced pipeline orchestrator demonstrating Python dataclasses, object-oriented design, dynamic JSON parsing, strict mode error handling (`try/except CommandError`), and splat expansion.
|
|
549
|
+
- [examples/modular_demo.spy](examples/modular_demo.spy) & [examples/devops_utils.spy](examples/devops_utils.spy): Modular multi-file architecture demonstrating how a `.spy` file can seamlessly import reusable functions, classes, and shell workflows from another `.spy` file or package.
|
|
550
|
+
|
|
551
|
+
Run them directly with `spy`:
|
|
552
|
+
```powershell
|
|
553
|
+
spy examples/demo.spy
|
|
554
|
+
spy examples/syntax_reference.spy
|
|
555
|
+
spy examples/advanced_features.spy
|
|
556
|
+
spy examples/parallel_async_jobs.spy
|
|
557
|
+
spy examples/complex_devops.spy
|
|
558
|
+
spy examples/modular_demo.spy
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
## Getting Started
|
|
563
|
+
|
|
564
|
+
### Prerequisites
|
|
565
|
+
|
|
566
|
+
- Python `>= 3.12`
|
|
567
|
+
- `uv` package manager
|
|
568
|
+
- Supported Platforms: Linux, macOS, and Windows (all tested in CI)
|
|
569
|
+
|
|
570
|
+
### Development Setup
|
|
571
|
+
|
|
572
|
+
```powershell
|
|
573
|
+
# Install dependencies
|
|
574
|
+
uv sync
|
|
575
|
+
|
|
576
|
+
# Run tests
|
|
577
|
+
uv run pytest
|
|
578
|
+
|
|
579
|
+
# Run CLI help
|
|
580
|
+
uv run pycli --help
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Editor Support
|
|
584
|
+
|
|
585
|
+
Language extensions and syntax highlighting definitions are provided in the `editors/` directory:
|
|
586
|
+
|
|
587
|
+
- **Visual Studio Code & Antigravity IDE** ([editors/vscode](editors/vscode)): Full Python + embedded shell grammar, command delimiter highlighting, interpolation scoping, and snippets (`cmd`, `cmdvar`, `cmdjson`, `cmdstrict`, `cmdsplat`, `cmdif`).
|
|
588
|
+
- To install in VSCode:
|
|
589
|
+
```powershell
|
|
590
|
+
Copy-Item -Recurse -Force "editors/vscode" "$env:USERPROFILE\.vscode\extensions\pycli-vscode"
|
|
591
|
+
```
|
|
592
|
+
- To install in Antigravity IDE:
|
|
593
|
+
```powershell
|
|
594
|
+
Copy-Item -Recurse -Force "editors/vscode" "$env:USERPROFILE\.antigravity-ide\extensions\pycli-vscode"
|
|
595
|
+
```
|
|
596
|
+
- **Notepad++** ([editors/notepadplusplus](editors/notepadplusplus)): User Defined Language (UDL) definition for `.spy` files.
|
|
597
|
+
- To install locally:
|
|
598
|
+
```powershell
|
|
599
|
+
Copy-Item -Force "editors/notepadplusplus/pycli.xml" "$env:APPDATA\Notepad++\userDefineLangs\"
|
|
600
|
+
```
|
|
601
|
+
|