Last active
May 22, 2026 01:18
-
-
Save tysm/92fc19fb591f7bed9e2d358c295c0b73 to your computer and use it in GitHub Desktop.
CLI tool for MicroBin pastebin - create, read, edit, delete pastes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| PASTRY_CONFIG="${PASTRY_CONFIG:-$HOME/.pastryrc}" | |
| VERSION="1.0.0" | |
| # --- Defaults --- | |
| SERVER="${PASTRY_SERVER:-http://localhost:8080}" | |
| AUTH="" | |
| DEFAULT_EXPIRY="1week" | |
| DEFAULT_PRIVACY="public" | |
| usage() { | |
| cat <<EOF | |
| usage: pastry [options] <command> [args] | |
| Commands: | |
| create Create a paste (text, file, or stdin) | |
| get View a paste in the browser | |
| raw Get raw content of a paste | |
| delete Delete a paste | |
| edit Edit an existing paste | |
| file Download a file attachment | |
| archive Download all attachments as a ZIP | |
| info Show paste metadata | |
| list List all pastes (requires auth) | |
| config Show or set configuration | |
| help Show this help message | |
| Options: | |
| -s, --server URL MicroBin server URL (default: \$PASTRY_SERVER or ~/.pastryrc) | |
| -u, --user USER Basic auth username | |
| -p, --pass PASS Basic auth password | |
| -v, --version Show version | |
| Create options: | |
| -c, --content TEXT Paste content (omit to read from stdin) | |
| -f, --file FILE Upload a file attachment | |
| -e, --expiry TIME Expiration: 1min, 10min, 1hour, 24hour, 3days, | |
| 1week, 1month, 6months, 1year, never (default: $DEFAULT_EXPIRY) | |
| -k, --privacy LVL Privacy: public, unlisted, readonly, private, secret | |
| (default: $DEFAULT_PRIVACY) | |
| -P, --password KEY Password for private/readonly/secret pastas | |
| -S, --syntax LANG Syntax highlighting (rs, py, js, go, html, json, yaml, md, none) | |
| -b, --burn-after N Burn after reading count (1, 10, 100, 1000, 10000) | |
| Examples: | |
| pastry create -c "Hello world" | |
| pastry create -f document.pdf -e 1week | |
| pastry create -c "code" -S py -k private -P mypass | |
| echo "hello" | pastry create | |
| pastry create -f photo.jpg -k public | |
| pastry raw cat-dog-fox | |
| pastry delete cat-dog-fox -P mypass | |
| pastry edit cat-dog-fox -c "New content" | |
| pastry file cat-dog-fox | |
| pastry archive cat-dog-fox | |
| pastry list | |
| EOF | |
| exit 0 | |
| } | |
| die() { | |
| echo "error: $*" >&2 | |
| exit 1 | |
| } | |
| # --- Config file helpers --- | |
| load_config() { | |
| if [[ -f "$PASTRY_CONFIG" ]]; then | |
| while IFS='=' read -r key value; do | |
| case "$key" in | |
| server) SERVER="$value" ;; | |
| auth) AUTH="$value" ;; | |
| esac | |
| done < "$PASTRY_CONFIG" | |
| fi | |
| } | |
| save_config() { | |
| mkdir -p "$(dirname "$PASTRY_CONFIG")" | |
| { | |
| echo "server=$SERVER" | |
| [[ -n "$AUTH" ]] && echo "auth=$AUTH" | |
| } > "$PASTRY_CONFIG" | |
| echo "config saved to $PASTRY_CONFIG" | |
| } | |
| # --- Server URL validation --- | |
| require_server() { | |
| if [[ -z "$SERVER" ]]; then | |
| die "no server configured. Set PASTRY_SERVER env, use -s/--server, or run 'pastry config'" | |
| fi | |
| SERVER="${SERVER%/}" | |
| } | |
| # --- Resolve localhost to network IP --- | |
| resolve_url() { | |
| local url="$1" | |
| local ip | |
| ip=$(hostname -I 2>/dev/null | awk '{print $1}') | |
| if [[ -n "$ip" ]]; then | |
| url="${url//http:\/\/localhost:/http:\/\/$ip:}" | |
| url="${url//http:\/\/127.0.0.1:/http:\/\/$ip:}" | |
| fi | |
| echo "$url" | |
| } | |
| # --- Syntax auto-detection --- | |
| detect_syntax() { | |
| local file="$1" | |
| local ext | |
| ext=$(echo "$file" | sed 's/.*\.//') | |
| case "$ext" in | |
| rs|rust) echo "rs" ;; | |
| py|python) echo "py" ;; | |
| js|javascript) echo "js" ;; | |
| ts|typescript) echo "ts" ;; | |
| go) echo "go" ;; | |
| rb|ruby) echo "rb" ;; | |
| java) echo "java" ;; | |
| c|cpp|h|hpp) echo "c" ;; | |
| cs|csharp) echo "cs" ;; | |
| sh|bash|zsh) echo "sh" ;; | |
| html|htm) echo "html" ;; | |
| css) echo "css" ;; | |
| json) echo "json" ;; | |
| yaml|yml) echo "yaml" ;; | |
| sql) echo "sql" ;; | |
| xml) echo "xml" ;; | |
| toml) echo "toml" ;; | |
| ini|cfg|conf) echo "ini" ;; | |
| dockerfile|Dockerfile) echo "dockerfile" ;; | |
| lua) echo "lua" ;; | |
| php) echo "php" ;; | |
| r) echo "r" ;; | |
| swift) echo "swift" ;; | |
| kt|kotlin) echo "kt" ;; | |
| scala) echo "scala" ;; | |
| dart) echo "dart" ;; | |
| hs|haskell) echo "hs" ;; | |
| pl|perl) echo "pl" ;; | |
| zig) echo "zig" ;; | |
| *) echo "" ;; | |
| esac | |
| } | |
| # --- Auth header --- | |
| auth_header() { | |
| if [[ -n "$AUTH" ]]; then | |
| echo "-u $AUTH" | |
| fi | |
| } | |
| # --- Parse response to extract slug/ID --- | |
| extract_slug() { | |
| local location | |
| location=$(tr -d '\r' | grep -i '^location:' | sed 's/^[Ll]ocation: //' | tr -d '[:space:]') | |
| if [[ -z "$location" ]]; then | |
| die "upload failed (no redirect)" | |
| fi | |
| local slug | |
| slug=$(echo "$location" | sed -E 's,.*/(upload|auth)/([^/?]+).*,\2,') | |
| echo "$slug" | |
| } | |
| # ============================================================ | |
| # COMMAND: create | |
| # ============================================================ | |
| cmd_create() { | |
| require_server | |
| local content="" file="" expiry="$DEFAULT_EXPIRY" privacy="$DEFAULT_PRIVACY" | |
| local password="" syntax="" burn_after="" stdin_mode=false explicit_syntax=false | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -c|--content) content="$2"; shift 2 ;; | |
| -f|--file) file="$2"; shift 2 ;; | |
| -e|--expiry) expiry="$2"; shift 2 ;; | |
| -k|--privacy) privacy="$2"; shift 2 ;; | |
| -P|--password) password="$2"; shift 2 ;; | |
| -S|--syntax) syntax="$2"; explicit_syntax=true; shift 2 ;; | |
| -b|--burn-after) burn_after="$2"; shift 2 ;; | |
| --stdin) stdin_mode=true; shift ;; | |
| --) shift; break ;; | |
| -*) die "unknown option: $1" ;; | |
| *) break ;; | |
| esac | |
| done | |
| # Read from stdin if no -c content provided and input is piped | |
| if [[ -z "$content" && ! -t 0 ]]; then | |
| stdin_mode=true | |
| content=$(cat) | |
| fi | |
| # If still no content or file, error | |
| if [[ -z "$content" && -z "$file" ]]; then | |
| die "provide content (-c), a file (-f), or pipe input" | |
| fi | |
| # Auto-detect syntax if not explicitly set | |
| if [[ "$explicit_syntax" == false ]]; then | |
| if [[ -n "$file" ]]; then | |
| local detected | |
| detected=$(detect_syntax "$file") | |
| [[ -n "$detected" ]] && syntax="$detected" | |
| elif [[ -n "$content" ]]; then | |
| local first_line | |
| first_line=$(echo "$content" | head -1) | |
| case "$first_line" in | |
| '#!/usr/bin/env python'*|'#!/usr/bin/python'*) syntax="py" ;; | |
| '#!/usr/bin/env bash'*|'#!/bin/bash'*|'#!/bin/sh'*) syntax="sh" ;; | |
| '#!/usr/bin/env node'*|'#!/usr/bin/node'*) syntax="js" ;; | |
| '#!/usr/bin/env ruby'*|'#!/usr/bin/ruby'*) syntax="rb" ;; | |
| '#!/usr/bin/env perl'*|'#!/usr/bin/perl'*) syntax="pl" ;; | |
| '#!/usr/bin/env lua'*|'#!/usr/bin/lua'*) syntax="lua" ;; | |
| '#!/usr/bin/env php'*|'#!/usr/bin/php'*) syntax="php" ;; | |
| esac | |
| fi | |
| fi | |
| local curl_args=(-s -D - -o /dev/null) | |
| eval "curl_args+=($(auth_header))" | |
| local form_args=() | |
| local tmpfile="" | |
| if [[ -n "$content" ]]; then | |
| tmpfile=$(mktemp) | |
| printf '%s' "$content" > "$tmpfile" | |
| form_args+=(-F "content=@$tmpfile") | |
| fi | |
| if [[ -n "$file" ]]; then | |
| form_args+=(-F "file=@$file") | |
| fi | |
| form_args+=(-F "expiration=$expiry") | |
| form_args+=(-F "privacy=$privacy") | |
| [[ -n "$password" ]] && form_args+=(-F "plain_key=$password") | |
| [[ -n "$syntax" ]] && form_args+=(-F "syntax_highlight=$syntax") || form_args+=(-F "syntax_highlight=auto") | |
| [[ -n "$burn_after" ]] && form_args+=(-F "burn_after=$burn_after") | |
| local slug | |
| slug=$(curl "${curl_args[@]}" -X POST "$SERVER/upload" "${form_args[@]}" | extract_slug) | |
| [[ -n "$tmpfile" ]] && rm -f "$tmpfile" | |
| resolve_url "$SERVER/upload/$slug" | |
| } | |
| # ============================================================ | |
| # COMMAND: get | |
| # ============================================================ | |
| cmd_get() { | |
| require_server | |
| local id="$1" | |
| [[ -z "$id" ]] && die "usage: pastry get <id>" | |
| local url | |
| url=$(resolve_url "$SERVER/upload/$id") | |
| echo "opening: $url" | |
| if command -v xdg-open &>/dev/null; then | |
| xdg-open "$url" | |
| elif command -v open &>/dev/null; then | |
| open "$url" | |
| else | |
| echo "$url" | |
| fi | |
| } | |
| # ============================================================ | |
| # COMMAND: raw | |
| # ============================================================ | |
| cmd_raw() { | |
| require_server | |
| local id="$1" password="" | |
| [[ -z "$id" ]] && die "usage: pastry raw <id> [-P password]" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -P|--password) password="$2"; shift 2 ;; | |
| *) die "unknown option: $1" ;; | |
| esac | |
| done | |
| local curl_args=(-s) | |
| eval "curl_args+=($(auth_header))" | |
| if [[ -n "$password" ]]; then | |
| curl "${curl_args[@]}" -X POST "$SERVER/raw/$id" -F "password=$password" | |
| else | |
| curl "${curl_args[@]}" "$SERVER/raw/$id" | |
| fi | |
| } | |
| # ============================================================ | |
| # COMMAND: delete | |
| # ============================================================ | |
| cmd_delete() { | |
| require_server | |
| local id="$1" password="" | |
| [[ -z "$id" ]] && die "usage: pastry delete <id> [-P password]" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -P|--password) password="$2"; shift 2 ;; | |
| *) die "unknown option: $1" ;; | |
| esac | |
| done | |
| local curl_args=(-s -D - -o /dev/null) | |
| eval "curl_args+=($(auth_header))" | |
| local resp | |
| if [[ -n "$password" ]]; then | |
| resp=$(curl "${curl_args[@]}" -X POST "$SERVER/remove/$id" -F "password=$password") | |
| else | |
| resp=$(curl "${curl_args[@]}" "$SERVER/remove/$id") | |
| fi | |
| if echo "$resp" | grep -qi 'location:.*/list'; then | |
| echo "deleted: $id" | |
| elif echo "$resp" | grep -qi '404\|not found'; then | |
| die "paste not found: $id" | |
| else | |
| die "delete failed (wrong password or unauthorized)" | |
| fi | |
| } | |
| # ============================================================ | |
| # COMMAND: edit | |
| # ============================================================ | |
| cmd_edit() { | |
| require_server | |
| local id="$1" content="" password="" | |
| [[ -z "$id" ]] && die "usage: pastry edit <id> -c 'new content' [-P password]" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -c|--content) content="$2"; shift 2 ;; | |
| -P|--password) password="$2"; shift 2 ;; | |
| *) die "unknown option: $1" ;; | |
| esac | |
| done | |
| [[ -z "$content" ]] && die "provide new content with -c" | |
| local curl_args=(-s -D - -o /dev/null) | |
| eval "curl_args+=($(auth_header))" | |
| local form_args=(-F "content=$content") | |
| [[ -n "$password" ]] && form_args+=(-F "password=$password") | |
| local resp | |
| resp=$(curl "${curl_args[@]}" -X POST "$SERVER/edit/$id" "${form_args[@]}") | |
| if echo "$resp" | grep -qi "location:.*/upload/$id"; then | |
| echo "edited: $(resolve_url "$SERVER/upload/$id")" | |
| elif echo "$resp" | grep -qi '404\|not found'; then | |
| die "paste not found: $id" | |
| else | |
| die "edit failed (wrong password?)" | |
| fi | |
| } | |
| # ============================================================ | |
| # COMMAND: file | |
| # ============================================================ | |
| cmd_file() { | |
| require_server | |
| local id="$1" fname="" preview=false password="" | |
| [[ -z "$id" ]] && die "usage: pastry file <id> [-f filename] [-P password] [-p]" | |
| shift | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -f|--fname) fname="$2"; shift 2 ;; | |
| -P|--password) password="$2"; shift 2 ;; | |
| -p|--preview) preview=true; shift ;; | |
| *) die "unknown option: $1" ;; | |
| esac | |
| done | |
| local curl_args=(-s -o "$id-file") | |
| eval "curl_args+=($(auth_header))" | |
| if [[ -n "$password" ]]; then | |
| local form_args=(-F "password=$password") | |
| [[ -n "$fname" ]] && form_args+=(-F "fname=$fname") | |
| curl "${curl_args[@]}" -X POST "$SERVER/secure_file/$id" "${form_args[@]}" | |
| else | |
| local query="" | |
| [[ -n "$fname" ]] && query="?fname=$fname" | |
| [[ "$preview" == true ]] && query="${query:+$query&}preview=true" | |
| curl "${curl_args[@]}" "$SERVER/file/$id$query" | |
| fi | |
| echo "downloaded: $id-file" | |
| } | |
| # ============================================================ | |
| # COMMAND: archive | |
| # ============================================================ | |
| cmd_archive() { | |
| require_server | |
| local id="$1" | |
| [[ -z "$id" ]] && die "usage: pastry archive <id>" | |
| curl -s -o "$id-archive.zip" "$SERVER/archive/$id" | |
| echo "downloaded: $id-archive.zip" | |
| } | |
| # ============================================================ | |
| # COMMAND: info | |
| # ============================================================ | |
| cmd_info() { | |
| require_server | |
| local id="$1" | |
| [[ -z "$id" ]] && die "usage: pastry info <id>" | |
| # Fetch the HTML page and extract metadata | |
| local html | |
| html=$(curl -s "$SERVER/upload/$id") | |
| if [[ -z "$html" ]] || echo "$html" | grep -qi '404\|Not Found'; then | |
| die "paste not found: $id" | |
| fi | |
| echo "ID: $id" | |
| echo "URL: $(resolve_url "$SERVER/upload/$id")" | |
| echo "Raw: $(resolve_url "$SERVER/raw/$id")" | |
| # Try to extract read count from the page | |
| local reads | |
| reads=$(echo "$html" | grep -oP 'Read \K\d+ times' 2>/dev/null || echo "?") | |
| echo "Reads: $reads" | |
| } | |
| # ============================================================ | |
| # COMMAND: list | |
| # ============================================================ | |
| cmd_list() { | |
| require_server | |
| local curl_args=(-s -o /dev/null -w '%{http_code}') | |
| eval "curl_args+=($(auth_header))" | |
| local status | |
| status=$(curl "${curl_args[@]}" "$SERVER/list" || true) | |
| if [[ "$status" == "401" ]]; then | |
| die "authentication required (use -u and -p or configure auth)" | |
| fi | |
| eval "curl_args=($(auth_header))" | |
| curl "${curl_args[@]}" -s "$SERVER/list" | sed -n '/<table/,/<\/table>/p' 2>/dev/null || \ | |
| curl "${curl_args[@]}" -s "$SERVER/list" | |
| } | |
| # ============================================================ | |
| # COMMAND: config | |
| # ============================================================ | |
| cmd_config() { | |
| load_config | |
| if [[ $# -eq 0 ]]; then | |
| echo "server: ${SERVER:-<not set>}" | |
| echo "auth: ${AUTH:-<not set>}" | |
| return | |
| fi | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| server) | |
| shift | |
| SERVER="${1%/}" | |
| save_config | |
| return | |
| ;; | |
| auth) | |
| shift | |
| AUTH="$1" | |
| save_config | |
| return | |
| ;; | |
| --clear) | |
| rm -f "$PASTRY_CONFIG" | |
| SERVER="" | |
| AUTH="" | |
| echo "config cleared" | |
| return | |
| ;; | |
| *) | |
| die "usage: pastry config [server <url>|auth <user:pass>|--clear]" | |
| ;; | |
| esac | |
| done | |
| } | |
| # ============================================================ | |
| # MAIN | |
| # ============================================================ | |
| load_config | |
| # Parse global options | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -s|--server) SERVER="${2%/}"; shift 2 ;; | |
| -u|--user) AUTH="${2}:${AUTH#*:}"; shift 2 ;; | |
| -p|--pass) AUTH="${AUTH%:*}:${2}"; shift 2 ;; | |
| -v|--version) echo "pastry v$VERSION"; exit 0 ;; | |
| -h|--help) usage ;; | |
| --) shift; break ;; | |
| *) break ;; | |
| esac | |
| done | |
| if [[ $# -eq 0 ]]; then | |
| if [[ ! -t 0 ]]; then | |
| cmd_create "" | |
| exit $? | |
| fi | |
| usage | |
| fi | |
| cmd="$1"; shift | |
| case "$cmd" in | |
| create) cmd_create "$@" ;; | |
| get) cmd_get "$@" ;; | |
| raw) cmd_raw "$@" ;; | |
| delete) cmd_delete "$@" ;; | |
| edit) cmd_edit "$@" ;; | |
| file) cmd_file "$@" ;; | |
| archive) cmd_archive "$@" ;; | |
| info) cmd_info "$@" ;; | |
| list) cmd_list "$@" ;; | |
| config) cmd_config "$@" ;; | |
| help) usage ;; | |
| *) cmd_create "$cmd" "$@" ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment