json1-4 Exercise 3: curl | jq, and Quoting in Three Shells ========================================================== Setup: srv.mjs (below) is a Node server on 127.0.0.1:8765. Run each part from its own shell (a PowerShell script started from Git Bash hung until its input was closed, and "cmd /c" from Git Bash started an interactive cmd). --- srv.mjs --- import http from 'node:http'; import fs from 'node:fs'; http.createServer((req, res) => { if (req.url === '/shop') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(fs.readFileSync('shop.json')); } else if (req.url === '/missing') { res.writeHead(404, { 'content-type': 'application/json' }); res.end('{"error":"not found"}'); } else if (req.url === '/html') { res.writeHead(502, { 'content-type': 'text/html' }); res.end('Bad Gateway'); } else { res.writeHead(200); res.end('{"hello":"world"}'); } }).listen(8765, '127.0.0.1'); --- ex3.sh (Git Bash) --- # Exercise 3, part A (Git Bash): curl | jq against a local server, and what each failure looks like export PATH="/c/lwdj/json/c4/bin:$PATH" node srv.mjs & SP=$!; sleep 1 U=http://127.0.0.1:8765 echo '1. curl -s | jq'; curl -s $U/shop | jq -r '.products[].name' | tr '\n' ' '; echo echo '2. curl WITHOUT -s, stderr merged into the pipe (2>&1)'; curl $U/hello 2>&1 | jq -c . 2>&1 | head -2 echo '3. 404 with a JSON error body, plain curl -s'; curl -s $U/missing | jq -c .; echo " exits: curl ${PIPESTATUS[0]}, jq ${PIPESTATUS[1]}" echo '4. same, curl -sf (fail on HTTP errors)'; curl -sf $U/missing | jq -c .; echo " exits: curl ${PIPESTATUS[0]}, jq ${PIPESTATUS[1]} (jq received nothing and printed nothing)" echo '5. same, curl -s --fail-with-body'; curl -s --fail-with-body $U/missing | jq -c .; echo " exits: curl ${PIPESTATUS[0]}, jq ${PIPESTATUS[1]}" echo '6. 502 HTML page, curl -s'; curl -s $U/html | jq . 2>&1; echo " exits: curl ${PIPESTATUS[0]}, jq ${PIPESTATUS[1]}" echo '7. with pipefail, curl -sf'; ( set -o pipefail; curl -sf $U/missing | jq -c .; echo " pipeline exit: $?" ) echo '8. keep the response, check the status, then parse' code=$(curl -s -o body.tmp -w '%{http_code}' $U/missing); echo " status $code"; [ "$code" = 200 ] && jq . body.tmp || { echo " not 200, body was:"; cat body.tmp; echo; } rm -f body.tmp kill $SP 2>/dev/null; wait $SP 2>/dev/null Output (part A): 1. curl -s | jq Tea Coffee Biscuits Milk 2. curl WITHOUT -s, stderr merged into the pipe (2>&1) jq: parse error: Invalid numeric literal at line 1, column 4 3. 404 with a JSON error body, plain curl -s {"error":"not found"} exits: curl 0, jq 0 4. same, curl -sf (fail on HTTP errors) exits: curl 22, jq 0 (jq received nothing and printed nothing) 5. same, curl -s --fail-with-body {"error":"not found"} exits: curl 22, jq 0 6. 502 HTML page, curl -s jq: parse error: Invalid numeric literal at line 1, column 10 exits: curl 0, jq 5 7. with pipefail, curl -sf pipeline exit: 22 8. keep the response, check the status, then parse status 404 not 200, body was: {"error":"not found"} --- ex3.ps1 (Windows PowerShell 5.1) --- # Exercise 3, part B (Windows PowerShell 5.1): the same filter, five ways to pass it $env:Path = "C:\lwdj\json\c4\bin;" + $env:Path Set-Location C:\lwdj\json\c4 "PowerShell " + $PSVersionTable.PSVersion "1. single quotes, no inner quotes"; jq '.products[0].name' shop.json "2. single quotes, inner double quotes"; jq '.products[] | select(.name == "Tea") | .sku' shop.json; " exit " + $LASTEXITCODE "3. inner quotes escaped with backslash"; jq '.products[] | select(.name == \"Tea\") | .sku' shop.json; " exit " + $LASTEXITCODE "4. --arg, no inner quotes at all"; jq --arg n Tea '.products[] | select(.name == $n) | .sku' shop.json; " exit " + $LASTEXITCODE "5. filter in a file with -f"; Set-Content -Encoding ascii f.jq '.products[] | select(.name == "Tea") | .sku'; jq -f f.jq shop.json; " exit " + $LASTEXITCODE "6. pipe a PowerShell value in"; @{ a = 1; b = 'x' } | ConvertTo-Json -Compress | jq -c '.' Output (part B): PowerShell 5.1.26100.9549 1. single quotes, no inner quotes "Tea" 2. single quotes, inner double quotes jq: error: Tea/0 is not defined at , line 1, column 31: .products[] | select(.name == Tea) | .sku ^^^ jq: 1 compile error exit 3 3. inner quotes escaped with backslash "A1" exit 0 4. --arg, no inner quotes at all "A1" exit 0 5. filter in a file with -f "A1" exit 0 6. pipe a PowerShell value in {"a":1,"b":"x"} --- ex3.cmd (cmd.exe) --- @echo off rem Exercise 3, part C (cmd.exe) set PATH=C:\lwdj\json\c4\bin;%PATH% cd /d C:\lwdj\json\c4 echo 1. double quotes around the filter jq ".shop" shop.json echo 2. single quotes around the filter jq '.shop' shop.json 2>&1 | findstr /n "error" echo 3. inner quotes escaped with backslash jq ".products[] | select(.name == \"Tea\") | .sku" shop.json echo 4. --arg, no inner quotes at all jq --arg n Tea ".products[] | select(.name == $n) | .sku" shop.json echo 5. filter in a file with -f jq -f f.jq shop.json Output (part C): 1. double quotes around the filter "Corner Shop" 2. single quotes around the filter 1:jq: error: syntax error, unexpected INVALID_CHARACTER, expecting end of file at , line 1, column 1: 4:jq: 1 compile error 3. inner quotes escaped with backslash "A1" 4. --arg, no inner quotes at all "A1" 5. filter in a file with -f "A1" WHY THIS WORKS AS AN ANSWER --------------------------- Part A shows that every curl|jq failure has a different signature. A 404 with a JSON body looks like success (both exit 0); curl -sf hides the body and exits 22 while jq, receiving nothing, still exits 0; --fail-with-body keeps the body and exits 22; an HTML error page produces jq's "Invalid numeric literal" and exit 5; pipefail makes the pipeline report curl's 22; and the status-first form (-w '%{http_code}' into a variable) lets the script decide before parsing. Case 2 shows why 2>&1 without -s is wrong: the progress meter text became part of jq's input. Parts B and C show quoting really differs: PowerShell 5.1 removes inner double quotes (the filter reached jq as ".name == Tea" and failed with exit 3), cmd does not understand single quotes, and the two methods that work everywhere are --arg (no inner quotes needed) and -f (the shell never sees the filter). Windows PowerShell 5.1 only; PowerShell 7 was not tested.