Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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:

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.

Here it looks like you loop over all the content twice.Once to add the line numbers, then once again to print. Is it possible to do this only looking at the data once?

@Alex-Jamshidi Alex-Jamshidi Sep 5, 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.

There is a reason for this. The data in all_file_contents is stored as two lots of nested arrays.

For 3 files it might look like this:
[[line, line, line, line], [line, line, line], [line, line, line, line]]

For a single file it looks like this:
[[line, line, line]]

So the first loop essentially just unwraps that first array.

I could update the code in read_files() to unpack all lines into a single array, even if there are multiple files... that would be something like...

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

(replacing lines 29 and 30).

This would then let me have a single loop in each of the two locations you flagged:
for line in all_file_contents:

But I chose to do it this way to preserve the information of which lines are from which files. The reason to do this was because, this version of cat implements only 2 flags, but was written with the view to be potentially expanded, so I wanted to maintain as much information about the input as possible, letting the print function unpack it all at the end.

This is also why there are two loops below.

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:

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.

Is there a reason to use two loops here? Could your data structure be simplified?

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.

See above comment.

print(line)

# ===== Run cat =====
cat(parser.parse_args())
101 changes: 101 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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)

cwd = os.getcwd()

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.

What do you use the cwd for in this program?

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

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

# ===== Flag Handling =====
def execute_flags(flag_status):
if getattr(args, "1"):

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.

Here, when you are doing things like if args.a …, then you set something to True. What benefit is there doing it this way over just referencing args.a when you need it?

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(cwd, fs_items):
return [
p
for p in fs_items
if (Path(cwd) / p).is_dir()
]

def get_file_args(cwd, fs_items, flag_status):
file_args = [
p
for p in fs_items
if not (Path(cwd) / 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?

return file_args

# Populating and Outputting Data
def populate_output(cwd, 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, cwd, 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, cwd, flag_status)
return output_string

def dir_output(dir_item, cwd, 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())
101 changes: 101 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import argparse
import os
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):
cwd = os.getcwd()
file_names = args.files

execute_flags()
all_files_data = add_totals(extract_files_data(file_names, cwd))
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, cwd):
all_files_data = []

for file_name in file_names:
file_data = {}
file_data["name"] = file_name
file_data["text"] = read_file(file_name, cwd)
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, cwd)

all_files_data.append(file_data)

return all_files_data

def read_file(file_name, cwd):
file_path = Path(cwd) / file_name

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.

Why are you using the Path / expression here?

return file_path.read_text(encoding="utf-8").rstrip()

def calculate_line_count(text):
if not text:
return 0
return len(text.splitlines())

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

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.

Is there a reason the word count method has a different structure to your line count method?


def read_byte_size(file_name, cwd):
file_path = os.path.join(cwd, file_name)
return os.path.getsize(file_path)

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):
for file in output_data:
output_string = ""

for metric in metrics:

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.

Do you need to loop over metrics and then check if it is in displayed_metrics? Could you simplify this?

if not displayed_metrics or metric in displayed_metrics:
output_string += str(file[metric]).rjust(8)

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

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