script.py

Help for writing shell scripts in Python

This module is mostly for folks who need to write a python script that will be run from the command line. You know, something that can parse command line arguments and run other external programs via the shell. The kind of script you can call with --help. Something that perhaps reads, copies, deletes, or otherwise manipulates files by their pathname.

If any of these apply, perhaps this module is for you!

A simple import script statement can provide your code with convenient command line parsing, à la carte --help documentation, useful path operations, shell command invocation with optional pipe redirection, and early run-time termination with exit status control. Read on for more. Cheers!

requirements

Python 3.0 or later.

usage

Here’s a script that could tell you which files in your music collection belong to a certain genre, e.g. classical, rock, jazz, etc.

#!/usr/bin/env python
from script import path, shell, opts
import script

script.doc.purpose = \
    'How many flac files in ~/Music/flac belong to GENRE?'
script.doc.args = 'GENRE'
opts.add('verbose')

def main():
    if len(script.args) != 1:
        script.exit(1, 'Please specify GENRE (or run with --help)')
    genre = script.args[0].lower()
    count = 0
    root = path('~/Music/flac')
    if opts.verbose: print('scanning', root)
    for parent, dirs, files in root.walk('*.flac'):
        for file in files:
            cmd = 'metaflac --show-tag=GENRE ' + path(parent/file).sh
            result = shell(cmd, stdout='PIPE').stdout
            if genre == result.lstrip('GENRE=').rstrip().lower():
                count += 1
    print('found {} {} files'.format(count, genre))

if __name__ == '__main__': script.run(main)

This example could be run with --help or --verbose.

contents

args

This object starts out as an empty list, but after calling run() it contains any arguments passed via the command line.

doc

If provided, the information in this object will be shown if the script is run with --help. Assign the purpose attribute to a string describing what your script does. Describe which arguments should be passed to your script by setting args.

script.doc.args = "FILES"
script.doc.purpose = "Save the World!"

exit(*args, sep=' ', end='\n', file=sys.stderr, status="default")

For one argument or no arguments: same as calling sys.exit

For multiple arguments: much like calling sys.exit with a string written via print, i.e. *args are written via print(*args, sep=sep, end=end, file=file) then sys.exit(1) is called (unless status is not set to "default" per the next paragraph)

If status is set to something else than "default", exit by calling sys.exit(status) rather than mimicking the default behavior of sys.exit .

Per sys.exit, arguments other than status codes are written to standard error (unless file is set to something else).

opts

This object parses then stores the options passed from the command line. Prior to calling run(), you may call:

opts.add() : Adds whatever command-line options you wish to support.

The arguments match optparse.OptionParser.add_option with one exception: if the first argument does not start with '-' (and is a valid python identifier string), it will be expanded to a short option, a long option, and the value of the dest parameter. For example, calling opts.add('foo', action='store_true') is the same as calling:

opts.add('-f', '--foo', dest='foo', action='store_true')

When script.run() is called, the opts object changes itself to contain whatever option values were parsed from the command line. It becomes the same kind of object typically returned from optparse.OptionParser.parse_args().

parent

Set to path(sys.path[0]) when the script module is first imported. Unless sys.path[0] is altered before import script, this is the directory containing the script passed to the python interpreter.

path(name, expand=True, normalize=True)

Instantiate and return a Path object, optionally expanding and normalizing the pathname prior to instantiation. Typically, this function is used more often than calling Path directly.

The name argument should be a pathname string.

If expand is True, the name string will be expanded using os.path.expanduser(os.path.expandvars(name)).

After any pathname expansion, if normalize is True, the name will be normalized via os.path.normpath(name).

Path(name)

A pathname or filename string, bundled with frequently used attributes and operations. Inspired by PEP 355.

Because filenames are strings, Path is a subclass of str.

The / operator may be used join paths into a new Path instance: path('x') / 'y' == path('x/y')

properties

All these are read-only property attributes. Except for sh and usize, each calls the function listed.

The abs, name, and parent properties are Path instances.

To just get the name of the file, without a path prefix or an extension, use .name.no_ext

The sh property computes an escaped path string suitable for evaluation in the bash shell. For example:

path('( &!?*"@`;,\)').sh == '\(\ \&\!\?\*\"\@\`\;\,\\)'

The usize property computes a string indicating the size of the file in three digits or less followed by a unit suffix: (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes, (T)erabytes, or (P)etabytes.

methods

Note that list, walk, and mkdir return or yield Path instances.

The list function iterates over each stand alone filename returned from os.listdir and returns self/filename (e.g. the filename prepended with path information)

The mkdir method does not throw an OSError if the directory already exists.

Items yielded by list may be filtered to match pattern. For example, list('*.py') yields only Path instances with .ext == '.py'.

The walk method yields (dirname,dirs,files). The items in files may also be filtered to match pattern. For example, walk('*.py') yields a files list containing only Path instances with .ext == '.py'.

paths(*args)

Returns an iterator yielding path(p) for each item glob.iglob(arg) in args. Each argument may be a str or an iterator, in which case the glob is applied to every item inside it.

run(main)

Once opts and doc are configured to your liking, call this method to parse the command line and call main(). Exit quietly, with return code of 1, if the user presses control-c.

shell(command, stdin=None, stdout=None, stderr=None, failcode='non-zero')

Runs a single command string in the shell as a subprocess.

The stdin, stdout, and stderr parameters control the standard input, output, and error file handles of the subprocess. By default, each one defaults to None to indicate that the subprocess should use the stdin, stdout, or stderr of the sys module. Each may be set to an existing file object or file descriptor (a positive integer).

You may redirect the standard error of the subprocess into the same file handle as its standard output by setting stderr to 'STDOUT'.

To capture the standard output or standard error of the subprocess into a bytes value in memory, set either stdout or stderr to 'PIPE'. Avoid this setting if the subprocess is going to return an especially large or unlimited amount of data.

Once the subprocess completes, a script.CommandResult object is returned with the following attributes:

If the returncode attribute matches the failcode parameter passed into the call to shell(), a script.CommandFailed exception will be raised. The failcode parameter may be set to None, a specific return code, or 'non-zero' to match any non-zero return code.

If the stdout or stderr parameter passed into the call to shell() was set to 'PIPE', the stdout_bytes or stderr_bytes attribute will be a bytes value containing all the standard output or error produced by the subprocess.

When the stdout_bytes or stderr_bytes property contains a bytes value, a reference to the stdout or stderr property will have the same effect as calling stdout_bytes.decode() or stderr_bytes.decode().