pypi package 'zxpy'

Popularity: Medium (more popular than 90% of all packages)
Description: Shell scripts made simple
Installation: pip install zxpy
Last version: 1.6.3 (Download)
Homepage: https://github.com/tusharsadhwani/zxpy
Size: 8.75 kB
License: MIT

Activity

Last modified: February 21, 2023 10:58 AM (a month ago)
Versions released in one year: 3
Weekly downloads: 13
04/03/202206/19/202209/04/202211/20/202203/05/202302040608001234released versions / week
  • Versions released
  • Weekly downloads

What's new in version 1.6.2

Delta between version 1.6.1 and version 1.6.2

Source: Github
Commits:
  • 9707678f83b7d176d7fe533858acce2cfc9b1eed, May 8, 2022 8:30 PM:
    Fix interactive test
  • e401cc0b872a5d6c3b99e6d6de84d90942c628ef, May 15, 2022 6:08 PM:
    Remove unused imports
  • 15eb6170a033027512ad83b4923651336c801c48, August 6, 2022 11:37 AM:
    Replace the custom interpreter with a builtin one (#49)
    
    * Replace the custom interpreter with a builtin one
    
    * Exit the interpreter whten repl is closed
  • aa08eb5877bad27157f08f8d705e64250123e9eb, August 6, 2022 11:53 AM:
    Bump version
Files changed:
setup.cfg CHANGED
@@ -1,6 +1,6 @@
1
  [metadata]
2
  name = zxpy
3
- version = 1.6.1
4
  description = Shell scripts made simple
5
  long_description = file: README.md
6
  long_description_content_type = text/markdown
1
  [metadata]
2
  name = zxpy
3
+ version = 1.6.2
4
  description = Shell scripts made simple
5
  long_description = file: README.md
6
  long_description_content_type = text/markdown
tests/zxpy_test.py CHANGED
@@ -1,8 +1,6 @@
1
  import ast
2
- import io
3
  import os
4
  import subprocess
5
- import sys
6
  from textwrap import dedent
7
 
8
  import pytest
@@ -91,27 +89,28 @@ def test_raise() -> None:
91
  assert exc.value.args == (1,)
92
 
93
 
94
- def test_interactive(
95
- monkeypatch: pytest.MonkeyPatch,
96
- capsys: pytest.CaptureFixture[str],
97
- ) -> None:
98
  process = subprocess.Popen(
99
  ["zxpy", "-i"],
100
  stdin=subprocess.PIPE,
101
  stdout=subprocess.PIPE,
102
  stderr=subprocess.PIPE,
103
  )
104
 
105
- stdout, stderr = process.communicate(input=b"~'echo hi'\nprint(10)\n")
106
- assert stderr == b''
107
- assert stdout.decode() == dedent(
108
  """\
109
- zxpy shell
110
- Python 3.9.7 (default, Sep 10 2021, 14:59:43)
111
- [GCC 11.2.0]
112
-
113
- >>> hi
114
- >>> 10
115
- >>>
 
116
  """
117
  )
 
 
 
 
 
1
  import ast
 
2
  import os
3
  import subprocess
 
4
  from textwrap import dedent
5
 
6
  import pytest
89
  assert exc.value.args == (1,)
90
 
91
 
92
+ def test_interactive() -> None:
 
 
 
93
  process = subprocess.Popen(
94
  ["zxpy", "-i"],
95
  stdin=subprocess.PIPE,
96
  stdout=subprocess.PIPE,
97
  stderr=subprocess.PIPE,
98
  )
99
 
100
+ code = dedent(
 
 
101
  """\
102
+ ~'echo hi'
103
+ print(10)
104
+ def f(n):
105
+ if n < 2:
106
+ return 1
107
+ return f(n-1) + f(n-2)
108
+
109
+ print(f(5))
110
  """
111
  )
112
+
113
+ stdout, stderr = process.communicate(input=code.encode())
114
+ assert stderr == b'\n'
115
+ outlines = [line for line in stdout.decode().splitlines() if line.startswith('>>>')]
116
+ assert outlines == [">>> hi", ">>> 10", ">>> ... ... ... ... >>> 8", ">>> "]
zx.py CHANGED
@@ -28,7 +28,6 @@
28
  import code
29
  import codecs
30
  import contextlib
31
- from fileinput import filename
32
  import inspect
33
  import shlex
34
  import subprocess
@@ -274,6 +273,35 @@ def setup_zxpy_repl() -> None:
274
  print()
275
 
276
  install()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
 
278
 
279
  def install() -> None:
@@ -292,81 +320,24 @@ def install() -> None:
292
  if len(frames) > 1:
293
  parent_frame = frames[1]
294
  parent_locals = parent_frame.frame.f_locals
295
- locals().update(parent_locals)
296
-
297
- # setup zxpy globals
298
- globals().update(
299
- {
300
- "$run_shell": run_shell,
301
- "$run_shell_alternate": run_shell_alternate,
302
- "$run_shell_print": run_shell_print,
303
- "$shlex_quote": shlex.quote,
304
- }
305
- )
306
 
307
  # For tab completion and arrow key support
308
  if sys.platform != "win32":
309
  import readline
310
 
311
  readline.parse_and_bind("tab: complete")
312
 
313
- command = ""
314
- continued_command = False
315
- while True:
316
- try:
317
- prompt = "... " if continued_command else ">>> "
318
- new_input = input(prompt)
319
- except KeyboardInterrupt:
320
- print()
321
- if continued_command:
322
- continued_command = False
323
- command = ""
324
- continue
325
- except EOFError:
326
- print()
327
- sys.exit(0)
328
-
329
- # TODO: refactor the next 10 lines.
330
- # probably move command = '...' stuff somewhere else
331
- if continued_command:
332
- command += "\n"
333
- else:
334
- command = ""
335
-
336
- if new_input != "":
337
- command += new_input
338
- else:
339
- continued_command = False
340
-
341
- if continued_command:
342
- continue
343
-
344
- try:
345
- ast_obj = ast.parse(command, "<input>", "single")
346
- except SyntaxError:
347
- try:
348
- code_obj = code.compile_command(command)
349
- if code_obj is None:
350
- continued_command = True
351
- continue
352
-
353
- except BaseException:
354
- traceback.print_exc()
355
- continue
356
 
357
- assert isinstance(ast_obj, ast.Interactive)
358
- patch_shell_commands(ast_obj)
359
-
360
- try:
361
- code_obj = compile(ast_obj, "<input>", "single")
362
- assert code_obj is not None
363
- exec(code_obj)
364
-
365
- except SystemExit as e:
366
- sys.exit(e.code)
367
-
368
- except BaseException:
369
- traceback.print_exc()
370
 
371
 
372
  if __name__ == "__main__":
28
  import code
29
  import codecs
30
  import contextlib
 
31
  import inspect
32
  import shlex
33
  import subprocess
273
  print()
274
 
275
  install()
276
+ sys.exit()
277
+
278
+
279
+ class ZxpyConsole(code.InteractiveConsole):
280
+ """Runs zxpy over"""
281
+
282
+ def runsource(
283
+ self,
284
+ source: str,
285
+ filename: str = "<console>",
286
+ symbol: str = "single",
287
+ ) -> bool:
288
+ # First, check if it could be incomplete input, return True if it is.
289
+ # This will allow it to keep taking input
290
+ with contextlib.suppress(SyntaxError, OverflowError):
291
+ if code.compile_command(source) == None:
292
+ return True
293
+
294
+ try:
295
+ ast_obj = ast.parse(source, filename, mode=symbol)
296
+ assert isinstance(ast_obj, ast.Interactive)
297
+ patch_shell_commands(ast_obj)
298
+ code_obj = compile(ast_obj, filename, mode=symbol)
299
+ except (ValueError, SyntaxError):
300
+ # Let the original implementation take care of incomplete input / errors
301
+ return super().runsource(source, filename, symbol)
302
+
303
+ self.runcode(code_obj)
304
+ return False
305
 
306
 
307
  def install() -> None:
320
  if len(frames) > 1:
321
  parent_frame = frames[1]
322
  parent_locals = parent_frame.frame.f_locals
323
+ else:
324
+ parent_locals = {}
 
 
 
 
 
 
 
 
 
325
 
326
  # For tab completion and arrow key support
327
  if sys.platform != "win32":
328
  import readline
329
 
330
  readline.parse_and_bind("tab: complete")
331
 
332
+ zxpy_locals = {
333
+ **parent_locals,
334
+ "$run_shell": run_shell,
335
+ "$run_shell_alternate": run_shell_alternate,
336
+ "$run_shell_print": run_shell_print,
337
+ "$shlex_quote": shlex.quote,
338
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ ZxpyConsole(locals=zxpy_locals).interact(banner="", exitmsg="")
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
 
343
  if __name__ == "__main__":

Readme

zxpy

Shell scripts made simple 🐚

zxpy lets you seamlessly write shell commands inside Python code, to create readable and maintainable shell scripts.

Inspired by Google's zx, but made much simpler and more accessible using Python.

Rationale

Bash is cool, and it's extremely powerful when paired with linux coreutils and pipes. But apart from that, it's a whole another language to learn, and has a (comparatively) unintuitive syntax for things like conditionals and loops.

zxpy aims to supercharge bash by allowing you to write scripts in Python, but with native support for bash commands and pipes.

Let's use it to find all TODOs in one of my other projects, and format them into a table:

#! /usr/bin/env zxpy
todo_comments = ~"git grep -n TODO"
for todo in todo_comments.splitlines():
    filename, lineno, code = todo.split(':', 2)
    *_, comment = code.partition('TODO')
    print(f"{filename:40} on line {lineno:4}: {comment.lstrip(': ')}")

Running this, we get:

$ ./todo_check.py
README.md                                on line 154 : move this content somewhere more sensible.
instachat/lib/models/message.dart        on line 7   : rename to uuid
instachat/lib/models/update.dart         on line 13  : make int
instachat/lib/services/chat_service.dart on line 211 : error handling
server/api/api.go                        on line 94  : move these to /chat/@:address
server/api/user.go                       on line 80  : check for errors instead of relying on zero value

Writing something like this purely in bash or in Python would be much harder than this. Being able to use linux utilities seamlessly with a readable, general purpose language is what makes this a really powerful tool.

A larger, practical example

You can find a comparison between a practical-ish script written in bash and zxpy in EXAMPLE.md

Installation <a href="https://pypi.org/project/zxpy"><img src="https://img.shields.io/badge/pypi-zxpy-blue?style=flat"></a>

pip install zxpy

pipx

If you have pipx installed, you can try out zxpy without installing it, by running:

pipx run zxpy

Basic Examples

Make a file script.py (The name and extension can be anything):

#! /usr/bin/env zxpy
~'echo Hello world!'

file_count = ~'ls -1 | wc -l'
print("file count is:", file_count)

And then run it:

$ chmod +x ./script.py

$ ./script.py
Hello world!
file count is: 3

Run >>> help('zx') in Python REPL to find out more ways to use zxpy.

A slightly more involved example: run_all_tests.py

#! /usr/bin/env zxpy
test_files = (~"find -name '*_test\.py'").splitlines()

for filename in test_files:
    try:
        print(f'Running {filename:.<50}', end='')
        output = ~f'python {filename}'  # variables in your shell commands :D
        assert output == ''
        print('Test passed!')
    except:
        print(f'Test failed.')

Output:

$ ./run_all_tests.py
Running ./tests/python_version_test.py....................Test failed.
Running ./tests/platform_test.py..........................Test passed!
Running ./tests/imports_test.py...........................Test passed!

More examples are in EXAMPLE.md, and in the examples folder.

stderr and return codes

To get stderr and return code information out of the shell command, there is an alternative way of invoking the shell.

To use it, just use 3 variables on the left side of your ~'...' shell string:

stdout, stderr, return_code = ~'echo hi'
print(stdout)       # hi
print(return_code)  # 0

More examples are in the examples folder.

CLI Arguments

When writing a shell script, you often want to pass CLI arguments to it.

Like so:

$ cat ./foo.sh
echo arg is: $1

$ ./foo.sh 123
arg is: 123

To do the same in zxpy, pass the script arguments after a -- in the zxpy CLI command.

#!/usr/bin/env zxpy

import sys
print("Argv is:", sys.argv)

~"echo output: $1 $2 $3"
$ ./test.py
Argv is: ['/bin/sh']
output:

$ ./test.py -- abc def
Argv is: ['/bin/sh', 'abc', 'def']
output: abc def

Both $1 and sys.argv[1] will do the same thing.

Quoting

Take this shell command:

$ uname -a
Linux pop-os 5.11.0 [...] x86_64 GNU/Linux

Now take this piece of code:

>>> cmd = 'uname -a'
>>> ~f'{cmd}'
/bin/sh: 1: uname -a: not found

Why does this not work?

This is because uname -a was quoted into 'uname -a'. All values passed inside f-strings are automatically quoted to avoid shell injection.

To prevent quoting, the :raw format_spec can be used:

>>> cmd = 'uname -a'
>>> ~f'{cmd:raw}'
Linux pop-os 5.11.0 [...] x86_64 GNU/Linux

This disables quoting, and the command is run as-is as provided in the string.

Note that this shouldn't be used with external data, or this will expose you to shell injection.

Interactive mode

$ zxpy
zxpy shell
Python 3.8.5 (default, Jan 27 2021, 15:41:15)
[GCC 9.3.0]

>>> ~"ls | grep '\.py'"
__main__.py
setup.py
zx.py
>>>

Also works with path/to/python -m zx

It can also be used to start a zxpy session in an already running REPL. Simply do:

>>> import zx; zx.install()

and zxpy should be enabled in the existing session.

Development/Testing

To install from source, clone the repo, and do the following:

$ source ./venv/bin/activate  # Always use a virtualenv!
$ pip install -r requirements-dev.txt
Processing ./zxpy
[...]
Successfully installed zxpy-1.X.X
$ pytest  # runs tests