Input and Output

📡 Topic 3 — Input and Output

Almost everything a script does involves reading something in or writing something out. This chapter covers the full toolkit: echo and printf for output, read for interactive user input, the redirection operators that send data to files, and pipes that chain commands together. By the end you will also understand how here-documents let you embed multi-line input directly inside a script.

1 — Standard Streams

Every process on Linux has three standard data streams automatically connected when it starts. Understanding them is the key to understanding redirection and pipes.

0stdin
standard input
Your
Script
1stdout
standard output
2stderr
standard error
StreamFDDefault connectionUsed for
stdin0KeyboardInput that the script reads
stdout1Terminal screenNormal output (echo, printf)
stderr2Terminal screenError messages — separate from stdout so errors can be handled independently

Redirection operators change where these streams connect — to files, to other streams, or to other commands via pipes.

2 — Output with echo

echo is the simplest way to print to stdout. It outputs its arguments followed by a newline.

🐧 echo options
# Basic output — adds a newline at the end echo "Hello, World!" Hello, World! # -n suppresses the trailing newline echo -n "Enter your name: " # Cursor stays on the same line — useful before read # -e enables interpretation of escape sequences echo -e "Line one\nLine two\nLine three" Line one Line two Line three echo -e "Column 1\tColumn 2\tColumn 3" Column 1 Column 2 Column 3 # Useful escape sequences with -e # \n — newline \t — tab \\ — backslash # \a — alert bell \b — backspace
The behaviour of echo with no flags varies slightly between systems. For consistent formatted output across platforms, use printf (see section 3).

Writing to stderr

By convention, error messages should go to stderr (file descriptor 2), not stdout. This lets the caller separate normal output from errors.

🐧 Sending output to stderr
# Redirect echo's output to stderr using >&2 echo "ERROR: File not found." >&2 # Practical pattern: write a reusable error function error() { echo "ERROR: $1" >&2 } error "Could not read config file." ERROR: Could not read config file. # printed to stderr

3 — Formatted Output with printf

printf gives you precise control over formatting. It works like C's printf: a format string with placeholders, followed by the values to insert. Unlike echo, it does not add a newline automatically — you must include \n explicitly.

🐧 printf format specifiers
# %s — string, %d — integer, %f — floating point printf "Hello, %s!\n" "Philip" Hello, Philip! printf "You have %d messages.\n" 42 You have 42 messages. printf "Price: %.2f\n" 9.5 Price: 9.50 # Width and alignment — great for building tables # %-20s — left-align in a 20-char column # %8d — right-align integer in 8-char column printf "%-20s %8s %10s\n" "Name" "Age" "City" printf "%-20s %8d %10s\n" "Philip" 32 "London" printf "%-20s %8d %10s\n" "Anna" 28 "Budapest" printf "%-20s %8d %10s\n" "Kenji" 35 "Tokyo" Name Age City Philip 32 London Anna 28 Budapest Kenji 35 Tokyo # Store formatted output in a variable line=$(printf "%-20s %5d" "count" 99) echo "$line"
SpecifierTypeExample
%sStringprintf "%s" "hello"hello
%dInteger (decimal)printf "%d" 4242
%fFloating pointprintf "%.2f" 3.141593.14
%05dZero-padded integerprintf "%05d" 700007
%-10sLeft-aligned string, 10 chars wideprintf "%-10s|" "hi"hi |
%10sRight-aligned string, 10 chars wideprintf "%10s|" "hi" hi|
\nNewlineMust be explicit — printf does not add one automatically
\tTab 

4 — Reading User Input with read

The read built-in reads a line from stdin and stores it in one or more variables. It is the standard way to make an interactive script that prompts the user for information.

🐧 Basic read usage
#!/bin/bash # Basic: prompt then read echo -n "What is your name? " read name echo "Hello, $name!" # -p: inline prompt (cleaner — no need for a separate echo) read -p "Enter your city: " city echo "You live in $city." # Read multiple variables — words split on whitespace read -p "Enter first and last name: " first last echo "First: $first Last: $last" # If more words than variables, the last variable gets the remainder # read first last → "John Paul Jones" gives first=John last="Paul Jones"

Useful read Options

🐧 read flags
# -s: silent mode — input is not echoed (for passwords) read -s -p "Password: " password echo # print newline after hidden input echo "Password stored (not shown)." # -n: read exactly N characters (no Enter needed) read -n 1 -p "Press any key to continue..." echo # -t: timeout in seconds — returns non-zero exit if time expires read -t 5 -p "You have 5 seconds to answer: " answer if [ "$?" -ne 0 ]; then echo "\nTime's up!" fi # -r: raw mode — backslash is NOT treated as an escape character # Always use -r when reading file paths or arbitrary input read -r -p "Enter a file path: " filepath # -a: read words into an array read -r -a colours -p "Enter colours: " echo "First colour: ${colours[0]}"
Always use read -r as the default — without it, a backslash at the end of a line acts as a line continuation, which can cause silent data loss.
💡 Reading from a file line by line — the most common use of read beyond interactive input is reading a file line by line in a loop: while IFS= read -r line; do echo "$line"; done < file.txt. The IFS= prevents leading/trailing whitespace from being stripped. This pattern is covered in depth in Topic 6 (Loops).

