Shell Scripting Bash File Operations Basics May 2026

Shell Scripting File & Directory Operations

Master file creation, reading, writing, copying, moving, and deletion. Learn find with actions, stat for metadata, permission management, safe temp files with mktemp, and rsync for reliable transfers.

File and directory manipulation is at the core of system automation. Every backup script, deployment pipeline, and log management tool works with files. Knowing how to do it safely — with proper error checking, no data loss, and correct permissions — is essential production knowledge.

BASH
# ── Create files ──────────────────────────────────────────
touch newfile.txt               # create empty or update timestamp
touch file{1,2,3}.txt           # create file1.txt file2.txt file3.txt
echo "content" > file.txt        # create with content
cat > file.txt << 'EOF'
multi-line
content here
EOF

# ── Read files ────────────────────────────────────────────
cat file.txt                    # print entire file
head -5 file.txt                # first 5 lines
tail -5 file.txt                # last 5 lines
content=$(cat file.txt)          # read into variable
first_line=$(head -1 file.txt)   # first line into variable

# ── Write / append ────────────────────────────────────────
echo "line" > file.txt           # overwrite
echo "line" >> file.txt          # append
printf "%s\n" "line" >> file.txt  # printf append (more control)

# ── Create directories ────────────────────────────────────
mkdir mydir                     # single directory
mkdir -p /deep/nested/path      # create all parents (no error if exists)
mkdir -p logs/{2026,2025}/{jan,feb,mar}  # brace expansion structure
BASH
# ── Copy ──────────────────────────────────────────────────
cp file.txt backup.txt          # copy file
cp -r srcdir/ destdir/          # copy directory recursively
cp -p file.txt backup.txt       # preserve permissions + timestamps
cp -a srcdir/ destdir/          # archive: recursive + preserve all
cp -u src dst                   # only copy if src is newer
cp -v file.txt backup.txt       # verbose — show what's happening
cp -i file.txt backup.txt       # interactive — prompt before overwrite

# ── Move / rename ─────────────────────────────────────────
mv old.txt new.txt              # rename
mv file.txt /backup/            # move to directory
mv -v file.txt /backup/         # verbose
mv -n file.txt /backup/         # no-clobber: don't overwrite

# ── Delete safely ─────────────────────────────────────────
rm file.txt                     # delete file
rm -f file.txt                  # force (no error if missing)
rm -r mydir/                    # delete directory recursively
rm -rf mydir/                   # force recursive (DANGEROUS — no undo!)
rmdir emptydir/                 # remove empty directory only

# ── Safe delete pattern — always check path is not empty ──
safe_rm() {
  local path="${1:?safe_rm: path required}"
  [[ -z "${path}" ]] && { echo "ERROR: empty path" >&2; return 1; }
  [[ "${path}" == "/" ]] && { echo "ERROR: refusing to delete /" >&2; return 1; }
  rm -rf "${path}"
}

# Never do: rm -rf "${var}/"   if $var could be empty!
# That becomes: rm -rf /
⚠ Never rm -rf "${var}/" without guarding — if $var is empty, this becomes rm -rf / and destroys the system. Always validate the variable is non-empty before any destructive operation.
BASH
# ── Basic find ────────────────────────────────────────────
find /var/log -name "*.log"             # by name pattern
find /var/log -iname "*.LOG"           # case-insensitive
find /app -type f                       # files only
find /app -type d                       # directories only
find /app -type l                       # symlinks only

# ── By age ────────────────────────────────────────────────
find /tmp -mtime +7                     # modified > 7 days ago
find /tmp -mtime -1                     # modified within 1 day
find /var/log -mmin +60                 # modified > 60 minutes ago
find /backups -newer reference.txt      # newer than reference file

# ── By size ───────────────────────────────────────────────
find /var -size +100M                   # > 100 MB
find /tmp -size 0                       # empty files
find /logs -size +1G -size -5G         # between 1G and 5G

# ── With actions ──────────────────────────────────────────
find /tmp -name "*.tmp" -delete        # delete in one step
find /logs -mtime +30 -exec gzip {} \; # compress old logs
find /app -name "*.sh" -exec chmod +x {} \;  # make scripts executable
find /var/log -mtime +7 -print0 | xargs -0 rm -f  # safe with spaces

# ── Combine conditions ────────────────────────────────────
find /var/log -name "*.log" -size +10M -mtime +3
find /app \( -name "*.pyc" -o -name "__pycache__" \) -delete
BASH
# ── Permissions ───────────────────────────────────────────
chmod 755 script.sh              # rwxr-xr-x
chmod 640 config.env             # rw-r----- (owner rw, group r)
chmod +x script.sh               # add execute for all
chmod go-w file.txt              # remove write from group+others
chmod -R 755 /app/               # recursive
chown vriddh:www-data file.txt   # change owner:group
chown -R app:app /opt/myapp/     # recursive ownership

# ── stat — file metadata ──────────────────────────────────
stat file.txt                    # full metadata
stat -c "%s" file.txt            # file size in bytes
stat -c "%U:%G" file.txt         # owner:group
stat -c "%a" file.txt            # octal permissions
stat -c "%y" file.txt            # last modified time
stat -c "%i" file.txt            # inode number

# ── mktemp — safe temporary files ────────────────────────
tmpfile=$(mktemp)                # /tmp/tmp.XXXXXXXXXX
tmpdir=$(mktemp -d)              # temporary directory
tmpfile=$(mktemp /tmp/myapp.XXXXXX)  # custom prefix/suffix

# Always clean up temp files on exit
cleanup() { rm -f "${tmpfile}"; rm -rf "${tmpdir}"; }
trap cleanup EXIT INT TERM
BASH
# ── rsync basics ──────────────────────────────────────────
rsync -av src/ dest/             # archive + verbose
rsync -av --delete src/ dest/   # exact mirror (delete extras)
rsync -av --dry-run src/ dest/  # preview without copying
rsync -avz src/ remote:/dest/   # compress over network
rsync -avz --progress src/ remote:/dest/  # with progress bar

# ── Exclude patterns ──────────────────────────────────────
rsync -av --exclude='*.log' --exclude='.git' src/ dest/
rsync -av --exclude-from='.rsync-exclude' src/ dest/

# ── Backup script using rsync ─────────────────────────────
BACKUP_SRC="/app/data"
BACKUP_DST="backup@bkp-01:/backups/$(date +%Y/%m/%d)"

rsync -avz \
  --delete \
  --exclude='*.tmp' \
  --exclude='cache/' \
  --log-file=/var/log/rsync.log \
  "${BACKUP_SRC}/" \
  "${BACKUP_DST}/"

echo "Backup exit code: $?"
bash — file operations
vriddh@prod-01:~/scripts$find /var/log -name "*.log" -mtime +7 -size +1M
/var/log/nginx/access.log.8
/var/log/myapp/app.log.12
vriddh@prod-01:~/scripts$stat -c "%s %U:%G %a %n" /etc/passwd
2741 root:root 644 /etc/passwd
vriddh@prod-01:~/scripts$tmpf=$(mktemp); echo "data" > "$tmpf"; echo $tmpf
/tmp/tmp.aX4kR92bPq
✔ File operation safety rules — Always use mktemp for temp files, never /tmp/myfile.$$. Always trap cleanup EXIT to remove temp files. Never rm -rf without validating the path variable is non-empty. Use cp -a or rsync -av for backups to preserve metadata. Use find ... -print0 | xargs -0 for filenames with spaces.