Posted on
Artificial Intelligence

Prompt Engineering Projects

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

Prompt Engineering Projects for Your Linux Shell: 4 Hands-On Bash Tools

Your shell can already grep logs, orchestrate servers, and deploy apps. What if it could also read, summarize, and write like an expert—on command? Prompt engineering isn’t just for web UIs; with a few lines of Bash and curl, you can turn any OpenAI-compatible model into a composable command-line tool. This post shows why that’s valuable and walks you through four practical, reproducible projects you can build today.

Why do this in Bash?

  • Reproducibility: Save prompts, inputs, and outputs in git. Re-run and diff them like code.

  • Composability: Pipe AI steps into your existing CLI flows and cron jobs.

  • Versioning and audits: Track prompt changes and outputs over time; tag releases alongside application code.

  • Cost and latency control: Script temperature, max tokens, and models per task.

  • Flexible backends: Call any OpenAI-compatible HTTP endpoint (cloud or local), without vendor lock-in.

Prerequisites

You’ll use standard CLI tooling. Install these packages:

  • curl: HTTP requests to the API

  • jq: JSON building and parsing

  • git: For the git-based project

  • gettext/envsubst: Simple, safe prompt templating via environment variables

Install on Debian/Ubuntu (apt):

sudo apt-get update
sudo apt-get install -y curl jq git gettext-base

Install on Fedora/RHEL/CentOS (dnf):

sudo dnf install -y curl jq git gettext

Install on openSUSE (zypper):

sudo zypper refresh
sudo zypper install -y curl jq git gettext-runtime

Environment configuration (generic OpenAI-compatible API):

export AI_BASE_URL="https://api.openai.com"
export AI_MODEL="gpt-4o-mini"    # or any model your provider supports
export AI_API_KEY="sk-...your key..."
export AI_TEMPERATURE="0.2"      # optional default

Tip: Put these in ~/.bashrc or in a project-local .env you source before running.

A tiny Bash helper to call models (drop-in function)

Every project below uses the same call wrapper. Save this function into a file you can source (e.g., ~/.config/pe/llm.sh).

#!/usr/bin/env bash
set -euo pipefail

call_llm() {
  local system="${1:-You are a concise assistant.}"
  local user="${2:-}"
  local temperature="${AI_TEMPERATURE:-0.2}"

  : "${AI_API_KEY:?Set AI_API_KEY environment variable}"
  : "${AI_MODEL:?Set AI_MODEL environment variable}"
  : "${AI_BASE_URL:=https://api.openai.com}"

  local payload
  payload=$(jq -n \
    --arg model "$AI_MODEL" \
    --arg sys "$system" \
    --arg usr "$user" \
    --argjson temp "$temperature" \
    '{
      model: $model,
      temperature: $temp,
      messages: [
        {role:"system", content:$sys},
        {role:"user", content:$usr}
      ]
    }'
  )

  curl -sS -X POST "$AI_BASE_URL/v1/chat/completions" \
    -H "Authorization: Bearer '"$AI_API_KEY"'" \
    -H "Content-Type: application/json" \
    -d "$payload"
}

Usage example:

source ~/.config/pe/llm.sh
call_llm "You summarize logs." "Summarize: $(journalctl -u ssh --since '1 hour ago' | tail -n 100)" \
  | jq -r '.choices[0].message.content'

Project 1: pe-run — Prompt templating + reproducible runs

Goal: Render a prompt template with variables, send to the model, and save timestamped outputs for later diffing.

Why it’s useful:

  • Prompts become files you can version control.

  • Swap variables (like PRODUCT_NAME or INPUT_FILE) without touching the prompt body.

  • Auto-save outputs into a runs/ folder with metadata.

Script:

#!/usr/bin/env bash
set -euo pipefail

# pe-run: render a prompt template and run it through the model.
# Template uses $VARS expanded by envsubst (from gettext).
# Example:
#   PRODUCT="Acme Router" INPUT_FILE=specs.txt pe-run prompt.tmpl
#   cat prompt.tmpl:
#     System: You are a networking expert.
#     User: Summarize the product "$PRODUCT" based on:
#     $(cat "$INPUT_FILE")