5 — Redirection

Redirection operators change where a command's stdin, stdout, or stderr is connected. Instead of the terminal, you can send output to a file, read input from a file, or route error messages separately.

Output Redirection

🐧 Writing output to files
# > creates (or overwrites) a file with stdout echo "Hello" > output.txt cat output.txt Hello # >> appends to a file (does not overwrite) echo "World" >> output.txt cat output.txt Hello World # 2> redirects stderr to a file ls /nonexistent 2> errors.log cat errors.log ls: cannot access '/nonexistent': No such file or directory # 2>> appends stderr to a file ls /another_bad_path 2>> errors.log # &> (or >&) redirects both stdout AND stderr to a file ./my_script.sh &> all_output.log

Input Redirection

🐧 Reading input from a file
# < feeds a file into a command's stdin sort < names.txt # same as: sort names.txt (for commands that accept file arguments) # but < works universally for any command that reads stdin # Useful when a command does not accept a filename argument while read -r line; do echo "Line: $line" done < data.txt

Combining Redirections

🐧 Separating and merging stdout and stderr
# Send stdout to one file, stderr to another ./script.sh > output.log 2> errors.log # Redirect stderr to the same place as stdout (order matters!) ./script.sh > all.log 2>&1 # Read as: stdout → all.log, then stderr → wherever stdout now points # Common mistake — reversed order sends stderr to the OLD stdout (terminal) ./script.sh 2>&1 > all.log # WRONG: stderr still goes to terminal # Discard all output (send to /dev/null — the black hole) ./script.sh >/dev/null 2>&1 # Discard only errors ./script.sh 2>/dev/null
/dev/null is a special device that discards anything written to it and returns EOF when read. It is the standard way to suppress output you don't care about.
OperatorEffect
cmd > fileWrite stdout to file (overwrite)
cmd >> fileAppend stdout to file
cmd < fileRead stdin from file
cmd 2> fileWrite stderr to file (overwrite)
cmd 2>> fileAppend stderr to file
cmd &> fileWrite both stdout and stderr to file
cmd > file 2>&1Write both to file (POSIX-compatible form)
cmd 2>/dev/nullDiscard all error output
cmd >/dev/null 2>&1Discard all output entirely

6 — Pipes

A pipe | connects the stdout of one command directly to the stdin of the next, letting you chain commands together into a processing pipeline. No intermediate file is needed — data flows in memory.

🐧 Building command pipelines
# Count lines in a file cat names.txt | wc -l # Sort a file, remove duplicates, show the first 5 cat names.txt | sort | uniq | head -5 # Find all running bash processes ps aux | grep "bash" | grep -v "grep" # Count how many lines contain the word "error" (case-insensitive) cat app.log | grep -i "error" | wc -l # Convert a list of filenames to uppercase ls | tr '[:lower:]' '[:upper:]'
The exit status of a pipeline is the exit status of its last command. To catch failures in earlier commands, use set -o pipefail (covered in Topic 11).

tee — Branch a Pipeline

The tee command reads stdin and writes it to both stdout and a file simultaneously — like a T-junction in a pipe. Useful when you want to log output and still see it on screen.

🐧 Using tee to log and display at the same time
# Display output on screen AND save to a file ./build.sh | tee build.log # Append to the file instead of overwriting ./test.sh | tee -a test.log # Capture both stdout and stderr, display and log ./script.sh 2>&1 | tee all.log

7 — Here-Documents

A here-document (heredoc) lets you embed a block of multi-line text directly in a script and feed it as stdin to a command. This is far cleaner than running many echo statements in a row.

🐧 Basic here-document syntax
#!/bin/bash # The delimiter (EOF here, but any word works) marks the start and end cat <<EOF This is line one. This is line two. Today is $(date +%Y-%m-%d) and the user is $USER. EOF This is line one. This is line two. Today is 2026-06-09 and the user is philip. # Write a multi-line file in one block cat <<EOF > config.txt host=localhost port=8080 debug=false EOF # Suppress variable expansion with a quoted delimiter cat <<'EOF' The variable $USER will not be expanded here. This is printed literally. EOF The variable $USER will not be expanded here. # Indent the closing delimiter with <<- (strips leading TABS, not spaces) if true; then cat <<-EOF This heredoc is indented with tabs. The leading tabs are stripped from output. EOF fi
The closing delimiter must appear on a line by itself with no leading spaces (unless using <<- with tabs). A common source of "unexpected EOF" errors.
Practical here-doc: generating a report file
Here-docs are ideal for generating config files, email bodies, or HTML fragments from within a script.
#!/bin/bash report_file="report_$(date +%Y%m%d).txt" cat <<EOF > "$report_file" ======================================== System Report — $(date) ======================================== Host : $(hostname) User : $USER Uptime : $(uptime -p) Disk : $(df -h / | tail -1 | awk '{print $5 " used"}') ======================================== EOF echo "Report saved to $report_file"

