(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