Remix.run Logo
quotemstr 15 days ago

People are generally bad about following their own advice. They're worst at following advice to avoid shell scripts. :-) We all know in theory we should move things to Python or another robust language once they become sufficiently complex, but in the moment, adding just this one feature takes precedence. Iterate "just one feature" a few times and you end up with a 1,000-line bash program. (The word "script" becomes inapplicable.) Now what? You want to add configuration support to your bash program? Well, you might as well use something robust to do it.

pwdisswordfishz 14 days ago | parent | next [-]

Which is why you rewrite in Python long before reaching that threshold. I start considering it as early as when I need to add any non-trivial command line parsing: argparse is just so much better than "while getopt".

thyristan 14 days ago | parent | prev | next [-]

This is why my threshold for not using any shell is "does it have more than one loop, more than one if-statement, an argument or file format parser or more than 10 lines?". Meaning that only very trivial things can be shell scripts and you rewrite early enough such that it isn't really a hassle or painful.

theamk 15 days ago | parent | prev [-]

It does not have to be all-at-once.

Your bash program takes 10 input args and requires 15 env variables to be set? Don't reach out for that bash ini parser, create a python script which parses configs and invokes the original bash script. Next time you change feature, move it to python. Eventually you'll be mostly python.

Something that helps greatly with that is that you can embed bash in python. So initial conversion can be as simple as file rename + 3 lines of code.

    import subprocess
    subprocess.run(["bash", "-c", """
    echo This is bash
    if hostname | grep box; then
       echo running on a box
    fi
    """], check=True)
this lets you start by trivial .sh -> .py conversion, and then you can change one block at a time. Variables are tricky so do them first; or in the worst case, "declare -p" + "eval" + temporary files will save you.
quotemstr 15 days ago | parent [-]

It's not that simple. How are you passing argv to the bash script? What about how `subprocess.run` changes the error output to include Python exception spam? Plus, in most editors, you lose all the bash syntax highlighting in the bash program chunk. I haven't seen a lot of hybrid programs like this work well.

pwdisswordfishz 14 days ago | parent | next [-]

That's why it's just the first stage of an incremental rewrite.

argv is easy to pass, though.

theamk 14 days ago | parent | prev [-]

It is that simple. You can pass arguments to `bash -c` with no problems(just don't forget a dummy $0). If you don't want exception spam, replace `check=True` with explicit exit. Here you go, it's 5 lines now.

    #!/usr/bin/env python3
    import sys
    import subprocess
    result = subprocess.run(["bash", "-c", """
    echo I am in bash, I have $# args, arg1 is "$1"
    set -x
    "$@"
    """, "dummy.sh"] + sys.argv[1:])
    sys.exit(result.returncode)
python's shell tooling is really good (if a bit verbose). And another cool thing is "shlex.quote", so you can write something like:

    subprocess.run(["bash", "-c", f"rsync {shlex.quote(filename)} somehost: | some-script"])
and have "filename" safely inserted into script fragment.. No command escapes, and newlines, quotes, backticks etc... are all handled properly.

The bash syntax highlighting will disappear, correct, and that is a downside. But hey, you are supposed to be rewriting this in python, so it's just more motivation :)