Python 3 Scripting for System Administrators:-Part:2


#1

The not Operation

Sometimes we want to know the opposite boolean value for something. This might not sound intuitive, but sometimes we want to execute an if statement when a value is False, but that’s not how the if statement works. Here’s an example of how we can use not to make this work:

>>> name = ""

>>> not name

True

>>> if not name:

… print("No name given")

>>>

We know that an empty string is a “falsy” value, so not "" will always return True. not will return the opposite boolean value for whatever it’s operating on.

The or Operation

Occasionally, we want to carry out a branch in our logic if one condition OR the other condition is True. Here is where we’ll use the or operation. Let’s see or in action with an if statement:

>>> first = ""

>>> last = "Thompson"

>>> if first or last:

… print("The user has a first or last name")

The user has a first or last name

>>>

If both first and last were “falsy” then the print would never happen:

>>> first = ""

>>> last = ""

>>> if first or last:

… print("The user has a first or last name")

>>>

Another feature of or that we should know is that we can use it to set default values for variables:

>>> last = ""

>>> last_name = last or "Doe"

>>> last_name

‘Doe’

>>>

The or operation will return the first value that is “truthy” or the last value in the chain:

>>> 0 or 1

1

>>> 1 or 2

1

The and Operation

The opposite of or is the and operation, which requires both conditions to be True. Continuing with our first and last name example, let’s conditionally print based on what we know:

>>> first = "Keith"

>>> last = ""

>>> if first and last:

… print(f"Full name: {first} {last}")

… elif first:

… print(f"First name: {first}")

… elif last:

… print(f"Last name: {last}")

First name: Keith

>>>

Now let’s try the same thing with both first and last:

>>> first = "Keith"

>>> last = "Thompson"

>>> if first and last:

… print(f"Full name: {first} {last}")

… elif first:

… print(f"First name: {first}")

… elif last:

… print(f"Last name: {last}")

Full name: Keith Thompson

>>>

The and operation will return the first value that is “falsy” or the last value in the chain:

>>> 0 and 1

0

>>> 1 and 2

2

>>> (1 == 1) and print("Something")

Something

>>> (1 == 2) and print("Something")

False

Accepting User Input Using input

We’re going to build a script that requests three pieces of information from the user after the script runs. Let’s collect this data:

name - The user’s name as a string

birthdate - The user’s birthdate as a string

age - The user’s age as an integer (we’ll need to convert it)

~/bin/age

#!/usr/bin/env python3.6

name = input("What is your name? ")

birthdate = input("What is your birthdate? ")

age = int(input("How old are you? "))

print(f"{name} was born on {birthdate}")

print(f"Half of your age is {age / 2}")

Function Basics

We can create functions in Python using the following:

The def keyword

The function name - lowercase starting with a letter or underscore (_)

Left parenthesis (()

0 or more argument names

Right parenthesis ())

A colon :

An indented function body

Here’s an example without an argument:

>>> def hello_world():

… print("Hello, World!")

>>> hello_world()

Hello, World!

>>>

If we want to define an argument we will put the variable name we want it to have within the parentheses:

>>> def print_name(name):

… print(f"Name is {name}")

>>> print_name("Keith")

Name is Keith

Let’s try to assign the value from print_name to a variable:

>>> output = print_name("Keith")

Name is Keith

>>> output

>>>

Neither of these examples has a return value, but we will usually want to have a return value unless the function is our “main” function or carries out a “side-effect” like printing. If we don’t explicitly declare a return value, then the result will be None.

We can declare what we’re returning from a function using the return keyword:

>>> def add_two(num):

… return num + 2

>>> result = add_two(2)

>>> result

4

Encapsulating Behavior with Functions

To dig into functions, we’re going to write a script that prompts the user for some information and calculates the user’s Body Mass Index (BMI). That isn’t a common problem, but it’s something that makes sense as a function and doesn’t require us to use language features that we haven’t learned yet.

Here’s the formula for BMI:

BMI = (weight in kg / height in meters squared )

