Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
node_modules

# Ignore Python virtual environments
implement-cowsay/.venv/
51 changes: 51 additions & 0 deletions implement-shell-tools/cat/cat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3

import sys

def cat_file(file, options, line_number):
try:
with open(file, 'r', encoding='utf-8') as f:
lines = f.readlines()

for line in lines:
if options['number_non_blank'] and line.strip():
print(f"{line_number:6}\t{line}", end='')
line_number += 1
elif options['number_all']:
print(f"{line_number:6}\t{line}", end='')
line_number += 1
else:
print(line, end='')

return line_number
except FileNotFoundError:
print(f"cat: {file}: No such file or directory", file=sys.stderr)
sys.exit(1)

def main():
args = sys.argv[1:]
options = {
'number_all': False,
'number_non_blank': False,
}

files = []

for arg in args:
if arg == '-n':
options['number_all'] = True
elif arg == '-b':
options['number_non_blank'] = True
else:
files.append(arg)

if not files:
print("Usage: cat [-n | -b] <file>...", file=sys.stderr)
sys.exit(1)

line_number = 1
for file in files:
line_number = cat_file(file, options, line_number)

if __name__ == "__main__":
main()
65 changes: 65 additions & 0 deletions implement-shell-tools/ls/ls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env python3

import locale
import os
import sys


def parse_args(args):
one_per_line = False
show_all = False
paths = []

for arg in args:
if arg.startswith("-") and arg != "-":
for flag in arg[1:]:
if flag == "1":
one_per_line = True
elif flag == "a":
show_all = True
else:
print(f"ls: invalid option -- '{flag}'", file=sys.stderr)
sys.exit(1)
else:
paths.append(arg)

if not one_per_line:
print("Usage: ls.py -1 [-a] [path]", file=sys.stderr)
sys.exit(1)

if len(paths) > 1:
print("Usage: ls.py -1 [-a] [path]", file=sys.stderr)
sys.exit(1)

return show_all, (paths[0] if paths else ".")


def list_entries(path, show_all):
try:
entries = os.listdir(path)
except FileNotFoundError:
print(f"ls: cannot access '{path}': No such file or directory", file=sys.stderr)
sys.exit(1)
except NotADirectoryError:
print(os.path.basename(path))
return

if show_all:
entries = [".", ".."] + entries
else:
entries = [name for name in entries if not name.startswith(".")]

locale.setlocale(locale.LC_COLLATE, "")
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.

Good idea to consider how this might affect sorting.

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.

Thanks! I switched argument parsing to argparse and added combined short-flag expansion, so -1a/-a1 work like standard tools.
I also adjusted the -a path so . and .. are included before sorting, keeping sort behavior consistent.

entries = sorted(entries, key=locale.strxfrm)

for entry in entries:
print(entry)


def main():
show_all, path = parse_args(sys.argv[1:])
list_entries(path, show_all)


if __name__ == "__main__":
main()
107 changes: 107 additions & 0 deletions implement-shell-tools/wc/wc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3

import sys


def count_file(file):
try:
with open(file, 'r', encoding='utf-8') as f:
data = f.read()

lines = data.count('\n')
words = len(data.split())
bytes_count = len(data.encode('utf-8'))

return lines, words, bytes_count
except FileNotFoundError:
print(f"wc: {file}: No such file or directory", file=sys.stderr)
sys.exit(1)


def selected_keys(options):
if not any(options.values()):
return ['lines', 'words', 'bytes']

keys = []
if options['lines']:
keys.append('lines')
if options['words']:
keys.append('words')
if options['bytes']:
keys.append('bytes')
return keys


def values_for_keys(counts, keys):
lines, words, bytes_count = counts
mapping = {
'lines': lines,
'words': words,
'bytes': bytes_count,
}
return [mapping[key] for key in keys]


def print_rows(rows, keys):
align_columns = len(keys) > 1 or len(rows) > 1

if not align_columns:
values, name = rows[0]
print(f"{values[0]} {name}")
return

widths = []
for index in range(len(keys)):
max_len = max(len(str(values[index])) for values, _ in rows)
widths.append(max(3, max_len))

for values, name in rows:
formatted_values = " ".join(
f"{value:>{width}}" for value, width in zip(values, widths)
)
print(f"{formatted_values} {name}")

def main():
args = sys.argv[1:]
options = {
'lines': False,
'words': False,
'bytes': False,
}

files = []

for arg in args:
if arg == '-l':
options['lines'] = True
elif arg == '-w':
options['words'] = True
elif arg == '-c':
options['bytes'] = True
else:
files.append(arg)

if not files:
print("Usage: wc [-l | -w | -c] <file>...", file=sys.stderr)
sys.exit(1)

total_lines = 0
total_words = 0
total_bytes = 0
keys = selected_keys(options)
rows = []

for file in files:
lines, words, bytes_count = count_file(file)
total_lines += lines
total_words += words
total_bytes += bytes_count
rows.append((values_for_keys((lines, words, bytes_count), keys), file))

if len(files) > 1:
rows.append((values_for_keys((total_lines, total_words, total_bytes), keys), 'total'))

print_rows(rows, keys)

if __name__ == "__main__":
main()
Loading