Input Redirection (<)

Input redirection lets you feed the contents of a file into a command as if you had typed it in by hand at the keyboard. Instead of a command waiting for you to type lines and press Enter, it reads those lines straight from a file you point it at with the < operator. This is one half of the shell’s redirection toolkit — you’ll pair it constantly with > (output redirection) and | (pipes) to build commands and scripts that process data with zero manual typing. Once you understand how < connects a file to a command’s standard input, a lot of shell scripting starts to click into place.

Overview: How Input Redirection Works

Every process on Linux is handed three open file descriptors by the kernel the moment it starts: file descriptor 0 is standard input (stdin), file descriptor 1 is standard output (stdout), and file descriptor 2 is standard error (stderr). Normally, when you run an interactive command, all three are connected to your terminal: stdin reads whatever you type, and stdout/stderr print to your screen.

A file descriptor is just a small integer the kernel uses as a handle for something it has open — a regular file, a terminal, a pipe, a socket. Because Linux treats almost everything as a byte stream accessible through this same descriptor mechanism, a command that reads from stdin doesn’t actually know or care whether fd 0 is connected to your keyboard, a regular file on disk, or the output of another process. It just calls read() on file descriptor 0 and gets bytes back.

This is exactly what the < operator exploits. When you write command < file.txt, the shell does the following before it even runs command: it opens file.txt for reading, gets back a new file descriptor for it, and then uses the dup2() system call to make that file the target of file descriptor 0, closing whatever was there before (normally your terminal). Only after this plumbing is in place does the shell fork() and exec() the command. From the command’s point of view, nothing looks different — it still reads from fd 0 — but fd 0 is now wired to the file instead of your terminal.

This means input redirection only changes anything for commands that actually read from stdin. Commands like sort, grep, wc, cat, tr, and read are designed to consume stdin when no filename argument is given. Commands like ls or mkdir don’t read stdin at all for their normal operation, so redirecting a file into them with < does nothing useful — the file descriptor is open, but the program never looks at it.

Syntax

The general forms of input redirection look like this:

command < input_file
command < input_file > output_file
command <<< "string"
Form Meaning
< Redirects the contents of a file to a command’s stdin (fd 0)
<< A heredoc — feeds a multi-line block of literal text, terminated by a delimiter, to stdin
<<< A herestring — feeds a single string (plus a trailing newline) to stdin

You can combine < with > on the same command line to redirect input and output at once, and the order of redirections generally doesn’t matter to Bash as long as they come after the command name. Heredocs and herestrings are covered in depth in their own lesson, but they’re mentioned here because they’re close relatives of < — all three exist to get bytes onto a command’s stdin without you typing them interactively.

Examples

Example 1: Sorting a file’s contents

Suppose employees.txt contains names in no particular order:

cat employees.txt

Output:

Dana Okafor
Alice Chen
Carol Nguyen
Bob Martinez

Redirecting that file into sort‘s stdin prints it alphabetically, without sort ever needing a filename argument:

sort < employees.txt

Output:

Alice Chen
Bob Martinez
Carol Nguyen
Dana Okafor

Note that sort employees.txt (without <) produces the identical result — sort accepts a filename argument directly. The < form matters most for commands that only know how to read stdin, or when you want to make the data flow explicit in a script.

Example 2: Counting lines without a filename in the output

wc -l < /var/log/app.log

Output:

128

Compare that to running wc with the filename as an argument instead:

wc -l /var/log/app.log

Output:

128 /var/log/app.log

When you give wc a filename, it knows the name and prints it alongside the count. When you redirect the file to stdin with <, wc only ever sees an anonymous stream of bytes on fd 0 — it has no idea what file (if any) that stream came from, so it can’t print a name even if it wanted to. This is a useful trick whenever you want a bare number for use elsewhere, such as capturing it into a variable with count=$(wc -l < /var/log/app.log).

Example 3: Reading a file line by line in a script

A very common pattern is looping over a file’s lines with the read builtin, redirecting the file into the done keyword at the end of the loop:

#!/usr/bin/env bash
set -euo pipefail

while IFS= read -r line; do
    echo "Processing: $line"
done < servers.txt

Output:

Processing: web01.internal
Processing: web02.internal
Processing: db01.internal

