Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
78bdfd5
cat implemented
Alex-Jamshidi Jul 28, 2026
60b323e
cat updated
Alex-Jamshidi Jul 28, 2026
3a35a7a
wc implemented
Alex-Jamshidi Jul 28, 2026
085224e
ls complete
Alex-Jamshidi Jul 30, 2026
b28f41d
remove unused code
Alex-Jamshidi Jul 31, 2026
7d6a298
updated eronious boolean in ls
Alex-Jamshidi Aug 12, 2026
fce72f0
updated ls so that data doesn't rely on global variables and is passe…
Alex-Jamshidi Aug 28, 2026
14d4d33
updated flag a
Alex-Jamshidi Aug 28, 2026
2b981ed
collapsed getuserargs function
Alex-Jamshidi Aug 28, 2026
fa7436b
rearranged some argument orders in ls
Alex-Jamshidi Aug 28, 2026
d4720b1
added comments to ls
Alex-Jamshidi Aug 28, 2026
5c64429
further added comments to ls
Alex-Jamshidi Aug 28, 2026
769706f
wc refactored
Alex-Jamshidi Aug 28, 2026
65e60ae
refactored cat
Alex-Jamshidi Aug 28, 2026
efdd20b
updated getcwd
Alex-Jamshidi Aug 29, 2026
61a6eaa
Add .venv to gitignore
Alex-Jamshidi Aug 29, 2026
eeb4d7a
removed implement shell tools files
Alex-Jamshidi Aug 29, 2026
5455734
written wc in python
Alex-Jamshidi Aug 30, 2026
a0572e4
removed cowsay files from branch
Alex-Jamshidi Aug 30, 2026
0cb7c2e
updated arguments
Alex-Jamshidi Aug 30, 2026
696e335
arranged environment folders
Alex-Jamshidi Aug 30, 2026
68d08db
arranged environment folders again
Alex-Jamshidi Aug 30, 2026
27ffc84
completed ls in python
Alex-Jamshidi Aug 30, 2026
0e0f38b
completed cat in python
Alex-Jamshidi Aug 30, 2026
6b73659
fixed line numbering bug for b flag
Alex-Jamshidi Aug 30, 2026
eb70f3e
Update help text for file_names argument
Alex-Jamshidi Aug 31, 2026
5c4328e
comments to PR
Alex-Jamshidi Sep 12, 2026
36b5584
removed cwd variable to use in built Path library functionaility
Alex-Jamshidi Sep 12, 2026
84192ba
refactored use of Path functions and removed os library in place of Path
Alex-Jamshidi Sep 12, 2026
85ee312
refactored
Alex-Jamshidi Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions implement-shell-tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
65 changes: 65 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import argparse
import os
from pathlib import Path

# ===== Argument Handling =====
parser = argparse.ArgumentParser(
prog="cat",
description="Prints file content",
)

parser.add_argument("-b", action="store_true", help="Numbers lines that aren't empty")
parser.add_argument("-n", action="store_true", help="Numbers all lines")
parser.add_argument("file_names", nargs="*", help="Files for which to display content")

args = parser.parse_args()

# ===== cat Procedure =====
def cat(args):
cwd = os.getcwd()
all_files_contents = read_files(args.file_names, cwd)
execute_flags(all_files_contents)
print_lines(all_files_contents)

# ===== Extracting Data from Arguments =====
def read_files(file_names, cwd):
all_files_contents = []

for file_name in file_names:
file_content = read_file(file_name, cwd)
all_files_contents.append(file_content.splitlines())
return all_files_contents

def read_file(file_name, cwd):
file_path = Path(cwd) / file_name
with open(file_path, "r", encoding="utf-8") as f:
return f.read().rstrip()

# ===== Flag Handling =====
def execute_flags(all_files_contents):
if args.b:
for file_content in all_files_contents:
Comment thread
LonMcGregor marked this conversation as resolved.
line_number = 1
for line_idx, line in enumerate(file_content):
if line != "":
file_content[line_idx] = f"{line_number:>6}\t{line}"
line_number += 1

elif args.n:
for file_idx, file_content in enumerate(all_files_contents):
all_files_contents[file_idx] = [
f"{line_idx:>6}\t{line}"
for line_idx, line in enumerate(file_content, start=1)
]

def make_list(output_string):
return output_string.replace("\t", "\n").replace("\n\n", "\n")

# ===== Print Output =====
def print_lines(all_files_contents):
for file_content in all_files_contents:
for line in file_content:
Comment thread
LonMcGregor marked this conversation as resolved.
print(line)

# ===== Run cat =====
cat(parser.parse_args())
105 changes: 105 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import argparse
import os
from pathlib import Path

# Argument Handling
parser = argparse.ArgumentParser(
prog="ls",
description="Print line, word, and byte counts for each file.",
)

parser.add_argument("-1", action="store_true", help="Show output on separate lines")
parser.add_argument("-a", action="store_true", help="Show hidden files")
parser.add_argument("file_system_items", nargs="*", help="Files or folders to display")

args = parser.parse_args()


# ===== ls Procedure =====
def ls(args):
flag_status = {"print_in_list": False, "show_all": False}
execute_flags(flag_status)

fs_items = check_args_length(args.file_system_items)
dir_args = get_dir_args(fs_items)
file_args = get_file_args(fs_items, flag_status)

