Posted on
Artificial Intelligence

Prompt Engineering Best Practices

Author
  • User
    linuxbash
    Posts by this author
    Posts by this author

Prompt Engineering Best Practices for Bash Users

If you spend your day in a terminal, you’ve probably tried piping a log or a README into an LLM and gotten back… something. Sometimes it’s brilliant. Other times it’s verbose, off-target, or hard to parse. The problem isn’t just the model. It’s the prompt. With a handful of prompt engineering best practices tailored to a Bash workflow, you can make AI outputs consistent, scriptable, and dependable.

This post shows why prompt craft matters at the command line and gives you actionable patterns plus copy‑pasteable Bash snippets. By the end, you’ll have a small toolkit to turn AI into a reliable filter, formatter, and co‑pilot inside your shell.

Why this matters in a Linux/Bash workflow

  • You need reproducible output you can parse with jq, awk, or grep.

  • You want to automate: run the same prompt over hundreds of files, CI jobs, or logs.

  • You care about cost, latency, and failure modes, not just “nice” prose.

Well‑engineered prompts turn AI into a predictable CLI component rather than a chat toy.

Quick setup

We’ll use curl for HTTP and jq for JSON parsing. Install them with your package manager:

  • Debian/Ubuntu (apt)

    sudo apt update
    sudo apt install -y curl jq
    
  • Fedora/RHEL/CentOS (dnf)

    sudo dnf install -y curl jq
    
  • openSUSE (zypper)

    sudo zypper refresh
    sudo zypper install -y curl jq
    

Set your API details (replace placeholders with your own). Endpoints and models vary by provider; the snippet below targets a common Chat Completions interface.

export OPENAI_API_KEY="your_api_key_here"
export OPENAI_BASE_URL="https://api.openai.com"
export MODEL="your-model-name"   # e.g., gpt-4o-mini, gpt-4o, etc.

A tiny Bash helper to call the API and return just the assistant’s text:

ai() {
  local prompt="$1"
  curl -sS \
    -H "Authorization: Bearer ${OPENAI_API_KEY}" \
    -H "Content-Type: application/json" \
    -d "$(jq -n \
      --arg m "$MODEL" \
      --arg p "$prompt" \
      '{
         model: $m,
         temperature: 0,
         max_tokens: 800,
         messages: [
           {role:"system", content:"You are a precise, concise assistant that follows instructions exactly."},
           {role:"user", content:$p}
         ]
       }')" \
    "${OPENAI_BASE_URL}/v1/chat/completions" \
  | jq -r '.choices[0].message.content'
}

Tip: store your key securely (e.g., pass, GNOME Keyring) and export it in a shell profile you trust.


1) Be explicit: role, goal, constraints

Vague prompts yield vague results. Tell the model what it is, what you want, and what to avoid. For CLI use, specify formatting, brevity, and whether to include explanations.

Example: turn a natural-language task into a robust, grep‑friendly answer.

read -r -d '' PROMPT <<'EOF'
Role: Senior Bash engineer.
Goal: Convert the following requirement into a single, portable POSIX shell command.
Constraints:

- Output only the final command, no explanations.

- Use standard utilities available on most Linux distros.
Requirement:
"List all .log files modified in the last 24 hours under /var/log and show their sizes (human-readable)."
EOF

ai "$PROMPT"

Why it works:

  • Role primes the model’s expertise.

  • Goal is concrete.

  • Constraints remove chatter and force one-liners.


2) Demand structure: machine-readable JSON

If you’ll script against the output, make the model produce strict JSON. Then enforce it with jq. This reduces ambiguity and makes failures obvious.

Example: summarize an nginx access log into JSON KPIs you can parse downstream.

read -r -d '' PROMPT <<'EOF'
You are a log analysis assistant.
Task: Read the nginx access log excerpt and output a single JSON object with these fields:

- total_requests: integer

- unique_ips: integer

- top_paths: array of {path: string, hits: integer} (max 5)

- status_counts: object where keys are status codes as strings and values are integers
Rules:

- Output only JSON. No commentary.

- If data is ambiguous, make a best effort but keep valid JSON.

-----BEGIN LOG-----
127.0.0.1 - - [11/Jul/2026:13:50:16 +0000] "GET /index.html HTTP/1.1" 200 1024
127.0.0.2 - - [11/Jul/2026:13:50:17 +0000] "GET /about HTTP/1.1" 200 512
127.0.0.1 - - [11/Jul/2026:13:50:20 +0000] "GET /index.html HTTP/1.1" 304 0
-----END LOG-----
EOF

ai "$PROMPT" | jq .

Now you can reliably access fields:

ai "$PROMPT" | jq -r '.top_paths[0].path'

Pro tip: ask the model to “respond with exactly this JSON schema” and include an example object. The stricter you are, the more parseable the output.


3) Use delimiters and few‑shot examples

Clearly separate instructions from data with unique markers. For pattern‑matching tasks, show one or two labeled examples (few‑shot) to anchor the format.

Example: transforming messy requirements into a JSON checklist.

read -r -d '' PROMPT <<'EOF'
Task: Convert user stories into a JSON checklist.