if [[ $# -lt 1 ]]; then
  echo "Usage: $0 PROMPT_TEMPLATE [OUTPUT_DIR]" >&2
  exit 1
fi

TEMPLATE="$1"
OUTDIR="${2:-runs}"
mkdir -p "$OUTDIR"

# shellcheck disable=SC2016
SYSTEM_DEFAULT='You are a helpful, terse expert. Reply only in the requested format.'
SYSTEM="${SYSTEM:-$SYSTEM_DEFAULT}"

# Render the user prompt by expanding $VARS using envsubst.
# Tip: export variables before calling, e.g., export INPUT_FILE=..., etc.
USER_PROMPT="$(envsubst < "$TEMPLATE")"

# shellcheck disable=SC1091
source "${PE_LLM_SH:-$HOME/.config/pe/llm.sh}"

RAW_JSON="$(call_llm "$SYSTEM" "$USER_PROMPT")"
CONTENT="$(jq -r '.choices[0].message.content // empty' <<<"$RAW_JSON")"
TOKENS="$(jq -r '.usage // {} | @json' <<<"$RAW_JSON")"
STAMP="$(date +'%Y%m%d_%H%M%S')"

# Save artifacts
RUN_DIR="$OUTDIR/$STAMP"
mkdir -p "$RUN_DIR"
printf '%s\n' "$SYSTEM" > "$RUN_DIR/system.txt"
printf '%s\n' "$USER_PROMPT" > "$RUN_DIR/user.txt"
printf '%s\n' "$CONTENT" > "$RUN_DIR/output.txt"
printf '%s\n' "$RAW_JSON" > "$RUN_DIR/response.json"
printf '%s\n' "$TOKENS" > "$RUN_DIR/usage.json"

echo "Saved run to: $RUN_DIR"
echo
echo "$CONTENT"

Install:

  • Save as ~/.local/bin/pe-run and make executable:
mkdir -p ~/.local/bin
chmod +x ~/.local/bin/pe-run
  • Ensure ~/.local/bin is in your PATH.

Try it:

export PRODUCT="Acme Router"
export INPUT_FILE="README.md"
pe-run prompt.tmpl

Best practice:

  • Start with a clear “System” role that sets style and constraints (e.g., “Output valid JSON only.”).

  • Keep variables separate from the prompt body for reusability.


Project 2: prompt-bench — A/B test prompts, models, and temperatures

Goal: Compare outcomes across prompt variants and settings, and record metrics like token usage and JSON validity.

Why it’s useful:

  • Quantify which prompt produces the most accurate or parseable output.

  • Track cost and latency over time.

Script:

#!/usr/bin/env bash
set -euo pipefail

# prompt-bench: run multiple prompt variants against a single input.
# Outputs a CSV with model, temp, label, tokens, and JSON validity status.
#
# Example:
#   echo 'Extract entities from the following text. Output JSON array "entities". Text: '"$TEXT" > p1.txt
#   echo 'You are a strict JSON emitter. Extract... Output only JSON.' > p2.txt
#   ./prompt-bench "My sample text goes here" p1.txt p2.txt

if [[ $# -lt 2 ]]; then
  echo "Usage: $0 INPUT_TEXT PROMPT1 [PROMPT2 ...]" >&2
  exit 1
fi

INPUT="$1"; shift
PROMPTS=("$@")

# shellcheck disable=SC1091
source "${PE_LLM_SH:-$HOME/.config/pe/llm.sh}"

TEMPS=(${AI_BENCH_TEMPS:-0.0 0.2 0.7})
echo "model,temp,label,total_tokens,json_ok,ms"

for t in "${TEMPS[@]}"; do
  export AI_TEMPERATURE="$t"
  for p in "${PROMPTS[@]}"; do
    label="$(basename "$p")"
    user="Follow all instructions. Output only JSON, no prose.
Schema:
{
  \"entities\": [ {\"text\": string, \"type\": string} ]
}
Text:
$(cat "$p")
---
INPUT:
$INPUT"
    start_ms=$(date +%s%3N)
    raw="$(call_llm "You emit only valid JSON per schema. No explanations." "$user")" || raw="{}"
    end_ms=$(date +%s%3N)
    dur=$((end_ms - start_ms))
    content="$(jq -r '.choices[0].message.content // empty' <<<"$raw")"
    usage="$(jq -r '.usage.total_tokens // 0' <<<"$raw")"
    # Validate JSON
    if jq -e . >/dev/null 2>&1 <<<"$content"; then ok=true; else ok=false; fi
    echo "${AI_MODEL},${t},${label},${usage},${ok},${dur}"
  done
done

Run:

./prompt-bench "The quick brown fox jumps over the lazy dog." strict.json.tmpl permissive.json.tmpl

Tips:

  • Force a strict output contract via the system message and ask for JSON only.

  • Use jq to validate parseability and fail fast in downstream scripts.


Project 3: git-cc-msg — Conventional Commit messages from diffs

Goal: Generate Conventional Commit–style messages based on your staged changes. You still review and edit, but save minutes per commit.

Why it’s useful:

  • Faster, more consistent commit logs.

  • Assistant reads the diff and proposes a clear title + body.

Hook script (prepare-commit-msg):

#!/usr/bin/env bash
set -euo pipefail
# .git/hooks/prepare-commit-msg
# Generates a Conventional Commit message if there isn't one yet.
# Requires call_llm helper.

COMMIT_MSG_FILE="$1"
COMMIT_SOURCE="${2:-}"
SHA="${3:-}"

# If message already present (e.g., merge, squash), skip
if [[ -s "$COMMIT_MSG_FILE" ]]; then
  exit 0
fi

# shellcheck disable=SC1091
source "${PE_LLM_SH:-$HOME/.config/pe/llm.sh}"

DIFF="$(git diff --staged --no-color)"
if [[ -z "$DIFF" ]]; then
  exit 0
fi

SYS="You are a senior developer. Write a Conventional Commit message.

- Title: type(scope): summary

- Body: concise bullet points (wrap at ~72 chars)

- No code blocks, no extra commentary."

USR="Create a commit message for the following diff. If tests or docs changed, note it.
Diff:
$DIFF"

RAW="$(call_llm "$SYS" "$USR")" || exit 0
MSG="$(jq -r '.choices[0].message.content // empty' <<<"$RAW")"

if [[ -n "$MSG" ]]; then
  printf '%s\n' "$MSG" > "$COMMIT_MSG_FILE"
fi

Install:

chmod +x .git/hooks/prepare-commit-msg

Packages you may need:

  • Debian/Ubuntu:

    • git: sudo apt-get install -y git
  • Fedora/RHEL/CentOS:

    • git: sudo dnf install -y git
  • openSUSE:

    • git: sudo zypper install -y git

Note:

  • Always review the proposed message before committing.

  • Keep the system prompt opinionated about format (type(scope): summary).


Project 4: log-summarize — TL;DR for verbose logs

Goal: Summarize recent service logs (systemd, app logs) into a crisp report. Great for on-call, daily stand-ups, and incident reviews.

Why it’s useful:

  • Quickly extract failures, counts, and key events from thousands of lines.

  • Chain summaries: chunk raw logs, summarize chunks, then summarize the summaries.

Script:

#!/usr/bin/env bash
set -euo pipefail

# log-summarize: summarize logs from a command (journalctl, kubectl logs, etc.)
# It chunks input to avoid overlong prompts, summarizes each chunk,
# then summarizes all chunk summaries into a final TL;DR.

if [[ $# -lt 1 ]]; then
  echo "Usage: $0 'LOG_COMMAND...'" >&2
  echo "Example: $0 \"journalctl -u nginx --since '2 hours ago'\"" >&2
  exit 1
fi

# shellcheck disable=SC1091
source "${PE_LLM_SH:-$HOME/.config/pe/llm.sh}"

LOG_CMD="$*"
CHUNK_BYTES="${CHUNK_BYTES:-16000}"   # tune per model limits

# 1) Collect logs
LOGS="$(bash -lc "$LOG_CMD" || true)"

if [[ -z "$LOGS" ]]; then
  echo "No logs." && exit 0
fi

# 2) Chunk logs by bytes
mapfile -t CHUNKS < <(awk -v max="$CHUNK_BYTES" '
  BEGIN {i=0; size=0}
  {
    line=$0 ORS
    if (size + length(line) > max) { i++; size=0 }
    chunk[i]=chunk[i] line
    size += length(line)
  }
  END {
    for (j=0; j<=i; j++) {
      gsub(/\r$/, "", chunk[j])
      print chunk[j] "\n---CHUNK-END---"
    }
  }
' <<<"$LOGS")

SUMMARIES=()
SYS="You are a log analysis assistant. Be precise and terse.
Output in plain text with sections:

- Errors (top 5 unique with counts)

- Warnings (top 5)

- Notable events

- Timeline highlights

- Suggested next actions"

for c in "${CHUNKS[@]}"; do
  USR="Summarize the following logs:
$c"
  RAW="$(call_llm "$SYS" "$USR")" || continue
  SUM="$(jq -r '.choices[0].message.content // empty' <<<"$RAW")"
  [[ -n "$SUM" ]] && SUMMARIES+=("$SUM")
done

# 3) Final synthesis
JOINED="$(printf '\n\n--- SUMMARY CHUNK ---\n\n%s' "${SUMMARIES[@]}")"
FINAL_RAW="$(call_llm "$SYS" "Synthesize these chunk summaries into a final concise report:\n$JOINED")" || true
FINAL="$(jq -r '.choices[0].message.content // empty' <<<"$FINAL_RAW")"

printf '%s\n' "$FINAL"

Try it:

./log-summarize "journalctl -u ssh --since '1 hour ago'"

Notes:

  • Adjust CHUNK_BYTES to fit your model’s token limits.

  • For strict machine parsing, change the system message to “Output valid JSON only” and add a schema.


Prompt engineering best practices that work well in Bash

  • State clear roles and constraints up front (system message).

  • Enforce output formats (e.g., “Output only JSON conforming to this schema”).

  • Control variability with temperature and optionally a seed (if your API supports it).

  • Measure: Always capture usage tokens and time. Add regression tests with golden files.

  • Fail safely: Validate outputs with jq before using them downstream.


Conclusion and next steps

With these four projects, you’ve turned prompt engineering into something testable, reproducible, and automatable—true command-line power. Start with pe-run to template prompts, use prompt-bench to pick a stable prompt, adopt git-cc-msg to speed up your workflow, and bring sanity to incident reviews with log-summarize.

Your next step:

  • Create a new repo (e.g., prompt-lab), drop in these scripts, and start versioning your prompts and runs.

  • Wire the tools into CI to guard for JSON validity and cost regressions.

  • Expand to domain-specific projects: data cleaning pipelines, migration planners, or API doc generators.

If you found this useful, try adapting one project to your stack today—then share what you automated!