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:

  • It can generate short, unique codes and store simple key-value mappings using flat files or SQLite.
  • Combined with a minimal web server (or a CGI script under something like nginx or busybox httpd), it can serve redirects without needing a full backend framework.
  • It’s ideal for personal, internal, or lightweight use cases where spinning up a whole application server feels like overkill.

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

  • /dev/urandom is a source of cryptographically strong pseudo-random bytes provided by the kernel.
  • tr -dc 'A-Za-z0-9' deletes (-d) every character that is NOT (-c) in the specified set, leaving only alphanumeric characters.
  • head -c "$length" truncates the output to the desired number of characters (6 by default).

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

  • touch "$db_file" ensures the database file exists even on the very first run.
  • The while grep -q "^${code}," "$db_file" loop protects against code collisions by checking whether the generated code is already in use, regenerating if so.
  • echo "${code},${url}" >> "$db_file" appends the new mapping as a CSV row.

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

  • QUERY_STRING is an environment variable automatically populated by the web server with the URL’s query string (e.g., code=ab12cd).
  • "${QUERY_STRING#code=}" strips the literal prefix code=, leaving just the short code value.
  • The script outputs raw HTTP headers manually (Status: and Location:), which is what CGI expects — a 302 Found response with a Location header triggers a browser redirect.

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

  • Internal documentation links: Shortening long internal wiki URLs for use in chat messages or terminal output.
  • CLI tool output: Automatically shortening long generated report URLs before printing them to the console.
  • Personal bookmarking: A private, self-hosted alternative to public shorteners for frequently shared links.
  • Script-generated links: Automatically shortening dynamically generated dashboard or log links inside automated reports or alerts.
  • QR code pairing: Combining a URL shortener with a QR code generator (see the companion article on generating QR codes in Bash) to produce compact, scannable codes for long URLs.

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

  • Always check for short-code collisions before writing a new mapping, especially as your database grows.
  • Use a reasonably long code length (6+ characters) to keep the collision probability low as your URL count scales.
  • Store your database in a location with proper backup coverage — a flat CSV file is convenient but fragile without backups.
  • Validate that submitted URLs are well-formed before storing them, to avoid broken redirects later.
  • Consider migrating to SQLite (sqlite3 CLI) once your flat-file database grows beyond a few thousand entries, since grep-based lookups become slower at scale.

Security Considerations

  • Open redirect vulnerabilities: A URL shortener is inherently a redirect service — ensure it’s not exploitable to redirect to malicious or phishing destinations if you’re exposing it publicly. Consider validating or allow-listing target domains.
  • Input validation: Sanitize submitted URLs to prevent injection into your storage file (e.g., a URL containing a newline character crafted to inject a fake row into your CSV database).
  • Access control: If self-hosting a CGI-based shortener, ensure only authorized users can create new short links, especially if it will be reachable from the public internet.
  • Rate limiting: Without any throttling, an exposed shortening endpoint can be abused to generate large volumes of spam short links — consider adding basic rate limiting at the web server level.

Optimization Tips

  • Switch from grep-based lookups in a flat CSV file to a proper key-value store (sqlite3, or even a simple indexed file with awk) once you have more than a few thousand mappings, since flat-file scans become linearly slower with size.
  • Cache frequently resolved codes in memory (or a fast key-value store like Redis) if your shortener sees high-traffic redirect volume.
  • Use a hash of the URL to generate consistent short codes for identical input, avoiding duplicate entries for the same destination.

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

  • Not checking for short-code collisions, leading to overwritten or ambiguous mappings.
  • Exposing an unauthenticated shortening endpoint publicly, inviting spam or abuse.
  • Storing URLs without validating their format, resulting in broken or malicious redirects.
  • Using a flat CSV file at high scale without considering the performance implications of linear scans.

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

  • Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html
  • GNU grep manual: https://www.gnu.org/software/grep/manual/grep.html
  • CGI specification overview (RFC 3875): https://www.rfc-editor.org/rfc/rfc3875
  • flock command documentation: https://man7.org/linux/man-pages/man1/flock.1.html

Total
1
Shares

Leave a Reply

Previous Post
How to Use 'getopts' for Command Line Options in Bash

How to Use ‘getopts’ for Command Line Options in Bash

Next Post
How to Generate QR Codes in Bash

How to Generate QR Codes in Bash

Related Posts