Paths
Paths
Almost every admin script works with files: log files to analyze, configuration files to read, backups to create. To do so, the script needs to know where those files are, i.e. it needs a path.
Until now, we've written paths as plain strings, such as "message.txt" or "/var/backups". That works as long as the script only ever runs on one system. As soon as the same script has to run on both Windows and Linux, or has to build a path from several pieces, plain strings become a source of bugs.
Windows and Linux use a different character to separate the folders in a path:
| Operating system | Separator | Example |
|---|---|---|
| Linux / macOS | / | /var/log/nginx/access.log |
| Windows | __ | C:\logs\nginx\access.log |
A script that builds a path by concatenating strings therefore only works on the system it was written for:
log_directory = "/var/log"
service = "nginx"
# Works on Linux, produces a nonsensical path on Windows.
log_file = log_directory + "/" + service + "/access.log"Concatenation also makes it easy to forget or add an extra separator:
log_directory = "/var/log/"
log_file = log_directory + "/" + "syslog" # '/var/log//syslog'
log_file = "/var/log" + "syslog" # '/var/logsyslog'Windows paths written as strings have one more pitfall. In the first chapter we saw that the backslash is used to escape characters in a string, so Python interprets part of the path as a special character:
path = "C:\new\test.txt"
print(path)C:
ew est.txt\n became a new line and \t a tab. You can avoid this by escaping each backslash ("C:\\new\\test.txt") or by putting an r in front of the string, which creates a raw string in which backslashes have no special meaning (r"C:\new\test.txt"). The better solution however is to not write separators at all, and let Python build the path for you.
Building paths
Concept: The pathlib module
The pathlib module allows us to construct paths that work on both Windows and *nix variants (Linux, BSD, macOS) and never include an extra or missing slash.
Paths are concatenated using the / operator which in this case has nothing to do with division.
from pathlib import Path
log_file = Path("/var/log") / "nginx" / "access.log"
print(log_file) # Output: /var/log/nginx/access.logOn Windows the same code produces \var\log\nginx\access.log, without a single change to the script.
Note
A Path is not a string, it is a separate type with its own methods. Printing a single Path or using it in an f-string shows the path as text, but printing a list of paths shows the type as well:
print([Path("/var/log"), Path("/etc")]) # Output: [PosixPath('/var/log'), PosixPath('/etc')]When a function requires a string, convert the path with str(). Functions from the standard library, such as open(), accept a Path directly.
The home directory
The welcome banner should show the last time the user logged in, in order to do this we'll write a log entry to a file in the user's home directory.
There are two viable approaches to this problem. Depending on the OS, the user's home directory will either be stored in the HOME or USERPROFILE environment variable. As previously discussed, we can use the sys.platform function to detect the OS and then select the appropriate variable. This is, however, rather cumbersome. The Path.home() function offers a clean alternative that works on both Windows and *nix variants.
The current date can be retrieved using the datetime module.
Note that we also use the os.path.exists() function to check whether the log file already exists before attempting to read it.
#!/usr/bin/env python3
import os
import platform
import sys
from pathlib import Path
from datetime import datetime
# --- Configuration ---
username = os.getlogin()
configuration = {
"SERVER NAME": platform.node(),
"ENVIRONMENT": "ENVIRONMENT",
"ADMIN CONTACT": "admin@thomasmore.be",
"OPERATING SYSTEM": sys.platform,
}
warning_msg = "UNAUTHORIZED ACCESS IS STRICTLY PROHIBITED"
login_log_path = Path.home() / '.login_log'
if os.path.exists(login_log_path):
with open(login_log_path, 'r') as login_log:
last_login = login_log.readlines()[-1]
else:
last_login = "Never"
with open(login_log_path, 'a') as login_log:
login_log.write(datetime.today().isoformat() + "\n")
# --- Banner Execution ---
print("=" * 60)
# The ^ operator can be used to center text in an f-string.
# In this case text will be centered within 60 characters.
print(f"{'SYSTEM ACCESS CONTROL':^60}")
print("=" * 60, end="\n\n")
for key, value in configuration.items():
key = key + ':'
print(f"{key:<30}{value}")
print("\n" + "-" * 60)
print(f"{warning_msg:^60}")
print("-" * 60)
print(f"\nWelcome, {username}.")
print(f"Last login: {last_login}\n")
print("Please ensure all changes are logged.")
print("=" * 60)