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
nginxorbusybox 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:
- Generate a short, unique identifier for a given long URL.
- Store the mapping between the short identifier and the original URL.
- 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/urandomis 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_STRINGis 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 prefixcode=, leaving just the short code value.- The script outputs raw HTTP headers manually (
Status:andLocation:), which is what CGI expects — a302 Foundresponse with aLocationheader 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 (
sqlite3CLI) once your flat-file database grows beyond a few thousand entries, sincegrep-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 withawk) 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
grepmanual: https://www.gnu.org/software/grep/manual/grep.html - CGI specification overview (RFC 3875): https://www.rfc-editor.org/rfc/rfc3875
flockcommand documentation: https://man7.org/linux/man-pages/man1/flock.1.html