1
\$\begingroup\$

Write a program that precisely clones GNU cat version 8.32, which is installed on my computer.

In any case, if called with the same arguments, same stdin, and same files, your program's output to both stdout and stderr must by byte-for-byte identical to that of GNU cat, and the exit code must be the same. This applies to both valid and invalid commands. It must be impossible to distinguish between GNU cat and your program without inspecting the file content or analyzing performance.

As usual, arguments starting with hyphens are considered options, except that -- indicates that arguments after that point are not options. cat -- --help reads a file literally called --help from the current directory. Options and non-options may be freely mixed, except no options are allowed after --.

cat has no options that take arguments. Options may be long or short. Long options begin with two hyphens, and must be given separately. Short options begin with one hyphen and can be combined; cat -v -E -T can be combined into cat -vET with precisely the same effect.

The options are documented in the output of cat --help:

Usage: cat [OPTION]... [FILE]...
Concatenate FILE(s) to standard output.

With no FILE, or when FILE is -, read standard input.

  -A, --show-all           equivalent to -vET
  -b, --number-nonblank    number nonempty output lines, overrides -n
  -e                       equivalent to -vE
  -E, --show-ends          display $ at end of each line
  -n, --number             number all output lines
  -s, --squeeze-blank      suppress repeated empty output lines
  -t                       equivalent to -vT
  -T, --show-tabs          display TAB characters as ^I
  -u                       (ignored)
  -v, --show-nonprinting   use ^ and M- notation, except for LFD and TAB
      --help     display this help and exit
      --version  output version information and exit

Examples:
  cat f - g  Output f's contents, then standard input, then g's contents.
  cat        Copy standard input to standard output.

GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation <https://www.gnu.org/software/coreutils/cat>
or available locally via: info '(coreutils) cat invocation'

There are some exceptions to the requirement for a perfect clone. Your program does not have to properly handle file read errors except "file not found" and "permission denied," and it does not need to be internationalized.

Calling GNU cat is cheating.

Because cat 8.32 is not available on all systems, a Python reference implementation is provided. I believe it meets the challenge requirements, although it is entirely ungolfed. Any variation between GNU cat and this program, except as allowed in this challenge, is a bug in this program. You are welcome to use it as the basis of an answer if you would like.

#!/usr/bin/env python3
import sys
import getopt

try:
    full_opts, file_names = getopt.gnu_getopt(
        sys.argv[1:],
        "AbeEnstTuv",
        longopts=[
            "show-all",
            "number-nonblank",
            "show-ends",
            "number",
            "squeeze-blank",
            "show-tabs",
            "show-nonprinting",
            "help",
            "version",
        ],
    )
except getopt.GetoptError as e:
    invalid_option = str(e)[7:-15]
    if invalid_option.startswith("--"):
        print(f"cat: unrecognized option '{invalid_option}'", file=sys.stderr)
    else:
        letter = invalid_option[1]
        print(f"cat: invalid option -- '{letter}'", file=sys.stderr)
    print("Try 'cat --help' for more information.", file=sys.stderr)

    sys.exit(1)

raw_opts = [opt[0] for opt in full_opts]

opts = []

for raw_opt in raw_opts:
    if raw_opt in ["-A", "--show-all"]:
        opts.append("--show-nonprinting")
        opts.append("--show-ends")
        opts.append("--show-tabs")
    elif raw_opt == "-e":
        opts.append("--show-nonprinting")
        opts.append("--show-ends")
    elif raw_opt == "-t":
        opts.append("--show-nonprinting")
        opts.append("--show-tabs")
    elif raw_opt == "-u":
        pass
    elif raw_opt.startswith("--"):
        opts.append(raw_opt)
    else:
        opts.append(
            {
                "-b": "--number-nonblank",
                "-E": "--show-ends",
                "-n": "--number",
                "-s": "--squeeze-blank",
                "-T": "--show-tabs",
                "-v": "--show-nonprinting",
            }[raw_opt]
        )

help_text = """Usage: cat [OPTION]... [FILE]...
Concatenate FILE(s) to standard output.

With no FILE, or when FILE is -, read standard input.

  -A, --show-all           equivalent to -vET
  -b, --number-nonblank    number nonempty output lines, overrides -n
  -e                       equivalent to -vE
  -E, --show-ends          display $ at end of each line
  -n, --number             number all output lines
  -s, --squeeze-blank      suppress repeated empty output lines
  -t                       equivalent to -vT
  -T, --show-tabs          display TAB characters as ^I
  -u                       (ignored)
  -v, --show-nonprinting   use ^ and M- notation, except for LFD and TAB
      --help     display this help and exit
      --version  output version information and exit

Examples:
  cat f - g  Output f's contents, then standard input, then g's contents.
  cat        Copy standard input to standard output.

GNU coreutils online help: <https://www.gnu.org/software/coreutils/>
Full documentation <https://www.gnu.org/software/coreutils/cat>
or available locally via: info '(coreutils) cat invocation'"""

