| name | lang-bash-scripting |
| description | Use when writing, reviewing, or debugging Bash shell scripts on Linux for embedded development workflows. Covers script structure, error handling (set -euo pipefail), argument parsing, string/array manipulation, file operations, process management, ADB automation, build system helpers (AOSP/Yocto), log parsing, and CI/CD integration for automotive IVI / HUD / RSE projects running on Linux or QNX targets.
|
| argument-hint | <script-name> [write|review|debug] |
Bash Scripting — Linux Embedded Development
Expert practices for writing reliable, portable Bash scripts that support
Android (AOSP), Linux BSP (Yocto/Buildroot), and QNX embedded
development workflows.
Standards baseline: Bash 4.4+ · POSIX sh where cross-distro portability is required.
When to Use This Skill
- Writing build helper, flash, or deployment scripts for embedded targets.
- Automating ADB interactions (flashing, log capture, screen recording).
- Parsing build logs, coredumps, or serial console output.
- Writing CI/CD pipeline step scripts (Jenkins, GitLab CI).
- Wrapping AOSP
m/make, Yocto bitbake, or CMake commands.
- Managing SSH connections to target boards.
Script Skeleton — Always Start Here
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
PRODUCT="${PRODUCT:-aosp_car_x86_64}"
VARIANT="${VARIANT:-userdebug}"
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
die() { echo "[ERROR] $*" >&2; exit 1; }
cleanup() {
log "Cleaning up temporary files..."
}
trap cleanup EXIT
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS]
Options:
--product PRODUCT AOSP product name (default: $PRODUCT)
--variant VARIANT Build variant: eng|userdebug|user (default: $VARIANT)
-h, --help Show this help
Environment:
PRODUCT, VARIANT Override defaults via env vars
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--product) PRODUCT="$2"; shift 2 ;;
--variant) VARIANT="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) die "Unknown argument: $1" ;;
esac
done
}
main() {
parse_args "$@"
log "Flashing $PRODUCT ($VARIANT)..."
}
main "$@"
Rules
set -euo pipefail MUST be the first statement — exits on error, unbound variable, or pipe failure.
IFS=$'\n\t' prevents word-splitting bugs on filenames with spaces.
- Use
readonly for constants — prevents accidental overwrite.
- Always define
cleanup() + trap cleanup EXIT when creating temp files or long-running processes.
Error Handling
die() { echo "[ERROR] $*" >&2; exit 1; }
require_cmd() {
for cmd in "$@"; do
command -v "$cmd" &>/dev/null || die "Required command not found: $cmd"
done
}
require_cmd adb fastboot python3 repo
if ! adb devices | grep -q "device$"; then
die "No ADB device connected. Check USB connection."
fi
adb logcat | tee /tmp/logcat.log | grep -i "fatal" || true
Variables and Strings
log "Building: $PRODUCT"
log "Building: ${PRODUCT}"
log "Path: ${SCRIPT_DIR}/out/${PRODUCT}"
OUT_DIR="${OUT_DIR:-/tmp/build_output}"
MAX_JOBS="${MAX_JOBS:-$(nproc)}"
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
KERNEL_VERSION="$(uname -r)"
FILENAME="system-image-123.zip"
NAME="${FILENAME%.zip}"
EXT="${FILENAME##*.}"
BASE="${FILENAME##*/}"
Arrays
MODULES=("MyCarApp" "CarService" "AudioHAL")
for module in "${MODULES[@]}"; do
log "Building: $module"
m "$module"
done
log "Module count: ${#MODULES[@]}"
MODULES+=("NewModule")
build_modules "${MODULES[@]}"
build_modules() {
local modules=("$@")
for m in "${modules[@]}"; do echo " $m"; done
}
File Operations
[[ -f "$CONFIG_FILE" ]] || die "Config not found: $CONFIG_FILE"
[[ -d "$OUT_DIR" ]] || mkdir -p "$OUT_DIR"
[[ -x "$TOOL_PATH" ]] || die "Tool not executable: $TOOL_PATH"
TMPFILE="$(mktemp /tmp/build_XXXXXX.log)"
trap 'rm -f "$TMPFILE"' EXIT
while IFS= read -r line; do
echo "Processing: $line"
done < "$INPUT_FILE"
find "${OUT_DIR}" -name "*.apk" -type f | while IFS= read -r apk; do
log "APK: $apk"
done
ADB Automation
wait_for_device() {
log "Waiting for ADB device..."
adb wait-for-device
adb shell getprop sys.boot_completed | grep -q "1" || {
log "Waiting for boot to complete..."
adb wait-for-device shell while [[ \"\$(getprop sys.boot_completed)\" != \"1\" ]]\; do sleep 1\; done
}
log "Device ready."
}
install_apk() {
local apk="$1"
local retries=3
local i=0
while (( i++ < retries )); do
if adb install -r -t "$apk"; then
log "Installed: $apk"
return 0
fi
warn "Install attempt $i failed, retrying..."
sleep 2
done
die "Failed to install $apk after $retries attempts."
}
capture_logcat() {
local duration="${1:-10}"
local output="${2:-/tmp/logcat_$(date +%s).log}"
adb logcat -c
adb logcat > "$output" &
local PID=$!
sleep "$duration"
kill "$PID" 2>/dev/null || true
log "Logcat saved: $output"
}
push_and_sync() {
local local_path="$1"
local remote_path="$2"
adb push "$local_path" "$remote_path"
adb shell sync
}
AOSP Build Helpers
setup_aosp_env() {
local aosp_root="${1:?AOSP root required}"
[[ -f "$aosp_root/build/envsetup.sh" ]] || die "Not an AOSP tree: $aosp_root"
source "$aosp_root/build/envsetup.sh"
lunch "${PRODUCT}-${VARIANT}"
}
build_module() {
local module="$1"
log "Building $module..."
local start_ts; start_ts=$(date +%s)
m "$module" 2>&1 | tee "${LOG_DIR}/${module}_build.log"
local elapsed=$(( $(date +%s) - start_ts ))
log "Built $module in ${elapsed}s"
}
check_out_dir() {
local out_product
out_product="$(cat "${ANDROID_BUILD_TOP}/out/.product" 2>/dev/null || echo "")"
if [[ "$out_product" != "$PRODUCT" ]]; then
warn "out/ is from a different product ($out_product). Consider: make installclean"
fi
}
Process Management
run_with_timeout() {
local timeout="$1"; shift
timeout "$timeout" "$@" || die "Command timed out after ${timeout}s: $*"
}
start_serial_monitor() {
minicom -D /dev/ttyUSB0 -b 115200 > "$LOG_DIR/serial.log" 2>&1 &
SERIAL_PID=$!
trap 'kill $SERIAL_PID 2>/dev/null' EXIT
log "Serial monitor PID: $SERIAL_PID"
}
for module in "${MODULES[@]}"; do
m "$module" &
done
wait
log "All modules built"
CI/CD Patterns
is_ci() { [[ "${CI:-false}" == "true" ]]; }
set_ci_output() {
local key="$1" value="$2"
if [[ -n "${GITHUB_OUTPUT:-}" ]]; then
echo "${key}=${value}" >> "$GITHUB_OUTPUT"
elif [[ -n "${GITLAB_CI:-}" ]]; then
echo "${key}=${value}" | tee -a build.env
fi
}
if is_ci || [[ ! -t 1 ]]; then
RED="" GREEN="" YELLOW="" RESET=""
else
RED="\033[0;31m" GREEN="\033[0;32m" YELLOW="\033[1;33m" RESET="\033[0m"
fi
log() { echo -e "${GREEN}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
Prerequisites
- Bash 4.0+ (
bash --version to verify).
- Standard GNU coreutils installed.
- For ADB scripts: Android Platform-Tools on PATH.
shellcheck installed for linting (apt install shellcheck or brew install shellcheck).
Step-by-Step Workflows
Step 1: Create the script file
Add #!/usr/bin/env bash as the first line and set -euo pipefail as the second.
Step 2: Parse arguments
Use getopts for short flags or a case "$1" block; validate all required arguments early.
Step 3: Implement with functions
Use main() as the entry point called at the bottom; keep each function focused on one task.
Step 4: Add error handling and traps
Use trap 'cleanup' EXIT; check return codes of commands that may fail silently.
Step 5: Test and lint
Run bash -n script.sh (syntax check) and shellcheck script.sh (lint); fix all findings.
Troubleshooting
syntax error near unexpected token — check for Windows-style line endings (\r\n); convert with dos2unix script.sh.
- Variable unexpectedly empty — quote all expansions:
"$VAR" not $VAR; use ${VAR:-default} for safe defaults.
command not found in CI — add the tool install path to PATH in the CI environment; use absolute paths for external tools.
- Script exits silently —
set -e exits on first non-zero return; add || true for commands where failure is acceptable.
Pre-Commit Checklist
References