print_output(
populate_output(fs_items, dir_args, file_args, flag_status), flag_status
)


# ===== Flag Handling =====
def execute_flags(flag_status):
if getattr(args, "1"):
Comment thread
LonMcGregor marked this conversation as resolved.
flag_status["print_in_list"] = True
if args.a:
flag_status["show_all"] = True


def make_list(output_string):
return output_string.replace("\t", "\n").replace("\n\n", "\n")


# Extracting Data from Arguments
def check_args_length(fs_items):
if len(fs_items) == 0:
fs_items.append(".")
return fs_items


def get_dir_args(fs_items):
return [p for p in fs_items if Path(p).is_dir()]


def get_file_args(fs_items, flag_status):
file_args = [p for p in fs_items if not Path(p).is_dir()]
if not flag_status["show_all"]:
file_args = remove_dot_files(file_args)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There are multiple places where you use remove_dot_files in this program. Is there a reason for that?

@Alex-Jamshidi Alex-Jamshidi Sep 12, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The program takes the original filesystem items (which could be files or folders, or both).

It then splits these items into files (get_file_args) and directories (get_dir_args).

The list of file arguments may contain dot files, like .hidden.txt, so these are removed at this stage.

If directory items are found, these need to be opened up, the contents of which are to be displayed on a separate line, this is done using the dir_output function. These directories may also contain dot files, so these need to be removed.

If the second instance of remove_dot_files were removed:
In the case of the sample files given, none of the sub directories actually contain dot files, so the output of ls and ls.py would be equivalent.

but if I ran, for example: python3 ls.py ./* whereby sample-files is now a subdirectory to be opened.

then when the dir_output function populates the files in sample-files, the .hidden.txt would be incorrectly shown.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OK, I understand. The dotfiles are treated a little differently in the case of directories that need to be inspected. That makes sense in this case.

return file_args


# Populating and Outputting Data
def populate_output(fs_items, dir_args, file_args, flag_status):
output_string = ""

if len(fs_items) == 1:
for file in file_args:
output_string += file + " "
for dir_item in dir_args:
output_string += dir_output(dir_item, flag_status)
else:
for file in file_args:
output_string += file + "\t"
for dir_item in dir_args:
output_string += f"\n\n{dir_item}:\n" + dir_output(dir_item, flag_status)
return output_string


def dir_output(dir_item, flag_status):
target_path = Path.cwd() / dir_item
contents = os.listdir(target_path)
output_str = ""
if flag_status["show_all"]:
output_str += ".\t..\t"
else:
contents = remove_dot_files(contents)

for item in contents:
output_str += item + "\t"
return output_str


def remove_dot_files(file_list):
return [item for item in file_list if not item.startswith(".")]


def print_output(output_string, flag_status):
output = output_string
if flag_status["print_in_list"]:
output = make_list(output)
print(output.rstrip())


# ===== Run ls =====
ls(parser.parse_args())
106 changes: 106 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import argparse
from pathlib import Path

# Argument Handling
parser = argparse.ArgumentParser(
prog="wc",
description="Print line, word, and byte counts for each file.",
)

parser.add_argument("-l", action="store_true", help="Show line count")
parser.add_argument("-w", action="store_true", help="Show word count")
parser.add_argument("-c", action="store_true", help="Show byte size")
parser.add_argument("files", nargs="*", help="File names to process")

args = parser.parse_args()

# Global Variables
metrics = ["line_count", "word_count", "byte_size"]
displayed_metrics = []


# ===== wc Procedure =====
def wc(args):
file_names = args.files

execute_flags()
all_files_data = add_totals(extract_files_data(file_names))
print_output(all_files_data)


# ===== Flag Handling =====
def execute_flags():
if args.w:
displayed_metrics.append("word_count")
if args.l:
displayed_metrics.append("line_count")
if args.c:
displayed_metrics.append("byte_size")


# ===== Extracting Files Data =====
def extract_files_data(file_names):
all_files_data = []

for file_name in file_names:
file_data = {}
file_data["name"] = file_name
file_data["text"] = read_file(file_name)
file_data["line_count"] = calculate_line_count(file_data["text"])
file_data["word_count"] = calculate_word_count(file_data["text"])
file_data["byte_size"] = read_byte_size(file_name)

all_files_data.append(file_data)

return all_files_data


def read_file(file_name):
file_path = Path(file_name)
return file_path.read_text(encoding="utf-8").rstrip()


def calculate_line_count(text):
return len(text.splitlines())


def calculate_word_count(text):
return len(text.split())


def read_byte_size(file_name):
file_path = Path(file_name)
return Path(file_path).stat().st_size


def add_totals(all_files_data):
if len(all_files_data) <= 1:
return all_files_data

totals_data = {"name": "total"}

for metric in metrics:
metric_sum = 0
for file in all_files_data:
metric_sum += file[metric]
totals_data[metric] = metric_sum

all_files_data.append(totals_data)
return all_files_data


# ===== Outputting Data =====
def print_output(output_data):
active_metrics = displayed_metrics or metrics
for file in output_data:
output_string = ""

for metric in active_metrics:
output_string += str(file[metric]).rjust(8)

output_string += f" {file['name']}"
print(output_string)


# ===== Run wc =====
wc(parser.parse_args())
Loading