hpr4747 :: UNIX Curio #13 - Replacements for cat

There's more than one way to do it - or is there?

Hosted by Vance on Tuesday, 2026-10-13 is flagged as Clean and is released under a CC-BY-SA license.
unix, unix curio, cat. (Be the first).

Listen in ogg, opus, or mp3 format. Play now:

Duration: 00:25:46
Download the transcription and subtitles.

general.

This series is dedicated to exploring little-known—and occasionally useful—trinkets lurking in the dusty corners of UNIX-like operating systems.

Thirteen is one of my favorite numbers, so I am going to use this episode to indulge myself. As part of UNIX Curio #8 ( HPR episode 4657 ), I went on a little flight of fancy and suggested that the comm utility could be used in place of cat . On further reflection, I realized that there are quite a few utilities that can do that job, almost. This episode is the result of me investigating how many possible ways there are to replace that functionality with standard utilities. Imagine that some prankster has deleted the cat utility from your machine, and you need a way to restore that capability without re-installing it.

I should warn you that as opposed to the rest of the series, this episode will not focus heavily on obscure tools, and it is unlikely the examples given will be very useful. Also, I am not going to dive into the history of any of these commands. However, you might still learn something, and it could be a little entertaining to hear about the ways some utilities can be twisted to this purpose.

Let's start by reviewing what the functionality of cat [1] is. Under POSIX, all it does is read the data provided on standard input or from each file named as an argument, and writes it to standard output. Briefly, it con cat enates all of the entries. The only option required by the standard is -u , which causes cat to write data without any delay (if that option were not given, the program could choose to buffer the data). While many implementations offer additional options that modify the output, I am going to completely ignore them and just focus on that one job. The files named as arguments are processed in the order in which they are named. One or more of these can be - , in which case cat will use the data provided on standard input and then continue to the next file in the argument list. I will actually narrow things even further and allow discussion of cases where only one file is involved, or where cat is reading from standard input via redirection from a file.

I will start off with the TL;DR conclusion—none of the standard utilities are able to fully duplicate its functionality on their own. Some of them come close, but can't quite be relied on in all respects. One important reason for this lies with the POSIX definition of a text file [2]. This is a file treated as being separated into lines, divided by newline characters. There are three limitations: none of the lines can contain a NUL byte (value 0), the lines cannot exceed the maximum line length defined by the system (required to be at least 4096 bytes including the trailing newline), and the file must end with a newline character. (The last rule is implied by the definition of "line" , which states a newline must appear at the end of a line.) It might seem strange, but a text file may contain non-ASCII data; the only restriction on the values of bytes is that they cannot be NUL. Many of the standard utilities are only required to operate on text files and therefore cannot necessarily be relied on to handle arbitrary data. While a particular implementation might not have these limitations, there is no guarantee of that. I should note that both cut and paste are only required by POSIX to operate on text files, but with the exception that the lines can be of any length.

In addition to cat , the only utilities covered in this episode that are required by POSIX to handle any arbitrary file are dd , tail (when used with the -c option), tee , tr , and uuencode . The wording for the compress utility does not specifically say it must handle any file, but in practice I think this is true. However, all of these utilities have at least one limitation of their own: they cannot handle more than one file or they can only operate on standard input. Some other utilities are able to deal with multiple files, standard input, or both, but those are only required to operate on text files. In practice, this typically means that if an input file does not end with a newline character, the program will add one to the output, destroying the idea that the output of cat should be identical to the input.

Let's start with the least-capable options and work our way towards those that could serve as closer drop-in replacements. The first candidate is a utility that I thought might be persuaded to work, but it doesn't quite. This is nl , a command used to number lines of files or standard input [3]. It normally prints the content of this input and, on the left, prints the number of each line. It is possible to turn off the numbering, and to set the separator between the numbers and the text to a null string. However, it does not seem to be possible to have absolutely nothing appear in front of each line. The POSIX specification states that the -w option can be given to define how many characters to the left of each line will be reserved for the line numbers—while the default value is six, it says nothing about what values must be accepted. I tested several implementations (specifically, from GNU, FreeBSD, NetBSD, and OpenIndiana, which is an open-source derivative of System V) and none of them allow setting the width of the space for line numbers to be less than one. Even if it were possible to remove the preceding space entirely, POSIX only requires nl to accept one file argument and to operate on text files, so it would still be limited.

