How to Create a Bash URL Shortener

How to Create a Bash URL Shortener

How to Create a Bash URL Shortener

I wanted a personal URL shortener for links I share often in scripts, documentation, and chat — something self-hosted, without relying on a third-party shortening service that could go down or start injecting ads. I ended up building a lightweight version entirely in Bash, backed by a simple flat-file database and a tiny CGI-style web server. It’s not going to replace Bitly, but for personal or internal use, it works remarkably well. Here’s how I put it together.

Why Build a URL Shortener in Bash

This might sound like an odd fit at first, but Bash is capable of more than most people expect:

The Core Concept

A URL shortener needs three basic capabilities:

  1. Generate a short, unique identifier for a given long URL.
  2. Store the mapping between the short identifier and the original URL.
  3. Redirect visitors from the short URL to the original one.

Step 1: Generating a Short Code

#!/bin/bash

generate_short_code() {
    local length="${1:-6}"
    tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length"
}

generate_short_code
echo

How This Works Internally

Step 2: Storing URL Mappings

For simplicity, we’ll use a flat file database where each line is short_code,original_url.

#!/bin/bash

set -euo pipefail

db_file="urls.csv"
touch "$db_file"

shorten_url() {
    local url="$1"
    local code

    code=$(generate_short_code 6)

    # Ensure the code isn't already used
    while grep -q "^${code}," "$db_file"; do
        code=$(generate_short_code 6)
    done

    echo "${code},${url}" >> "$db_file"
    echo "Short URL created: http://short.local/${code}"
}

generate_short_code() {
    local length="${1:-6}"
    tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length"
}

shorten_url "$1"

Breaking Down the Logic

Step 3: Resolving a Short Code Back to a URL

#!/bin/bash

set -euo pipefail

db_file="urls.csv"

resolve_url() {
    local code="$1"
    local match

    match=$(grep "^${code}," "$db_file" || true)

    if [ -z "$match" ]; then
        echo "Error: short code '$code' not found." >&2
        exit 1
    fi

    echo "${match#*,}"
}

resolve_url "$1"

This looks up the given code in the CSV file and prints the original URL. ${match#*,} uses parameter expansion to strip everything up to and including the first comma, leaving just the URL portion.

Serving Redirects with a CGI Script

To make this actually function as a web-accessible shortener, you can wire it into a minimal CGI setup served by something like nginx with fcgiwrap, or the built-in busybox httpd with CGI support.

Here’s a CGI-compatible Bash script (/cgi-bin/redirect.sh):

#!/bin/bash

db_file="/var/www/urlshortener/urls.csv"
code="${QUERY_STRING#code=}"

match=$(grep "^${code}," "$db_file" || true)

if [ -n "$match" ]; then
    target="${match#*,}"
    echo "Status: 302 Found"
    echo "Location: $target"
    echo ""
else
    echo "Status: 404 Not Found"
    echo "Content-type: text/plain"
    echo ""
    echo "Short URL not found."
fi

How the CGI Script Works

A Complete Command-Line Tool

Combining the pieces above into a single, reusable CLI tool:

#!/bin/bash

set -euo pipefail

db_file="$HOME/.urlshortener/urls.csv"
mkdir -p "$(dirname "$db_file")"
touch "$db_file"

generate_short_code() {
    tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 6
}

shorten() {
    local url="$1"
    local code
    code=$(generate_short_code)
    while grep -q "^${code}," "$db_file"; do
        code=$(generate_short_code)
    done
    echo "${code},${url}" >> "$db_file"
    echo "Short code: $code"
}

resolve() {
    local code="$1"
    grep "^${code}," "$db_file" | cut -d',' -f2- || echo "Not found."
}

case "${1:-}" in
    shorten)
        shorten "$2"
        ;;
    resolve)
        resolve "$2"
        ;;
    *)
        echo "Usage: $0 {shorten <url>|resolve <code>}"
        exit 1
        ;;
esac

Usage:

./urlshortener.sh shorten "https://example.com/very/long/path"
./urlshortener.sh resolve ab12cd

Real-World Use Cases

Automation Example

Here’s a script that watches a log for newly generated report URLs and automatically shortens them, appending the short link back into the log:

#!/bin/bash

set -euo pipefail

db_file="$HOME/.urlshortener/urls.csv"
report_log="/var/log/reports.log"

generate_short_code() {
    tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 6
}

tail -F "$report_log" | while IFS= read -r line; do
    if [[ "$line" =~ https?://[^[:space:]]+ ]]; then
        url="${BASH_REMATCH[0]}"
        code=$(generate_short_code)
        while grep -q "^${code}," "$db_file"; do
            code=$(generate_short_code)
        done
        echo "${code},${url}" >> "$db_file"
        echo "$(date '+%F %T') Shortened $url -> $code" >> /var/log/urlshortener.log
    fi
done

This uses tail -F to continuously watch a log file for new lines, and a regex match (=~) to detect URLs, automatically shortening any it finds.

Best Practices

Security Considerations

Optimization Tips

Troubleshooting Common Issues

Problem: Duplicate short codes appear in the database. This means the collision-check loop isn’t functioning correctly, or two processes wrote to the file simultaneously without locking — consider using flock to serialize writes.

Problem: CGI script returns a blank page instead of redirecting. Double-check that the blank line after your headers is present — CGI requires an empty line to separate headers from the body, and a missing Location header will silently fail to redirect.

Problem: Redirect works locally but not when deployed. Confirm that your web server is correctly configured to execute Bash scripts as CGI (correct permissions, shebang line, and CGI handler configuration).

Problem: Concurrent writes corrupt the CSV database. Wrap your write operations with flock to prevent simultaneous writes from interleaving and corrupting rows.

Common Mistakes to Avoid

Frequently Asked Questions

Can this handle thousands of URLs efficiently? A flat CSV file works fine for small to moderate scale, but for anything beyond a few thousand entries, switching to sqlite3 will keep lookups fast.

Is this suitable for a public-facing production URL shortener? It can be, but you’ll want to add proper input validation, rate limiting, HTTPS termination, and probably migrate storage to a real database rather than a flat file.

How do I make short codes more memorable? Instead of purely random alphanumeric strings, you could generate codes from word lists (e.g., combining two random dictionary words) for more human-friendly, memorable links.

Can I track how many times a short URL has been visited? Yes — extend the CGI redirect script to increment a visit counter (stored alongside the URL mapping) each time a code is resolved, before issuing the redirect.

What happens if two people generate the same short code at the same time? This is a race condition risk; using flock around the write operation prevents two processes from writing conflicting mappings simultaneously.

Summary

A Bash-based URL shortener is a fun demonstration of how far the shell can go beyond typical scripting tasks — generating unique codes, managing a flat-file database, and even serving live redirects through a CGI script. For personal or internal use, it’s a lightweight, dependency-free alternative to relying on third-party shortening services. The most important details to get right are collision handling, input validation, and — if you expose it publicly — basic protections against abuse and open redirects.

References

Exit mobile version