|
#!/usr/bin/env bash |
|
# dev.sh — start a local PHP dev server (Linux/macOS/Windows Git Bash/WSL) |
|
# Usage: |
|
# ./dev.sh [-p port] [-h host] [-d docroot] [-r router.php] [-o] [--free-port] |
|
# Examples: |
|
# ./dev.sh |
|
# ./dev.sh -p 8080 -d public -o |
|
# ./dev.sh --free-port -r router.php |
|
|
|
set -euo pipefail |
|
|
|
cd "$(dirname "$0")" |
|
|
|
DOCROOT="v1.0" # Specific to this project |
|
HOST="127.0.0.1" # safer than "localhost" on Windows |
|
PORT="8000" |
|
AUTO_OPEN="false" |
|
FIND_FREE="false" |
|
ROUTER="" |
|
|
|
print_help() { |
|
cat <<EOF |
|
Usage: $(basename "$0") [options] |
|
Options: |
|
-p, --port <n> Port to listen on (default: ${PORT}) |
|
-h, --host <host> Host interface (default: ${HOST}) |
|
-d, --docroot <dir> Document root (default: ${DOCROOT}) |
|
-r, --router <file> Optional router script (e.g., router.php) |
|
-o, --open Open the URL in your browser after start |
|
-f, --free-port If PORT is busy, increment until a free port is found |
|
-?, --help Show this help |
|
Examples: |
|
$(basename "$0") -p 8080 -d public -o |
|
$(basename "$0") --free-port -r router.php |
|
EOF |
|
} |
|
|
|
# --- parse args --- |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-p|--port) PORT="${2:-}"; shift 2 ;; |
|
-h|--host) HOST="${2:-}"; shift 2 ;; |
|
-d|--docroot) DOCROOT="${2:-}"; shift 2 ;; |
|
-r|--router) ROUTER="${2:-}"; shift 2 ;; |
|
-o|--open) AUTO_OPEN="true"; shift ;; |
|
-f|--free-port) FIND_FREE="true"; shift ;; |
|
-\?|--help) print_help; exit 0 ;; |
|
*) echo "Unknown option: $1"; print_help; exit 64 ;; |
|
esac |
|
done |
|
|
|
# --- php detection --- |
|
if ! command -v php >/dev/null 2>&1; then |
|
echo "❌ PHP not found in PATH." |
|
exit 127 |
|
fi |
|
PHP_BIN="$(command -v php)" |
|
|
|
# --- sanity checks --- |
|
if [[ ! -d "$DOCROOT" ]]; then |
|
echo "❌ Docroot '$DOCROOT' not found." |
|
exit 66 |
|
fi |
|
if [[ -n "$ROUTER" && ! -f "$ROUTER" ]]; then |
|
echo "❌ Router file '$ROUTER' not found." |
|
exit 66 |
|
fi |
|
# Port validation |
|
if ! [[ "$PORT" =~ ^[0-9]+$ ]] || (( PORT < 1 || PORT > 65535 )); then |
|
echo "❌ Invalid port: $PORT (must be 1–65535)." |
|
exit 65 |
|
fi |
|
|
|
# --- port check helpers (cross-platform best-effort) --- |
|
is_port_in_use() { |
|
local port="$1" |
|
|
|
# lsof |
|
if command -v lsof >/dev/null 2>&1; then |
|
lsof -i TCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 && return 0 || return 1 |
|
fi |
|
|
|
# ss (use process substitution to avoid subshell return bug) |
|
if command -v ss >/dev/null 2>&1; then |
|
while read -r line; do |
|
case "$line" in |
|
*":$port "*|*":$port"*) return 0 ;; |
|
esac |
|
done < <(ss -ltn 2>/dev/null) |
|
return 1 |
|
fi |
|
|
|
# netstat (Windows/mac/Linux; no grep) |
|
if command -v netstat >/dev/null 2>&1; then |
|
while IFS= read -r line; do |
|
case "$line" in |
|
*":$port "*|*":$port"*) |
|
case "$line" in |
|
*LISTEN*|*LISTENING*) return 0 ;; |
|
esac |
|
;; |
|
esac |
|
done < <(netstat -an 2>/dev/null) |
|
return 1 |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
show_port_owner() { |
|
local port="$1" |
|
# mac/Linux with lsof |
|
if command -v lsof >/dev/null 2>&1; then |
|
lsof -nP -iTCP:"$port" -sTCP:LISTEN 2>/dev/null | awk 'NR==2{print "🔒 In use by:", $1, "PID", $2}' || true |
|
return |
|
fi |
|
# Windows netstat with PIDs |
|
if command -v netstat >/dev/null 2>&1; then |
|
local line pid |
|
while IFS= read -r line; do |
|
case "$line" in |
|
*":$port "*|*":$port"*) |
|
case "$line" in |
|
*LISTEN*|*LISTENING*) |
|
# Last column is PID on Windows; try to print it |
|
pid="$(echo "$line" | awk '{print $NF}' 2>/dev/null || true)" |
|
[[ -n "$pid" ]] && echo "🔒 In use by PID $pid" |
|
return |
|
;; |
|
esac |
|
;; |
|
esac |
|
done < <(netstat -ano 2>/dev/null) |
|
fi |
|
} |
|
|
|
if [[ "$FIND_FREE" == "true" ]]; then |
|
original="$PORT" |
|
attempt=0 |
|
while is_port_in_use "$PORT"; do |
|
attempt=$((attempt+1)) |
|
PORT=$((PORT+1)) |
|
if (( attempt > 50 )); then |
|
echo "❌ Could not find a free port near $original (tried 50)." |
|
exit 69 |
|
fi |
|
done |
|
else |
|
if is_port_in_use "$PORT"; then |
|
echo "❌ Port $PORT is already in use. Use --free-port or pick another with -p." |
|
show_port_owner "$PORT" || true |
|
exit 98 |
|
fi |
|
fi |
|
|
|
URL="http://${HOST}:${PORT}" |
|
|
|
echo "🚀 Starting local PHP dev server" |
|
echo "➡️ ${URL}" |
|
echo "📂 Serving: ${DOCROOT}/" |
|
[[ -n "$ROUTER" ]] && echo "🧭 Router: ${ROUTER}" |
|
echo "⏹ Press Ctrl+C to stop." |
|
|
|
# Export for router scripts that read DOCROOT |
|
export DOCROOT |
|
|
|
# --- auto-open browser (best-effort) --- |
|
if [[ "$AUTO_OPEN" == "true" ]]; then |
|
if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" >/dev/null 2>&1 || true |
|
elif command -v open >/dev/null 2>&1; then open "$URL" >/dev/null 2>&1 || true |
|
elif command -v powershell.exe >/dev/null 2>&1; then powershell.exe -NoProfile -Command "Start-Process '$URL'" >/dev/null 2>&1 || true |
|
elif command -v cmd.exe >/dev/null 2>&1; then cmd.exe /c start "" "$URL" >/dev/null 2>&1 || true |
|
fi |
|
fi |
|
|
|
# --- run server (foreground) --- |
|
if [[ -n "$ROUTER" ]]; then |
|
exec "$PHP_BIN" -S "${HOST}:${PORT}" -t "$DOCROOT" "$ROUTER" |
|
else |
|
exec "$PHP_BIN" -S "${HOST}:${PORT}" -t "$DOCROOT" |
|
fi |