In the examples for this episode, myfile is used to represent the name of the file being operated on. The text myfile ... means a filename possibly followed by other filenames (with no limitation on the number). The nl example below prints a single space in front of each line.

nl -b n -s "" -w 1 myfile

Next up are methods that actually work, but are only usable with standard input. When combined with input redirection from a file, they can accept a single file and send it to standard output. The read utility [4] takes a line of input and assigns it to a shell variable. It requires a little help, however—it can't output what it reads, so it has to be paired with something like echo or printf . Because it reads input one line at a time, it needs to be used in a loop to read the entirety of the data. In addition, the IFS shell variable must be set to a null string to prevent read from doing field splitting; we don't want it to modify the data being read at all. The shell loop while IFS="" read -r foo; do echo "$foo"; done < myfile will do the job, except that read is only required to handle text files. It is not clear to me whether POSIX allows the shell to have a limit on how much data can be stuffed into a variable; if it does, that could be a problem even if your implementation of read can handle lines longer than the maximum length.

The tr utility [5] is another one that only works with standard input. It takes the input, replaces any characters in the first string argument with the corresponding characters in the second string argument, and sends the result to standard output. If the first and second strings are the same, such as tr a a < myfile , it will behave like cat .

The tee command [6] also fits into this category. It copies standard input to standard output, just like cat does. It also sends a copy to any filename arguments given. If none of these are present, it just passes the data through untouched. You could use tee < myfile to print a file. Be careful, however, not to enter your filename as an argument (as in tee myfile ), as that will cause it to be overwritten! The good news with both tr and tee is that they aren't limited to working with text files.

Let's now consider a couple of options that only work on a file, not with standard input. In addition, they can only handle one single file. This might seem to defeat the purpose of cat , which can combine multiple files, but cat is often used with only one file. These are ed [7] and ex [8], two text editors. By feeding them the right command sequence on standard input, they can be convinced to output the entire file and then exit. The appropriate invocations would be printf '%s\n' '1,$p' Q | ed -s myfile and printf '%s\n' '1,$p' 'q!' | ex -s myfile . The POSIX specification for ex indicates that more than one filename can be given as an argument. I attempted to take advantage of this with the command line while true ; do printf '%s\n' '1,$p' 'n!' ; done | ex -s myfile ... , which should print the first file named as an argument to standard output and move to the next one. In my reading of the specification, once the last file has been printed, the n! command (meaning "next") should result in an error, causing the program to exit. However, the implementations that I tested don't exit and just stay on the last file, printing it out endlessly. Perhaps I'm misunderstanding how ex works, but regardless, both it and ed are only required to handle text files, so can't completely serve in place of cat anyway.

The following category of replacements is capable of using either standard input or a file, but they can only work with one file. Let's start with comm [9], which was responsible for me exploring this question. The command comm myfile /dev/null will send myfile to standard output. If you want to use standard input instead of a file, you need to replace the first filename with a hyphen (as in comm - /dev/null < myfile ). The paste utility [10] works similarly. It can only be used with one file because if more are given, it will try to interleave the lines of these files together. Another possibility is tail [11]. When used with the -c +1 option, it outputs the entirety of the file. Unlike other implementations I tried, the NetBSD version of tail is capable of taking multiple filenames as arguments. However, in that case, it precedes the content of each file with the filename like head does, so doesn't produce a clean copy. The sort utility [12] used with the -m option, which I talked about in UNIX Curio #11 ( HPR 4687 ), can also do the job. All of these are only required to handle text files by POSIX except tail , which must handle any arbitrary file when the -c option is used.

One more utility that can take either one file or standard input is dd [13]. It is a bit different from these others in a couple ways. First, it writes statistics about the data processed to standard error, so you would need to redirect that to /dev/null to replicate the behavior of cat . To use standard input, no arguments are needed. The syntax for arguments is unusual compared to other UNIX utilities; for this purpose, dd if=myfile 2>/dev/null would be the command to use.

The compress [14] and uuencode [15] utilities are in the same category, but do a lot more processing on the data along the way. The purpose of these is described respectively in UNIX Curios #7 ( HPR 4647 ) and #1 ( HPR 4587 ). Their output can be piped into a corresponding uncompress or uudecode utility to get back the original data and send it to standard output. The two commands would be compress -c myfile | uncompress -c and uuencode myfile /dev/stdout | uudecode —if you wanted to use standard input instead, just omit the filename arguments. I should point out that when I tested the uuencode method on FreeBSD 15.0, it didn't work; however, it did work on NetBSD 10.1 and OpenIndiana 2025.10.

Next, we have two possibilities that come very close, but don't quite behave exactly like cat . They send to standard output either the files named as arguments in order, or, if there are none, standard input. However, POSIX does not require them to take a hyphen as an argument, representing standard input. First up, we have sed [16]; when called with an empty script (as in sed "" myfile ... or sed :a myfile ... ), it just prints out the contents without any transformations. The grep utility [17] works the same way—when given an empty regular expression (as in grep "" myfile ... ), it treats that as matching anything and prints each line of input to standard output.

Finally, we have arrived at the utilities that can fully replace cat , albeit only for text files. They can be given one or more files, including a hyphen representing standard input, and will output them in the order they are given as arguments. If no filename arguments are given, they just use standard input instead. Starting out, we have awk [18]. While an empty pattern matches every line of input and an empty action prints the entire line, POSIX states that if both the pattern and action are empty, the utility is to simply exit without reading or writing anything. Therefore, we need to at least provide a pattern or an action—a pattern, as in awk "/.*/" myfile ... , can be shorter. Another one that has the capabilities is cut -b 1- myfile ... [19]. However, as noted earlier, these will add a newline character if the input doesn't already end with one, so they aren't exact replacements.

I was hoping that a simple invocation of one of the standard utilities could be used in place of the cat program that our prankster has deleted; however, I think I've demonstrated that isn't possible. Let's return to those utilities that can handle arbitrary files, but can't take more than one at a time. It would be possible to write a shell script to manage any options and arguments, then use the utility to do the actual work of sending through the data file by file. In my view, tail would be the best fit because it can take a bare filename as an argument and, unlike dd , uses the standard syntax for UNIX utilities.

The following script does just that—unfortunately, it is not really something simple that could be typed in from memory. So probably it would be easier in most situations to just re-install cat on your system instead of looking up this script, typing it in, and saving it where cat used to be.

#!/bin/sh
# Exit if any option other than -u is given; 'getopts' will issue an error
# message.  We don't actually do anything different if -u is given.
while getopts u catopt
do
  if [ "X$catopt" != "Xu" ]
  then
    exit 1
  fi
done

# Reset the positional parameters to remove any options given.
shift $(($OPTIND - 1))

case $# in
0)
  tail -c +1
;;
*)
  for fname
  do
    case "$fname" in
    -)
#      Single hyphen represents standard input, same as with no arguments.
      tail -c +1
    ;;
    *)
#      The -- ensures that $fname will be treated by 'tail' as an argument,
#      not an option.
      tail -c +1 -- "$fname"
    esac
  done
esac

It might be tempting to implement this substitute as an alias or a shell function rather than as a script, but those only exist in the current shell execution environment (and its subshells) and are not inherited by utilities and scripts run by you or others. So the easiest way to get it to be used by other scripts such as cron jobs would be to save a copy of it on your system where cat normally lives, making sure that the "execute" file mode bits are set .

There are certainly other widespread tools, such as Perl, that could also be used in place of cat . However, I wanted to focus only on utilities that are so ubiquitous that they appear in the POSIX standard. It should be noted that even some of these, such as compress and uuencode , aren't always present on modern systems.

References:

  1. Cat specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cat.html
  2. Definitions: Text File https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_403
  3. Nl specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/nl.html
  4. Read specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/read.html
  5. Tr specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tr.html
  6. Tee specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tee.html
  7. Ed specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ed.html
  8. Ex specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ex.html
  9. Comm specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/comm.html
  10. Paste specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/paste.html
  11. Tail specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tail.html
  12. Sort specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sort.html
  13. Dd specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/dd.html
  14. Compress specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/compress.html
  15. Uuencode specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/uuencode.html
  16. Sed specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html
  17. Grep specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/grep.html
  18. Awk specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/awk.html
  19. Cut specification https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cut.html


Comments

Subscribe to the comments RSS feed.

Leave Comment

Note to Verbose Commenters
If you can't fit everything you want to say in the comment below then you really should record a response show instead.

Note to Spammers
All comments are moderated. All links are checked by humans. We strip out all html. Feel free to record a show about yourself, or your industry, or any other topic we may find interesting. We also check shows for spam :).

Provide feedback
Your Name/Handle:
Title:
Comment:
Anti Spam Question: What does the letter P in HPR stand for?
Are you a spammer?
Who is the host of this show?
What does HPR mean to you?