Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PyBash

Streamline bash-command execution from python with an easy-to-use syntax. It combines the simplicity of writing bash scripts with the flexibility of python. Under the hood, any line or variable assignment starting with `>` or surrounded by parentheses is transformed to python `subprocess` calls and then injected into `sys.meta_path` as an import hook. All possible thanks to the wonderful [ideas](https://github.com/aroberge/ideas) project!
Streamline bash-command execution from python with a new syntax. It combines the simplicity of writing bash scripts with the flexibility of python. Under the hood, any line or variable assignment starting with `>` or surrounded by parentheses is transformed to python `subprocess` calls and then injected into `sys.meta_path` as an import hook. All possible thanks to the wonderful [ideas](https://github.com/aroberge/ideas) project!

For security and performance reasons, PyBash will NOT execute as shell, unless explicitly specified with a `$` instead of a single `>` before the command. While running commands as shell can be convenient, it can also spawn security risks and if you're not too careful. If you're curious about the transformations, look at the [unit tests](test_pybash.py) for some quick examples.

Expand Down Expand Up @@ -86,30 +86,50 @@ WHENDY WORLD
$ls .github/*
```

### 10. Interpolation
Denoted by {{variable_or_function_call_here}}. Only static interpolation is supported currently so no quotes, spaces or expressions within the {{}} or in the string being injected.
### 10. Static interpolation
Denoted by {{variable_or_function_call_here}}. For static interpolation, no quotes, spaces or expressions within the {{}} or in the string being injected.

```python
# GOOD
## GOOD
command = "status"
def get_option(command):
return "-s" if command == "status" else "-v"
>git {{command}} {{get_option(command)}}

display_type = "labels"
>k get pods --show-{{display_type}}=true
>kubectl get pods --show-{{display_type}}=true

# BAD
## BAD
option = "-s -v"
>git status {{option}}

options = ['-s', '-v']
>git status {{" ".join(options)}}

# use dynamic interpolation
options = {'version': '-v'}
>git status {{options['version']}}
```

### 11. Dynamic interpolation
Denoted by {{{ any python variable, function call, or expression here }}}. The output of the variable, function call, or the expression must still not include spaces.

```python
## GOOD

# git -h
options = {'version': '-v', 'help': '-h'}
>git {{{options['h']}}}

# kubectl get pods --show-labels -n coffee
namespace = "coffee"
>kubectl get pods {{{"--" + "-".join(['show', 'labels'])}}} -n {{{namespace}}}

## BAD
option = "-s -v"
>git status {{{ option }}}
```

#### Also works inside methods!
```python
# PYBASH DEMO #
Expand Down
9 changes: 8 additions & 1 deletion demo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# PYBASH DEMO #
def run():
# command interpolation
# dynamic interpolation
options = {'version': '-v', 'help': '-h'}
>git {{{options['h']}}}

namespace = "coffee"
>kubectl get pods {{{"--" + "-".join(['show', 'labels'])}}} --namespace {{{namespace}}}

# static interpolation
git_command = "status"
option = "-v"

Expand Down
53 changes: 40 additions & 13 deletions pybash.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
from ideas import import_hook


@staticmethod
def source_init():
"""Adds subprocess import"""
import_subprocess = "import subprocess"
return import_subprocess


@staticmethod
def add_hook(**_kwargs):
"""Creates and automatically adds the import hook in sys.meta_path"""
hook = import_hook.create_hook(
Expand Down Expand Up @@ -40,10 +38,39 @@ def parse(self):
def transform(self) -> token_utils.Token:
raise NotImplementedError

def interpolate(self) -> str:
self.token.string = Processor.dynamic_interpolate(self.token_line, self.token.string)
self.token.string = Processor.static_interpolate(self.token.string)

@staticmethod
def interpolate(string: str) -> str:
"""Process {{ interpolations }} and substitute.
Interpolations are denotated by a {{ }} with a variable or a function call inside.
def dynamic_interpolate(token_string: str, parsed_command: str) -> str:
"""Process {{{ dynamic interpolations }}} and substitute.
Dynamic interpolations are denotated by a {{{ }}} with any expression inside.
Substitution in the parsed command string happens relative to the order of the interpolations in the original command string.

Args:
token_string (str): Original command string
parsed_command (str): Parsed command string

Returns:
str: Interpolated parsed command string
"""
pattern = r'{{{(.+?)}}}'
subs = re.findall(pattern, token_string)

if not subs:
return parsed_command

for sub in subs:
parsed_command = re.sub(pattern, '" + ' + sub + ' + "', parsed_command, 1)

return parsed_command

@staticmethod
def static_interpolate(string: str) -> str:
"""Process {{ static interpolations }} and substitute.
Static interpolations are denotated by a {{ }} with a variable or a function call inside.
Substitution happens directly on the parsed command string. Therefore, certain characters cannot be interpolated as they get parsed out before substitution.

Args:
string (str): String to interpolate
Expand Down Expand Up @@ -100,8 +127,6 @@ def transform(self):
self.token.string = ' '.join(self.parsed_line[: self.start_index])
self.token.string += Commander.build_subprocess_list_cmd("check_output", self.command) + '\n'

return self.token


class Wrapped(Processor):
# print(>cat test.txt)
Expand Down Expand Up @@ -143,19 +168,21 @@ def transform_source(source, **_kwargs):
# matches exact token
token_match = [tokenizer for match, tokenizer in Transformer.tokenizers.items() if token == match]
if token_match:
token = token_match[0](token).transform()
token.string = Processor.interpolate(token.string)
new_tokens.append(token)
parser = token_match[0](token)
parser.transform()
parser.interpolate()
new_tokens.append(parser.token)
continue

# matches anywhere in line
greedy_match = [
tokenizer for match, tokenizer in Transformer.greedy_tokenizers.items() if match in token.line
]
if greedy_match:
token = greedy_match[0](token).transform()
token.string = Processor.interpolate(token.string)
new_tokens.append(token)
parser = greedy_match[0](token)
parser.transform()
parser.interpolate()
new_tokens.append(parser.token)
continue

# no match
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "PyBash"
version = "0.2.2"
version = "0.2.3"
description = ">execute bash commands from python easily"
authors = ["Jay"]
readme = "README.md"
Expand Down
10 changes: 9 additions & 1 deletion test_pybash.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def test_shell_commands():
assert run_bash("$ls .github/*") == 'subprocess.run("ls .github/*", shell=True)\n'


def test_interpolate():
def test_static_interpolate():
assert run_bash(">git {{command}} {{option}}") == 'subprocess.run(["git","" + command + "","" + option + ""])\n'
assert (
run_bash(">git {{command}} {{process(option)}}")
Expand All @@ -109,6 +109,14 @@ def test_interpolate():
)


def test_dynamic_interpolate():
assert (
run_bash(">kubectl get pods {{{\"--\" + \"-\".join(['show', 'labels'])}}} -n {{{ namespace }}}")
== 'subprocess.run(["kubectl","get","pods","" + "--" + "-".join([\'show\', \'labels\']) + "","-n","" + namespace + ""])\n'
)
assert run_bash(">git {{{options['h']}}}") == 'subprocess.run(["git","" + options[\'h\'] + ""])\n'


def test_invalid_interpolate():
with pytest.raises(pybash.InvalidInterpolation):
assert run_bash(">git {{command}} {{ option }}")
Expand Down