Skip to content

Commit

Permalink
Flip exec and shell chars (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
jaykv authored May 25, 2023
1 parent 2edb85d commit a6327c6
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 81 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
check: lint test

SOURCE_FILES=pybash test_pybash.py run.py
SOURCE_FILES=pybash test_pybash.py

install:
pip install -e .
Expand Down
50 changes: 25 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
![PyPI](https://img.shields.io/pypi/v/pybash)
![GitHub](https://img.shields.io/github/license/jaykv/pybash)

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!
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 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.
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 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.

Note: this is a mainly experimental library.

Expand All @@ -20,7 +20,7 @@ Note: this is a mainly experimental library.
```python
from pybash.transformer import transform

transform(">echo hello world") # returns the python code for the bash command as string
transform("$echo hello world") # returns the python code for the bash command as string
```

## As script runner
Expand All @@ -31,20 +31,20 @@ transform(">echo hello world") # returns the python code for the bash command as
```py
#
text = "HELLO WORLD"
>echo f{text}
$echo f{text}
```

### Run script:
```bash
$ python -m pybash hello.py
python -m pybash hello.py
```

# Supported transforms

### 1. Simple execution with output
```python
>python --version
>echo \\nthis is an echo
$python --version
$echo \\nthis is an echo
```
outputs:
```
Expand All @@ -55,7 +55,7 @@ this is an echo

### 2. Set output to variable and parse
```python
out = >cat test.txt
out = $cat test.txt
test_data = out.decode('utf-8').strip()
print(test_data.replace("HELLO", "HOWDY"))
```
Expand All @@ -66,7 +66,7 @@ HOWDY WORLD

### 3. Wrapped, in-line execution and parsing
```python
print((>cat test.txt).decode('utf-8').strip())
print(($cat test.txt).decode('utf-8').strip())
```
outputs:
```
Expand All @@ -75,12 +75,12 @@ HELLO WORLD

### 4. Redirection
```python
>echo "hello" >> test4.txt
$echo "hello" >> test4.txt
```

### 5. Pipe chaining
```python
>cat test.txt | sed 's/HELLO/HOWDY/g' | sed 's/HOW/WHY/g' | sed 's/WHY/WHEN/g'
$cat test.txt | sed 's/HELLO/HOWDY/g' | sed 's/HOW/WHY/g' | sed 's/WHY/WHEN/g'
```
outputs:
```
Expand All @@ -89,25 +89,25 @@ WHENDY WORLD

### 6. Redirection chaining
```python
>cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test1.txt >> test2.txt > test3.txt
$cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test1.txt >> test2.txt > test3.txt
```

### 7. Chaining pipes and redirection- works in tandem!
```python
>cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test5.txt
$cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test5.txt
```

### 8. Input redirection
```python
>sort < test.txt >> sorted_test.txt
$sort < test.txt >> sorted_test.txt
```

```python
>sort < test.txt | sed 's/SORT/TEST\\n/g'
$sort < test.txt | sed 's/SORT/TEST\\n/g'
```
### 9. Glob patterns with shell
```python
$ls .github/*
>ls .github/*
```

### 10. Direct interpolation
Expand All @@ -118,21 +118,21 @@ Denoted by {{code here}}. Interpolated as direct code replace. The value/output
command = "status"
def get_option(command):
return "-s" if command == "status" else "-v"
>git {{command}} {{get_option(command)}}
$git {{command}} {{get_option(command)}}

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

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

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

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

### 11. f-string interpolation
Expand All @@ -143,22 +143,22 @@ Denoted by f{ any python variable, function call, or expression here }. Interpol

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

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

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

#### Also works inside methods!
```python
# PYBASH DEMO #
def cp_test():
>cp test.txt test_copy.txt
$cp test.txt test_copy.txt

cp_test()
```
Expand Down
37 changes: 19 additions & 18 deletions examples/demo.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,60 @@
# PYBASH DEMO #
# run with either: python run.py or python -m pybash demo.py

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

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

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

>git {{git_command}} {{option}}
$git {{git_command}} {{option}}

#TODO:
#a = >echo "hello" >> test.txt
#a = >sort < test.txt | grep "HELLO"
#a = $echo "hello" >> test.txt
#a = $sort < test.txt | grep "HELLO"
#print(f"test: {a}")

# 1. use inside methods
>echo "SORT TEST\nHELLO WORLD" > test.txt
$echo "SORT TEST\nHELLO WORLD" > test.txt
def cp_test():
print("1. >cp test.txt test_copy.txt")
>cp test.txt test_copy.txt
$cp test.txt test_copy.txt

cp_test()

# 2. simple bash command execution with output
print("2. >python --version\n >echo \\nthis is an echo")
>python --version
>echo \\nthis is an echo
$python --version
$echo \\nthis is an echo

# 3. set output to python variable directly
out = >cat test.txt
out = $cat test.txt
test_data = out.decode('utf-8').strip()
print(test_data.replace("HELLO", "HOWDY"))

# 4. wrapped, in-line execution
print((>cat test.txt).decode('utf-8').strip())
print(($cat test.txt).decode('utf-8').strip())

# 5. redirection
>echo "hello" >> test4.txt
$echo "hello" >> test4.txt

# 6. pipe chaining
>cat test.txt | sed 's/HELLO/HOWDY/g' | sed 's/HOW/WHY/g' | sed 's/WHY/WHEN/g'
$cat test.txt | sed 's/HELLO/HOWDY/g' | sed 's/HOW/WHY/g' | sed 's/WHY/WHEN/g'

# 7. chaining pipes and redirection- works with all!
>cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test5.txt
$cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test5.txt

# 8. chained redirection
>cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test1.txt >> test2.txt > test3.txt
$cat test.txt | sed 's/HELLO/HOWDY\\n/g' > test1.txt >> test2.txt > test3.txt

# 9. input redirection
>sort < test.txt >> sorted_test.txt
>sort < test.txt | sed 's/SORT/TEST\\n/g'
$sort < test.txt >> sorted_test.txt
$sort < test.txt | sed 's/SORT/TEST\\n/g'

# 10. Glob patterns
$ls .github/*
>ls ./*
2 changes: 1 addition & 1 deletion examples/hello.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@

text = "HELLO WORLD"
>echo f{text}
$ echo f{text}
2 changes: 1 addition & 1 deletion run.py → examples/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
if __name__ == "__main__":
import demo

print(f"running {demo}")
print(f"~ completed running {demo}")
24 changes: 16 additions & 8 deletions pybash/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class InvalidInterpolation(Exception):


class Processor:
command_char = ">"
command_char = "$"

def __init__(self, token: token_utils.Token) -> None:
self.token = token
Expand Down Expand Up @@ -78,7 +78,7 @@ def direct_interpolate(string: str) -> str:

class Shelled(Processor):
# $ls .github/*
command_char = "$"
command_char = ">"

def transform(self) -> token_utils.Token:
command_str = " ".join(self.command)
Expand Down Expand Up @@ -107,12 +107,13 @@ def parse(self) -> None:

def transform(self) -> None:
pipeline_command = Pipeline(self.command).parse_command(variablized=True)

if pipeline_command != self.command:
self.token.string = pipeline_command
self.token.string += ';' if pipeline_command[-1] != ';' else ''
self.token.string += ' '.join(self.parsed_line[: self.start_index]) + ' cmd1\n'
else:
self.token.string = ' '.join(self.parsed_line[: self.start_index])
self.token.string = ' '.join(self.parsed_line[: self.start_index]) + ' '
self.token.string += Commander.build_subprocess_list_cmd("check_output", self.command) + '\n'


Expand All @@ -128,7 +129,7 @@ def transform(self) -> token_utils.Token:
# shlex strips out single quotes and double quotes-- use raw_line for the code around the wrapped command
self.token.string = (
' '.join(self.raw_line[: self.start_index])
+ self.raw_line[self.start_index][: self.raw_line[self.start_index].index('>')]
+ self.raw_line[self.start_index][: self.raw_line[self.start_index].index('$')]
)
self.token.string += (
Commander.build_subprocess_list_cmd("check_output", self.command)
Expand Down Expand Up @@ -329,7 +330,7 @@ def get_start_index(parsed_line: list) -> int:
int: starting index
"""
for i, val in enumerate(parsed_line):
if '>' in val:
if '$' in val:
return i

return 0
Expand All @@ -339,7 +340,7 @@ def get_bash_command(
parsed_line: list,
start_index: Union[int, None] = None,
wrapped: Union[bool, None] = None,
command_char: str = ">",
command_char: str = "$",
) -> list:
"""Parses line to bash command
Expand All @@ -361,6 +362,8 @@ def get_bash_command(
# > may be at the beginning or somewhere in the middle of this arg
# examples: >ls, print(>cat => strip up to and including >
command[0] = command[0][command[0].index(command_char) + 1 :].strip()
if command[0] == '':
del command[0]

# remove everything after and including first )- not part of the command
if wrapped:
Expand Down Expand Up @@ -412,8 +415,8 @@ def build_subprocess_list_cmd(method: str, args: list, **kwargs) -> str:
return command


TOKENIZERS = {"$": Shelled, ">": Execed}
GREEDY_TOKENIZERS = {"= >": Variablized, "(>": Wrapped}
TOKENIZERS = {">": Shelled, "$": Execed}
GREEDY_TOKENIZERS = {"= $": Variablized, "($": Wrapped}


def transform(source, **_kwargs):
Expand Down Expand Up @@ -443,3 +446,8 @@ def transform(source, **_kwargs):
new_tokens.extend(line)

return token_utils.untokenize(new_tokens)


if __name__ == "__main__":
pyscript = transform("test = $ echo hi")
print(pyscript)
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.3.1"
version = "0.3.2"
description = ">execute bash commands from python easily"
authors = ["Jay <jay.github0@gmail.com>"]
readme = "README.md"
Expand Down
Loading

0 comments on commit a6327c6

Please sign in to comment.