Skip to content

London | Nadika Zavodovska | Module-Tools | Sprint 4 | Implement Shell Tools in Python #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
.venv
79 changes: 79 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Buil-in module to parse command-line arguments and options
import argparse
# Finds all file paths matching a pattern
import glob

# Setup argument parser. Creates an instance(object) from built-in ArgumentParser class
parser = argparse.ArgumentParser(
prog="cat command",
description="Implement 'cat' command with -n and -b flags",
epilog="Now you can see the content of files with numbers of lines")

# Define flags and arguments
parser.add_argument("-n", action="store_true", help="Show the content with numbers of each lines")
parser.add_argument("-b", action="store_true", help="Show the content with numbers of each lines which is not empty")
# Add positional argument for one or more file paths (nargs="+")
parser.add_argument("paths", nargs="+", help="Specify file paths, included wildcards")

# Create args - an instance of the class argparse.Namespace
args = parser.parse_args()

# A global variable to keep track of line numbers across all files
line_number = 1

# Function to display content and lines for specific condition which a user provides
def display_file_contents(file_path, display_line_numbers = False, display_not_empty_line_numbers = False):
# Make sure that this variable refers to the global line_number to persist across different files
global line_number
try:
# Open file. "r" - read mode. with - ensure the file will be closed after the block of code finishes running.
with open(file_path, "r", encoding = "utf-8") as file_object:
# loopp through each line
for line in file_object:
# rstrip("\n") - removes any trailing newline character (\n) from the end of each line
line_to_print = line
# if a user set "-b" flag
if display_not_empty_line_numbers:
# Print if there is not empty line after removing any whitespace from the start and end of the line
if line.strip():
# {line_number:6} - string formatting
print(f"{line_number:6} {line_to_print}", end="")
# Increments the line number after printing the line.
line_number += 1
else:
print(line_to_print)
elif display_line_numbers:
# f - f-string, formatted string literal
print(f"{line_number:6} {line_to_print}", end="")
line_number += 1
else:
print(line_to_print, end="")
# Handle errors

# Exception - is the base class for all built-in exceptions
# error - an exception object
except Exception as error:
print(f"Error reading file {file_path}: {error}")

# Collect all matched files into matched_files list
matched_files = []
# args.paths- is a list of file paths provided by the user
# glob.glob() takes a string path pattern from user, and returns a list of all files that match it.
# Add, using .extend() method, to the matched_files list
for path in args.paths:
matched_files.extend(glob.glob(path))

# print error message if file is not exist
if not matched_files:
print(f"No files matched the given pattern: {args.paths}")
# exit(1) - stop the program with 1 code that indicates the error. exit(0)
exit(1)

# For in loop to process each file using for in loop
for file_path in matched_files:
display_file_contents(file_path, display_line_numbers = args.n, display_not_empty_line_numbers = args.b)





83 changes: 83 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Buil-in module to parse command-line arguments and options
import argparse
# Buil-in module to provide functions for interacting with the operating system, open, write, manipulate (listing directory contents)
import os
# For converting the given path to an absolute path
from pathlib import Path
# For colouring terminal output (blue for directories)
from colorama import Fore, Style, init

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Colorama isn't a standard library is it? Doesn't this mean you need a requirements.txt?


# Sets up colorama for blue colour for directories
init()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generally considered best practice to have function definitions first, top level code at the end of the file. It makes it easier if all of the top level logic is in one place, and where another coder would expect to find it.


# Function to list directories, files
def list_directory_files(file_path, display_hidden_files = False, display_one_per_line = False):
try:
# Get all directories and files using os module listdir() method and return a list
entries = os.listdir(file_path)
# A list to store . and ..
dot_dirs = []

# If the user set '-a', will store '.', '..'
if display_hidden_files:
# Adds "." (current directory) and ".." (parent directory) to the list
dot_dirs = [".", ".."]

# Create an empty list to store non-hidden files, directories
visible_entries = []

for entry in entries:
# If it is non-hidden files, add it to the list
if not entry.startswith(".") or display_hidden_files:
visible_entries.append(entry)
# Sort alphabetically lamba function for ignoring "." and case-insensitive
visible_entries.sort(key=lambda name: name.lstrip(".").lower())

sorted_entries = dot_dirs + visible_entries
# Create a list for styled directories, files
styled_entries = []

