PyBash 0.3.5__tar.gz → 0.4.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.
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, macos-latest]
18
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: astral-sh/setup-uv@v10.3.0
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ enable-cache: true
25
+ - run: uv sync
26
+ - run: make lint
27
+ - run: make test
@@ -0,0 +1,18 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ contents: read
13
+ id-token: write # PyPI trusted publishing
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: astral-sh/setup-uv@v10.3.0
17
+ - run: uv build
18
+ - run: uv publish
@@ -0,0 +1,129 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
pybash-0.4.0/Makefile ADDED
@@ -0,0 +1,30 @@
1
+ check: format lint test
2
+
3
+ SOURCE_FILES=pybash tests
4
+
5
+ install:
6
+ uv sync
7
+
8
+ test:
9
+ uv run pytest -vv -rs
10
+
11
+ format:
12
+ uv run ruff format ${SOURCE_FILES}
13
+ uv run ruff check ${SOURCE_FILES} --fix
14
+
15
+ lint:
16
+ uv run ruff format --check --diff ${SOURCE_FILES}
17
+ uv run ruff check ${SOURCE_FILES}
18
+ uv run mypy
19
+
20
+ demo:
21
+ cd examples && uv run pybash demo.py
22
+
23
+ clean:
24
+ rm -rf build/ dist/ *.egg-info .pytest_cache .mypy_cache .ruff_cache
25
+ find . -name '__pycache__' -exec rm -rf {} +
26
+
27
+ package: clean
28
+ uv build
29
+
30
+ .PHONY: check install test format lint demo clean package
pybash-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: PyBash
3
+ Version: 0.4.0
4
+ Summary: >execute bash commands from python easily
5
+ Project-URL: Repository, https://github.com/jaykv/PyBash
6
+ Author-email: Jay <jay.github0@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: bash,shell,subprocess,transpiler
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Programming Language :: Python :: 3.15
18
+ Classifier: Topic :: System :: Shells
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.11
21
+ Provides-Extra: script
22
+ Description-Content-Type: text/markdown
23
+
24
+ # PyBash
25
+
26
+ ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/jaykv/pybash/ci.yml?branch=main)
27
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pybash)
28
+ ![PyPI](https://img.shields.io/pypi/v/pybash)
29
+ ![GitHub](https://img.shields.io/github/license/jaykv/pybash)
30
+
31
+ Write shell commands directly in Python. PyBash rewrites lines that start with `$` or `>` into
32
+ `subprocess` calls, so you get the brevity of a shell script with the rest of Python around it.
33
+
34
+ ```python
35
+ branch = ($git rev-parse --abbrev-ref HEAD).decode().strip()
36
+ $git fetch origin && git rebase origin/{{branch}} || echo "rebase failed"
37
+ count = $git log --oneline | wc -l
38
+ print(f"{branch}: {int(count)} commits")
39
+ ```
40
+
41
+ - `$` commands never go through a shell. Arguments are split at transform time, and pipes, redirects
42
+ and `&&` / `||` / `;` are run by PyBash itself, so a value can never turn into shell syntax.
43
+ - `>` commands are handed to the shell as written, for globbing and expansion. Interpolated values are
44
+ quoted for you.
45
+ - One line in, one line out: tracebacks point at the line you wrote.
46
+ - No dependencies. Python 3.11+.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install pybash
52
+ ```
53
+
54
+ ## Use
55
+
56
+ ### Run a script
57
+
58
+ ```bash
59
+ pybash script.py [args...] # or: python -m pybash script.py
60
+ pybash --show script.py # print the generated Python instead of running it
61
+ pybash -c '$echo hello'
62
+ ```
63
+
64
+ Modules the script imports from its own project may use the syntax too.
65
+
66
+ ### Transform source yourself
67
+
68
+ ```python
69
+ from pybash import transform
70
+
71
+ transform("$echo hello world") # 'subprocess.run(["echo", "hello", "world"])'
72
+ ```
73
+
74
+ Plain commands compile to bare `subprocess` calls. Commands with operators call a small helper in
75
+ `pybash.runtime`; by default the generated code imports it in place, so the output only needs
76
+ `import subprocess`. For tidier output, name the helper yourself and add the header:
77
+
78
+ ```python
79
+ import pybash
80
+
81
+ body = pybash.transform(source, runtime=pybash.RUNTIME_NAME)
82
+ code = pybash.header() + body # imports pybash.runtime
83
+ code = pybash.header(standalone=True) + body # pastes the helper in: no pybash needed at run time
84
+ ```
85
+
86
+ `pybash.compile_source(source, filename)` returns a code object ready for `exec`, and
87
+ `pybash.add_hook()` installs an import hook for the current process.
88
+
89
+ Mistakes raise `PybashSyntaxError`, a `SyntaxError` whose `filename`, `lineno`, `offset` and `text`
90
+ point at the original line.
91
+
92
+ ## Syntax
93
+
94
+ ### Run: `$command`
95
+
96
+ ```python
97
+ $python --version
98
+ $cp test.txt test_copy.txt
99
+ ```
100
+
101
+ Returns the `subprocess.CompletedProcess`. A non-zero exit does not raise.
102
+
103
+ ### Capture: `x = $command`, `($command)`, `$(command)`
104
+
105
+ ```python
106
+ out = $cat test.txt
107
+ print(($cat test.txt).decode().strip())
108
+ lines = $(wc -l test.txt).decode().split()[0]
109
+ ```
110
+
111
+ Captures are `bytes`, and raise `subprocess.CalledProcessError` on a non-zero exit.
112
+ A capture not wrapped in brackets runs to the end of the line.
113
+
114
+ ### Operators
115
+
116
+ | Operator | Meaning |
117
+ |---|---|
118
+ | `a \| b` | pipe stdout of `a` into `b` |
119
+ | `a \|& b` | pipe stdout and stderr |
120
+ | `a && b`, `a \|\| b`, `a ; b` | run `b` if `a` succeeded / failed / either way |
121
+ | `< file` | read stdin from a file |
122
+ | `> file`, `>> file` | write / append stdout |
123
+ | `2> file`, `2>> file` | write / append stderr |
124
+ | `&> file`, `&>> file` | write / append both |
125
+ | `2>&1`, `>&2` | send stderr to stdout / stdout to stderr |
126
+
127
+ ```python
128
+ $sort < in.txt | uniq -c | sort -rn > counts.txt
129
+ $make build > build.log 2>&1 && echo ok || echo failed
130
+ newest = $ls -t | head -n 1
131
+ ```
132
+
133
+ Operators work without surrounding spaces (`a|b`, `2>err.log`). Quote or escape one to pass it as
134
+ text: `$echo 'a && b' \| c`. Several output files on one command all receive the output
135
+ (`$echo hi > a.txt >> b.txt`).
136
+
137
+ Background jobs (`&`), here-documents (`<<`) and other file descriptors are not supported in `$`
138
+ commands; use a `>` shell command for those.
139
+
140
+ ### Quoting
141
+
142
+ `$` commands follow POSIX shell quoting: single quotes are literal, double quotes allow `\"` and `\\`,
143
+ and a backslash escapes the next character. An unquoted `#` at the start of a word begins a comment.
144
+ There is no globbing, `~` or `$VAR` expansion; use `>` when you need the shell.
145
+
146
+ A trailing backslash continues the command on the next line.
147
+
148
+ ### Interpolation
149
+
150
+ | Form | Meaning |
151
+ |---|---|
152
+ | `{{ expr }}` | the value of any Python expression, converted with `str()` |
153
+ | `f{ expr }` | the same, with f-string formatting available: `f{n:03d}`, `f{name!r}` |
154
+
155
+ ```python
156
+ $git {{command}} {{get_option(command)}}
157
+ $kubectl get pods --show-{{display_type}}=true -n f{namespace}
158
+ $echo "{{user}} has f{count:,} files"
159
+ ```
160
+
161
+ In a `$` command an interpolated value is always exactly one argument, whatever it contains.
162
+ Interpolation also applies inside quotes. To write the markers literally, escape them: `\{{` and `\f{`.
163
+
164
+ ### Shell: `>command`
165
+
166
+ ```python
167
+ >ls .github/* | wc -l
168
+ >echo "hello {{name}}" > out.txt
169
+ ```
170
+
171
+ The line is passed to the shell untouched. Interpolated values are quoted for where they appear
172
+ (bare, inside `"..."` or inside `'...'`), so they are always data. To splice shell syntax in on
173
+ purpose, use `{{! expr }}`:
174
+
175
+ ```python
176
+ pattern = "*.txt"
177
+ >ls {{! pattern }}
178
+ ```
179
+
180
+ ## Upgrading from 0.3
181
+
182
+ - Python 3.11+ is required; the `ideas` and `token-utils` dependencies are gone.
183
+ - Arguments reach the command exactly as a shell would pass them. Previously backslashes were
184
+ re-interpreted by Python (`'a\\nb'` became a newline) and a `"` inside an argument produced invalid code.
185
+ - `{{ x }}` is converted with `str()`, may contain spaces and quotes, and no longer raises `InvalidInterpolation`
186
+ for them.
187
+ - Interpolated values in `>` commands are now quoted. Use `{{! x }}` for the old raw behavior.
188
+ - `>` commands keep their quoting; previously quotes were stripped before the shell saw them.
189
+ - `a = $x | y` now captures the pipeline's output instead of assigning a process object, and no
190
+ `cmd1` / `fout` variables leak into your code.
191
+ - `$a > f1 > f2` writes the output to both files; previously the second file received a copy of the first.
192
+ - `&&`, `||`, `;`, `2>`, `&>`, `2>&1`, `|&` and `$( )` are new. `&&` used to raise `NotImplementedError`.
193
+ - Generated lists are formatted `["ls", "-la"]` and the result no longer ends with an added newline.
194
+ - Text such as `"total = $5"` or `"($x)"` inside a Python string is no longer mistaken for a command.
195
+
196
+ ## Development
197
+
198
+ ```bash
199
+ uv sync
200
+ make check # format, lint (ruff + mypy), test
201
+ make demo
202
+ ```
pybash-0.4.0/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # PyBash
2
+
3
+ ![GitHub Workflow Status (with branch)](https://img.shields.io/github/actions/workflow/status/jaykv/pybash/ci.yml?branch=main)
4
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/pybash)
5
+ ![PyPI](https://img.shields.io/pypi/v/pybash)
6
+ ![GitHub](https://img.shields.io/github/license/jaykv/pybash)
7
+
8
+ Write shell commands directly in Python. PyBash rewrites lines that start with `$` or `>` into
9
+ `subprocess` calls, so you get the brevity of a shell script with the rest of Python around it.
10
+
11
+ ```python
12
+ branch = ($git rev-parse --abbrev-ref HEAD).decode().strip()
13
+ $git fetch origin && git rebase origin/{{branch}} || echo "rebase failed"
14
+ count = $git log --oneline | wc -l
15
+ print(f"{branch}: {int(count)} commits")
16
+ ```
17
+
18
+ - `$` commands never go through a shell. Arguments are split at transform time, and pipes, redirects
19
+ and `&&` / `||` / `;` are run by PyBash itself, so a value can never turn into shell syntax.
20
+ - `>` commands are handed to the shell as written, for globbing and expansion. Interpolated values are
21
+ quoted for you.
22
+ - One line in, one line out: tracebacks point at the line you wrote.
23
+ - No dependencies. Python 3.11+.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install pybash
29
+ ```
30
+
31
+ ## Use
32
+
33
+ ### Run a script
34
+
35
+ ```bash
36
+ pybash script.py [args...] # or: python -m pybash script.py
37
+ pybash --show script.py # print the generated Python instead of running it
38
+ pybash -c '$echo hello'
39
+ ```
40
+
41
+ Modules the script imports from its own project may use the syntax too.
42
+
43
+ ### Transform source yourself
44
+
45
+ ```python
46
+ from pybash import transform
47
+
48
+ transform("$echo hello world") # 'subprocess.run(["echo", "hello", "world"])'
49
+ ```
50
+
51
+ Plain commands compile to bare `subprocess` calls. Commands with operators call a small helper in
52
+ `pybash.runtime`; by default the generated code imports it in place, so the output only needs
53
+ `import subprocess`. For tidier output, name the helper yourself and add the header:
54
+
55
+ ```python
56
+ import pybash
57
+
58
+ body = pybash.transform(source, runtime=pybash.RUNTIME_NAME)
59
+ code = pybash.header() + body # imports pybash.runtime
60
+ code = pybash.header(standalone=True) + body # pastes the helper in: no pybash needed at run time
61
+ ```
62
+
63
+ `pybash.compile_source(source, filename)` returns a code object ready for `exec`, and
64
+ `pybash.add_hook()` installs an import hook for the current process.
65
+
66
+ Mistakes raise `PybashSyntaxError`, a `SyntaxError` whose `filename`, `lineno`, `offset` and `text`
67
+ point at the original line.
68
+
69
+ ## Syntax
70
+
71
+ ### Run: `$command`
72
+
73
+ ```python
74
+ $python --version
75
+ $cp test.txt test_copy.txt
76
+ ```
77
+
78
+ Returns the `subprocess.CompletedProcess`. A non-zero exit does not raise.
79
+
80
+ ### Capture: `x = $command`, `($command)`, `$(command)`
81
+
82
+ ```python
83
+ out = $cat test.txt
84
+ print(($cat test.txt).decode().strip())
85
+ lines = $(wc -l test.txt).decode().split()[0]
86
+ ```
87
+
88
+ Captures are `bytes`, and raise `subprocess.CalledProcessError` on a non-zero exit.
89
+ A capture not wrapped in brackets runs to the end of the line.
90
+
91
+ ### Operators
92
+
93
+ | Operator | Meaning |
94
+ |---|---|
95
+ | `a \| b` | pipe stdout of `a` into `b` |
96
+ | `a \|& b` | pipe stdout and stderr |
97
+ | `a && b`, `a \|\| b`, `a ; b` | run `b` if `a` succeeded / failed / either way |
98
+ | `< file` | read stdin from a file |
99
+ | `> file`, `>> file` | write / append stdout |
100
+ | `2> file`, `2>> file` | write / append stderr |
101
+ | `&> file`, `&>> file` | write / append both |
102
+ | `2>&1`, `>&2` | send stderr to stdout / stdout to stderr |
103
+
104
+ ```python
105
+ $sort < in.txt | uniq -c | sort -rn > counts.txt
106
+ $make build > build.log 2>&1 && echo ok || echo failed
107
+ newest = $ls -t | head -n 1
108
+ ```
109
+
110
+ Operators work without surrounding spaces (`a|b`, `2>err.log`). Quote or escape one to pass it as
111
+ text: `$echo 'a && b' \| c`. Several output files on one command all receive the output
112
+ (`$echo hi > a.txt >> b.txt`).
113
+
114
+ Background jobs (`&`), here-documents (`<<`) and other file descriptors are not supported in `$`
115
+ commands; use a `>` shell command for those.
116
+
117
+ ### Quoting
118
+
119
+ `$` commands follow POSIX shell quoting: single quotes are literal, double quotes allow `\"` and `\\`,
120
+ and a backslash escapes the next character. An unquoted `#` at the start of a word begins a comment.
121
+ There is no globbing, `~` or `$VAR` expansion; use `>` when you need the shell.
122
+
123
+ A trailing backslash continues the command on the next line.
124
+
125
+ ### Interpolation
126
+
127
+ | Form | Meaning |
128
+ |---|---|
129
+ | `{{ expr }}` | the value of any Python expression, converted with `str()` |
130
+ | `f{ expr }` | the same, with f-string formatting available: `f{n:03d}`, `f{name!r}` |
131
+
132
+ ```python
133
+ $git {{command}} {{get_option(command)}}
134
+ $kubectl get pods --show-{{display_type}}=true -n f{namespace}
135
+ $echo "{{user}} has f{count:,} files"
136
+ ```
137
+
138
+ In a `$` command an interpolated value is always exactly one argument, whatever it contains.
139
+ Interpolation also applies inside quotes. To write the markers literally, escape them: `\{{` and `\f{`.
140
+
141
+ ### Shell: `>command`
142
+
143
+ ```python
144
+ >ls .github/* | wc -l
145
+ >echo "hello {{name}}" > out.txt
146
+ ```
147
+
148
+ The line is passed to the shell untouched. Interpolated values are quoted for where they appear
149
+ (bare, inside `"..."` or inside `'...'`), so they are always data. To splice shell syntax in on
150
+ purpose, use `{{! expr }}`:
151
+
152
+ ```python
153
+ pattern = "*.txt"
154
+ >ls {{! pattern }}
155
+ ```
156
+
157
+ ## Upgrading from 0.3
158
+
159
+ - Python 3.11+ is required; the `ideas` and `token-utils` dependencies are gone.
160
+ - Arguments reach the command exactly as a shell would pass them. Previously backslashes were
161
+ re-interpreted by Python (`'a\\nb'` became a newline) and a `"` inside an argument produced invalid code.
162
+ - `{{ x }}` is converted with `str()`, may contain spaces and quotes, and no longer raises `InvalidInterpolation`
163
+ for them.
164
+ - Interpolated values in `>` commands are now quoted. Use `{{! x }}` for the old raw behavior.
165
+ - `>` commands keep their quoting; previously quotes were stripped before the shell saw them.
166
+ - `a = $x | y` now captures the pipeline's output instead of assigning a process object, and no
167
+ `cmd1` / `fout` variables leak into your code.
168
+ - `$a > f1 > f2` writes the output to both files; previously the second file received a copy of the first.
169
+ - `&&`, `||`, `;`, `2>`, `&>`, `2>&1`, `|&` and `$( )` are new. `&&` used to raise `NotImplementedError`.
170
+ - Generated lists are formatted `["ls", "-la"]` and the result no longer ends with an added newline.
171
+ - Text such as `"total = $5"` or `"($x)"` inside a Python string is no longer mistaken for a command.
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ uv sync
177
+ make check # format, lint (ruff + mypy), test
178
+ make demo
179
+ ```
@@ -0,0 +1,40 @@
1
+ # PYBASH DEMO #
2
+ # run with: pybash demo.py (or: python -m pybash demo.py)
3
+ # see the generated Python with: pybash --show demo.py
4
+
5
+ # 1. run a command; works anywhere a statement does
6
+ $printf 'SORT TEST\nHELLO WORLD\n' > test.txt
7
+
8
+ def cp_test():
9
+ $cp test.txt test_copy.txt
10
+
11
+ cp_test()
12
+
13
+ # 2. capture output as bytes
14
+ out = $cat test.txt
15
+ print(out.decode().strip().replace("HELLO", "HOWDY"))
16
+
17
+ # 3. capture inline, with ($ ...) or $(...)
18
+ print(($cat test.txt).decode().strip())
19
+ print($(wc -l test.txt).decode().split()[0], "lines")
20
+
21
+ # 4. pipes, redirects and && || ; run without a shell
22
+ $cat test.txt | sed 's/HELLO/HOWDY/g' | sed 's/HOW/WHY/g'
23
+ $sort < test.txt >> sorted_test.txt
24
+ $test -f sorted_test.txt && echo "sorted" || echo "not sorted"
25
+ $ls no-such-file 2> errors.txt; echo "kept going"
26
+ count = $sort test.txt | uniq | wc -l
27
+ print("unique lines:", int(count))
28
+
29
+ # 5. interpolate Python values; each one is always a single argument
30
+ name = "world; echo not-a-second-command"
31
+ $echo hello {{name}}
32
+ $echo f{len(name):04d} chars
33
+
34
+ # 6. a real shell when you want globbing or expansion: start the line with >
35
+ >ls ./*.txt | head -n 3
36
+ >echo "safely quoted: {{name}}"
37
+
38
+ # 7. long commands can continue onto the next line
39
+ $rm -f test.txt test_copy.txt \
40
+ sorted_test.txt errors.txt
@@ -0,0 +1,2 @@
1
+ text = "HELLO WORLD"
2
+ $ echo f{text}
@@ -0,0 +1,41 @@
1
+ """Run shell commands from Python source with `$` and `>` lines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+
7
+ from pybash import runtime as _runtime
8
+ from pybash.errors import InvalidInterpolation, PybashSyntaxError
9
+ from pybash.hook import add_hook, compile_source, remove_hook
10
+ from pybash.transformer import transform
11
+
12
+ __all__ = [
13
+ "RUNTIME_NAME",
14
+ "InvalidInterpolation",
15
+ "PybashSyntaxError",
16
+ "add_hook",
17
+ "compile_source",
18
+ "header",
19
+ "remove_hook",
20
+ "runtime_source",
21
+ "transform",
22
+ ]
23
+
24
+ #: Pass as `transform(..., runtime=RUNTIME_NAME)` when the output is prefixed with `header()`.
25
+ RUNTIME_NAME = "_pybash"
26
+
27
+
28
+ def runtime_source() -> str:
29
+ """Source of the stdlib-only helper class that generated code calls for operators."""
30
+ return inspect.getsource(_runtime._pybash)
31
+
32
+
33
+ def header(*, standalone: bool = False) -> str:
34
+ """Lines to put above code produced by `transform(..., runtime=RUNTIME_NAME)`.
35
+
36
+ With `standalone=True` the helper is pasted in, so the result runs without
37
+ pybash installed.
38
+ """
39
+ if standalone:
40
+ return f"import subprocess\nfrom typing import Any\n\n\n{runtime_source()}\n"
41
+ return f"import subprocess\n\nimport pybash.runtime as {RUNTIME_NAME}\n"