For Imperial systems, it’s the same formula except you multiply the result by 703.

We want to prompt the user for their information, gather the results, and make the calculations if we can. If we can’t understand the measurement system, then we need to prompt the user again after explaining the error.

Gathering Info

Since we want to be able to prompt a user multiple times we’re going to package up our calls to input within a single function that returns a tuple with the user given information:

def gather_info():

height = float(input("What is your height? (inches or meters) "))

weight = float(input("What is your weight? (pounds or kilograms) "))

system = input("Are your measurements in metric or imperial units? ").lower().strip()

return (height, weight, system)

We’re converting the height and weight into float values, and we’re okay with a potential error if the user inputs an invalid number. For the system, we’re going to standardize things by calling lower to lowercase the input and then calling strip to remove the whitespace from the beginning and the end.

The most important thing about this function is the return statement that we added to ensure that we can pass the height, weight, and system back to the caller of the function.

Calculating and Printing the BMI

Once we’ve gathered the information, we need to use that information to calculate the BMI. Let’s write a function that can do this:

def calculate_bmi(weight, height, system=‘metric’):

"""

Return the Body Mass Index (BMI) for the

given weight, height, and measurement system.

"""

if system == ‘metric’:

bmi = (weight / (height ** 2))

else:

bmi = 703 * (weight / (height ** 2))

return bmi

This function will return the calculated value, and we can decide what to do with it in the normal flow of our script.

The triple-quoted string we used at the top of our function is known as a “documentation string” or “doc string” and can be used to automatically generated documentation for our code using tools in the Python ecosystem.

Setting Up The Script’s Flow

Our functions don’t do us any good if we don’t call them. Now it’s time for us to set up our scripts flow. We want to be able to re-prompt the user, so we want to utilize an intentional infinite loop that we can break out of. Depending on the system, we’ll determine how we should calculate the BMI or prompt the user again. Here’s our flow:

while True:

height, weight, system = gather_info()

if system.startswith(‘i’):

bmi = calculate_bmi(weight, system=‘imperial’, height=height)

print(f"Your BMI is {bmi}")

break

elif system.startswith(‘m’):

bmi = calculate_bmi(weight, height)

print(f"Your BMI is {bmi}")

break

else:

print("Error: Unknown measurement system. Please use imperial or metric.")

Full Script
Once we’ve written our script, we’ll need to make it executable (using chmod u+x ~/bin/bmi).
~/bin/bmi
#!/usr/bin/env python3.6

def gather_info():
height = float(input("What is your height? (inches or meters) "))
weight = float(input("What is your weight? (pounds or kilograms) "))
system = input("Are your mearsurements in metric or imperial systems? ").lower().strip()
return (height, weight, system)

def calculate_bmi(weight, height, system=‘metric’):
if system == ‘metric’:
bmi = (weight / (height ** 2))
else:
bmi = 703 * (weight / (height ** 2))
return bmi

while True:
height, weight, system = gather_info()
if system.startswith(‘i’):
bmi = calculate_bmi(weight, system=‘imperial’, height=height)
print(f"Your BMI is {bmi}")
break
elif system.startswith(‘m’):
bmi = calculate_bmi(weight, height)
print(f"Your BMI is {bmi}")
break
else:
print(“Error: Unknown measurement system. Please use imperial or metric.”)

Working with Environment Variables

By importing the os package, we’re able to access a lot of miscellaneous operating system level attributes and functions, not the least of which is the environ object. This object behaves like a dictionary, so we can use the subscript operation to read from it.

Let’s create a simple script that will read a ‘STAGE’ environment variable and print out what stage we’re currently running in:

~/bin/running

#!/usr/bin/env python3.6

import os

stage = os.environ["STAGE"].upper()

output = f"We’re running in {stage}"

if stage.startswith("PROD"):

output = "DANGER!!! - " + output

print(output)

We can set the environment variable when we run the script to test the differences:

$ STAGE=staging running

We’re running in STAGING