Here-Strings

A here-string <<< is a compact way to pass a single string as stdin to a command — without a file or a full heredoc.

🐧 Here-string examples
# Feed a string to grep without needing echo | grep grep "World" <<< "Hello, World!" Hello, World! # Useful with read to parse a string into variables csv_line="Philip,32,London" IFS=',' read -r name age city <<< "$csv_line" echo "Name: $name Age: $age City: $city" Name: Philip Age: 32 City: London

8 — Quick Reference

Command / SyntaxWhat it does
echo "text"Print text with a trailing newline
echo -n "text"Print without trailing newline
echo -e "a\nb"Print with escape sequences interpreted
echo "msg" >&2Print to stderr
printf "%s\n" "text"Formatted print (no automatic newline)
read -r varRead a line from stdin into var
read -r -p "prompt" varPrompt then read
read -r -s -p "pw: " pwRead silently (password)
read -r -t 5 varRead with 5-second timeout
cmd > fileRedirect stdout to file (overwrite)
cmd >> fileAppend stdout to file
cmd 2> fileRedirect stderr to file
cmd > f 2>&1Redirect stdout and stderr to file
cmd >/dev/nullDiscard output
cmd1 | cmd2Pipe stdout of cmd1 to stdin of cmd2
cmd | tee fileDisplay output AND save to file
cmd <<EOF … EOFHere-document: feed block of text as stdin
cmd <<< "string"Here-string: feed single string as stdin

✏️ Exercises

Apply what you have learned in this chapter. Try each exercise yourself before looking at the sample solution.

Exercise 1
Write an interactive script called register.sh that asks the user for their first name, last name, and age (each on a separate prompt), then prints a formatted summary. The age prompt should use read -t 10 — if the user doesn't respond in 10 seconds, print "No age given" and continue.
Hint: use read -r -p for the name prompts, read -r -t 10 -p for the age prompt, and check $? after the age read to detect a timeout.
Sample Solution
#!/bin/bash # register.sh read -r -p "First name: " first read -r -p "Last name: " last read -r -t 10 -p "Age (10 sec): " age if [ "$?" -ne 0 ]; then echo age="No age given" fi printf "\n--- Registration Summary ---\n" printf "Full name : %s %s\n" "$first" "$last" printf "Age : %s\n" "$age"
Exercise 2
Write a script called logger.sh that accepts a message as a command-line argument, writes it (with a timestamp) to a file called app.log, and also prints it to the screen. If no argument is given, write "ERROR: no message provided" to stderr and exit. Run it several times to verify it appends rather than overwrites.
Hint: use ${1:?...} or an explicit if [ -z "$1" ] check, >> to append to the log file, and tee -a to show and log simultaneously.
Sample Solution
#!/bin/bash # logger.sh if [ -z "$1" ]; then echo "ERROR: no message provided" >&2 exit 1 fi timestamp=$(date +"%Y-%m-%d %H:%M:%S") entry="[$timestamp] $1" echo "$entry" | tee -a app.log
Exercise 3
Write a script called table.sh that uses printf to print a neatly aligned table of at least four items with three columns: Name, Price (formatted to 2 decimal places), and In Stock (Yes/No). Include a header row with a separator line made of dashes.
Hint: use printf "%-20s %8s %10s\n" for the header and printf "%-20s %8.2f %10s\n" for the data rows. Generate the separator line with printf '%0.s-' {1..42} or a hardcoded string.
Sample Solution
#!/bin/bash # table.sh printf "%-20s %10s %10s\n" "Name" "Price" "In Stock" printf '%.0s-' {1..44}; echo printf "%-20s %10.2f %10s\n" "Raspberry Pi 5" 74.99 "Yes" printf "%-20s %10.2f %10s\n" "USB-C Cable" 8.5 "Yes" printf "%-20s %10.2f %10s\n" "HDMI Adapter" 12.0 "No" printf "%-20s %10.2f %10s\n" "MicroSD 64GB" 11.99 "Yes" printf '%.0s-' {1..44}; echo
Exercise 4
Write a script called gen_config.sh that uses a here-document to generate a configuration file called server.conf. The file should include the current hostname, current date, and a fixed set of configuration values. Also redirect any errors from the file-write to a file called gen_config.err.
Hint: use cat <<EOF > server.conf 2> gen_config.err. Include at least one $(command) substitution inside the heredoc to embed live system values.
Sample Solution
#!/bin/bash # gen_config.sh cat <<EOF > server.conf 2> gen_config.err # server.conf — generated by gen_config.sh # Generated : $(date) # Host : $(hostname) listen_address = 0.0.0.0 listen_port = 8080 max_connections = 100 log_level = info log_file = /var/log/server.log EOF if [ "$?" -eq 0 ]; then echo "Config written to server.conf" else echo "Failed to write config — see gen_config.err" >&2 fi