(2025-07-28) Two popular external commands can make you a shell REST API king ----------------------------------------------------------------------------- There exists a widespread myth that you need at least Python or some other full-featured programming environment to interact with modern REST APIs of various online services. In fact though, if you are running a normal OS like Linux or BSD, you only need two external commands available in any distribution's package manager. Everything else is already there in the POSIX environment, and I'm going to show you how to make use of it. The first of these two commands is, of course, curl. This is the Swiss army knife of clients that does pretty much all the legwork for you when it comes to connection through all sorts of different protocols (up to the newest HTTP versions, WebSockets (need to research that more though) and even Gopher, yeah), all you need is specify what to send and (optionally) which parts of the response to return. The only things curl can't do (_yet_) are torrenting and parallel chunk downloads (well, you've got aria2 for those things) but those are not relevant to our API story anyway. The mainstream has adopted curl so much that all sorts of API manuals even show its command-line calls alongside Python, JS and other languages in their example sections. That's quite a success story, I must admit. Sending requests and receiving responses, however, is just one half of the story. The other one is preparing those requests and processing those responses. Of course, the GET request fields, as well as standard HTTP form fields, can be handled with curl itself, but how do we deal with the other data types? Given that the de facto standard of POSTing and responses for modern HTTP APIs is JSON, only one command-line program comes to the rescue, one that's much younger than curl but no less important: jq. Yep, that's the second tool in our today's box. Again, there is a common misconception that jq only can parse JSON text, but in fact, it also can serialize various data into JSON too. It even has a small sublanguage to perform variable subsitution in the required places while it fully handles all the escaping for you. And I'm gonna show how we can make use of it... right now. So, how to showcase the possibilities of this sh+curl+jq wombo-combo in the most succint way possible? For once, let's ride the hype wave and create the simplest possible chat client for OpenAI-compatible API endpoints. Because yeah, there are plenty of them, even Ollama exposes one if you must. For this example, I'm gonna hardcode everything and assume a lot has been set already, but will include the link to the full chat script ([1]) in the footer. Without further ado, here's the example in POSIX shell: #!/bin/sh hist='' inithist='' API_ROOT='https://text.pollinations.ai/openai' MODEL='openai-fast' TEMP=0.6 prompt() { printf "\e[1;32m>> \e[0m"; } clearhist() { hist="$inithist"; } startswith() { case $1 in "$2"*) true;; *) false;; esac; } setsys() { inithist="$(jq -nc --arg sp "$*" '[{role:"system",content:$sp}]')" clearhist } setsys 'You are a helpful assistant.' while prompt && read -r line; do [ -z "$line" ] && continue [ "$line" == "/clear" ] && clearhist && echo "Session cleared" && continue hist="$(jq -nc --argjson h "$hist" --arg msg "$line" \ '$h+[{"role":"user","content":$msg}]')" data="$(jq -nc --arg m "$MODEL" --arg t "$TEMP" --argjson msgs "$hist" \ '{model:$m,stream:true,private:true,messages:$msgs,temperature:$t}')" curl -sSLN "${API_ROOT}/chat/completions" \ -H "Content-Type: application/json" \ ${OPENAI_API_KEY:+-H "Authorization: Bearer $OPENAI_API_KEY"} \ -H "Accept: text/event-stream" --data-raw "$data" 2>/dev/null | \ while IFS= read -r chunk; do [ -z "$chunk" ] && continue data="${chunk#data: }" [ "$data" == "[DONE]" ] && break delta='' startswith "$data" '{' && delta="$(printf '%s' "$data" \ | jq -rj '.choices[0].delta.content // empty' 2>/dev/null;printf x)" delta="${delta%?}" [ -n "$delta" ] && printf "\e[0;33m%s\e[0m" "$delta" tailbuf="$tailbuf$delta" done echo hist="$(jq -nc --argjson h "$hist" --arg a "$tailbuf" \ '$h+[{"role":"assistant","content":$a}]')" tailbuf="" line='' done This example chat script uses the "openai-fast" (GPT 4.1 Nano) model from the pollinations.ai provider (more famously known for its free image generation service). Of course, the full shee.sh script has a lot more going on but this example also is pretty self-sufficient and shows everything that needs to be shown: - terminal escape sequences; - pure POSIX startswith function implementation; - shaping complex JSON objects with jq -nc; - parsing complex JSON objects with jq -rj; - specifying optional command parameters based on environment variable presence; - preserving trailing newlines by appending another character and then chopping it off; - passing SSE response stream from curl for line-by-line shell processing. Mind you, this is a pretty complicated case, most APIs won't even have a thing or two shown here. For instance, not everyone uses SSE, let alone SSE with JSON response chunks, not everyone needs to maintain JSON state across calls, not everyone needs to make authorization tokens optional and dependent upon the environment. But this example combines all those techniques for you to make use of them whenever you find them necessary. And, at the end of the day, this is just an LLM chat app written in POSIX shell with no external dependencies except curl and jq. Why? Because we can. --- Luxferre --- [1]: gopher://hoi.st/9/files/shee.sh