Format (strict JSON):
{
  "story": "string",
  "acceptance_criteria": [
    {"id": "AC-1", "text": "string"},
    {"id": "AC-2", "text": "string"}
  ]
}

Example:
-----BEGIN STORY-----
As a user, I want to reset my password so that I can regain account access.
-----END STORY-----
Expected JSON:
{
  "story": "As a user, I want to reset my password so that I can regain account access.",
  "acceptance_criteria": [
    {"id": "AC-1", "text": "A reset link is emailed to the registered address."},
    {"id": "AC-2", "text": "The link expires after 30 minutes."}
  ]
}

Now process:
-----BEGIN STORY-----
As a sysadmin, I need daily compressed backups of /etc and /var so that I can recover from misconfiguration.
-----END STORY-----
Rules:

- Output only JSON.

- IDs should be sequential AC-1, AC-2, ...
EOF

ai "$PROMPT" | jq .

Why it works:

  • BEGIN/END markers reduce accidental instruction leakage.

  • The example demonstrates target shape and style.


4) Iterate with a test harness

Treat prompts like code. Write tests, diff outputs, and lock them down. You’ll catch regressions when model versions or inputs change.

Create a mini test runner for prompt revisions:

mkdir -p prompts tests expected out

# Example prompt template with a placeholder
cat > prompts/summarize.md <<'T'
Role: Summarizer.
Goal: Summarize the text in <= 3 bullet points.
Constraints: No extra lines beyond bullets.
Text:
<<<
{{TEXT}}
>>>
T

# One test input
cat > tests/t1.txt <<'E'
Bash is a Unix shell and command language. It supports scripting, job control, and command history.
E

# Expected output (lock this after reviewing once)
cat > expected/t1.txt <<'E'

- Bash is a Unix shell and scripting language.

- It supports job control and command history.
E

# Runner script
cat > run_tests.sh <<'BASH'
#!/usr/bin/env bash
set -euo pipefail

PROMPT_TPL="prompts/summarize.md"

render_prompt() {
  local text="$1"
  sed "s|{{TEXT}}|$(printf '%s' "$text" | sed 's/[&/\]/\\&/g')|g" "$PROMPT_TPL"
}

pass=0; fail=0
for f in tests/*.txt; do
  name="$(basename "$f")"
  prompt="$(render_prompt "$(cat "$f")")"
  out="out/$name"
  ai "$prompt" > "$out"
  if diff -u "expected/$name" "$out"; then
    echo "OK $name"
    pass=$((pass+1))
  else
    echo "FAIL $name"
    fail=$((fail+1))
  fi
done

echo "Passed: $pass  Failed: $fail"
test "$fail" -eq 0
BASH

chmod +x run_tests.sh
./run_tests.sh

Use this loop to safely evolve prompts. When you intentionally change output shape, update the expected files and commit with a message explaining why.


5) Control variability, size, and cost (and handle big inputs)

For automation, turn down creativity and keep responses tight.

  • temperature: 0 (deterministic, better for tests)

  • max_tokens: enough to fit your format, but not more

  • Chunk big inputs: summarize or pre‑digest before a final pass

Example: chunk a large log, summarize per chunk, then merge.

# Split to ~50KB chunks without breaking lines
split -C 50k -d --additional-suffix=.part big.log big.log.

# Summarize each chunk to JSON
for c in big.log.*.part; do
  read -r -d '' P <<EOF
Task: Summarize the following log chunk into JSON:
{
  "time_range": "start..end (if detectable)",
  "total_lines": int,
  "top_errors": [{"message": "string", "count": int}]
}
Rules: Output only JSON.

-----BEGIN CHUNK-----
$(cat "$c")
-----END CHUNK-----
EOF
  ai "$P" > "$c.json"
done

# Merge JSON summaries with jq
jq -s '{
  chunks: .,
  total_lines: (map(.total_lines)|add),
  aggregated_errors: (map(.top_errors[]) | group_by(.message) | map({message:.[0].message, count: (map(.count)|add)}) | sort_by(-.count))
}' big.log.*.part.json

This pattern scales to codebases, multi‑GB logs, and long docs while keeping costs and time under control.


Real‑world quick wins

  • Stable CLI generators: Ask for “only the command” and parseable flags to avoid prose.

  • Config linters: Feed a snippet, demand JSON findings with line numbers, then pipe to jq to fail CI if severity >= error.

  • Data extraction: Scrape semi‑structured text into strict JSON, then load into a database or run ad‑hoc queries.


Conclusion and next steps

Good prompts turn AI into a reliable Unix filter you can trust in scripts and CI. The core practices:

  • Be explicit about role, goal, and constraints.

  • Demand strict, parseable output (JSON).

  • Use delimiters and examples.

  • Treat prompts like code: test, diff, iterate.

  • Control variability and chunk large inputs.

Your next step:

  • Drop the ai() helper and one best‑practice pattern into a real task you run weekly (log triage, config checks, doc summaries).

  • Wrap it with a tiny test like in section 4.

  • Iterate until the output is boringly consistent.

When your prompts are reliable, you can plug them into cron, pipelines, and monitoring the same way you trust grep or jq—because now they behave like good shell citizens.