$ STAGE=production running

DANGER!!! - We’re running in PRODUCTION

What happens if the ‘STAGE’ environment variable isn’t set though?

$ running

Traceback (most recent call last):

File "/home/user/bin/running", line 5, in

stage = os.environ["STAGE"].upper()

File "/usr/local/lib/python3.6/os.py", line 669, in getitem

raise KeyError(key) from None

KeyError: ‘STAGE’

This potential KeyError is the biggest downfall of using os.environ, and the reason that we will usually use os.getenv.

Handling A Missing Environment Variable

If the ‘STAGE’ environment variable isn’t set, then we want to default to ‘DEV’, and we can do that by using the os.getenv function:

~/bin/running

#!/usr/bin/env python3.6

import os

stage = os.getenv("STAGE", "dev").upper()

output = f"We’re running in {stage}"

if stage.startswith("PROD"):

output = "DANGER!!! - " + output

print(output)

Now if we run our script without a ‘STAGE’ we won’t have an error:

$ running

We’re running in DEV

Interacting with Files

It’s pretty common to need to read the contents of a file in a script and Python makes that pretty easy for us. Before we get started, let’s create a text file that we can read from called xmen_base.txt:

~/xmen_base.txt

Storm

Wolverine

Cyclops

Bishop

Nightcrawler

Now that we have a file to work with, let’s experiment from the REPL before writing scripts that utilize files.

Opening and Reading a File

Before we can read a file, we need to open a connection to the file. Let’s open the xmen_base.txt file to see what a file object can do:

>>> xmen_file = open(‘xmen_base.txt’, ‘r’)

>>> xmen_file

<_io.TextIOWrapper name=‘xmen_base.txt’ mode=‘r’ encoding=‘UTF-8’>

The open function allows us to connect to our file by specifying the path and the mode. We can see that our xmen_file object is an _io.TextIOWrapper so we can look at the documentation to see what we can do with that type of object.

There is a read function so let’s try to use that:

>>> xmen_file.read()

‘Storm\nWolverine\nCyclops\nBishop\nNightcrawler\n’

>>> xmen_file.read()

‘’

read gives us all of the content as a single string, but notice that it gave us an empty string when we called the function as second time. That happens because the file maintains a cursor position and when we first called read the cursor was moved to the very end of the file’s contents. If we want to reread the file we’ll need to move the beginning of the file using the seek function like so:

>>> xmen_file.seek(0)

0

>>> xmen_file.read()

‘Storm\nWolverine\nCyclops\nBishop\nNightcrawler\n’

>>> xmen_file.seek(6)

6

>>> xmen_file.read()

‘Wolverine\nCyclops\nBishop\nNightcrawler\n’

By seeking to a specific point of the file, we are able to get a string that only contains what is after our cursor’s location.

Another way that we can read through content is by using a for loop:

>>> xmen_file.seek(0)

0

>>> for line in xmen_file:

… print(line, end="")

Storm

Wolverine

Cyclops

Bishop

Nightcrawler

>>>

Notice that we added a custom end to our printing because we knew that there were already newline characters (\n) in each line.

Once we’re finished working with a file, it is import that we close our connection to the file using the closefunction:

>>> xmen_file.close()

>>> xmen_file.read()

Traceback (most recent call last):

File "", line 1, in

ValueError: I/O operation on closed file.

>>>

Creating a New File and Writing to It

We now know the basics of reading a file, but we’re also going to need to know how to write content to files. Let’s create a copy of our xmen file that we can add additional content to:

>>> xmen_base = open(‘xmen_base.txt’)

>>> new_xmen = open(‘new_xmen.txt’, ‘w’)

We have to reopen our previous connection to the xmen_base.txt so that we can read it again. We then create a connection to a file that doesn’t exist yet and set the mode to w, which stands for “write”. The opposite of the read function is the write function, and we can use both of those to populate our new file:

>>> new_xmen.write(xmen_base.read())

>>> new_xmen.close()

>>> new_xmen = open(new_xmen.name, ‘r+’)

