On 30 April 2020, I had a chance to talk with Dustin Reybrouck for The SysAdminShow Podcast.
We mostly discussed why sysadmins might like to add Python as a tool in addition to shell scripting.
Most of his audience is probably familiar with bash or Powershell, so I showed how I might write somewhat simple but parameterized and documented version of head in bash and how it would be translated to Python 3.
Following is a summary of our discussion.
Sysadmins spend much of their lives on the command-line, so it makes sense that they would seek to automate tasks using the language of their command line — something like bash or Powershell.
While it is possible to write many useful programs in these languages, a higher-level language like Python might prove to be a better choice especially given that a Python program is portable between systems that natively understand bash (e.g., Linux or Mac) and Powershell (Window).
As an exercise, let’s write a bash implementation of the head command, then we’ll compare how we could write it in Python.
Let’s start by imagining how we might write our own implementation of the venerable head command.
For example, given the text of the US Constitution, we would expect to see the first few lines of a given file, usually 10:
$ head const.txt We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America. Article 1. Section 1 All legislative Powers herein granted shall be vested in a Congress of the
And we would expect to be able to modify that number using an option like -n:
$ head -n 3 const.txt We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to
It’s commonplace for command-line tools to respond to -h or --help with a "usage" statement about how the program should be invoked.
In the case of head [1], it does not give a usage because we ask for it but rather because it does not recognize these as valid options.
Still, it manages to produce a "usage" under some circumstances which is better than nothing:
$ head -h head: illegal option -- h usage: head [-n lines | -c bytes] [file ...]
Let’s start off with a version in bash that handles just one file and a possible number of lines which will default to 10.
If run with no arguments, it will print a "usage" statement:
$ ./simple-head.sh Usage: simple-head.sh FILE [NUM]
When run with a file as the only argument, it will print the first 10 lines:
$ ./simple-head.sh const.txt We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America. Article 1. Section 1 All legislative Powers herein granted shall be vested in a Congress of the
We can provide an optional second argument to change the number of lines we show:
$ ./simple-head.sh const.txt 2 We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common
Note that the program fails rather gracelessly. For instance, if we give it a non-existent file:
$ ./simple-head.sh foo ./simple-head.sh: line 24: foo: No such file or directory
Our program also fails to show an error if the second argument is not a number and will, in fact, print the entire file. Try running the program like so:
$ ./simple-head.sh const.txt foo
Still, it’s instructive to look at this program:
#!/bin/bash (1)
#
# Author : Ken Youens-Clark <kyclark@gmail.com> (2)
# Purpose: simple bash implementation of `head`
#
# Check number of arguments is 1 or 2
if [[ $# -lt 1 ]] || [[ $# -gt 2 ]]; then (3)
echo "Usage: $(basename "$0") FILE [NUM]" (4)
exit 1 (5)
fi
FILE=$1 (6)
NUM=${2:-10} (7)
LINE_NUM=0 (8)
while read -r LINE; do (9)
echo "$LINE" (10)
LINE_NUM=$((LINE_NUM+1)) (11)
if [[ $LINE_NUM -eq $NUM ]]; then (12)
break (13)
fi
done < "$FILE"
-
This line is often called the "shebang," and it is common to see the path to
bashhard-coded like this. It’s not necessarily best practice, however, asbashmight well be located at/usr/local/bin/bash. -
Any text following
#is ignored bybash. Here we add comments to the program, but you can also use this to temporarily disable code. It’s polite to document your code so that other might contact you with questions. -
Everything in
bashis a string, but we can use operators like-lt(less than) and-gt(greater than) to get numeric comparisons. The$#variable holds the number of arguments to our program, so we’re trying to see if we do not have exactly 1 or 2 arguments. -
We print a "usage"-type statement to explain to the user how to invoke the program. The
FILEis a required position argument while the[NUM]is shown in[]to indicate that it is optional. -
We
exitwith a non-zero value (1 is fine) to indicate that the program failed to run as expected. -
Since we know we have at least 1 argument, we can copy the value of the first argument in
$1to ourFILEvariable. -
We may or may not have a second argument, so we can either copy
$2or a default value of10to ourNUMvariable. -
Initialize a
LINE_NUMvariable to0so we can count how many lines of our file we have shown. -
A
whileloop is a common idiom for reading a file line-by-line inbash. -
The
echocommand will print text to the terminal. -
The
$(())evaluation will allow us to perform a bit of arithmetic with what is otherwise a string value. Here we want to add 1 to the value ofLINE_NUM. -
The
-eqis the numeric equality operator inbash. Here we check if theLINE_NUMis equal to the number of lines we mean to show. -
The
breakstatement will cause thewhileloop to exit.
The previous simple-head.sh version shows some basic ideas of how to handle many systems-level tasks in bash such as:
-
Documenting the language of the program with a shebang line
-
Documenting the author and purpose program with comments
-
Parameterizing your program so as to values as arguments rather than hard-coding values
-
Documenting the program parameters with an automatically generated "usage" when needed by the user
-
Exiting the program with non-zero values when the program does not complete as normally expected
-
Defining reasonable default values for optional arguments
Still, this is a rather sophomoric replacement for head because:
-
It does not handle multiple files
-
It fails to validate if the arguments are actually readable files
-
There is no
-noption because the program handles only positional arguments and so cannot handle options -
The program will not print a "usage" for
-h, again because it fails to handle options
Let’s write a better implementation that is a complete replacement for head:
#!/usr/bin/env bash (1)
#
# Author : Ken Youens-Clark <kyclark@gmail.com> (2)
# Purpose: bash implementation of `head`
#
# Die on use of uninitialize variables
set -u (3)
# Default value for the argument
NUM_LINES=10 (4)
# A function to print the "usage"
function USAGE() { (5)
printf "Usage:\n %s -n NUM_LINES [FILE ...]\n\n" "$(basename "$0")"
echo "Options:"
echo " -n NUM_LINES"
echo
exit "${1:-0}"
}
# Die if we have no arguments at all
[[ $# -eq 0 ]] && USAGE 1 (6)
# Process command line options
while getopts :n:h OPT; do (7)
case $OPT in (8)
n)
NUM_LINES="$OPTARG" (9)
shift 2 (10)
;;
h)
USAGE (11)
;;
:)
echo "Error: Option -$OPTARG requires an argument." (12)
exit 1
;;
\?)
echo "Error: Invalid option: -${OPTARG:-""}" (13)
exit 1
esac
done
# Verify that NUM_LINES looks like a positive integer
if [[ $NUM_LINES -lt 1 ]]; then (14)
echo "-n \"${NUM_LINES}\" must be > 0"
exit 1
fi
# Process the positional arguments
FNUM=0 (15)
for FILE in "$@"; do (16)
FNUM=$((FNUM+1)) (17)
# Verify this argument is a readable file
if [[ ! -f "$FILE" ]] || [[ ! -r "$FILE" ]]; then (18)
echo "\"${FILE}\" is not a readable file"
continue (19)
fi
# Print a header in case of mulitiple files
[[ $# -gt 1 ]] && echo "==> ${FILE} <==" (20)
# Initialize a counter variable
LINE_NUM=0 (21)
# Loop through each line of the file
while read -r LINE; do (22)
echo $LINE
# Increment the counter and see if it's time to break
LINE_NUM=$((LINE_NUM+1))
[[ $LINE_NUM -eq $NUM_LINES ]] && break (23)
done < "$FILE"
[[ $# -gt 1 ]] && [[ $FNUM -lt $# ]] && echo (24)
done
exit 0
-
Using the
envprogram (which is pretty universally located at/usr/bin/env) to findbashis more flexible than hard-coding the path as/bin/bash. -
Same documentation as comments.
-
This will cause
bashto die if we attempt to use an uninitialized variable and is one of the few safety features offered by the language. -
Here we set a default value for the
NUM_LINESto show which can be overridden by an option. -
Since there are a multiple times I might want to show the usage and exit with an error (e.g., no arguments or as requested by
-h), I can put this into afunctionto call later. -
If the number of arguments to the program
$#is 0, then exit with a "usage" statement and a non-zero value. -
We can use
getoptsinbashto manually parse the command-line arguments. We are specifically looking for flags-nwhich takes a value and-hwhich does not. -
$OPTwill have the flag value such asnfor-norhfor-h. -
The
$OPTARGwill have the value for the-nflag. We can copy that to ourNUM_LINESvariable to save it. -
Now that we have processed
-n 3, for instance, we useshift 2to discard those two values from the program arguments$@. -
If processing the
-hflag, call theUSAGEfunction which will cause the program to exit. -
This handles when an option like
-ndoes not have an accompanying value. -
This handles an option we didn’t define.
-
This use the
-ltoperator to coerce theNUM_LINESto a numeric value. If it is less than-lt1, we throw an error and exit with a non-zero value. -
Now that we have handled the optional arguments, we can handle the rest of the positional arguments found in
$@. We start off by defining aFNUMso we can track the file number we are working with. That is, this is the index value of the current file. -
We can use a
forloop to iterate through the positional arguments found in$@. -
Add 1 to the
FNUMvariable. -
The
-ftest will return a "true" value if the given argument is a file, and!will negate this. Ditto as-rwill report if the argument is a readable file. -
The
continuestatement will cause theforloop to immediately advance to the next iteration, skipping all the code below. -
If the number of positional arguments is greater than
-gt1, then print a header showing the current file’s name. -
Initialize a line count variable for reading the file.
-
This is the same loop as before that we used to read a given number of lines from the file. This one is improved, however, because we check if the number argument from the user is actually a positive integer!
-
This is a shorter way to write a single-line
ifstatement. -
If there are multiple files to process and we’re not currently on the last file, then print an extra newline to separate the outputs.
If you are new to bash programming, the syntax will probably look rather cryptic!
The entirely manual handling of the command-line options and positional arguments is especially cumbersome.
I will admit this is not an easy program to write correctly, and, even when it finally works on my Linux and Max machines, I won’t be able to give it to a Windows user unless they have something like WSL (Windows Subsystem for Linux) or Cygwin installed.
Still, this program works rather well!
It will print nice documentation if we run with no arguments or if you run ./head.sh -h, which is actually an improvement over head:
$ ./head.sh Usage: head.sh -n NUM_LINES [FILE ...] Options: -n NUM_LINES
It rejects bad options:
$ ./head.sh -x 8 const.txt Error: Invalid option: -x
It can handle both options and positional arguments, provides a reasonable default for the -n option, and correctly skips non-file arguments:
$ ./head.sh -n 3 foo const.txt "foo" is not a readable file ==> const.txt <== We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to
And it mimics the output from head for multiple files:
$ ./head.sh -n 1 const.txt simple-head.sh head.sh ==> const.txt <== We the People of the United States, in Order to form a more perfect Union, ==> simple-head.sh <== #!/bin/bash ==> head.sh <== #!/usr/bin/env bash
For what it’s worth, I used the included new_bash.py program to create this program.
If you find yourself stuck writing a bash program and don’t wish to start from scratch, this program might be useful to you.
I have included a test.py that is a Python program that will run the head.sh program to ensure it actually does what it is supposed to do.
If you look at the contents of this program, you will see a number of functions with names that start with test_.
This is because I use the pytest module/program to run these functions as a test suite.
I like to use the -x flag to indicate that testing should halt at the first failing test and the -v flag for "verbose" output.
These can be specified individually or combined like -xv or -vx:
$ pytest -xv test.py ============================= test session starts ============================== ... test.py::test_exists PASSED [ 14%] test.py::test_usage PASSED [ 28%] test.py::test_bad_file PASSED [ 42%] test.py::test_bad_num PASSED [ 57%] test.py::test_default PASSED [ 71%] test.py::test_n PASSED [ 85%] test.py::test_multiple_files PASSED [100%] ============================== 7 passed in 0.56s ===============================
It’s a bit of a nuisance to have to write the tests for a program in a different language from the program itself, but I know of no testing framework in bash that I’d could use (or would like to learn) that can run a test suite such as the above!
To write a similar version in Python, we’ll rely heavily on the standard argparse module to handle the validation of all the command-line arguments as well as generating the "usage" statements.
Here is a version that, similar to the simple-head.py, will handle just one file:
#!/usr/bin/env python3 (1)
""" (2)
Author : Ken Youens-Clark
Purpose: Python implementation of head
This version only handles one file!
"""
import argparse (3)
import os
import sys
# --------------------------------------------------
def get_args(): (4)
"""Get command-line arguments""" (5)
parser = argparse.ArgumentParser( (6)
description='Python implementation of head',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('file', (7)
metavar='FILE',
type=argparse.FileType('rt'), (8)
help='Input file')
parser.add_argument('-n', (9)
'--num',
help='Number of lines',
metavar='int',
type=int, (10)
default=10) (11)
args = parser.parse_args() (12)
if args.num < 1: (13)
parser.error(f'--num "{args.num}" must be > 0') (14)
return args (15)
# --------------------------------------------------
def main(): (16)
"""Make a jazz noise here"""
args = get_args() (17)
for i, line in enumerate(args.file, start=1): (18)
print(line, end='') (19)
if i == args.num: (20)
break (21)
# --------------------------------------------------
if __name__ == '__main__': (22)
main()
-
The "shebang" uses the
envprogram to find the firstpython3in our$PATH. -
The triple quotes allow us to create a string that spans multiple lines. Here we’re creating a string but not assigning it to a variable. This is a convention for creating documentation also called a "docstring." This docstring summarizes the program itself. I like to document at least who wrote it and why.
-
We can
importcode from other modules. While we canimportseveral modules separated by commas, it’s recommended to put each on a separate line. Specifically we want to useargparseto handle the command-line arguments, and we’ll also use theos(operating system) andsys(systems) modules. -
I like to always define a
get_args()function that exclusively deals withargparsefor creating the program’s parameters and validating the arguments. I always place this first so I can see it immediately when I’m reading the program. -
This is a docstring for the function. It’s ignored like a comment would be, but it has significance to Python and would appear if I were to
importthis module and ask forhelp(get_args). -
This creates a
parserthat will handle the command-line arguments. I add a description for the program that will appear in any "usage" statements, and I always like to haveargparsedisplay any default values for the user. -
Positional arguments have no leading dashes in their names. Here we define a single positional argument that we can refer to internally as
file. -
The default
typefor all arguments is astr(string). We can askargparseto enforce a different type likeintand it will print an error when the user fails to provide a value that can be parsed into an integer value. Here we are using the specialargparsetype that defined a "readable" ('r') "text" ('t') file. If the user provides anything other than a readable text file,argparsewill halt the program, print an error and usage, and exit with a non-zero value. -
The leading
-on-n(short name) and--num(long name) for the "number" argument means this will be an option. -
The user must provide a value that can be parsed into a
intvalue. -
The default value will be 10.
-
After defining the program’s parameters, we ask the
parserto parse the arguments. If there are any problems like the wrong number or types of arguments,argparsewill stop the program here. -
If we get to this point, the arguments were valid as far as
argparseis concerned. We can perform additional manual checks such as verifying thatargs.numis greater than 0. -
The
parser.error()function is a way for us to manually invoke the error-out function ofargparse. -
Functions in Python must explicitly
returna value or theNonewill be returned by default. Here was want to return theargsto the calling function. -
Convention dictates the starting function be called
main(), but this is not a requirement, and Python will not automatically call this function to start the program. Neitherget_args()normain()accept arguments, but, if they did, they would be listed in the parens. -
All the work to define the parameters, validate the arguments, and handle help and usage has now been hidden in the
get_args()function. We can think of this as a "unit" that encapsulates those ideas. If our program successfully callsget_args()and returns with someargs, then we can move forward knowing the arguments are actually correct and useful. -
We don’t have to initialize a counting variable like in
bashas we can use theenumerate()function to return the index and value of any sequence of items. Here theargs.fileis actually an open file handle provided byargparsebecause we defined theargs.fileas a "file" type. That means I’ll be iterating over the lines in the file handle. I can use thestartoption toenumerate()to start counting at 1 instead of 0. -
The
print()function is like theechostatement inbash. Here there will be a newline stuck to thelinefrom the file, so I use theend=''to indicate thatprint()should not add the customary newline to the output. -
While
bashuses-eqfor numeric comparison and==for string equality, Python uses==for both. -
Both Python and
bashusecontinueandbreakin loops to skip and leave loops, respectively. -
This is the idiom in Python to detect when a program/module is being run from the command line. Here we want to execute the
main()function to start the program to running.
The above program has been contributed as py-head/solution1.py, and you can run it to see how it will create usage for no arguments:
$ ./solution1.py usage: solution1.py [-h] [-n int] FILE solution1.py: error: the following arguments are required: FILE
Note that we did not define the -h and --help flags to argparse as those are reserved specifically for generating help:
$ ./solution1.py -h usage: solution1.py [-h] [-n int] FILE Python implementation of head positional arguments: FILE Input file optional arguments: -h, --help show this help message and exit -n int, --num int Number of lines (default: 10)
Note that argparse can actually handle options following positional arguments:
$ ./solution1.py const.txt -n 3 We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, promote the general Welfare, and secure the Blessings of Liberty to
The previous Python version demonstrates many shortcuts to creating usable and documented programs that are easier to write and more reliable than the bash version.
Still, this program is not yet a full replacement either for head or even head.sh.
Let’s see how we can expand the program to handle one or more positional arguments:
#!/usr/bin/env python3
"""
Author : Ken Youens-Clark
Purpose: Python implementation of head
This version handles multiple files
and is very similar to the bash version.
"""
import argparse
# --------------------------------------------------
def get_args():
"""Get command-line arguments"""
parser = argparse.ArgumentParser(
description='Python implementation of head',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('file',
metavar='FILE',
type=argparse.FileType('rt'),
nargs='+', (1)
help='Input file')
parser.add_argument('-n',
'--num',
help='Number of lines',
metavar='int',
type=int,
default=10)
args = parser.parse_args()
if args.num < 1:
parser.error(f'--num "{args.num}" must be > 0')
return args
# --------------------------------------------------
def main():
"""Make a jazz noise here"""
args = get_args()
num_files = len(args.file) (2)
for fnum, fh in enumerate(args.file, start=1): (3)
if num_files > 1: (4)
print(f'==> {fh.name} <==') (5)
for line_num, line in enumerate(fh, start=1): (6)
print(line, end='')
if line_num == args.num:
break
if num_files > 1 and fnum < num_files: (7)
print()
# --------------------------------------------------
if __name__ == '__main__':
main()
-
The
nargsoption can take*to indicate zero or more,+for one or more, and?for zero or one. -
Since I’ll refer to the number of files (
num_files) several times, I put them into a variable. Theargs.fileargument is now alistof open file handles. I can use thelen()function to ask the length of thislistwhich will tell me the number of files provided as arguments. I know they are actually readable text files because of thetypeconstraint I added to this argument. -
Again I want both the index (position) and value of each element in
args.file, so I can useenumerate(), starting the counting at 1 instead of 0. -
Decide whether to print a header.
-
I call the variable
fhto remind me that this is an open file handle. I can get the name of the file itself usingfh.name. Thef''(f-string) allows me to interpolate thefh.namevalue inside the string given toprint(). -
A second loop to iterate over the lines in the file. This is the same code as above.
-
Decide whether to
print()an extra newline between multiple files.
This version is a full implementation of a typical head program and demonstrates many common systems-level programming concepts a sysadmin might need.
The os and sys modules are particularly rich in functions for dealing with files and directories and permissions and the like.
The argparse code allows one to outsource program validation to another module allowing the coder to focus on the tasks at hand rather than the implementation of tedious and repetitive tasks.
“Without requirements or design, programming is the art of adding bugs to an empty text file." - Louis Srygley
Python’s pytest module provides a rather simple and elegant way to construct a test suite.
I have included a test.py to demonstrate how I typically write integration tests which exercise a program externally and verify that they work as intended.
In my own programs, I also tend to write many unit tests that similarly exercise individual functions (the "units" of programming) to ensure they work as expected.
By combining both unit and integration tests, I come to have greater confidence that my code works. More importantly, I feel free to refactor my code to improve algorithms and add features without fearing I will break features that worked previously. When testing, I always run the entire test suite in such a way that testing halts at the first failure:
$ make test pytest -xv test.py ============================= test session starts ============================== ... test.py::test_exists PASSED [ 14%] test.py::test_usage PASSED [ 28%] test.py::test_bad_file PASSED [ 42%] test.py::test_bad_num PASSED [ 57%] test.py::test_default PASSED [ 71%] test.py::test_n PASSED [ 85%] test.py::test_multiple_files PASSED [100%] ============================== 7 passed in 0.84s ===============================
This test.py is almost identical to the one for the head.sh.
The differences mostly account for how I felt it best to handle errors in the two programs.
The best improvement, of course, is that now my tests are in the same language as the program, so it’s easier for me to go back and forth between them!
The previous version of head.py is reflected in the solution2.py version.
We can briefly look at solution3.py to explore more advanced ideas in Python like function definition, list comprehensions, mock file handles, and unit testing.
#!/usr/bin/env python3
"""
Author : Ken Youens-Clark
Purpose: Python implementation of head
This version handles multiple files
and uses more advanced ideas like function definition,
list comprehensions, mock file handles, unit testing, etc.
"""
import argparse
import io
# --------------------------------------------------
def get_args():
"""Get command-line arguments"""
parser = argparse.ArgumentParser(
description='Python implementation of head',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('file',
metavar='FILE',
type=argparse.FileType('rt'),
nargs='+',
help='Input file')
parser.add_argument('-n',
'--num',
help='Number of lines',
metavar='int',
type=int,
default=10)
args = parser.parse_args()
if args.num < 1:
parser.error(f'--num "{args.num}" must be > 0')
return args
# --------------------------------------------------
def main():
"""Make a jazz noise here"""
args = get_args()
show_header = len(args.file) > 1 (1)
heads = [head(fh, args.num, show_header) for fh in args.file] (2)
print('\n'.join(heads)) (3)
# --------------------------------------------------
def head(fh, num, show_header): (4)
"""Return num lines from file handle"""
lines = [f'==> {fh.name} <==\n'] if show_header else [] (5)
for line_num, line in enumerate(fh, start=1): (6)
lines.append(line)
if line_num == num:
break
return ''.join(lines) (7)
# --------------------------------------------------
def test_head(): (8)
"""Test head"""
assert head(io.StringIO('foo\nbar\nbaz\n'), 1, False) == 'foo\n' (9)
assert head(io.StringIO('foo\nbar\nbaz\n'), 2, False) == 'foo\nbar\n' (10)
# --------------------------------------------------
if __name__ == '__main__':
main()
-
Whether or not we show a header between each file is a function of there being 1 or more files.
-
We use a list comprehension to create the
headof each file. -
print()all the values ofhead()for each file. -
Here we define a function called
head()that takes three arguments. -
We initialize a
linesvariable using anifexpression that hinges on whether toshow_header. -
This is the same logic as before, but now rather than calling
print()on eachline, we append it to thelistoflines. -
Return the
linesjoined on the empty string. Note that eachlinestill has any newline from the file. -
Any function starting with
test_will be run bypytest. Here I define a unit test to run thehead()function. -
The
head()function expects something like an open file handle. I can useio.StringIOto make a sort of mock file handle from a string. Each value ending with a newline\nwill appear as a "line" of text. -
The
assertstatement will throw an exception if the given value does not evaluate as "true" (but not necessarily asTrue). Here I want thehead()function to return the stringfoo\nbar\nwhen I run the function with the given values.
I can copy this version to head.py (or modify test.py to use solution3.py instead of head.py`_ and run `pytest -xv test.py to validate the program.
Additionally, I can run pytest directly on the program to run my unit tests:
$ pytest -xv solution3.py ============================= test session starts ============================== ... solution3.py::test_head PASSED [100%] ============================== 1 passed in 0.01s ===============================
As programs grow in length and complexity, it makes more sense to write small, tested functions that can be composed into larger, more stable programs.
Similar to the new_bash.py program I mentioned in the bash section, I have included the new.py program I use to create new Python programs that use argparse to validate arguments.
You run it with -h for help, of course:
$ ./new.py -h
usage: new.py [-h] [-n NAME] [-e EMAIL] [-p PURPOSE] [-f] program
Create Python argparse program
positional arguments:
program Program name
optional arguments:
-h, --help show this help message and exit
-n NAME, --name NAME Name for docstring (default: Ken Youens-Clark)
-e EMAIL, --email EMAIL
Email for docstring (default: kyclark@gmail.com)
-p PURPOSE, --purpose PURPOSE
Purpose for docstring (default: Rock the Casbah)
-f, --force Overwrite existing (default: False)
If I wanted to create, for instance, a Python implementation of cat now, I might do this:
$ ./new.py cat.py Done, see new script "cat.py."
And now I have a cat.py program that I can immediately execute that shows the typical kinds of positional and optional parameters a program might have:
$ ./cat.py -h usage: cat.py [-h] [-a str] [-i int] [-f FILE] [-o] str Rock the Casbah positional arguments: str A positional argument optional arguments: -h, --help show this help message and exit -a str, --arg str A named string argument (default: ) -i int, --int int A named integer argument (default: 0) -f FILE, --file FILE A readable file (default: None) -o, --on A boolean flag (default: False)
You can modify the get_args() function to reflect the needs of your new program.
Place this program into your $PATH to use system-wide whenever you need to create a new Python program.
I hope you may find this speeds you in your development of new Python programs!
-
Tiny Python Projects: All of these ideas about
argparseand testing are discussed in greater detail in this book available now from Manning Publications. -
Make tutorial: Dustin and I also briefly discussed the use of
makeandMakefileto document and automate the running of tests and shortcuts and commands and such. -
GitHub repo: All the code and tests for Tiny Python Projects
-
YouTube: Videos for the chapters of Tiny Python Projects that demonstrate how to start writing Python programs and work through the requirements and tests and solutions.
Ken Youens-Clark
head or grep can vary among systems and distributions. I tried head on both Linux and Mac, and neither recognized the help flags.