~ Return to the rest of the site

# POSIX cat(1)

### updated 2021-08-06

`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.

## Cited media and further reading

- Articles
  - McIlroy, M. Douglas - “A Research Unix Reader”
  - ShellCheck wiki page SC2262 ("This alias can't be defined and used in the same parsing unit. Use a function instead.")
- Manual pages
  - cat(1) (UNIX v1)
  - cat(1p) (The Open Group, 2003)
  - dd(1p) (The Open Group, 2003)
  - printf(1p) (The Open Group, 2003)
- Notable cat implementations
  - 1979 - Bell Labs/UNIX v7 - /usr/src/cmd/cat.c
  - 1992 - Bell Labs/Plan 9 from Bell Labs Fourth Edition - /sys/src/cmd/cat.c
  - ongoing started 1992 - GNU/coreutils - src/cat.c
  - 1995 - University of California, Berkeley/4.4BSD-Lite2 - /usr/src/bin/cat/cat.c
  - ongoing forked from 4.4BSD-Lite2 - NetBSD/src - bin/cat/cat.c
  - ongoing started 1995 - busybox/busybox - coreutils/cat.c

## Acknowledgements

- Content help
  - Miles
  - WeedSmokingJew
- JavaScript help
  - adamz01h
  - wiresToGround
- JavaScript libraries used
  - highlight.js
- Sample code help
  - Ando_Bando
  - Miles
  - u/oh5nxo
  - WeedSmokingJew