version_text = """cat (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Torbjorn Granlund and Richard M. Stallman."""

nonprinting = {
    b"\x00": b"^@",
    b"\x01": b"^A",
    b"\x02": b"^B",
    b"\x03": b"^C",
    b"\x04": b"^D",
    b"\x05": b"^E",
    b"\x06": b"^F",
    b"\x07": b"^G",
    b"\x08": b"^H",
    b"\x0b": b"^K",
    b"\x0c": b"^L",
    b"\r": b"^M",
    b"\x0e": b"^N",
    b"\x0f": b"^O",
    b"\x10": b"^P",
    b"\x11": b"^Q",
    b"\x12": b"^R",
    b"\x13": b"^S",
    b"\x14": b"^T",
    b"\x15": b"^U",
    b"\x16": b"^V",
    b"\x17": b"^W",
    b"\x18": b"^X",
    b"\x19": b"^Y",
    b"\x1a": b"^Z",
    b"\x1b": b"^[",
    b"\x1c": b"^\\",
    b"\x1d": b"^]",
    b"\x1e": b"^^",
    b"\x1f": b"^_",
    b"\x7f": b"^?",
    b"\x80": b"M-^@",
    b"\x81": b"M-^A",
    b"\x82": b"M-^B",
    b"\x83": b"M-^C",
    b"\x84": b"M-^D",
    b"\x85": b"M-^E",
    b"\x86": b"M-^F",
    b"\x87": b"M-^G",
    b"\x88": b"M-^H",
    b"\x89": b"M-^I",
    b"\x8a": b"M-^J",
    b"\x8b": b"M-^K",
    b"\x8c": b"M-^L",
    b"\x8d": b"M-^M",
    b"\x8e": b"M-^N",
    b"\x8f": b"M-^O",
    b"\x90": b"M-^P",
    b"\x91": b"M-^Q",
    b"\x92": b"M-^R",
    b"\x93": b"M-^S",
    b"\x94": b"M-^T",
    b"\x95": b"M-^U",
    b"\x96": b"M-^V",
    b"\x97": b"M-^W",
    b"\x98": b"M-^X",
    b"\x99": b"M-^Y",
    b"\x9a": b"M-^Z",
    b"\x9b": b"M-^[",
    b"\x9c": b"M-^\\",
    b"\x9d": b"M-^]",
    b"\x9e": b"M-^^",
    b"\x9f": b"M-^_",
    b"\xa0": b"M- ",
    b"\xa1": b"M-!",
    b"\xa2": b'M-"',
    b"\xa3": b"M-#",
    b"\xa4": b"M-$",
    b"\xa5": b"M-%",
    b"\xa6": b"M-&",
    b"\xa7": b"M-'",
    b"\xa8": b"M-(",
    b"\xa9": b"M-)",
    b"\xaa": b"M-*",
    b"\xab": b"M-+",
    b"\xac": b"M-,",
    b"\xad": b"M--",
    b"\xae": b"M-.",
    b"\xaf": b"M-/",
    b"\xb0": b"M-0",
    b"\xb1": b"M-1",
    b"\xb2": b"M-2",
    b"\xb3": b"M-3",
    b"\xb4": b"M-4",
    b"\xb5": b"M-5",
    b"\xb6": b"M-6",
    b"\xb7": b"M-7",
    b"\xb8": b"M-8",
    b"\xb9": b"M-9",
    b"\xba": b"M-:",
    b"\xbb": b"M-;",
    b"\xbc": b"M-<",
    b"\xbd": b"M-=",
    b"\xbe": b"M->",
    b"\xbf": b"M-?",
    b"\xc0": b"M-@",
    b"\xc1": b"M-A",
    b"\xc2": b"M-B",
    b"\xc3": b"M-C",
    b"\xc4": b"M-D",
    b"\xc5": b"M-E",
    b"\xc6": b"M-F",
    b"\xc7": b"M-G",
    b"\xc8": b"M-H",
    b"\xc9": b"M-I",
    b"\xca": b"M-J",
    b"\xcb": b"M-K",
    b"\xcc": b"M-L",
    b"\xcd": b"M-M",
    b"\xce": b"M-N",
    b"\xcf": b"M-O",
    b"\xd0": b"M-P",
    b"\xd1": b"M-Q",
    b"\xd2": b"M-R",
    b"\xd3": b"M-S",
    b"\xd4": b"M-T",
    b"\xd5": b"M-U",
    b"\xd6": b"M-V",
    b"\xd7": b"M-W",
    b"\xd8": b"M-X",
    b"\xd9": b"M-Y",
    b"\xda": b"M-Z",
    b"\xdb": b"M-[",
    b"\xdc": b"M-\\",
    b"\xdd": b"M-]",
    b"\xde": b"M-^",
    b"\xdf": b"M-_",
    b"\xe0": b"M-`",
    b"\xe1": b"M-a",
    b"\xe2": b"M-b",
    b"\xe3": b"M-c",
    b"\xe4": b"M-d",
    b"\xe5": b"M-e",
    b"\xe6": b"M-f",
    b"\xe7": b"M-g",
    b"\xe8": b"M-h",
    b"\xe9": b"M-i",
    b"\xea": b"M-j",
    b"\xeb": b"M-k",
    b"\xec": b"M-l",
    b"\xed": b"M-m",
    b"\xee": b"M-n",
    b"\xef": b"M-o",
    b"\xf0": b"M-p",
    b"\xf1": b"M-q",
    b"\xf2": b"M-r",
    b"\xf3": b"M-s",
    b"\xf4": b"M-t",
    b"\xf5": b"M-u",
    b"\xf6": b"M-v",
    b"\xf7": b"M-w",
    b"\xf8": b"M-x",
    b"\xf9": b"M-y",
    b"\xfa": b"M-z",
    b"\xfb": b"M-{",
    b"\xfc": b"M-|",
    b"\xfd": b"M-}",
    b"\xfe": b"M-~",
    b"\xff": b"M-^?",
}

