What's the difference between single quotes (') and double quotes (") in bash?
Short answer: Single quotes preserve everything literally, double quotes allow variable and command substitution.
Detailed explanation:
Single quotes ('):
- Literal interpretation - everything inside is treated as plain text
- No variable expansion
- No command substitution
- No escape sequences
name="Alice"
echo 'Hello, $name' # Output: Hello, $name
echo 'Today is $(date)' # Output: Today is $(date)
echo 'Price: $10' # Output: Price: $10Double quotes ("):
- Variable expansion enabled -
$varreplaced with value - Command substitution works -
$(command)executes - Escape sequences work -
\n,\t, etc. - Preserves spaces
name="Alice"
echo "Hello, $name" # Output: Hello, Alice
echo "Today is $(date)" # Output: Today is Mon Dec 16...
echo "Price: \$10" # Output: Price: $10Comparison table:
| Feature | Single Quotes | Double Quotes |
|---|---|---|
| Variable expansion | No | Yes |
| Command substitution | No | Yes |
| Escape sequences | No | Yes |
Literal $ |
Yes | No (use \$) |
| Preserve spaces | Yes | Yes |
When to use:
- Single quotes: When you want exact text, no interpretation
- Double quotes: When you need variable values, command output
How to create alias permanently? How to check existing aliases?
Creating permanent alias:
Step 1: Edit ~/.bashrc
vim ~/.bashrcStep 2: Add alias definition
alias lst='ls -lt | head'
alias ll='ls -lah'
alias gs='git status'Step 3: Reload configuration
source ~/.bashrcOr add without editing:
echo "alias lst='ls -lt | head'" >> ~/.bashrc
source ~/.bashrcWhy ~/.bashrc?
- Executed every time BASH starts
- Applies to all new terminal sessions
- User-specific (in home directory)
Checking existing aliases:
Method 1: Show all aliases
aliasOutput:
alias ll='ls -lah'
alias ls='ls --color=auto'
alias lst='ls -lt | head'
Method 2: Check specific alias
alias lst
# Output: alias lst='ls -lt | head'Method 3: Check if alias exists
type lst
# Output: lst is aliased to `ls -lt | head'
type nonexistent
# Output: bash: type: nonexistent: not foundTemporary vs Permanent:
| Method | Scope | Survives logout? |
|---|---|---|
alias name='cmd' |
Current session | No |
Add to ~/.bashrc |
All future sessions | Yes |
What are environment variables? How to create them?
Definition:
Environment variables are named values that:
- Store system-wide or user-specific configuration
- Are inherited by child processes
- Affect program behavior
Common environment variables:
| Variable | Purpose | Example |
|---|---|---|
HOME |
User's home directory | /home/student |
USER |
Current username | student |
PATH |
Command search directories | /usr/bin:/bin |
SHELL |
Current shell | /bin/bash |
EDITOR |
Default text editor | vim |
LANG |
Language/locale | en_US.UTF-8 |
Creating environment variables:
Method 1: Export command
export MY_VAR="value"Method 2: Declare then export
MY_VAR="value"
export MY_VARMethod 3: One-liner
export DB_HOST="localhost" DB_PORT="5432"Permanent environment variables:
Add to ~/.bashrc:
echo 'export MY_EDITOR=vim' >> ~/.bashrc
source ~/.bashrcViewing environment variables:
All environment variables:
env
# Or:
printenvSpecific variable:
echo $HOME
echo $PATH
printenv HOMEDifference from regular variables:
# Regular variable (not exported)
MY_VAR="test"
bash # Start new shell
echo $MY_VAR # Empty! Not inherited
# Environment variable (exported)
export MY_VAR="test"
bash # Start new shell
echo $MY_VAR # Output: test (inherited!)Key uses:
- Configure programs (e.g.,
EDITOR,PAGER) - Modify system behavior (e.g.,
PATH) - Store credentials (e.g.,
API_KEY) - Set locale (e.g.,
LANG,LC_ALL)
Objective: Create permanent alias lst showing recently modified files.
Solution:
Step 1: Design the command
ls -lt | headExplanation:
ls -lt- List files, sorted by modification time (newest first)head- Show first 10 lines (most recent files)
Step 2: Create alias
alias lst='ls -lt | head'Step 3: Test it
lstExample output:
total 48
-rw-r--r-- 1 student student 234 Dec 16 14:30 recent_file.txt
-rw-r--r-- 1 student student 456 Dec 16 14:15 another.txt
drwxr-xr-x 2 student student 4096 Dec 16 13:45 mydir
...
Step 4: Make permanent
echo "alias lst='ls -lt | head'" >> ~/.bashrc
source ~/.bashrcVerification:
# Check alias exists
alias lst
# Test in new shell
bash
lstAlternative versions:
Show top 5:
alias lst='ls -lt | head -5'Show only files (no directories):
alias lst='ls -lt | grep "^-" | head'Human-readable sizes:
alias lst='ls -lht | head'Objective: Create 2-line prompt showing directory, background jobs, and $ symbol.
Solution:
Desired prompt format:
/home/student/project [2]
$
Step 1: Design PS1 variable
Components needed:
\w- Current directory (full path)\j- Number of background jobs\n- Newline\$-$symbol (or#for root)
PS1 definition:
PS1="\w [\j]\n\$ "Step 2: Test temporarily
export PS1="\w [\j]\n\$ "Your prompt changes immediately:
/home/student [0]
$ cd project
/home/student/project [0]
$ sleep 100 &
[1] 12345
/home/student/project [1]
$
Step 3: Make permanent
echo 'export PS1="\w [\j]\n\$ "' >> ~/.bashrc
source ~/.bashrcVerification:
# Logout and login again
exit
# SSH back in
ssh student@server
# Prompt should be 2-lineEnhanced version with colors:
PS1="\[\e[34m\]\w\[\e[0m\] \[\e[33m\][\j]\[\e[0m\]\n\$ "Colors:
- Blue directory
- Yellow job count
- Normal rest
To add:
echo 'export PS1="\[\e[34m\]\w\[\e[0m\] \[\e[33m\][\j]\[\e[0m\]\n\$ "' >> ~/.bashrc
source ~/.bashrcTesting background jobs:
sleep 100 &
sleep 200 &
jobs
# Prompt shows [2]Objective: Rename all .JPG files to .jpg in ~/lab6 directory.
Setup:
mkdir -p ~/lab6
# Copy files from /home/wojnicki/lab/zasoby
cp /home/wojnicki/lab/zasoby/*.JPG ~/lab6/
cd ~/lab6
lsExample files:
photo1.JPG
photo2.JPG
image.JPG
Solution:
for file in *.JPG; do
# Get filename without extension
base=$(basename "$file" .JPG)
# Rename to lowercase extension
mv "$file" "${base}.jpg"
doneStep-by-step breakdown:
1. Loop through all .JPG files:
for file in *.JPG; do2. Extract base name (without .JPG):
base=$(basename "$file" .JPG)Example: basename photo1.JPG .JPG → photo1
3. Rename file:
mv "$file" "${base}.jpg"Example: mv photo1.JPG photo1.jpg
Complete command (one-liner):
for file in *.JPG; do mv "$file" "$(basename "$file" .JPG).jpg"; doneVerification:
ls
# Output:
# photo1.jpg
# photo2.jpg
# image.jpgSafer version (check before overwriting):
for file in *.JPG; do
base=$(basename "$file" .JPG)
newname="${base}.jpg"
if [ -e "$newname" ]; then
echo "Warning: $newname already exists, skipping"
else
mv "$file" "$newname"
echo "Renamed: $file -> $newname"
fi
doneObjective: Create executable command JPG2jpg with options.
Requirements:
- No argument: Convert in current directory
- One argument (directory): Convert in that directory
-hoption: Show help
Solution:
Step 1: Create ~/bin directory
mkdir -p ~/binStep 2: Create script
vim ~/bin/JPG2jpgScript content:
#!/bin/bash
# JPG2jpg - Convert .JPG files to .jpg
# Function to convert files in directory
convert_directory() {
local dir="$1"
local count=0
cd "$dir" || exit 1
for file in *.JPG; do
# Check if any .JPG files exist
if [ ! -e "$file" ]; then
echo "No .JPG files found in $dir"
return
fi
base=$(basename "$file" .JPG)
mv "$file" "${base}.jpg"
echo "Converted: $file -> ${base}.jpg"
count=$((count + 1))
done
echo "Total converted: $count files"
}
# Show help
show_help() {
cat << EOF
JPG2jpg - Convert .JPG files to .jpg extension
Usage:
JPG2jpg Convert .JPG files in current directory
JPG2jpg DIR Convert .JPG files in specified directory
JPG2jpg -h Show this help message
Examples:
JPG2jpg
JPG2jpg ~/photos
JPG2jpg /tmp/images
Description:
Renames all files ending in .JPG to .jpg (lowercase extension)
Does not modify the actual image data, only the filename
EOF
}
# Main logic
case "$1" in
-h|--help)
show_help
exit 0
;;
"")
# No argument - use current directory
convert_directory "."
;;
*)
# Check if argument is a directory
if [ -d "$1" ]; then
convert_directory "$1"
else
echo "Error: $1 is not a directory" >&2
echo "Use 'JPG2jpg -h' for help" >&2
exit 1
fi
;;
esacStep 3: Make executable
chmod +x ~/bin/JPG2jpgStep 4: Add ~/bin to PATH
Check if already in PATH:
echo $PATH | grep "$HOME/bin"If not in PATH, add to ~/.bashrc:
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcVerification:
which JPG2jpg
# Output: /home/student/bin/JPG2jpgUsage examples:
1. Show help:
JPG2jpg -h2. Convert current directory:
cd ~/lab6
JPG2jpgOutput:
Converted: photo1.JPG -> photo1.jpg
Converted: photo2.JPG -> photo2.jpg
Total converted: 2 files
3. Convert specific directory:
JPG2jpg ~/lab6Output:
Converted: photo1.JPG -> photo1.jpg
Converted: photo2.JPG -> photo2.jpg
Total converted: 2 files
4. Error handling:
JPG2jpg /nonexistent
# Output: Error: /nonexistent is not a directoryObjective: Delete files in ~/lab6 that are not JPEG images.
Solution:
cd ~/lab6
for file in *; do
# Skip if not a regular file
if [ ! -f "$file" ]; then
continue
fi
# Check file type
filetype=$(file "$file")
# If not JPEG, remove it
if ! echo "$filetype" | grep -q "JPEG"; then
echo "Removing non-JPEG: $file"
rm "$file"
else
echo "Keeping JPEG: $file"
fi
doneStep-by-step breakdown:
1. Loop through all files:
for file in *; do2. Skip directories:
if [ ! -f "$file" ]; then
continue
fi3. Check file type:
filetype=$(file "$file")Example output of file:
image.jpg: JPEG image datadocument.txt: ASCII text
4. Check if JPEG:
if ! echo "$filetype" | grep -q "JPEG"; then5. Remove if not JPEG:
rm "$file"Safer version (dry run):
for file in *; do
if [ ! -f "$file" ]; then
continue
fi
if ! file "$file" | grep -q "JPEG"; then
echo "Would remove: $file ($(file -b "$file"))"
# Uncomment to actually delete:
# rm "$file"
fi
doneOne-liner version:
for f in *; do [ -f "$f" ] && ! file "$f" | grep -q JPEG && rm "$f"; doneExample scenario:
Before:
$ ls -l
photo1.jpg # JPEG
photo2.jpg # JPEG
readme.txt # Text file
data.csv # CSV file
fake.jpg # Actually a PNG with wrong extension
Check file types:
file *
# photo1.jpg: JPEG image data
# photo2.jpg: JPEG image data
# readme.txt: ASCII text
# data.csv: ASCII text
# fake.jpg: PNG image dataRun script:
After:
$ ls -l
photo1.jpg # Kept (JPEG)
photo2.jpg # Kept (JPEG)
# readme.txt removed (not JPEG)
# data.csv removed (not JPEG)
# fake.jpg removed (PNG, not JPEG)
Objective: Create reminder system that displays messages on login for today's date.
Part A: Create reminder file
Create ~/reminder:
vim ~/reminderFormat: YYYY-MM-DD text to remind
Example content:
2024-12-20 Submit lab report
2024-12-25 Christmas!
2025-01-01 New Year
2024-12-16 Today's reminder test
Part B: Create reminder script
vim ~/check_reminders.shScript content:
#!/bin/bash
# check_reminders.sh - Display reminders for today
REMINDER_FILE="$HOME/reminder"
# Check if reminder file exists
if [ ! -f "$REMINDER_FILE" ]; then
exit 0 # Silently exit if no reminders
fi
# Get today's date in YYYY-MM-DD format
today=$(date +%Y-%m-%d)
# Check for reminders matching today's date
while IFS= read -r line; do
# Extract date (first field)
reminder_date=$(echo "$line" | cut -d' ' -f1)
# Check if date matches today
if [ "$reminder_date" = "$today" ]; then
# Extract reminder text (everything after first space)
reminder_text=$(echo "$line" | cut -d' ' -f2-)
echo "=== REMINDER ==="
echo "Date: $reminder_date"
echo "Message: $reminder_text"
echo "================"
fi
done < "$REMINDER_FILE"Make executable:
chmod +x ~/check_reminders.shTest manually:
~/check_reminders.shOutput (if today is 2024-12-16):
=== REMINDER ===
Date: 2024-12-16
Message: Today's reminder test
================
Part C: Auto-run on login
Add to ~/.bashrc:
echo '~/check_reminders.sh' >> ~/.bashrcOr with header:
cat >> ~/.bashrc << 'EOF'
# Check for reminders
if [ -f ~/check_reminders.sh ]; then
~/check_reminders.sh
fi
EOFReload:
source ~/.bashrcTest:
# Logout and login
exit
ssh student@server
# Reminders should display automaticallyEnhanced version with colors:
#!/bin/bash
REMINDER_FILE="$HOME/reminder"
if [ ! -f "$REMINDER_FILE" ]; then
exit 0
fi
today=$(date +%Y-%m-%d)
found=0
while IFS= read -r line; do
reminder_date=$(echo "$line" | cut -d' ' -f1)
if [ "$reminder_date" = "$today" ]; then
if [ $found -eq 0 ]; then
echo -e "\n\e[33m╔════════════════════════════════════╗\e[0m"
echo -e "\e[33m║ TODAY'S REMINDERS ║\e[0m"
echo -e "\e[33m╚════════════════════════════════════╝\e[0m"
fi
reminder_text=$(echo "$line" | cut -d' ' -f2-)
echo -e "\e[32m➤\e[0m $reminder_text"
found=1
fi
done < "$REMINDER_FILE"
if [ $found -eq 1 ]; then
echo ""
fiObjective: Create stronnicuj command to split directory files into paginated groups.
Arguments:
- Source directory (default: current)
- Max files per page (default: 10)
- Preserve structure: true/false (default: false)
- Destination directory (default: {source}_stronnicowany)
Solution:
vim ~/bin/stronnicujScript content:
#!/bin/bash
# stronnicuj - Paginate files from directory into smaller groups
# Default values
SOURCE_DIR="."
MAX_FILES=10
PRESERVE_STRUCTURE=false
DEST_DIR=""
# Show help
show_help() {
cat << EOF
stronnicuj - Paginate directory files into smaller groups
Usage:
stronnicuj [SOURCE_DIR] [MAX_FILES] [PRESERVE_STRUCTURE] [DEST_DIR]
Arguments:
SOURCE_DIR Source directory (default: current directory)
MAX_FILES Maximum files per page (default: 10)
PRESERVE_STRUCTURE Keep directory structure: true/false (default: false)
DEST_DIR Destination directory (default: {source}_stronnicowany)
Examples:
stronnicuj
stronnicuj ~/photos 20
stronnicuj ~/docs 15 true
stronnicuj ~/src 10 false ~/backup
Description:
Splits files from source directory into paginated subdirectories (1, 2, 3, ...)
- If PRESERVE_STRUCTURE=true: maintains directory hierarchy
- If PRESERVE_STRUCTURE=false: flattens all files into pages
EOF
}
# Parse arguments
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
show_help
exit 0
fi
# Set source directory
if [ -n "$1" ]; then
SOURCE_DIR="$1"
fi
# Set max files per page
if [ -n "$2" ]; then
MAX_FILES="$2"
fi
# Set preserve structure flag
if [ -n "$3" ]; then
if [ "$3" = "true" ]; then
PRESERVE_STRUCTURE=true
elif [ "$3" = "false" ]; then
PRESERVE_STRUCTURE=false
else
echo "Error: PRESERVE_STRUCTURE must be 'true' or 'false'" >&2
exit 1
fi
fi
# Set destination directory
if [ -n "$4" ]; then
DEST_DIR="$4"
else
# Default: {source_name}_stronnicowany
SOURCE_BASENAME=$(basename "$SOURCE_DIR")
DEST_DIR="${SOURCE_BASENAME}_stronnicowany"
fi
# Validate source directory
if [ ! -d "$SOURCE_DIR" ]; then
echo "Error: Source directory '$SOURCE_DIR' does not exist" >&2
exit 1
fi
# Create destination directory
mkdir -p "$DEST_DIR"
echo "Source: $SOURCE_DIR"
echo "Destination: $DEST_DIR"
echo "Max files per page: $MAX_FILES"
echo "Preserve structure: $PRESERVE_STRUCTURE"
echo ""
# Find all files recursively
page_num=1
file_count=0
if [ "$PRESERVE_STRUCTURE" = "true" ]; then
# Preserve directory structure
find "$SOURCE_DIR" -type f | while read -r filepath; do
# Get relative path
relpath="${filepath#$SOURCE_DIR/}"
# Calculate page number
page_num=$(( (file_count / MAX_FILES) + 1 ))
# Create destination path
dest_path="$DEST_DIR/$page_num/$relpath"
dest_dir=$(dirname "$dest_path")
# Create directory structure
mkdir -p "$dest_dir"
# Copy file
cp "$filepath" "$dest_path"
echo "Copied to page $page_num: $relpath"
file_count=$((file_count + 1))
done
else
# Flatten structure
find "$SOURCE_DIR" -type f | while read -r filepath; do
# Get just filename
filename=$(basename "$filepath")
# Calculate page number
page_num=$(( (file_count / MAX_FILES) + 1 ))
# Create page directory
mkdir -p "$DEST_DIR/$page_num"
# Copy file
cp "$filepath" "$DEST_DIR/$page_num/$filename"
echo "Copied to page $page_num: $filename"
file_count=$((file_count + 1))
done
fi
echo ""
echo "Done! Copied $file_count files into $page_num pages"Make executable:
chmod +x ~/bin/stronnicujUsage examples:
Example 1: Paginate current directory (default 10 files/page):
mkdir test_dir
cd test_dir
touch file{1..35}.txt
cd ..
stronnicuj test_dirResult:
test_dir_stronnicowany/
├── 1/
│ ├── file1.txt
│ ├── file2.txt
│ └── ... (10 files)
├── 2/
│ └── ... (10 files)
├── 3/
│ └── ... (10 files)
└── 4/
└── ... (5 files)
Example 2: Custom page size:
stronnicuj ~/photos 20Example 3: Preserve directory structure:
mkdir -p src/core src/utils
touch src/core/file{1..15}.h
touch src/utils/util{1..8}.c
stronnicuj src 10 trueResult:
src_stronnicowany/
├── 1/
│ ├── core/
│ │ ├── file1.h
│ │ └── ... (10 files total)
├── 2/
│ ├── core/
│ │ └── ... (remaining core files)
│ └── utils/
│ └── ... (utils files)
Example 4: Flatten structure:
stronnicuj src 10 falseResult:
src_stronnicowany/
├── 1/
│ ├── file1.h
│ ├── file2.h
│ ├── util1.c
│ └── ... (10 files mixed)
├── 2/
│ └── ... (10 files)
Steps to create custom command accessible as JPG2jpg:
1. Create script file:
vim ~/bin/JPG2jpg2. Add shebang:
#!/bin/bash3. Write script logic:
#!/bin/bash
echo "My custom command"4. Make executable:
chmod +x ~/bin/JPG2jpg5. Ensure ~/bin in PATH:
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc6. Use command from anywhere:
JPG2jpgKey requirements:
- Script file with shebang
- Execute permission (
chmod +x) - Located in PATH directory (like
~/bin,/usr/local/bin) - No extension needed (not
JPG2jpg.sh, justJPG2jpg)
Verification:
which JPG2jpg # Shows path
type JPG2jpg # Shows it's a file
JPG2jpg --help # Run it# Wrong - breaks with spaces
for file in $files; do
# Correct
for file in "$files"; do# Wrong
var = "value"
# Correct
var="value"# Wrong
if [ $count = 5 ]; then
# Correct
if [ $count -eq 5 ]; then# Create script
vim myscript.sh
# Wrong
./myscript.sh # Permission denied
# Correct
chmod +x myscript.sh
./myscript.sh# Wrong (no shebang)
echo "Hello"
# Correct
#!/bin/bash
echo "Hello"# Create command in ~/bin
chmod +x ~/bin/mycommand
# Won't work if ~/bin not in PATH
mycommand # Command not found
# Fix:
export PATH="$HOME/bin:$PATH"- Single quotes preserve literal text, double quotes allow variable expansion
- Permanent aliases go in
~/.bashrc - Environment variables use
exportand are inherited by child processes - Custom commands need: shebang, execute permission, location in PATH
- basename extracts filename from path
- file command detects file type
- PS1 variable controls prompt appearance
- Loops process multiple files or items
- Conditionals make decisions based on tests
- Functions organize reusable code
BASH scripting transforms repetitive tasks into automated tools!