Edit: Since writing this article and recently re-reviewing it, I want to scrub it from existence as I’m appalled at my choice. Argparse already had this feature in build and can work with positional arguments no problems. Ah well!

Whoever says that they write code with 0 errors is probably lying in the job interview or you should hire them straight away, after a few test problems.

I know I don’t always write the most elegant code and half the time I’m pulling out my hair trying to just get it to work. This methodology sadly stems from being self taught and knowing what my code does at every point. When you are in a team scenario you shouldn’t have to read half the code to understand how to use it.

Sadly by design, or lack there of, many shell scripts or python scripts are created with a goal in mind and not how the goal is achieved.

You might have something of the following python script:

#!/usr/env/python
# sample_script.py <File.txt> <justdoit>
import sys

file = sys.argv[1]
action = sys.argv[2]

contents = open(file, 'r').read()
if action == "justdoit":
    print("%s" % contents)
else:
    print("I have a hidden power attach")

Now there may be something terribly wrong with this script, but the fact remains, sometimes things like this make it into production and you need backwards compatibility.

Argparse and sys.argv, co-existing


#!/usr/env/python
# the_same_script.py <File.txt> <justdoit>
import sys
import argparse

if __name__ == "__main__":
    # Our default argparse function
    argparser = argparse.ArgumentParser(description="Slowbro Hidden Power Sample 2")
    argparser.add_argument("--file", action="store", help="File")
    argparser.add_argument("--action", action="store", help="Action to take")
    # You must create the variables we test against
    file = ""
    action = ""

This is where the magic happens. You could do a few things here as argparse will have 4 variables --file being at sys.argv[1] and --action being at sys.argv[3]:

  • check if len(sys.argv) is greater than 2
  • check if the first 2 characters are “–” denoting we are going to use argparse instead of normal sys.argv

I chose the latter. There is 1 edge case that I thought of where if you have a file named as “–badly_named_file” your going to have a bad time. But a simple try:/except: over the reading of your file will render this edge cased fixed handled.

    if sys.argv[1][0:2] == "--":
        arguments = argparser.parse_args()
        if arguments.file:
            file = arguments.file
        if arguments.action:
            action = arguments.action
    else:
        try:
            if file == "":
                file = sys.argv[1]
            if action == "":
                action = sys.argv[2]
        except Exception as e:
            print("Exception: %s" % e)
            argparser.print_help()
            sys.exit(1)
    try:
        contents = open(file, 'r').read()
    except Exception as e:
        print("Exception: %s" % e)
        sys.exit(1)
    if action == "justdoit":
        print("%s" % contents)
    else:
        print("I have a hidden power, it's not scripting")

And there you have it. A script which can function with not only sys.argv but has argparser capabilities as well. Now all your cron jobs on your server fleet you don’t have under git or ansible or salt or some type of source control, will continue to function while all your new scripts take advantage of argparse.

Any questions feel free to comment below, and the full script if you would like it, can be found in a gist here