if "--help" in opts or "--version" in opts:
    for opt in opts:
        if opt == "--help":
            print(help_text)
            sys.exit(0)
        if opt == "--version":
            print(version_text)
            sys.exit(0)

if not file_names:
    file_names = ["-"]

index = 1

exit_code = 0

for file_name in file_names:
    if file_name == "-":
        is_file = False
        f = sys.stdin.buffer
    else:
        is_file = True
        try:
            f = open(file_name, "rb")
        except FileNotFoundError:
            print(
                f"cat: {file_name}: No such file or directory", file=sys.stderr
            )
            exit_code = 1
            continue
        except PermissionError:
            print(f"cat: {file_name}: Permission denied", file=sys.stderr)
            exit_code = 1
            continue

    last_blank = False

    while True:
        line = f.readline()

        if line:
            blank = line == b"\n"
            if blank and last_blank and "--squeeze-blank" in opts:
                continue
            last_blank = blank

            number = ("--number-nonblank" in opts and not blank) or (
                "--number" in opts and not "--number-nonblank" in opts
            )
            prefix = f"{index:>6}\t".encode("utf-8") if number else b""

            if "--show-ends" in opts:
                line = line.replace(b"\n", b"$\n")

            if "--show-tabs" in opts:
                line = line.replace(b"\t", b"^I")

            if "--show-nonprinting" in opts:
                for key, value in nonprinting.items():
                    line = line.replace(key, value)

            sys.stdout.buffer.write(prefix + line)
            sys.stdout.buffer.flush()

            if number:
                index += 1
        else:
            break

    if is_file:
        f.close()

