Professional scripts need proper logging, robust error handling, and the ability to manipulate text data. This comprehensive guide covers the tee
command for simultaneous output logging, exit codes for error handling, and powerful string manipulation techniques that every Bash scripter should know.
🎯 What You'll Learn: In this hands-on tutorial, you'll discover:
- Using the
tee
command to display AND save output - Understanding exit codes and their meanings
- Implementing proper error handling in scripts
- Calculating string length with
${#variable}
- Extracting substrings with position and length
- Replacing text patterns within strings
- Building scripts with comprehensive error messages
- Real-world examples with complete explanations
📝 Output Logging with tee
: Display AND Save
The tee
command is like a T-junction in plumbing - it splits the output into two directions: to your screen AND to a file simultaneously.
Why Use tee
?
Without tee
:
# Option 1: See output but don't save it
echo "Important message"
# Option 2: Save output but don't see it
echo "Important message" > log.txt
You have to choose: see it OR save it.
With tee
:
# Do both: see it AND save it!
echo "Important message" | tee log.txt
🚀 Your First tee
Script
Let's create a logging script that demonstrates tee
in action.
Step 1: Create the Directory
mkdir logging_redirecting_output
cd logging_redirecting_output
What these commands do:
mkdir logging_redirecting_output
- Creates a new directory for our logging examplescd logging_redirecting_output
- Changes into that directory
Step 2: Create the Script
nano multiline_output.sh
Step 3: Write the Logging Script
#!/bin/bash
{
echo "Welcome to the Logging and Redirecting Output Lab!"
echo "This script demonstrates output management."
echo "Each message will be logged to a file and displayed on the console."
} | tee output.log
Understanding the Script
Grouped Commands with Curly Braces
{
echo "Welcome to the Logging and Redirecting Output Lab!"
echo "This script demonstrates output management."
echo "Each message will be logged to a file and displayed on the console."
}
Purpose: Groups multiple commands so their combined output can be piped together.
Why curly braces:
{ ... }
groups commands in the current shell- All three
echo
statements produce output - That combined output is treated as a single stream
- The entire stream is then piped to
tee
💡 Curly Brace Syntax: Notice the space after {
and before }
. Also, the closing }
must be on its own line or preceded by a semicolon. This is required Bash syntax.
The tee
Command
| tee output.log
Purpose: Splits the output into two streams.
What happens:
- The pipe
|
sends all echo output totee
tee
writes the data tooutput.log
tee
also displays the data on your terminal- You see the messages AND they're saved
The tee
command: Named after T-shaped pipe fittings in plumbing, it splits data flow in two directions.
Step 4: Make Executable and Run
chmod +x multiline_output.sh
./multiline_output.sh
Output on terminal:
Welcome to the Logging and Redirecting Output Lab!
This script demonstrates output management.
Each message will be logged to a file and displayed on the console.
Verify it was saved:
ls
Output:
multiline_output.sh output.log
Check the log file contents:
cat output.log
Output:
Welcome to the Logging and Redirecting Output Lab!
This script demonstrates output management.
Each message will be logged to a file and displayed on the console.
Perfect! The output appears both on screen and in the file.
📂 Using tee
with System Commands
The tee
command works with any command output:
ls -l | tee listing.txt
Output:
total 8
-rw-r--r--. 1 centos9 centos9 0 Oct 4 00:15 listing.txt
-rwxr-xr-x. 1 centos9 centos9 223 Oct 4 00:13 multiline_output.sh
-rw-r--r--. 1 centos9 centos9 163 Oct 4 00:13 output.log
What happened:
ls -l
generated a detailed file listing- Output was sent to
tee
tee
displayed it on screentee
wrote it tolisting.txt
Verify the file:
cat listing.txt
Output:
total 8
-rw-r--r--. 1 centos9 centos9 0 Oct 4 00:15 listing.txt
-rwxr-xr-x. 1 centos9 centos9 223 Oct 4 00:13 multiline_output.sh
-rw-r--r--. 1 centos9 centos9 163 Oct 4 00:13 output.log
✅ Use Case: The tee
command is perfect for logging script output during automation. You can see what's happening in real-time while keeping a permanent record.
🔧 tee
Command Options
Command | Purpose | Behavior |
---|---|---|
command | tee file.txt | Save and display | Overwrites file |
command | tee -a file.txt | Append and display | Appends to file |
command | tee file1.txt file2.txt | Multiple files | Writes to both files |
command | tee -i file.txt | Ignore interrupts | Continues on SIGINT |
⚠️ Error Handling with Exit Codes
Exit codes (also called return codes or exit status) are numeric values that programs return to indicate success or failure. Understanding them is crucial for robust scripting.
Understanding Exit Codes
Exit Code | Meaning | Common Usage |
---|---|---|
0 | Success | Command completed successfully |
1 | General error | Catchall for general errors |
2 | Misuse | Incorrect usage of command |
126 | Not executable | Command found but not executable |
127 | Not found | Command not found |
128+n | Fatal signal | Terminated by signal n |
130 | Ctrl+C | Script terminated by Ctrl+C |
255 | Exit status out of range | Exit code outside 0-255 range |
🎯 Building an Error-Handling Script
Let's create a script that demonstrates proper error handling.
Step 1: Create the Directory and Script
cd ..
mkdir error_handling
cd error_handling
nano script.sh
Step 2: Write the Error-Checking Script
#!/bin/bash
# Check if the directory exists
DIRECTORY="/path/to/directory"
if [ -d "$DIRECTORY" ]; then
echo "Directory exists."
else
echo "Directory does not exist."
exit 1
fi
Understanding the Error Handling
Defining the Directory Path
DIRECTORY="/path/to/directory"
Purpose: Stores the directory path we want to check.
Why use a variable: Makes the script maintainable - you can change the path in one place.
Testing Directory Existence
if [ -d "$DIRECTORY" ]; then
echo "Directory exists."
else
echo "Directory does not exist."
exit 1
fi
Purpose: Checks if the directory exists and exits with error if it doesn't.
Breaking it down:
[ -d "$DIRECTORY" ]
- The-d
test returns true if the path exists AND is a directory"$DIRECTORY"
- Quoted to handle paths with spacesecho "Directory exists."
- Success message (optional)exit 1
- Crucial: Terminates the script with error code 1
⚠️ Why Exit Codes Matter: Without exit 1
, the script continues even after errors. Other scripts or automation tools checking your script's status will think it succeeded when it actually failed!
Step 3: Test the Script
bash script.sh
Output:
Directory does not exist.
Check the exit code:
echo $?
Output:
1
What $?
does: This special variable holds the exit code of the last command that ran. Here, it's 1
, indicating failure.
Step 4: Enhanced Error Messages
Update the script with more helpful messages:
nano script.sh
#!/bin/bash
DIRECTORY="/path/to/directory"
if [ -d "$DIRECTORY" ]; then
echo "Directory exists."
else
echo "Error: Directory does not exist."
echo "Please create the directory before proceeding."
exit 1
fi
Run it:
bash script.sh
Output:
Error: Directory does not exist.
Please create the directory before proceeding.
Why better error messages matter:
- Users understand what went wrong
- Clear guidance on how to fix the problem
- Professional appearance
- Easier debugging
📏 String Manipulation: Finding String Length
Bash provides built-in operators for working with strings. Let's start with measuring string length.
Step 1: Create the String Manipulation Lab
cd ..
mkdir string_manipulation
cd string_manipulation
nano script.sh
Step 2: Calculate String Length
#!/bin/bash
echo "Enter a string:"
read user_input
string_length=${#user_input}
echo "The length of the string you entered: $string_length"
Understanding String Length Syntax
Reading User Input
echo "Enter a string:"
read user_input
Purpose: Prompts the user and stores their input in user_input
.
The read
command: Waits for user input and assigns it to the specified variable.
Calculating Length
string_length=${#user_input}
Purpose: Counts the number of characters in the string.
Syntax breakdown:
${...}
- Parameter expansion (curly braces required)#
- Length operatoruser_input
- Variable name- Result: Number of characters including spaces
💡 String Length Operator: The #
inside ${#variable}
is a special operator that returns the length. Don't confuse it with #
used for comments!
Step 3: Test String Length
chmod +x script.sh
./script.sh
Test 1:
Enter a string:
matter
The length of the string you entered: 6
What happened: "matter" has 6 characters.
Test 2:
Enter a string:
my name is Owais Abbasi
The length of the string you entered: 23
What happened: Spaces are counted as characters! There are 23 total characters including 4 spaces.
✂️ Extracting Substrings
You can extract portions of a string using position and length parameters.
Substring Syntax
${variable:offset:length}
variable
- The string variableoffset
- Starting position (0-based)length
- Number of characters to extract
Example: Extract Middle Characters
Update script.sh
:
#!/bin/bash
echo "Enter a string:"
read user_input
substring=${user_input:2:4}
echo "Substring from position 2 to 5 is: $substring"
Understanding Substring Extraction
substring=${user_input:2:4}
Purpose: Extracts 4 characters starting at position 2 (zero-based).
Position explained:
- Position 0 = first character
- Position 1 = second character
- Position 2 = third character (where we start)
- Extract 4 characters from position 2
Example breakdown with "abcdefgh":
- Position 0:
a
- Position 1:
b
- Position 2:
c
← start here - Position 3:
d
- Position 4:
e
- Position 5:
f
← end here (4 characters: c, d, e, f)
Test Substring Extraction
./script.sh
Input:
Enter a string:
abcdefgh
Output:
Substring from position 2 to 5 is: cdef
Perfect! Characters at positions 2, 3, 4, and 5 are c
, d
, e
, f
.
✅ Real-World Use: Substring extraction is perfect for parsing structured data like dates, extracting prefixes, or processing fixed-width fields.
🔄 String Pattern Replacement
Bash can find and replace text within strings using parameter expansion.
Replacement Syntax
${variable//pattern/replacement}
variable
- The string to search in//
- Replace ALL occurrences (use/
for first only)pattern
- Text to findreplacement
- Text to replace with
Example: Replace Text Pattern
Update script.sh
:
#!/bin/bash
echo "Enter a string:"
read user_input
modified_string=${user_input//abc/xyz}
echo "Modified string: $modified_string"
Understanding Pattern Replacement
modified_string=${user_input//abc/xyz}
Purpose: Replaces every occurrence of "abc" with "xyz".
Syntax breakdown:
${...}
- Parameter expansionuser_input
- Source string//
- Replace ALL matches (not just the first)abc
- Pattern to find/xyz
- Replace with "xyz"
Test Pattern Replacement
Test 1: Exact match
./script.sh
Input:
Enter a string:
abc
Output:
Modified string: xyz
What happened: "abc" was replaced with "xyz".
Test 2: Pattern within string
Enter a string:
abcdef
Output:
Modified string: xyzdef
What happened: "abc" at the beginning was replaced, "def" remained.
Test 3: No match
Enter a string:
ldksn
Output:
Modified string: ldksn
What happened: No "abc" found, string unchanged.
Test 4: Multiple occurrences
Enter a string:
nkdabcdfksn
Output:
Modified string: nkdxyzdfksn
What happened: "abc" in the middle was replaced with "xyz".
Single vs. Double Slash
Syntax | Behavior | Example |
---|---|---|
${var/abc/xyz} | Replace first match only | abcabc → xyzabc |
${var//abc/xyz} | Replace all matches | abcabc → xyzxyz |
📊 String Manipulation Reference
Operation | Syntax | Example |
---|---|---|
String length | ${#var} | str="hello" → length is 5 |
Substring | ${var:pos:len} | ${str:0:3} → "hel" |
Replace first | ${var/old/new} | ${str/l/L} → "heLlo" |
Replace all | ${var//old/new} | ${str//l/L} → "heLLo" |
Remove from start | ${var#pattern} | ${str#hel} → "lo" |
Remove from end | ${var%pattern} | ${str%lo} → "hel" |
To uppercase | ${var^^} | ${str^^} → "HELLO" |
To lowercase | ${var,,} | str="HELLO" → "hello" |
🎯 Best Practices
✅ Logging Best Practices
- Use
tee
for important operations: Keep records of critical script actions - Append with
-a
: Usetee -a
to maintain log history - Include timestamps: Add date/time to log entries for tracking
- Log to dedicated directory: Keep logs organized in
/var/log
or similar - Rotate logs: Implement log rotation to prevent disk space issues
- Log both output and errors: Capture stdout and stderr
✅ Error Handling Best Practices
- Always use exit codes: Return 0 for success, non-zero for failure
- Check critical operations: Validate file existence, permissions, etc.
- Provide helpful error messages: Explain what went wrong and how to fix it
- Use specific exit codes: Different errors can have different codes
- Clean up on error: Remove temporary files before exiting
- Document exit codes: Comment what each code means in your script
✅ String Manipulation Best Practices
- Quote variables: Always use
"${variable}"
to handle spaces - Validate input length: Check string length before substring extraction
- Test edge cases: Empty strings, very long strings, special characters
- Use descriptive variable names:
user_input
is better thanstr
- Consider regex for complex patterns: Use
grep
,sed
, orawk
when needed - Document string formats: Explain expected input format in comments
📝 Command Cheat Sheet
Logging with tee
# Display and save to file (overwrite)
command | tee output.log
# Display and append to file
command | tee -a output.log
# Save to multiple files
command | tee file1.log file2.log
# Group commands and log all output
{
echo "Step 1"
echo "Step 2"
echo "Step 3"
} | tee script.log
# Suppress terminal output (only save)
command | tee output.log > /dev/null
Error Handling
# Basic exit with error
if [ condition ]; then
echo "Error message"
exit 1
fi
# Check command success
command
if [ $? -ne 0 ]; then
echo "Command failed"
exit 1
fi
# Short circuit operators
command || { echo "Failed"; exit 1; }
command && echo "Success"
# Check file/directory existence
[ -f "file.txt" ] || { echo "File not found"; exit 1; }
[ -d "directory" ] || { echo "Directory not found"; exit 1; }
String Manipulation
# Length
string="hello world"
length=${#string} # 11
# Substring extraction
sub=${string:0:5} # "hello"
sub=${string:6} # "world" (from position 6 to end)
# Pattern replacement
modified=${string/world/earth} # "hello earth" (first occurrence)
modified=${string//l/L} # "heLLo worLd" (all occurrences)
# Remove prefix/suffix
filename="document.txt"
name=${filename%.txt} # "document" (remove .txt)
extension=${filename##*.} # "txt" (get extension)
# Case conversion
upper=${string^^} # "HELLO WORLD"
lower=${string,,} # "hello world"
🚀 What's Next?
📚 Continue Learning
In Part 4, we'll cover:
- Understanding cron scheduling syntax
- Creating and editing cron jobs with
crontab
- Writing scripts for automated execution
- Testing cron jobs effectively
- Debugging cron job issues
- Best practices for scheduled automation
- Real-world automation examples
Stay tuned for the final guide in this series!
🎉 Congratulations! You've mastered output logging with tee
, error handling with exit codes, and powerful string manipulation techniques. Your scripts can now handle errors gracefully and process text data efficiently.
What did you think of this guide? Share your logging strategies and string manipulation use cases in the comments below!
💬 Discussion
I'd love to hear about your experience:
- How are you using
tee
in your scripts? - What error handling patterns do you find most useful?
- What string manipulation challenges have you encountered?
- What automation tasks would you like to learn about?
Connect with me: