Shell Scripting User Input & Output: Complete Guide
Every meaningful shell script needs to communicate — whether it's asking the user for data, displaying progress, or saving results to files. Understanding how to handle input and output is what transforms a static script into an interactive, useful tool.
In this guide, we'll cover the read command for capturing user input, echo and printf for output, file redirections, and here documents for working with multi-line text.
Reading User Input with read
The read command is the primary way to capture input from the user or from a file. It's simple on the surface but packed with options for building robust interactive scripts.
Basic Input
1#!/bin/bash 2 3echo "What is your name?" 4read name 5 6echo "Hello, $name! Welcome to shell scripting."
Output:
What is your name?
Alice
Hello, Alice! Welcome to shell scripting.
Prompting with -p
Instead of using a separate echo before read, combine them with the -p flag:
1#!/bin/bash 2 3read -p "Enter your name: " name 4echo "Hello, $name!"
The -p option displays a prompt and waits for input on the same line — cleaner and more user-friendly.
Silent Input for Passwords
When collecting sensitive data like passwords, use -s to hide the input (no characters appear on screen):
1#!/bin/bash 2 3read -sp "Enter your password: " password 4echo "" # Move to a new line after silent input 5echo "Password length: ${#password} characters"
Security Note: The password is still stored in plain text in the variable. Never log it to files or echo it in production scripts.
Reading Multiple Values
You can read multiple values in a single command. The input is split by whitespace (spaces or tabs):
1#!/bin/bash 2 3read -p "Enter your first and last name: " first last 4echo "First name: $first" 5echo "Last name: $last"
Input: John Doe
Output:
First name: John
Last name: Doe
If the user enters more than two words, the last variable captures everything remaining:
1read -p "Enter your full name: " first rest 2# Input: "John Michael Doe" 3# first = "John", rest = "Michael Doe"
Reading into an Array
Use the -a flag to store space-separated input into an array:
1#!/bin/bash 2 3read -a colors -p "Enter your favorite colors (space-separated): " 4echo "You entered ${#colors[@]} colors:" 5for color in "${colors[@]}"; do 6 echo " - $color" 7done
Input: red blue green
Output:
You entered 3 colors:
- red
- blue
- green
Timeout with -t
Prevent your script from hanging indefinitely by setting a timeout:
1#!/bin/bash 2 3read -t 5 -p "Do you want to continue? (yes/no): " answer 4 5if [ -z "$answer" ]; then 6 echo "" 7 echo "No response received. Exiting..." 8 exit 1 9fi 10 11echo "You answered: $answer"
The -t 5 gives the user 5 seconds to respond. If they don't, read exits with a non-zero status and the variable remains empty.
Single Character Input with -n
For simple yes/no prompts, waiting for Enter is unnecessary. Use -n 1 to read just one character:
1#!/bin/bash 2 3read -n 1 -p "Continue? [y/n]: " choice 4echo "" # New line 5 6case $choice in 7 y|Y) echo "Proceeding..." ;; 8 n|N) echo "Aborted."; exit 0 ;; 9 *) echo "Invalid choice."; exit 1 ;; 10esac
Complete read Options Reference
| Option | Description | Example |
|---|---|---|
-p | Display a prompt | read -p "Name: " name |
-s | Silent mode (no echo) | read -sp "Password: " pass |
-t | Timeout in seconds | read -t 10 -p "Input: " val |
-n | Read N characters | read -n 1 -p "[y/n]: " ans |
-a | Read into array | read -a arr |
-r | Raw input (don't interpret backslashes) | read -r line |
-d | Use custom delimiter | read -d ',' item |
Output Commands: echo vs printf
Shell scripting offers two primary commands for output: echo and printf. While echo is simpler, printf provides formatting control similar to C programming.
Basic Output with echo
1#!/bin/bash 2 3name="Tech3Space" 4echo "Hello, World!" 5echo "Welcome to $name"
echo -n: Suppress Newline
By default, echo adds a newline at the end. Use -n when you want the cursor to stay on the same line:
1#!/bin/bash 2 3echo -n "Loading" 4for i in {1..3}; do 5 echo -n "." 6 sleep 1 7done 8echo " Done!"
Output:
Loading... Done!
echo -e: Enable Escape Sequences
The -e flag interprets backslash escapes for formatting:
1#!/bin/bash 2 3echo -e "Line 1\nLine 2\nLine 3" 4echo -e "Tab\there\tand\there" 5echo -e "Warning: \033[31mError occurred\033[0m"
Output:
Line 1
Line 2
Line 3
Tab here and here
Warning: Error occurred (in red text)
Common Escape Sequences:
| Sequence | Meaning |
|---|---|
\n | New line |
\t | Horizontal tab |
\\ | Backslash |
\a | Alert (bell) |
\033[ | Start of ANSI color code |
Formatted Output with printf
printf is more powerful than echo because it supports format specifiers and doesn't automatically add a newline.
1#!/bin/bash 2 3name="Alice" 4age=25 5pi=3.14159 6 7printf "Name: %s\n" "$name" 8printf "Age: %d years\n" "$age" 9printf "Pi value: %.2f\n" "$pi"
Output:
Name: Alice
Age: 25 years
Pi value: 3.14
Format Specifiers:
| Specifier | Description | Example |
|---|---|---|
%s | String | printf "%s" "hello" |
%d | Integer | printf "%d" 42 |
%f | Floating point | printf "%.2f" 3.14159 |
%x | Hexadecimal | printf "%x" 255 → ff |
%o | Octal | printf "%o" 8 → 10 |
%-10s | Left-align string in 10 chars | printf "%-10s" "hi" |
%10s | Right-align string in 10 chars | printf "%10s" "hi" |
Practical Table Formatting
1#!/bin/bash 2 3printf "%-15s %-10s %-10s\n" "USERNAME" "STATUS" "DISK_USAGE" 4printf "%-15s %-10s %-10s\n" "--------" "------" "----------" 5 6while read user status usage; do 7 printf "%-15s %-10s %-10s\n" "$user" "$status" "$usage" 8done <<EOF 9alice active 45% 10bob locked 12% 11carol active 89% 12EOF
Output:
USERNAME STATUS DISK_USAGE
-------- ------ ----------
alice active 45%
bob locked 12%
carol active 89%
File Redirections
Redirections control where command output goes and where input comes from. They're essential for logging, automation, and data processing.
Output Redirection (> and >>)
1#!/bin/bash 2 3# Overwrite: creates file or truncates existing 4echo "System backup started at $(date)" > backup.log 5 6# Append: adds to the end of the file 7echo "Backup completed successfully" >> backup.log
| Operator | Description | Behavior |
|---|---|---|
> | Redirect stdout | Overwrites file |
>> | Redirect stdout | Appends to file |
< | Redirect stdin | Read from file |
2> | Redirect stderr | Overwrites error log |
2>> | Redirect stderr | Appends to error log |
&> | Redirect stdout and stderr | Overwrites (Bash 4+) |
&>> | Redirect stdout and stderr | Appends (Bash 4+) |
> file 2>&1 | Redirect both streams | Classic portable method |
Input Redirection (<)
1#!/bin/bash 2 3# Read line by line from a file 4while read -r line; do 5 echo "Processing: $line" 6done < users.txt
Separating stdout and stderr
1#!/bin/bash 2 3# Send output to one file, errors to another 4./process_data.sh > results.txt 2> errors.log 5 6# Send both to the same file (method 1: Bash 4+) 7./process_data.sh &> all.log 8 9# Send both to the same file (method 2: portable) 10./process_data.sh > all.log 2>&1 11 12# Discard errors completely 13grep "pattern" *.log 2>/dev/null
/dev/null: The Black Hole
1#!/bin/bash 2 3# Suppress all output 4 noisy_command > /dev/null 2>&1 5 6# Suppress only stdout, keep errors 7 noisy_command > /dev/null 8 9# Suppress only errors, keep stdout 10 noisy_command 2> /dev/null
Practical Example: Logging Function
1#!/bin/bash 2 3LOG_FILE="script.log" 4 5log() { 6 echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" 7} 8 9log "Starting deployment..." 10# ... deployment steps ... 11log "Deployment completed."
The tee command writes to both stdout and a file, while -a appends instead of overwriting.
Here Documents (<<)
A here document allows you to pass multi-line input to a command without creating a separate file. It's incredibly useful for generating configuration files, sending emails, or creating SQL queries.
Basic Here Document
1#!/bin/bash 2 3cat << EOF 4This is a multi-line message. 5It preserves formatting and spacing. 6Today's date: $(date) 7Current user: $(whoami) 8EOF
The << EOF tells the shell to read everything until it finds a line containing only EOF. The delimiter can be any string you choose.
Here Document with Variable Expansion
By default, variables and command substitutions are expanded inside here documents:
1#!/bin/bash 2 3USER_NAME="Alice" 4APP_NAME="MyApp" 5 6cat << EOF 7Hello $USER_NAME, 8 9Your application "$APP_NAME" has been deployed successfully. 10Server: $(hostname) 11Time: $(date) 12 13Regards, 14DevOps Team 15EOF
Literal Here Document (<< 'EOF')
If you want to prevent variable expansion (useful for code snippets or literal text), quote the delimiter:
1#!/bin/bash 2 3cat << 'EOF' 4This text will NOT expand variables: 5- $HOME stays as $HOME 6- $(date) stays as $(date) 7- Backticks `like this` are literal 8EOF
Here Documents in Scripts
Example: Generate an Nginx Config
1#!/bin/bash 2 3DOMAIN="example.com" 4ROOT_DIR="/var/www/$DOMAIN" 5 6cat > /etc/nginx/sites-available/$DOMAIN << EOF 7server { 8 listen 80; 9 server_name $DOMAIN www.$DOMAIN; 10 root $ROOT_DIR; 11 index index.html index.htm; 12 13 location / { 14 try_files \$uri \$uri/ =404; 15 } 16 17 error_log /var/log/nginx/$DOMAIN-error.log; 18 access_log /var/log/nginx/$DOMAIN-access.log; 19} 20EOF 21 22echo "Nginx config created for $DOMAIN"
Note: We escaped
\$uriwith a backslash because$uriis an Nginx variable, not a shell variable. Without the escape, the shell would try to expand it (to nothing) before writing the file.
Indentation Trick with Tabs
If you want your here document indented for readability without including the tabs in the output, use <<-EOF:
1#!/bin/bash 2 3if true; then 4 cat <<- EOF 5 This line is indented in the script 6 but will NOT have leading tabs in output. 7 Only tabs are stripped, not spaces. 8 EOF 9fi
Here Strings (<<<)
A here string is like a mini here document — it passes a single string to a command's standard input:
1#!/bin/bash 2 3# Count words in a string 4wc -w <<< "Hello world from shell scripting" 5 6# Compare dates 7if [[ "$(date +%u)" -gt 5 ]]; then 8 echo "It's the weekend!" 9fi 10 11# Pass variable content to a command 12config_data="key1=value1\nkey2=value2" 13while IFS='=' read -r key value; do 14 echo "Key: $key, Value: $value" 15done <<< "$config_data"
Here strings are perfect when you need to pipe a single variable's content to a command without using echo "$var" | command.
Real-World Example: Interactive Setup Script
Let's combine everything into a practical script that sets up a new project:
1#!/bin/bash 2 3set -euo pipefail 4 5echo "=== Project Setup Wizard ===" 6echo "" 7 8# Get project name 9read -p "Enter project name: " project_name 10 11if [ -z "$project_name" ]; then 12 echo "Error: Project name cannot be empty!" 13 exit 1 14fi 15 16# Get project type 17echo "" 18echo "Select project type:" 19echo " 1) Web Application" 20echo " 2) API Service" 21echo " 3) CLI Tool" 22read -n 1 -p "Choice [1-3]: " project_type 23echo "" 24 25case $project_type in 26 1) type_name="web-app" ;; 27 2) type_name="api-service" ;; 28 3) type_name="cli-tool" ;; 29 *) echo "Invalid choice"; exit 1 ;; 30esac 31 32# Get author info 33read -p "Author name: " author_name 34read -p "Author email: " author_email 35 36# Confirm 37echo "" 38echo "Summary:" 39printf " %-15s %s\n" "Project:" "$project_name" 40printf " %-15s %s\n" "Type:" "$type_name" 41printf " %-15s %s\n" "Author:" "$author_name" 42printf " %-15s %s\n" "Email:" "$author_email" 43echo "" 44 45read -n 1 -p "Create project? [y/n]: " confirm 46echo "" 47 48if [[ ! "$confirm" =~ ^[Yy]$ ]]; then 49 echo "Cancelled." 50 exit 0 51fi 52 53# Create project structure 54mkdir -p "$project_name"/{src,tests,docs} 55cd "$project_name" 56 57# Create README with here document 58cat > README.md << EOF 59# $project_name 60 61**Type:** $type_name 62**Author:** $author_name <$author_email> 63**Created:** $(date +%Y-%m-%d) 64 65## Getting Started 66 67\`\`\`bash 68npm install 69npm start 70\`\`\` 71EOF 72 73# Create package.json 74cat > package.json << EOF 75{ 76 "name": "$project_name", 77 "version": "1.0.0", 78 "description": "$type_name project", 79 "author": "$author_name <$author_email>", 80 "main": "src/index.js", 81 "scripts": { 82 "start": "node src/index.js", 83 "test": "jest" 84 } 85} 86EOF 87 88echo "" 89echo "✅ Project '$project_name' created successfully!" 90ls -la
Best Practices for Input/Output
- Always quote variables when using them in output:
echo "$name"notecho $name - Use
printffor formatted tables — it's more reliable thanechowith tabs - Validate user input — never assume the user entered what you expected
- Use
read -rto prevent backslash interpretation issues:while read -r line; do ... - Redirect errors to logs in production scripts:
> output.log 2> error.log - Use here documents for config files — they're cleaner than multiple
echostatements - Set timeouts on interactive prompts to prevent hanging in automated environments
Quick Reference
1# Input 2read var # Basic input 3read -p "Prompt: " var # With prompt 4read -sp "Password: " var # Silent 5read -t 5 -p "Quick: " var # With timeout 6read -n 1 -p "[y/n]: " var # Single char 7read -a arr # Into array 8 9# Output 10echo "Hello" # Basic 11echo -n "Loading..." # No newline 12echo -e "Line1\nLine2" # Escape sequences 13printf "Name: %s\n" "$name" # Formatted 14 15# Redirection 16cmd > file # Overwrite stdout 17cmd >> file # Append stdout 18cmd < file # Read stdin 19cmd 2> file # Redirect stderr 20cmd &> file # Redirect both (Bash 4+) 21cmd > file 2>&1 # Redirect both (portable) 22 23# Here documents 24cat << EOF 25multi-line 26text 27EOF 28 29cat << 'EOF' # No variable expansion 30literal $HOME 31EOF
Mastering input and output is what makes your shell scripts interactive, user-friendly, and production-ready. Combined with variables from the previous chapter, you now have the foundation to build scripts that respond dynamically to users and systems.
Next up: Conditionals and flow control — the logic that makes your scripts truly intelligent.