sys.exit(exit_code)
\$\endgroup\$
2
  • \$\begingroup\$ the output to stderr of python3 cat.py cat.py | head is different than that of cat cat.py | head. you get a broken pipe error in python's output but not cat's. (maybe only sometimes? every once and a while I get a run where python doesn't give an error) \$\endgroup\$
    – guest4308
    Dec 27, 2023 at 7:53
  • \$\begingroup\$ similar but different error if you ignore the input to the pipe; eg python3 cat.py --version | head cat.py prints out the head of cat.py and a broken pipe error while cat --version | head cat.py just prints out the head of cat.py \$\endgroup\$
    – guest4308
    Dec 27, 2023 at 8:01

1 Answer 1

1
\$\begingroup\$

Python, 5179 bytes

a=b'\n'
Z=False
N='number'
v='version'
Y='--'+v
X='--help'
W='--squeeze-blank'
V='--'+N
O=f'--{N}-nonblank'
S='--show-'
I=S+'tabs'
H=S+'ends'
F=S+'nonprinting'
G=S+'all'
C="coreutils"
D="GNU "+C
s=" "
t="standard"
T=" output"
o=t+T
i=t+" input"
E=" and exit\n"
c="'s contents"
q="equivalent to -v"
f=f"Usage: cat [OPTION]... [FILE]...\nConcatenate FILE(s) to {o}.\n\nWith no FILE, or when FILE is -, read {i}.\n\n  -A, {G}{s*11}{q}ET\n  -b, {O}    {N} nonempty{T} lines, overrides -n\n  -e{s*23}{q}E\n  -E, {H}{s*10}display $ at end of each line\n  -n, {V}{s*13}{N} all{T} lines\n  -s, {W}{s*6}suppress repeated empty{T} lines\n  -t{s*23}{q}T\n  -T, {I}{s*10}display TAB characters as ^I\n  -u{s*23}(ignored)\n  -v, {F}   use ^ and M- notation, except for LFD and TAB\n{s*6}{X}     display this help{E}{s*6}{Y} {T} {v} information{E}\nExamples:\n  cat f - g  Output f{c}, then {i}, then g{c}.\n  cat{s*8}Copy {i} to {o}.\n\n{D} online help: <https://www.gnu.org/software/{C}/>\nFull documentation <https://www.gnu.org/software/{C}/cat>\nor available locally via: info '({C}) cat invocation'"
g=f'cat ({D}) 8.32\nCopyright (C) 2020 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL {v} 3 or later <https://gnu.org/licenses/gpl.html>.\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\n\nWritten by Torbjorn Granlund and Richard M. Stallman.'
E=print
from sys import*
from getopt import*
try:b,J=gnu_getopt(argv[1:],'AbeEnstTuv',[G[2:],O[2:],H[2:],N,W[2:],'show-tabs',F[2:],'help',v])
except GetoptError as c:
 K=str(c)[7:-15]
 if K[:2]=='--':E(f"cat: unrecognized option '{K}'",file=stderr)
 else:d=K[1];E(f"cat: invalid option -- '{d}'",file=stderr)
 E("Try 'cat --help' for more information.",file=stderr);exit(1)
e=[A[0]for A in b]
A=[]
a=A.append
for D in e:
 if D in['-A',G]:a(F);a(H);a(I)
 elif D=='-e':a(F);a(H)
 elif D=='-t':a(F);a(I)
 elif D=='-u':0
 elif D[:2]=='--':a(D)
 else:a({'-b':O,'-E':H,'-n':V,'-s':W,'-T':I,'-v':F}[D])
h={b'\x00':b'^@',b'\x01':b'^A',b'\x02':b'^B',b'\x03':b'^C',b'\x04':b'^D',b'\x05':b'^E',b'\x06':b'^F',b'\x07':b'^G',b'\x08':b'^H',b'\x0b':b'^K',b'\x0c':b'^L',b'\r':b'^M',b'\x0e':b'^N',b'\x0f':b'^O',b'\x10':b'^P',b'\x11':b'^Q',b'\x12':b'^R',b'\x13':b'^S',b'\x14':b'^T',b'\x15':b'^U',b'\x16':b'^V',b'\x17':b'^W',b'\x18':b'^X',b'\x19':b'^Y',b'\x1a':b'^Z',b'\x1b':b'^[',b'\x1c':b'^\\',b'\x1d':b'^]',b'\x1e':b'^^',b'\x1f':b'^_',b'\x7f':b'^?',b'\x80':b'M-^@',b'\x81':b'M-^A',b'\x82':b'M-^B',b'\x83':b'M-^C',b'\x84':b'M-^D',b'\x85':b'M-^E',b'\x86':b'M-^F',b'\x87':b'M-^G',b'\x88':b'M-^H',b'\x89':b'M-^I',b'\x8a':b'M-^J',b'\x8b':b'M-^K',b'\x8c':b'M-^L',b'\x8d':b'M-^M',b'\x8e':b'M-^N',b'\x8f':b'M-^O',b'\x90':b'M-^P',b'\x91':b'M-^Q',b'\x92':b'M-^R',b'\x93':b'M-^S',b'\x94':b'M-^T',b'\x95':b'M-^U',b'\x96':b'M-^V',b'\x97':b'M-^W',b'\x98':b'M-^X',b'\x99':b'M-^Y',b'\x9a':b'M-^Z',b'\x9b':b'M-^[',b'\x9c':b'M-^\\',b'\x9d':b'M-^]',b'\x9e':b'M-^^',b'\x9f':b'M-^_',b'\xa0':b'M- ',b'\xa1':b'M-!',b'\xa2':b'M-"',b'\xa3':b'M-#',b'\xa4':b'M-$',b'\xa5':b'M-%',b'\xa6':b'M-&',b'\xa7':b"M-'",b'\xa8':b'M-(',b'\xa9':b'M-)',b'\xaa':b'M-*',b'\xab':b'M-+',b'\xac':b'M-,',b'\xad':b'M--',b'\xae':b'M-.',b'\xaf':b'M-/',b'\xb0':b'M-0',b'\xb1':b'M-1',b'\xb2':b'M-2',b'\xb3':b'M-3',b'\xb4':b'M-4',b'\xb5':b'M-5',b'\xb6':b'M-6',b'\xb7':b'M-7',b'\xb8':b'M-8',b'\xb9':b'M-9',b'\xba':b'M-:',b'\xbb':b'M-;',b'\xbc':b'M-<',b'\xbd':b'M-=',b'\xbe':b'M->',b'\xbf':b'M-?',b'\xc0':b'M-@',b'\xc1':b'M-A',b'\xc2':b'M-B',b'\xc3':b'M-C',b'\xc4':b'M-D',b'\xc5':b'M-E',b'\xc6':b'M-F',b'\xc7':b'M-G',b'\xc8':b'M-H',b'\xc9':b'M-I',b'\xca':b'M-J',b'\xcb':b'M-K',b'\xcc':b'M-L',b'\xcd':b'M-M',b'\xce':b'M-N',b'\xcf':b'M-O',b'\xd0':b'M-P',b'\xd1':b'M-Q',b'\xd2':b'M-R',b'\xd3':b'M-S',b'\xd4':b'M-T',b'\xd5':b'M-U',b'\xd6':b'M-V',b'\xd7':b'M-W',b'\xd8':b'M-X',b'\xd9':b'M-Y',b'\xda':b'M-Z',b'\xdb':b'M-[',b'\xdc':b'M-\\',b'\xdd':b'M-]',b'\xde':b'M-^',b'\xdf':b'M-_',b'\xe0':b'M-`',b'\xe1':b'M-a',b'\xe2':b'M-b',b'\xe3':b'M-c',b'\xe4':b'M-d',b'\xe5':b'M-e',b'\xe6':b'M-f',b'\xe7':b'M-g',b'\xe8':b'M-h',b'\xe9':b'M-i',b'\xea':b'M-j',b'\xeb':b'M-k',b'\xec':b'M-l',b'\xed':b'M-m',b'\xee':b'M-n',b'\xef':b'M-o',b'\xf0':b'M-p',b'\xf1':b'M-q',b'\xf2':b'M-r',b'\xf3':b'M-s',b'\xf4':b'M-t',b'\xf5':b'M-u',b'\xf6':b'M-v',b'\xf7':b'M-w',b'\xf8':b'M-x',b'\xf9':b'M-y',b'\xfa':b'M-z',b'\xfb':b'M-{',b'\xfc':b'M-|',b'\xfd':b'M-}',b'\xfe':b'M-~',b'\xff':b'M-^?'}
if X in A or Y in A:
 for Q in A:
  if Q==X:E(f);exit(0)
  if Q==Y:E(g);exit(0)
if not J:J=['-']
R=1
L=0
for G in J:
 if G=='-':S=Z;M=stdin.buffer
 else:
  S=True
  try:M=open(G,'rb')
  except FileNotFoundError:E(f"cat: {G}: No such file or directory",file=stderr);L=1;continue
  except PermissionError:E(f"cat: {G}: Permission denied",file=stderr);L=1;continue
 T=Z
 while True:
  C=M.readline()
  if C:
   N=C==a
   if N and T and W in A:continue
   T=N;U=O in A and not N or V in A and not O in A;i=f"{R:>6}\t".encode('utf-8')if U else b''
   if H in A:C=C.replace(a,b'$\n')
   if I in A:C=C.replace(b'\t',b'^I')
   if F in A:
    for(j,k)in h.items():C=C.replace(j,k)
   stdout.buffer.write(i+C);stdout.buffer.flush()
   if U:R+=1
  else:break
 if S:M.close()
exit(L)

A golfed version of the example I wrote for the question.

\$\endgroup\$
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.