Skip to content

Instantly share code, notes, and snippets.

@tysm
Last active May 22, 2026 01:18
Show Gist options
  • Select an option

  • Save tysm/92fc19fb591f7bed9e2d358c295c0b73 to your computer and use it in GitHub Desktop.

Select an option

Save tysm/92fc19fb591f7bed9e2d358c295c0b73 to your computer and use it in GitHub Desktop.
CLI tool for MicroBin pastebin - create, read, edit, delete pastes
#!/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