I have used linux and Windows interchangeably over the last time, and I got used to the more comfortable and options-rich syntax of linux' commands, so I decided to replicate some of them in python (with some modification) so I can use them in Windows, and started with the ls
and cat
commands [after adding their parent dir under PATH
and associating py
files with python, they can be used like the originals].
I built cat
with almost the same features as the original (see linux man page), with the addition of the -r REPEAT
option that repeats every file REPEAT
times (default 1), and the -f
header option that prints the file name before every file (like windows' TYPE
), and the interpretation of filenames other than -
as file patterns - that will expand to cover all matching files, on cases like cat *s*.*
. All of the nbAeEtTv
options function the same.
I have been using python for a while now, and tried to make the code as efficient and "pythonic" as I could. However, I still believe there is a lot of room for improvements, let alone some issues as the need to scan the files before the printing loop on numbering mode to assure number format adjustment.
Here is my code:
import argparse, shutil, sys
import itertools
import os, os.path
import string, glob, re
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='concatenate files and print on the standard output')
parser.add_argument('filenames', nargs='+', help='directory to scan')
parser.add_argument('-r', '--repeat', dest='repeat', default=1, type=int, help='repeat output multiple times')
parser.add_argument('-f', '--header', dest='header', default=False, help='precede every file with its name', action='store_true')
parser.add_argument('-n', '--number', dest='number', default=False, help='number all output lines', action='store_true')
parser.add_argument('-b', '--number-nonblank', dest='number_nonblank', default=False, help='number nonempty output lines', action='store_true')
parser.add_argument('-A', '--show-all', dest='show_all', default=False, help='equivalent to -vET', action='store_true')
parser.add_argument('-e', dest='show_ends_and_nonprinting', default=False, help='equivalent to -vE', action='store_true')
parser.add_argument('-E', '--show-ends', dest='show_ends', default=False, help='display $ at end of each line', action='store_true')
parser.add_argument('-t', dest='show_tabs_and_nonprinting', default=False, help='equivalent to -vT', action='store_true')
parser.add_argument('-T', '--show-tabs', dest='show_tabs', default=False, help='display TAB characters as ^I', action='store_true')
parser.add_argument('-v', '--show-nonprinting', dest='show_nonprinting', default=False, help='use ^ and M- notation, except for LFD and TAB', action='store_true')
args = parser.parse_args()
# Special chars option overlapping
args.show_tabs = args.show_tabs or args.show_tabs_and_nonprinting or args.show_all
args.show_ends = args.show_ends or args.show_ends_and_nonprinting or args.show_all
args.show_nonprinting = args.show_nonprinting or args.show_tabs_and_nonprinting or args.show_ends_and_nonprinting or args.show_all
# Special chars definitions
tabs = '^I' if args.show_tabs else '\t'
ends = '$\n' if args.show_ends else '\n'
nonprinting = {k: '^' + v for k, v in zip(range(32), '@'+ string.ascii_uppercase + '[\]^!') if v not in 'IJ'} if args.show_nonprinting else {}
# Set filenames by provided patterns
GET_INPUT = 0
args.filenames = list(itertools.chain(*([GET_INPUT] if filepattern == '-' else glob.glob(filepattern) for filepattern in args.filenames)))
# Numbering settings
if args.number or args.number_nonblank:
line_number = 1
longest_line_number_length = len(str((sum(open(filename, 'rb').read().count(b'\n') if filename != GET_INPUT else 1 for filename in args.filenames) + 1) * args.repeat))
# The printing loop(s)
for filename in args.filenames:
if filename == GET_INPUT:
print(input())
continue
if args.header:
print('{:=^{}}'.format(filename, shutil.get_terminal_size().columns // 2))
for _ in range(args.repeat):
with open(filename, 'rb') as file:
for line in file:
nonblank_line = re.match('.*\S.*', line.decode(errors='replace'))
print(
' {:>{}} '.format(line_number, longest_line_number_length) if args.number or (args.number_nonblank and nonblank_line) \
else ' ' * (longest_line_number_length + 6) if args.number_nonblank else '', # For -E under -b
line.decode(errors='backslashreplace').rstrip('\n\r').replace('\t', tabs).translate(nonprinting),
sep='', end=ends
)
if args.number or (args.number_nonblank and nonblank_line):
line_number += 1
Demo:
C:\Dev>dump a -t 000000000000 │ 31 32 33 34 35 │ 12345 C:\Dev>dump b -t 000000000000 │ 04 09 41 42 43 44 45 46 │ ..ABCDEF C:\Dev>echo 789 | cat a - b >> c C:\Dev>cat c -Anr2 1 12345$ 2 789 $ 3 ^D^IABCDEF$ 4 12345$ 5 789 $ 6 ^D^IABCDEF$
_ ___ _.--.
\`.|\..----...-'` `-._.-'_.-'`
/ ' ` , __.--'
)/' _/ \ `-_, /
`-'" `"\_ ,_.-;_.-\_ ',
_.-'_./ {_.' ; /
{_.-``-' {_/