#!/usr/bin/env bash # zpst — CLI client for PastePHP # Usage: zpst [options] # # Commands: # create [options] Create a paste # view Print paste content to stdout # download Save paste content to a file # delete [token] Delete a paste # list List your pastes (requires API key) # info Show paste metadata # config Show/edit current config # # Options for create: # -t, --title Paste title # -l, --lang <language> Language (e.g. python, bash, json) # -e, --expiry <seconds> Expiry: 0 never, 300, 3600, 21600, 86400, 2592000 # -v, --visibility <vis> public (default), unlisted, private # -p, --password <password> Password protect the paste # -b, --burn Burn after read # -k, --key <api_key> API key (overrides config) set -euo pipefail # ── Config ──────────────────────────────────── CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/pastephp" CONFIG_FILE="$CONFIG_DIR/config" SITE_URL="https://zpst.net" # replaced at deploy time # Load config file if it exists [[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE" API_URL="${PASTEPHP_URL:-$SITE_URL}/api.php" API_KEY="${PASTEPHP_KEY:-}" # ── Colours ─────────────────────────────────── if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' else RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; RESET='' fi # ── Helpers ─────────────────────────────────── die() { echo -e "${RED}Error:${RESET} $*" >&2; exit 1; } info() { echo -e "${CYAN}→${RESET} $*" >&2; } ok() { echo -e "${GREEN}✓${RESET} $*" >&2; } require_cmd() { command -v "$1" &>/dev/null || die "'$1' is required but not installed." } require_cmd curl api_call() { local method="$1"; shift local url="$1"; shift local args=("$@") local key_header=() [[ -n "$API_KEY" ]] && key_header=(-H "X-API-Key: $API_KEY") curl -sS --fail-with-body \ -X "$method" \ "${key_header[@]}" \ "${args[@]}" \ "$url" } check_ok() { local json="$1" local ok_val if command -v python3 &>/dev/null; then ok_val=$(echo "$json" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('ok',''))") else ok_val=$(echo "$json" | grep -o '"ok":[^,}]*' | cut -d: -f2 | tr -d ' "') fi [[ "$ok_val" == "True" || "$ok_val" == "true" ]] } extract() { # extract <json> <key> — simple single-value extractor local json="$1" key="$2" if command -v python3 &>/dev/null; then echo "$json" | python3 -c " import sys, json d = json.load(sys.stdin) v = d.get('$key', '') if isinstance(v, bool): print(str(v).lower()) elif v is None: print('') else: print(v) " else # Fallback: naive grep (works for simple string/number values) echo "$json" | grep -o "\"$key\":[^,}]*" | head -1 | sed 's/.*://;s/[" ]//g;s/\\n/\n/g' fi } extract_content() { # Special handler — content may contain newlines local json="$1" if command -v python3 &>/dev/null; then echo "$json" | python3 -c " import sys, json d = json.load(sys.stdin) sys.stdout.write(d.get('content', '')) " else # Fallback: strip JSON wrapper and unescape (basic) echo "$json" | sed 's/.*"content":"\(.*\)","language.*/\1/' | python3 -c "import sys; print(sys.stdin.read())" 2>/dev/null || \ die "python3 is required to decode paste content." fi } usage() { cat <<EOF ${BOLD}zpst${RESET} — PastePHP CLI client ${BOLD}USAGE${RESET} zpst create <text> Create paste from text zpst create <file.txt> Create paste from file zpst view <slug> Print paste to stdout zpst download <slug> <out> Save paste to file zpst delete <slug> [token] Delete a paste zpst list List your pastes zpst info <slug> Show paste metadata zpst config Show current config ${BOLD}OPTIONS (create)${RESET} -t, --title <title> Title -l, --lang <lang> Language (python, bash, json, etc.) -e, --expiry <sec> 0=never 300=5m 3600=1h 21600=6h 86400=24h 2592000=1mo -v, --visibility <vis> public|unlisted|private -p, --password <pw> Password protect -b, --burn Burn after read -k, --key <api_key> Use this API key ${BOLD}ENVIRONMENT${RESET} PASTEPHP_URL Override site URL PASTEPHP_KEY API key ${BOLD}CONFIG FILE${RESET} $CONFIG_FILE Contains lines like: PASTEPHP_URL=https://example.com PASTEPHP_KEY=your_api_key_here ${BOLD}EXAMPLES${RESET} zpst create "hello world" zpst create main.py --lang python --title "My Script" zpst create notes.txt --expiry 3600 --visibility unlisted zpst view aB3xKq zpst download aB3xKq output.py zpst delete aB3xKq zpst list EOF exit 0 } # ── Config command ──────────────────────────── cmd_config() { echo -e "${BOLD}Config file:${RESET} $CONFIG_FILE" echo -e "${BOLD}API URL:${RESET} $API_URL" echo -e "${BOLD}API Key:${RESET} ${API_KEY:-(not set)}" echo "" echo "To set your API key:" echo " mkdir -p $CONFIG_DIR" echo " echo 'PASTEPHP_KEY=your_key_here' >> $CONFIG_FILE" echo " echo 'PASTEPHP_URL=https://yoursite.com' >> $CONFIG_FILE" } # ── Create command ──────────────────────────── cmd_create() { local content="" title="" language="plaintext" expiry=0 local visibility="public" password="" burn=0 from_file=0 filename="" # First positional arg = text or filename local first="${1:-}"; shift || true if [[ -z "$first" ]]; then # No arg — read from stdin info "Reading from stdin (Ctrl+D to finish)..." content=$(cat) elif [[ -f "$first" ]]; then content=$(cat "$first") filename=$(basename "$first") title="$filename" from_file=1 # Auto-detect language from extension local ext="${filename##*.}" language=$(ext_to_lang "$ext") else content="$first" fi # Parse remaining options while [[ $# -gt 0 ]]; do case "$1" in -t|--title) title="$2"; shift 2 ;; -l|--lang) language="$2"; shift 2 ;; -e|--expiry) expiry="$2"; shift 2 ;; -v|--visibility) visibility="$2"; shift 2 ;; -p|--password) password="$2"; shift 2 ;; -b|--burn) burn=1; shift ;; -k|--key) API_KEY="$2"; shift 2 ;; *) die "Unknown option: $1" ;; esac done [[ -z "$content" ]] && die "No content provided." # Build JSON payload local payload payload=$(python3 -c " import json, sys d = { 'content': sys.argv[1], 'title': sys.argv[2], 'language': sys.argv[3], 'expiry': int(sys.argv[4]), 'visibility': sys.argv[5], 'password': sys.argv[6], 'burn': sys.argv[7] == '1', } print(json.dumps(d)) " "$content" "$title" "$language" "$expiry" "$visibility" "$password" "$burn") \ || die "python3 is required to build JSON payload." local resp resp=$(api_call POST "${API_URL}?action=create" \ -H "Content-Type: application/json" \ -d "$payload") || die "Request failed." if ! check_ok "$resp"; then local err; err=$(extract "$resp" "error") die "$err" fi local url slug delete_token url=$(extract "$resp" "url") slug=$(extract "$resp" "slug") delete_token=$(extract "$resp" "delete_token") ok "Paste created!" echo -e "${BOLD}URL:${RESET} $url" echo -e "${BOLD}Slug:${RESET} $slug" [[ -n "$delete_token" ]] && echo -e "${YELLOW}${BOLD}Delete token:${RESET} $delete_token (save this!)" # If stdout is a pipe/redirect, just emit the URL cleanly [[ ! -t 1 ]] && echo "$url" } # ── View command ────────────────────────────── cmd_view() { local slug="${1:-}"; [[ -z "$slug" ]] && die "Usage: zpst view <slug>" local password="${2:-}" local pw_param="" [[ -n "$password" ]] && pw_param="&password=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$password")" local resp resp=$(api_call GET "${API_URL}?action=raw&slug=${slug}${pw_param}") || die "Request failed." echo "$resp" } # ── Download command ────────────────────────── cmd_download() { local slug="${1:-}" outfile="${2:-}" [[ -z "$slug" || -z "$outfile" ]] && die "Usage: zpst download <slug> <outfile>" local password="${3:-}" local pw_param="" [[ -n "$password" ]] && pw_param="&password=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))" "$password")" local resp resp=$(api_call GET "${API_URL}?action=raw&slug=${slug}${pw_param}") || die "Request failed." printf '%s' "$resp" > "$outfile" ok "Saved to $outfile" } # ── Delete command ──────────────────────────── cmd_delete() { local slug="${1:-}"; [[ -z "$slug" ]] && die "Usage: zpst delete <slug> [token]" local token="${2:-}" local token_param="" [[ -n "$token" ]] && token_param="&token=$token" local resp resp=$(api_call GET "${API_URL}?action=delete&slug=${slug}${token_param}") || die "Request failed." if ! check_ok "$resp"; then local err; err=$(extract "$resp" "error") die "$err" fi ok "Paste $slug deleted." } # ── List command ────────────────────────────── cmd_list() { [[ -z "$API_KEY" ]] && die "API key required for list. Set PASTEPHP_KEY or use -k." local limit="${1:-25}" local resp resp=$(api_call GET "${API_URL}?action=list&limit=${limit}") || die "Request failed." if ! check_ok "$resp"; then local err; err=$(extract "$resp" "error") die "$err" fi python3 -c " import json, sys d = json.load(sys.stdin) rows = d.get('pastes', []) if not rows: print('No pastes found.') sys.exit(0) print(f\"{'SLUG':<12} {'TITLE':<32} {'LANG':<14} {'VIS':<10} {'VIEWS':<6} {'CREATED'}\") print('-' * 90) for p in rows: slug = p.get('slug','')[:12] title = (p.get('title') or 'Untitled')[:32] lang = p.get('language','')[:14] vis = p.get('visibility','')[:10] views = str(p.get('views',''))[:6] created = p.get('created_at','')[:16] print(f'{slug:<12} {title:<32} {lang:<14} {vis:<10} {views:<6} {created}') print() print(f\"Total: {d.get('total', len(rows))}\") " <<< "$resp" } # ── Info command ────────────────────────────── cmd_info() { local slug="${1:-}"; [[ -z "$slug" ]] && die "Usage: zpst info <slug>" local resp resp=$(api_call GET "${API_URL}?action=get&slug=${slug}") || die "Request failed." if ! check_ok "$resp"; then local err; err=$(extract "$resp" "error") die "$err" fi python3 -c " import json, sys d = json.load(sys.stdin) fields = [ ('Slug', 'slug'), ('Title', 'title'), ('Language', 'language'), ('Visibility', 'visibility'), ('Author', 'author'), ('Views', 'views'), ('Burn', 'burn'), ('Expires', 'expires_at'), ('Created', 'created_at'), ('URL', 'url'), ('Raw URL', 'raw_url'), ('Image', 'image_url'), ] for label, key in fields: val = d.get(key) if val not in (None, '', False): print(f' {label+\":\":<14} {val}') " <<< "$resp" } # ── Extension → language map ────────────────── ext_to_lang() { local ext="${1,,}" declare -A map=( [js]=javascript [ts]=typescript [jsx]=javascript [tsx]=typescript [py]=python [rb]=ruby [php]=php [java]=java [c]=c [h]=c [cpp]=cpp [cs]=csharp [go]=go [rs]=rust [swift]=swift [kt]=kotlin [sh]=bash [bash]=bash [zsh]=bash [ps1]=powershell [sql]=sql [html]=html [htm]=html [xml]=xml [css]=css [scss]=scss [json]=json [yaml]=yaml [yml]=yaml [toml]=toml [ini]=ini [md]=markdown [diff]=diff [lua]=lua [r]=r [scala]=scala [pl]=perl [graphql]=graphql [http]=http ) echo "${map[$ext]:-plaintext}" } # ── Main dispatch ───────────────────────────── [[ $# -eq 0 ]] && usage cmd="${1}"; shift || true case "$cmd" in create) cmd_create "$@" ;; view) cmd_view "$@" ;; download) cmd_download "$@" ;; delete) cmd_delete "$@" ;; list) cmd_list "$@" ;; info) cmd_info "$@" ;; config) cmd_config ;; help|--help|-h) usage ;; *) die "Unknown command: $cmd. Run 'zpst help' for usage." ;; esac