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!
Python 3.0 or later.
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
.
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')
abs
: os.path.abspath
name
: os.path.basename
parent
: os.path.dirname
ext
: (os.path.splitext)[-1]
no_ext
: (os.path.splitext)[0]
exists
: os.path.exists
ctime
: os.path.getctime
mtime
: os.path.getmtime
is_file
: os.path.isfile
is_dir
: os.path.isdir
size
: os.path.getsize
sh
: shell escaped pathnameusize
: '{size:3g}{unit}'
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.
list(pattern=None)
: iterator over os.listdir
yielding self/filenamewalk(pattern=None)
: iterator over os.walk
touch(mode=0o644)
: unix touch commandmkdir(mode=0o755)
: os.makedirs
if not (.exists
and .is_dir
)open_utf8(mode='r')
: call open()
with encoding='utf8'
match
: fnmatch.fnmatch
cd
, chdir
: os.chdir
chown
: os.chown
remove
: os.remove
rename
: os.rename
rmdir
: os.rmdir
rmtree
: shutil.rmtree
copy
: shutil.copy
older(other)
: self.mtime < path(other).mtime
newer(other)
: self.mtime > path(other).mtime
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:
stdout_bytes
, stderr_bytes
: None
or a bytes
value, see below.stdout
, stderr
: None
or a str
value, see below.returncode
: the return code of the subprocess.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()
.