# For in loop for styling directories
for entry in sorted_entries:
# Create the full path of the item by combining
full_path = os.path.join(file_path, entry)
# Check for directory
if os.path.isdir(full_path):
# Apply styles for directory, add to the styled_entries
styled_entries.append(Fore.BLUE + Style.BRIGHT + entry + Style.RESET_ALL)
else:
# If file, just add to the list
styled_entries.append(entry)

# If the user set a "-1" flag, print each entry on a new line
if display_one_per_line:
print("\n".join(styled_entries))
# If the user doesn't set a "-1" flag, print all entries on the same line
else:
print(" ".join(styled_entries))

# Handle errors

# Exception - is the base class for all built-in exceptions
# error - an exception object
except Exception as error:
print(f"Error reading directory: {error}")

# Setup argument parser. Creates an instance(object) from built-in ArgumentParser class
parser = argparse.ArgumentParser(prog="ls command",
description="Implement 'ls' command with -1 and -a flags",
epilog="Now you can see the files in the chosen path"
)
# Define flags and arguments
# Define dest="one_per_line" for "-1", because -1 is not valid Python variable
parser.add_argument("-1", dest="one_per_line", action="store_true", help = "Display each file on a new line")
parser.add_argument("-a", action="store_true", help = "Display all files including hidden files")
# Positional argument. Defaults to . (current directory) if not provided. nargs="?" - optional, takes 0 or 1 value
parser.add_argument("path", nargs="?", default = ".", help = "Specify file path to the list")

# Parse the command-line arguments. args - an instance of the class argparse.Namespace
args = parser.parse_args()

