From 85c60fcee71e3c64af7f72c0cbc63f4c39e9d6b9 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Wed, 26 Nov 2025 15:51:49 +0000 Subject: [PATCH 1/5] Implement basic, -n and -b functionality for 'cat' command alternative --- implement-shell-tools/cat/cat.py | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 implement-shell-tools/cat/cat.py diff --git a/implement-shell-tools/cat/cat.py b/implement-shell-tools/cat/cat.py new file mode 100644 index 00000000..acbd3a7a --- /dev/null +++ b/implement-shell-tools/cat/cat.py @@ -0,0 +1,44 @@ +import argparse + + +parser = argparse.ArgumentParser( + prog="cat", + description="An alternative to the 'cat' command", +) + +parser.add_argument("files", nargs="+", help="The file(s) to process") +parser.add_argument("-n", "--number", action="store_true", help="Number all output lines") +parser.add_argument("-b", "--number-nonblank", action="store_true", help="Number non-blank output lines") + +args = parser.parse_args() + + + +def print_line(line, padding_width, line_number=None): + """Helper function to print a line with optional line numbering.""" + if line_number is not None: + print(f"{str(line_number).rjust(padding_width)} {line}") + else: + print(line) + +line_number = 1 +padding_width = 6 + +for file in args.files: + with open(file, "r") as f: + # read the content and split the lines ready to process as needed + content = f.read().splitlines() + + for line in content: + # use .strip() to remove leading and trailing whitespace or /n + if args.number_nonblank and line.strip(): + # number non-blank lines only + print_line(line, padding_width, line_number) + line_number += 1 + elif args.number: + # number all lines + print_line(line, padding_width, line_number) + line_number += 1 + else: + # no flags + print_line(line, padding_width) \ No newline at end of file From 97b42cb5758697933e53aecf8c40c1c83f20f23d Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Thu, 27 Nov 2025 16:48:34 +0000 Subject: [PATCH 2/5] Implement 'ls' command alternative with directory listing and options for hidden files --- implement-shell-tools/ls/ls.py | 139 +++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 implement-shell-tools/ls/ls.py diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py new file mode 100644 index 00000000..1a5fcae9 --- /dev/null +++ b/implement-shell-tools/ls/ls.py @@ -0,0 +1,139 @@ +import os +import argparse +import locale + + +#set locale to the machine default setting +locale.setlocale(locale.LC_ALL, '') + + +parser = argparse.ArgumentParser( + prog="ls", + description="An alternative to the 'ls' command", +) + +parser.add_argument("directory", nargs="?", default=".", help="The directory to list (defaults to the current directory)") +parser.add_argument("-1", "--single-column", action="store_true", help="List all files, one per line") +parser.add_argument("-a", "--all", action="store_true", help="Include hidden files (those starting with .) in the listing") + +args = parser.parse_args() + +# check if path exists +if not os.path.exists(args.directory): + print(f"ls: cannot access '${args.directory}': No such file or directory") + exit(1) + +# check if path is a directory +if os.path.isdir(args.directory): + entries = os.listdir(args.directory) + + # if -a flag set + if args.all: + entries.extend(['.', '..']) + + # filter hidden files if -a (all) flag not set + if not args.all: + entries = [entry for entry in entries if not entry.startswith(".")] + + # sort the entries using locale-aware comparison + entries.sort(key =locale.strxfrm) + + # print entries + if args.single_column: + for entry in entries: + print(entry) + else: + print(" ".join(entries)) + + + + + + + + + + + +# .argument("[directory]", "The directory to list") +# // Commander stores -1 as a string key that is accessed using options['1'] +# .option("-1", "List all files, one per line") +# .option("-a, --all", "Include hidden files (those starting with .) in the listing") +# .action(async (directory, options) => { +# try { +# // default to current directory if none is specified +# const dir = directory || "."; + +# await newLs(dir, options['1'], options.all); +# } catch (err) { +# console.error(`Error: ${err.message}`); +# } +# }); + +# program.parse(process.argv); + + +# // filter files based on visibility (includeHidden = true includes all files) +# function filterFiles(entries, includeHidden) { +# return entries.filter(name => +# includeHidden ? true : !name.startsWith(".") +# ); +# } + +# // sort entries: directories first, then files, +# function sortEntries(entries) { +# const dirs = entries.filter(entry => { +# try { +# return fs.statSync(entry).isDirectory(); +# } catch (err) { +# return false; +# } +# }); + +# const files = entries.filter(entry => { +# try { +# return fs.statSync(entry).isFile(); +# } catch (err) { +# return false; +# } +# }); +# // localeCompare = take into account rules of system language/region for ordering +# // undefined = uses the system default, numeric = regular number sorting, base = ignore case & accents +# return entries.sort((a, b) => +# a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) +# ); +# } + + +# // print entries either one per line (-1 flag) +# function printEntries(entries) { +# entries.forEach(entry => console.log(entry)); +# } + + +# async function newLs(directory, oneFlag, allFlag) { +# try { +# // check if path exists and determine if file or directory +# const stats = await fs.stat(directory); + +# // if a file, just print the name +# if (stats.isFile()) { +# console.log(directory); +# return; +# } + +# // reads directory contents +# const entries = await fs.readdir(directory); + +# // Filter out hidden files if no -a flag +# const filteredEntries = filterFiles(entries, allFlag); + +# // Sort the entries using the sortEntries helper +# const sortedEntries = sortEntries(filteredEntries); + +# // print entries for -1 flag (one per line) +# printEntries(sortedEntries); +# } catch (err) { +# console.error(`ls: cannot access '${directory}': ${err.message}`); +# } +# } \ No newline at end of file From e69ae436533a54eaffb8fb1c3cd0aebba45d2b88 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Fri, 28 Nov 2025 18:38:05 +0000 Subject: [PATCH 3/5] Implement 'wc' command alternative for counting lines, words, and bytes in files --- implement-shell-tools/wc/wc.py | 68 ++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 implement-shell-tools/wc/wc.py diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py new file mode 100644 index 00000000..65095193 --- /dev/null +++ b/implement-shell-tools/wc/wc.py @@ -0,0 +1,68 @@ +import argparse +import locale + + +#set locale to the machine default setting +locale.setlocale(locale.LC_ALL, '') + +parser = argparse.ArgumentParser( + prog="wc", + description="An alternative to the 'wc' command", +) +parser.add_argument("files", nargs="+", help="The file(s) to process") +parser.add_argument("-l", "--lines", action="store_true", help="Print the newline counts") +parser.add_argument("-w", "--words", action="store_true", help="Print the word counts") +parser.add_argument("-c", "--bytes", action="store_true", help="Print the byte counts") + +args = parser.parse_args() + +# when no specific flags are set +no_flags = not (args.lines or args.words or args.bytes) + +totals = {"lines": 0, "words": 0, "bytes": 0} + +def calculate_counts(content): + # returns a count object + return { + "lines": content.count('\n') + (1 if content and not content.endswith('\n') else 0), + "words": len(content.split()), + "bytes": len(content.encode('utf-8')), + } + + +def print_counts(counts, file): + parts = [] + # map flag args to the keys in the count object + flag_arg_to_key = { + "lines": args.lines, + "words": args.words, + "bytes": args.bytes + } + + PADDING = 3 + + for key, flag in flag_arg_to_key.items(): + if no_flags or flag: + # apply padding only when no_flags is True + padded_output = f"{counts[key]:>{PADDING}}" if no_flags else str(counts[key]) + parts.append(padded_output) + parts.append(file) + print(" ".join(parts)) + + +for file in args.files: + try: + with open(file, "r") as f: + # read the content and split the lines ready to process as needed + content = f.read() + counts = calculate_counts(content) + # update totals object with values from count object + for key in totals: + totals[key] += counts[key] + print_counts(counts, file) + except FileNotFoundError: + print(f"wc: {file}: No such file or directory found") + +# Print totals if more than one file +if len(args.files) > 1: + print_counts(totals, "total") From 3b6d43087f5281fa5b89a651e48d8e71dc35c1c5 Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Fri, 28 Nov 2025 21:27:31 +0000 Subject: [PATCH 4/5] Remove commented-out code and clean up the 'ls' command implementation --- implement-shell-tools/ls/ls.py | 94 +--------------------------------- 1 file changed, 1 insertion(+), 93 deletions(-) diff --git a/implement-shell-tools/ls/ls.py b/implement-shell-tools/ls/ls.py index 1a5fcae9..3bebdd59 100644 --- a/implement-shell-tools/ls/ls.py +++ b/implement-shell-tools/ls/ls.py @@ -44,96 +44,4 @@ print(entry) else: print(" ".join(entries)) - - - - - - - - - - - -# .argument("[directory]", "The directory to list") -# // Commander stores -1 as a string key that is accessed using options['1'] -# .option("-1", "List all files, one per line") -# .option("-a, --all", "Include hidden files (those starting with .) in the listing") -# .action(async (directory, options) => { -# try { -# // default to current directory if none is specified -# const dir = directory || "."; - -# await newLs(dir, options['1'], options.all); -# } catch (err) { -# console.error(`Error: ${err.message}`); -# } -# }); - -# program.parse(process.argv); - - -# // filter files based on visibility (includeHidden = true includes all files) -# function filterFiles(entries, includeHidden) { -# return entries.filter(name => -# includeHidden ? true : !name.startsWith(".") -# ); -# } - -# // sort entries: directories first, then files, -# function sortEntries(entries) { -# const dirs = entries.filter(entry => { -# try { -# return fs.statSync(entry).isDirectory(); -# } catch (err) { -# return false; -# } -# }); - -# const files = entries.filter(entry => { -# try { -# return fs.statSync(entry).isFile(); -# } catch (err) { -# return false; -# } -# }); -# // localeCompare = take into account rules of system language/region for ordering -# // undefined = uses the system default, numeric = regular number sorting, base = ignore case & accents -# return entries.sort((a, b) => -# a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" }) -# ); -# } - - -# // print entries either one per line (-1 flag) -# function printEntries(entries) { -# entries.forEach(entry => console.log(entry)); -# } - - -# async function newLs(directory, oneFlag, allFlag) { -# try { -# // check if path exists and determine if file or directory -# const stats = await fs.stat(directory); - -# // if a file, just print the name -# if (stats.isFile()) { -# console.log(directory); -# return; -# } - -# // reads directory contents -# const entries = await fs.readdir(directory); - -# // Filter out hidden files if no -a flag -# const filteredEntries = filterFiles(entries, allFlag); - -# // Sort the entries using the sortEntries helper -# const sortedEntries = sortEntries(filteredEntries); - -# // print entries for -1 flag (one per line) -# printEntries(sortedEntries); -# } catch (err) { -# console.error(`ls: cannot access '${directory}': ${err.message}`); -# } -# } \ No newline at end of file + \ No newline at end of file From eeb2ced24e8e2d26608043b31a9287e31b77379f Mon Sep 17 00:00:00 2001 From: Geraldine-Edwards Date: Wed, 31 Dec 2025 13:39:37 +0000 Subject: [PATCH 5/5] Fix comment formatting in calculate_counts function for clarity --- implement-shell-tools/wc/wc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implement-shell-tools/wc/wc.py b/implement-shell-tools/wc/wc.py index 65095193..d7246ec2 100644 --- a/implement-shell-tools/wc/wc.py +++ b/implement-shell-tools/wc/wc.py @@ -24,7 +24,7 @@ def calculate_counts(content): # returns a count object return { - "lines": content.count('\n') + (1 if content and not content.endswith('\n') else 0), + "lines": content.count('\n') + (1 if content and not content.endswith('\n') else 0), # count lines by '\n'; add 1 if last line has no newline to match real wc command behavior" "words": len(content.split()), "bytes": len(content.encode('utf-8')), }