>>> new_xmen.read()

‘Storm\nWolverine\nCyclops\nBishop\nNightcrawler\n’

We did quite a bit there, let’s break that down:
1. We read from the base file and used the return value as the argument to write for our new file.
2. We closed the new file.
3. We reopened the new file, using the r+ mode which will allow us to read and write content to the file.
4. We read the content from the new file to ensure that it wrote properly.

Now that we have a file that we can read and write from let’s add some more names:

>>> new_xmen.seek(0)

>>> new_xmen.write("Beast\n")

6

>>> new_xmen.write("Phoenix\n")

8

>>> new_xmen.seek(0)

0

>>> new_xmen.read()

‘Beast\nPhoenix\ne\nCyclops\nBishop\nNightcrawler\n’

What happened there? Since we are using the r+ we are overwriting the file on a per character basis since we used seek to go back to the beginning of the file. If we reopen the file in the w mode, the pre-existing contents will be truncated.

Appending to a File

A fairly common thing to want to do is to append to a file without reading its current contents. This can be done with the a mode. Let’s close the xmen_base.txt file and reopen it in the a mode to add another name without worrying about losing our original content. This time, we’re going to use the with statement to temporarily open the file and have it automatically closed after our code block has executed:

>>> xmen_file.close()

>>> with open(‘xmen_base.txt’, ‘a’) as f:

… f.write(‘Professor Xavier\n’)

17

>>> f = open(‘xmen_base.txt’, ‘a’)

>>> with f:

… f.write("Something\n")

10

>>> exit()

To test what we just did, let’s cat out the contents of this file:

$ cat xmen_base.txt

Storm

Wolverine

Cyclops

Bishop

Nightcrawler

Professor Xavier

Something

Building a CLI to Reverse Files

The tool that we’re going to build in this video will need to do the following:
1. Require a filename argument, so it knows what file to read.
2. Print the contents of the file backward (bottom of the script first, each line printed backward)
3. Provide help text and documentation when it receives the --help flag.
4. Accept an optional --limit or -l flag to specify how many lines to read from the file.
5. Accept a --version flag to print out the current version of the tool.
This sounds like quite a bit, but thankfully the argparse module will make doing most of this trivial. We’ll build this script up gradually as we learn what the argparse.ArgumentParser can do. Let’s start by building an ArgumentParser with our required argument:

~/bin/reverse-file

#!/usr/bin/env python3.6

import argparse

parser = argparse.ArgumentParser()

parser.add_argument(‘filename’, help=‘the file to read’)

args = parser.parse_args()

print(args)

Here we’ve created an instance of ArgumentParser without any arguments. Next, we’ll use the add_argument method to specify a positional argument called filename and provide some help text using the help argument. Finally, we tell the parser to parse the arguments from stdin using the parse_argsmethod and stored off the parsed arguments as the variable args.

Let’s make our script executable and try this out without any arguments:

$ chmod u+x ~/bin/reverse-file

$ reverse-file

usage: reverse-file [-h] filename

reverse-file: error: the following arguments are required: filename

Since filename is required and wasn’t given the ArgumentParser object recognized the problem and returned a useful error message. That’s awesome! We can also see that it looks like it takes the -h flag already, let’s try that now:

$ reverse-file -h
usage: reverse-file [-h] filename

positional arguments:
filename the file to read

optional arguments:
-h, --help show this help message and exit

It looks like we’ve already handled our requirement to provide help text. The last thing we need to test out is what happens when we do provide a parameter for filename:

$ reverse-file testing.txt

Namespace(filename=‘testing.txt’)

We can see here that args in our script is a Namespace object. This is a simple type of object that’s sole purpose is to hold onto named pieces of information from our ArgumentParser as attributes. The only attribute that we’ve asked it to hold onto is the filename attribute, and we can see that it set the value to ‘testing.txt’ since that’s what we passed in. To access these values in our code, we will chain off of our args object with a period:

>>> args.filename

‘testing.txt’