Here, < servers.txt redirects stdin for the entire while loop, not just the read call. Each iteration of read -r line consumes one more line from that same stdin stream until the file is exhausted. Setting IFS= stops read from trimming leading/trailing whitespace, and -r stops it from interpreting backslashes — both are standard habits for reading files reliably.

How It Works Step by Step

Take tr 'a-z' 'A-Z' < employees.txt > employees_upper.txt as a worked trace:

  • The shell parses the command line and finds two redirection operators before it does anything else.
  • It opens employees.txt for reading and, via dup2(), attaches that file to file descriptor 0.
  • It opens (or creates) employees_upper.txt for writing and attaches it to file descriptor 1, truncating it to zero bytes first because > is the overwrite form.
  • Only now does the shell fork() a child process and exec() tr inside it, inheriting both redirected descriptors.
  • tr itself never opens any file — it just reads bytes from fd 0 and writes translated bytes to fd 1, unaware that both are backed by files instead of a terminal.
  • When tr reaches end-of-file on fd 0 (the kernel’s read() returns 0 bytes), it exits, and the shell reports its exit status.

This separation — the shell wires up the descriptors, the program just reads and writes them — is why redirection works identically for every command, without each program needing special code to support it.

Common Mistakes

Mistake 1: Redirecting output back into the same file used for input

It’s tempting to “sort a file in place” like this:

sort < employees.txt > employees.txt

This looks reasonable but destroys your data. The shell sets up all redirections before running sort, and > truncates employees.txt to zero bytes immediately. By the time sort actually tries to read from fd 0, the file it’s reading from is already empty — the result is an empty file, not a sorted one. Fix it by writing to a different file and then replacing the original:

sort < employees.txt > employees.sorted.txt
mv employees.sorted.txt employees.txt

Mistake 2: Using < when you meant a herestring

New users sometimes expect < to accept literal text directly:

grep "error" < "This is my error log text"

This fails because < always expects a filename to open — Bash tries to open a file literally named This is my error log text, which doesn’t exist:

grep: This is my error log text: No such file or directory

To feed a literal string to a command’s stdin, use a herestring instead:

grep "error" <<< "This is my error log text"

Mistake 3: Assuming every command reads stdin

Redirecting a file into ls, hoping it will list each directory named inside that file, does nothing:

ls < dirlist.txt

ls simply ignores stdin for its normal directory-listing operation and lists the current directory instead, as if < dirlist.txt weren’t there at all. To actually use each line of a file as an argument to a command, loop over it explicitly:

while IFS= read -r dir; do
    ls "$dir"
done < dirlist.txt

Best Practices

  • Prefer command < file over cat file | command when only one command needs the file — it avoids spawning an extra cat process for no reason (the “useless use of cat” pattern).
  • Never use the same filename on both sides of < and > in one command; write to a temporary file and rename it afterward instead.
  • Use while IFS= read -r line; do ... done < file for line-by-line processing rather than piping into the loop — redirecting with < keeps the loop in the current shell, so variables you set inside it are still visible afterward, unlike piping into a loop, which runs it in a subshell.
  • Always quote strings used with <<< to prevent word-splitting and globbing on their contents.
  • Before relying on < with a command, confirm it actually reads stdin when no filename is given — check its manual page if you’re unsure.
  • Quote file paths used with < in scripts (e.g. via a variable like < "$input_file") so paths containing spaces don’t break.

Practice Exercises

  • Create ~/practice/colors.txt with several color names in random order, one per line. Use input redirection with sort to print them alphabetically without modifying the original file.
  • Write a script that reads a list of usernames from users.txt (one per line) using < and a while read loop, printing "Welcome, <username>!" for each — without using a pipe anywhere.
  • Use <<< to test whether the string "connection refused" matches the pattern refused with grep, without creating any file on disk.

Summary

  • < redirects a file’s contents to a command’s standard input (file descriptor 0), replacing the terminal as the source of input.
  • The shell sets up all redirections before the command runs, which is why redirecting output to the same file used for input truncates it first and loses the data.
  • < only affects commands that actually read from stdin when no filename is given; commands like ls ignore it entirely.
  • <<< (herestring) feeds a literal string to stdin; << (heredoc) feeds a multi-line block of text — neither is the same as <, which always expects a filename.
  • while IFS= read -r line; do ... done < file is the standard, subshell-free way to process a file line by line in Bash.