Matthew Hodge
Full Stack Developer

Bash Scripts Every Developer Should Have

Most developers spend a surprising amount of time on repetitive terminal tasks — switching between projects, running the same sequence of commands, tailing logs, deploying. A few well-placed Bash scripts can cut that friction significantly.

This isn't a Bash tutorial — it's a collection of scripts I actually use, with enough explanation that you can adapt them for your own workflow.


1. Project Switcher

If you're jumping between multiple projects throughout the day, this saves you from repeatedly navigating to directories and starting services.

#!/usr/bin/env bash
# ~/bin/project

PROJECTS_DIR="$HOME/Websites"

projects=(
    "matthew-blog:$PROJECTS_DIR/matthew-blog"
    "client-app:$PROJECTS_DIR/client-app"
    "api:$PROJECTS_DIR/api"
)

if [ -z "$1" ]; then
    echo "Available projects:"
    for project in "${projects[@]}"; do
        echo "  ${project%%:*}"
    done
    exit 0
fi

for project in "${projects[@]}"; do
    name="${project%%:*}"
    path="${project##*:}"

    if [ "$name" = "$1" ]; then
        cd "$path" || exit 1
        echo "Switched to $name ($path)"

        # Start any services you need — comment out what you don't
        # docker compose up -d 2>/dev/null
        # code .

        exec $SHELL
    fi
done

echo "Project '$1' not found."
exit 1

Make it executable and put it on your PATH:

chmod +x ~/bin/project
# Ensure ~/bin is in your PATH in ~/.bashrc or ~/.zshrc:
# export PATH="$HOME/bin:$PATH"

Usage:

project               # list all
project matthew-blog  # switch to it

2. Git Branch Cleanup

After a few weeks of active development, you end up with dozens of stale local branches. This script deletes all local branches that have already been merged into main (or whatever your default branch is).

#!/usr/bin/env bash
# ~/bin/git-clean

DEFAULT_BRANCH="${1:-main}"

echo "Fetching latest from remote..."
git fetch --prune

merged=$(git branch --merged "$DEFAULT_BRANCH" | grep -v "^\*" | grep -v "$DEFAULT_BRANCH" | tr -d ' ')

if [ -z "$merged" ]; then
    echo "No merged branches to clean up."
    exit 0
fi

echo "Branches to delete:"
echo "$merged"
echo ""
read -rp "Delete these branches? [y/N] " confirm

if [[ "$confirm" =~ ^[Yy]$ ]]; then
    echo "$merged" | xargs git branch -d
    echo "Done."
else
    echo "Aborted."
fi
git-clean        # cleans branches merged into main
git-clean dev    # cleans branches merged into dev

The git fetch --prune at the top removes references to remote branches that no longer exist, so your git branch -r output doesn't get cluttered either.


3. New Laravel Project Bootstrap

Starting a new Laravel project always involves the same steps. This script handles them all:

#!/usr/bin/env bash
# ~/bin/new-laravel

if [ -z "$1" ]; then
    echo "Usage: new-laravel <project-name>"
    exit 1
fi

PROJECT_NAME="$1"
PROJECTS_DIR="$HOME/Websites"
PROJECT_PATH="$PROJECTS_DIR/$PROJECT_NAME"

echo "Creating Laravel project: $PROJECT_NAME"

composer create-project laravel/laravel "$PROJECT_PATH"
cd "$PROJECT_PATH" || exit 1

# Copy .env
cp .env.example .env

# Set DB name to project name (replace hyphens with underscores)
DB_NAME="${PROJECT_NAME//-/_}"
sed -i "s/DB_DATABASE=laravel/DB_DATABASE=$DB_NAME/" .env

php artisan key:generate

# Init git
git init
git add -A
git commit -m "Initial Laravel install"

echo ""
echo "Done! Project created at $PROJECT_PATH"
echo "Next: cd $PROJECT_PATH && php artisan serve"

4. Log Watcher

Tailing logs is fine, but this adds colour-coding so errors jump out at you:

#!/usr/bin/env bash
# ~/bin/watch-log

LOG_FILE="${1:-storage/logs/laravel.log}"

if [ ! -f "$LOG_FILE" ]; then
    echo "Log file not found: $LOG_FILE"
    exit 1
fi

tail -f "$LOG_FILE" | while read -r line; do
    if echo "$line" | grep -q "ERROR\|CRITICAL\|EMERGENCY"; then
        echo -e "\033[31m$line\033[0m"   # Red
    elif echo "$line" | grep -q "WARNING"; then
        echo -e "\033[33m$line\033[0m"   # Yellow
    elif echo "$line" | grep -q "INFO"; then
        echo -e "\033[32m$line\033[0m"   # Green
    else
        echo "$line"
    fi
done

Run it from your project root:

watch-log                               # default Laravel log
watch-log /var/log/nginx/error.log      # any log file

5. Quick Git Stats

A summary of what's happened in a repo lately — useful when coming back to a project after time away:

#!/usr/bin/env bash
# ~/bin/git-stats

DAYS="${1:-7}"

echo "=== Git activity for the last $DAYS days ==="
echo ""

echo "--- Commits ---"
git log --oneline --since="$DAYS days ago"

echo ""
echo "--- Files changed ---"
git diff --stat "HEAD@{$DAYS days ago}" HEAD 2>/dev/null | tail -1

echo ""
echo "--- Contributors ---"
git shortlog -sn --since="$DAYS days ago"
git-stats      # last 7 days
git-stats 30   # last 30 days

6. Simple Deploy Script

This is a minimal deploy script for projects hosted on a VPS where you SSH in and pull. Adapt it for your own setup:

#!/usr/bin/env bash
# deploy.sh (in project root)

set -e  # exit immediately on error

SERVER_USER="ubuntu"
SERVER_HOST="your.server.com"
PROJECT_PATH="/var/www/your-app"

echo "Deploying to $SERVER_HOST..."

ssh "$SERVER_USER@$SERVER_HOST" << 'REMOTE'
    set -e
    cd /var/www/your-app

    echo "Pulling latest..."
    git pull origin main

    echo "Installing dependencies..."
    composer install --no-dev --optimize-autoloader

    echo "Running migrations..."
    php artisan migrate --force

    echo "Clearing caches..."
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache

    echo "Restarting queue workers..."
    php artisan queue:restart

    echo "Done."
REMOTE

echo "Deployment complete."

The set -e at the top is important — if any command fails (migration error, composer fail), the script stops immediately rather than blundering forward.


Making Scripts Available Everywhere

Put your scripts in ~/bin/ and add it to your PATH in ~/.bashrc or ~/.zshrc:

export PATH="$HOME/bin:$PATH"

Then source ~/.bashrc (or restart your terminal) and they're available from anywhere.

A few other tips:

  • Always add set -e to scripts where a failure partway through would leave things in a broken state.
  • Use set -u to catch references to undefined variables — catches a lot of typo bugs.
  • set -o pipefail makes pipeline errors propagate correctly (without it, failing_command | grep something exits 0).
  • Add usage messages at the top of every script — future-you will appreciate it.

Final Thoughts

The best Bash scripts are the ones that automate something you'd otherwise do manually three times a week. Start by looking at your own command history — history | sort | uniq -c | sort -rn | head -20 — and see what comes up repeatedly. That's your automation backlog.