From 2e6b8aa694c83b9d7707f6fcd8ea9509cccb69ca Mon Sep 17 00:00:00 2001 From: jaykv Date: Thu, 26 Jan 2023 23:54:31 -0500 Subject: [PATCH 1/4] Implemented dynamic interpolation --- README.md | 30 +++++++++++++++++++++++----- demo.py | 9 ++++++++- pybash.py | 53 +++++++++++++++++++++++++++++++++++++------------- test_pybash.py | 10 +++++++++- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index ecfa2dd..fd3be12 100644 --- a/README.md +++ b/README.md @@ -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 # diff --git a/demo.py b/demo.py index d1f8634..da93968 100644 --- a/demo.py +++ b/demo.py @@ -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" diff --git a/pybash.py b/pybash.py index fa75d16..fc9d593 100644 --- a/pybash.py +++ b/pybash.py @@ -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( @@ -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 @@ -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) @@ -143,9 +168,10 @@ 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 @@ -153,9 +179,10 @@ def transform_source(source, **_kwargs): 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 diff --git a/test_pybash.py b/test_pybash.py index 0546532..1313caa 100644 --- a/test_pybash.py +++ b/test_pybash.py @@ -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)}}") @@ -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 }}") From c7bbf7a7a83e9f92b304a52d9848df3796d24bf4 Mon Sep 17 00:00:00 2001 From: jaykv Date: Thu, 26 Jan 2023 23:55:28 -0500 Subject: [PATCH 2/4] Better test --- test_pybash.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_pybash.py b/test_pybash.py index 1313caa..4621b50 100644 --- a/test_pybash.py +++ b/test_pybash.py @@ -111,8 +111,8 @@ def test_static_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' + 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' From 994c480503bae4af007a7fa9b9a31b321c4c964b Mon Sep 17 00:00:00 2001 From: jaykv Date: Thu, 26 Jan 2023 23:56:12 -0500 Subject: [PATCH 3/4] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 90bd685..e4790e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From 3a27a601d19c0637e440d1be69a36ff178d05e3a Mon Sep 17 00:00:00 2001 From: jaykv Date: Fri, 27 Jan 2023 00:00:36 -0500 Subject: [PATCH 4/4] Update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fd3be12..62be5cf 100644 --- a/README.md +++ b/README.md @@ -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. @@ -86,7 +86,7 @@ WHENDY WORLD $ls .github/* ``` -### 10. Static Interpolation +### 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 @@ -127,7 +127,7 @@ namespace = "coffee" ## BAD option = "-s -v" ->git status {{option}} +>git status {{{ option }}} ``` #### Also works inside methods!