# Get absolute path
absolute_path = Path(args.path).resolve()
list_directory_files(str(absolute_path), display_hidden_files=args.a, display_one_per_line=args.one_per_line)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You convert the Path object to a string and pass it to your function. An alternative you might want to consider (I'm not saying you have to do it for this exercise, but it might be a good learning experience) having your function take a Path argument instead of a string, and use the Path functions to do a lot of the work inside the function

77 changes: 77 additions & 0 deletions implement-shell-tools/ls/ls_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Buil-in module to parse command-line arguments and options
import argparse
# For converting the given path to an absolute path
from pathlib import Path
# For colouring terminal output (blue for directories)
from colorama import Fore, Style, init

# Sets up colorama for blue colour for directories
init()

# Function to list directories, files
def list_directory_files(dir_path: Path, display_hidden_files=False, display_one_per_line=False):
try:
# Get all entries (files and directories) as Path objects
entries = list(dir_path.iterdir())
# A list to store . and ..
dot_dirs = []
# If the user set '-a', will store '.', '..'
if display_hidden_files:
# Adds "." (current directory) and ".." (parent directory) to the list
dot_dirs = [Path("."), Path("..")]

# Filter out hidden files if not showing them
if not display_hidden_files:
entries = [e for e in entries if not e.name.startswith(".")]

# Combine dot_dirs with the visible entries list
visible_entries = dot_dirs + entries

# Sort alphabetically lamba function for ignoring "." and case-insensitive
visible_entries.sort(key=lambda p: p.name.lstrip(".").lower())
# Create a list for styled directories, files
styled_entries = []

# For in loop for styling directories
for entry in visible_entries:
# Check for directory
if entry.is_dir():
# Apply styles for directory, add to the styled_entries
styled_entries.append(Fore.BLUE + Style.BRIGHT + entry.name + Style.RESET_ALL)
else:
# If file, just add to the list
styled_entries.append(entry.name)

# If the user set a "-1" flag, print each entry on a new line
if display_one_per_line:
print("\n".join(styled_entries))
# If the user doesn't set a "-1" flag, print all entries on the same line
else:
print(" ".join(styled_entries))

# Handle errors

# Exception - is the base class for all built-in exceptions
# error - an exception object

except Exception as error:
print(f"Error reading directory: {error}")

# Setup argument parser. Creates an instance(object) from built-in ArgumentParser class
parser = argparse.ArgumentParser(prog="ls command",
description="Implement 'ls' command with -1 and -a flags",
epilog="Now you can see the files in the chosen path"
)
# Define flags and arguments
# Define dest="one_per_line" for "-1", because -1 is not valid Python variable
parser.add_argument("-1", dest="one_per_line", action="store_true", help = "Display each file on a new line")
parser.add_argument("-a", action="store_true", help = "Display all files including hidden files")
# Positional argument. Defaults to . (current directory) if not provided. nargs="?" - optional, takes 0 or 1 value
parser.add_argument("path", nargs="?", default = ".", help = "Specify file path to the list")

# Parse the command-line arguments. args - an instance of the class argparse.Namespace
args = parser.parse_args()

# Get absolute path
absolute_path = Path(args.path).resolve()
list_directory_files(absolute_path, display_hidden_files=args.a, display_one_per_line=args.one_per_line)
1 change: 1 addition & 0 deletions implement-shell-tools/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
colorama
100 changes: 100 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Built-in module to parse command-line arguments and options
import argparse
# Finds all file paths matching a pattern
import glob
# Built-in module to provide functions for interacting with the operating system, open, write, manipulate (file size, paths etc.)
import os

# Function to compute line, word, and byte counts. filepath - a string (path to a file)
def wc_counts(filepath):
try:
# Open file. "r" - read mode. with - ensure the file will be closed after the block of code finishes running.
with open(filepath, "r", encoding="utf-8") as file_object:
# Read all the entire content and returns it as a string in file_content_string
file_content_string = file_object.read()
# Count the number of newline characters by using .count() method - counts the number of occurrences of the specified substring
lines = file_content_string.count("\n")
# Count the number of words (split by any whitespace by default) using length of the list
words = len(file_content_string.split())
# Get the size of the file in bytes. getsize() returns the size of the file in bytes
byte_size = os.path.getsize(filepath)
return lines, words, byte_size

# Handle errors

# Exception - is the base class for all built-in exceptions
# error - an exception object
except Exception as error:
# f - f-string, formatted string literal
print(f"Error reading file {filepath}: {error}")
# return fallback values
return 0, 0, 0

# Setup argument parser. Creates an instance(object) from built-in ArgumentParser class
parser = argparse.ArgumentParser(
prog="wc command",
description="Implementing 'wc' command (-c, -w, -l flags) functionality in Python",
epilog="That's how you count the bytes, words, and lines"
)

# Define flags and arguments
parser.add_argument("-l", action="store_true", help="Count lines")
parser.add_argument("-w", action="store_true", help="Count words")
parser.add_argument("-c", action="store_true", help="Count bytes")
# Add positional argument for one or more file paths (nargs="+")
parser.add_argument("paths", nargs="+", help="File paths (wildcards supported)")

# Parse the command-line arguments. args - an instance of the class argparse.Namespace
args = parser.parse_args()

# If no flags are provided, display all flags
display_all = not (args.l or args.w or args.c)

# Initialisation of count total of lines, words, bytes
total_lines, total_words, total_bytes = 0, 0, 0

# Collect all matched files into matched_files list
matched_files = []
# args.paths- is a list of file paths provided by the user
# glob.glob() takes a string path pattern from user, and returns a list of all files that match it.
# Add, using .extend() method, to the matched_files list
for path in args.paths:
matched_files.extend(glob.glob(path))

# print error message if file does not exist
if not matched_files:
print(f"No files matched the given pattern: {args.paths}")
# exit(1) - stop the program with 1 code that indicates the error. exit(0) - indicates the program ran successfully
exit(1)

# For in loop to process each file using for in loop
for filepath in matched_files:
# Get counts for the current file
lines, words, byte_size = wc_counts(filepath)

# Accumulate totals for summary (if multiple files)
total_lines += lines
total_words += words
total_bytes += byte_size

# Format the output line. str() - convert integer to a string, .rjust(7)- right-aligns the string representation of the number by padding it with spaces to a total length of 7 characters
# None - don't apply conditionals
# output - a list with integers of lines, words, bytes and file path
output = [
str(lines).rjust(3) if args.l or display_all else None,
str(words).rjust(3) if args.w or display_all else None,
str(byte_size).rjust(3) if args.c or display_all else None,
filepath
]
# Print the output line (filter out None entries)
print(" ".join([item for item in output if item is not None]))

# Conditional for multiple files to count and print totals of lines, words, bytes for each file
if len(matched_files) > 1:
output = [
str(total_lines).rjust(3) if args.l or display_all else None,
str(total_words).rjust(3) if args.w or display_all else None,
str(total_bytes).rjust(3) if args.c or display_all else None,
"total"
]
print(" ".join([item for item in output if item is not None]))