~ Return to the rest of the site
cat
on a POSIX or otherwise UNIX-like system is a program that exists to concatenate files; to “join” one file at its end to another at its start, and output that resulting file to standard output.
cat
was introduced in UNIX v1 to supercede the program pr which printed the contents of a single file to the screen (McIlroy); its first-edition manual page described cat as "about the easiest way to print a file" ("cat(1)").
cat
’s modern, typical use is more or less the same; it’s often introduced to UNIX beginners as a method to print the contents of a file to the screen, which is why many implementations of cat
include options that, while possibly useful, can be redundant - see the often-included cat
-e
, -t
, and -v
that replace the ends of lines, tabs, and invisible characters respectively with printing portrayals ("cat(1p)").
The POSIX standard as of 2003 requires only the option -u
to be implemented, which prevents cat
from buffering its output - on some systems, cat
buffers its output in 512-byte blocks (McIlroy), similarly to dd
’s default as defined by POSIX (“dd(1p)”), though most currently popular cat
implementations do this by default and ignore the -u
flag altogether (busybox, GNU coreutils).
POSIX doesn’t mandate buffering by default - specifically, -u
has to guarantee that the output is unbuffered, but cat
doesn't have to buffer it in the first place and can ignore -u
in that case.
This is a POSIX-compatible implementation of UNIX cat
with no additional features nor buffered output in C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STDIN_NAME "<stdin>"
#define STDOUT_NAME "<stdout>"
/* these are the predicted errors that could occur */
enum error_type{
FILE_ACCESS,
FILE_CLOSE,
FILE_WRITE
};
/* this is an error function that will print to standard error the error that
* occurred in the program and exit */
void
error(enum error_type type, char *argv0, char *file_name){
switch(type){
case FILE_ACCESS:
fprintf(stderr, "%s: %s: cannot open file\n", argv0, file_name);
break;
case FILE_CLOSE:
fprintf(stderr, "%s: %s: cannot close file\n", argv0, file_name);
break;
case FILE_WRITE:
fprintf(stderr, "%s: %s: cannot write to file\n", argv0, file_name);
break;
}
exit(1);
}
/* print input to output, returns 0 if successful and 1 if unsuccessful */
int
file_copy(FILE *input, FILE *output){
char c;
while((c = getc(input)) != EOF)
if(putc(c, output) == EOF)
return 1;
return 0;
}
int
main(int argc, char *argv[]){
/* the name of the file being printed (for diagnostics) */
char *file_name;
/* allocate this ahead of time */
char *stdin_file_name = STDIN_NAME;
/* the file pointer of the file being printed */
FILE *input;
/* this will always be stdout */
FILE *output = stdout;
int i;
/* whether or not options are being parsed */
int parsing_opts = 1;
/* usage with 0 arguments - print standard input to standard output */
if(argc == 1 && file_copy(stdin, stdout))
error(FILE_WRITE, argv[0], STDOUT_NAME);
else if(argc == 1)
return 0;
for(i = 1; i < argc; ++i){
/* parsing options */
/* after `--`, interpret `--`, `-`, and `-u` as literal
* filenames */
if(parsing_opts && !strcmp(argv[i], "--")){
parsing_opts = 0;
continue;
/* ignore `-u` if still parsing options */
}else if(parsing_opts && !strcmp(argv[i], "-u"))
continue;
/* take `-` to mean standard input if still parsing options */
else if(parsing_opts && !strcmp(argv[i], "-")){
file_name = stdin_file_name;
input = stdin;
/* non-option; open the file and make sure file_name points to
* the right string */
}else{
file_name = argv[i];
input = fopen(file_name, "r");
if(input == NULL)
error(FILE_ACCESS, argv[0], file_name);
}
/* print input to output */
if(file_copy(input, output))
error(FILE_WRITE, argv[0], STDOUT_NAME);
/* close input file if it's not stdin */
if(input != stdin && fclose(input))
error(FILE_CLOSE, argv[0], file_name);
}
/* exit successfully */
return 0;
}
This is also available at /knowledge/cat/cat.c on this website as a plain .c file with which you can toy.
It’s worth noting that this concept of cat as a utility that sequentially prints given files to standard output means cat
can be replaced by a simple shell script that does the same using dd
and printf
; cat
as defined by POSIX is actually totally redundant to other POSIX utilities. Here’s the shell script:
#!/bin/sh
# dd_ is used so that dd can easily be re-defined
dd_() { dd "$@"; }
# usage with 0 arguments - print standard input to standard output
if [ -z "$1" ]; then
dd_ 2>/dev/null
exit $?
fi
while [ -n "$1" ]; do
# parsing options
# after `--`, interpret `--`, `-`, and `-u` as literal filenames
[ "$1" = "--" ] && [ -z "$DONT_PARSE_ARGS" ] \
&& DONT_PARSE_ARGS=1 && shift 1 && continue \
|| true
# if `-u` specified and still parsing options, enable unbuffered output
# this is kind of a hack and a bit slow. technically it is buffered,
# just one byte at a time
[ "$1" = "-u" ] && [ -z "$DONT_PARSE_ARGS" ] \
&& dd_() { dd bs=1 "$@"; } && shift 1 && continue \
|| true
# take `-` to mean standard input if still parsing options
if [ "$1" = "-" ] && [ -z "$DONT_PARSE_ARGS" ]; then
dd_ </dev/stdin 2>/dev/null || exit $?
shift 1
continue
fi
# print input to output
dd_ <"$1" 2>/dev/null || exit $?
shift 1
done
# exit successfully
exit 0
It's worth noting that the dd_
shell function in the above sample that allows for re-aliasing of dd
to dd bs=1
could be replaced with a shell variable $DD
with the initial value dd
and a changed value according to -u
of dd bs=1
.
However, alias dd="dd bs=1"
would not work due to how shell aliases are parsed (ShellCheck).
cat
doesn't work well as a shell script though.
The script is relatively slow for short files and very slow for very large files (though dd
itself should probably be used to copy large files from one medium to another anyway).
This is provided for educational purposes.