shell-command-chaining

star 4

Implements shell command chaining patterns using &&, ||, and ; operators for conditional execution, validation gates, fallback chains, and safe sequential workflows in bash scripts.

paulpas By paulpas schedule Updated 6/4/2026

name: shell-command-chaining description: Implements shell command chaining patterns using &&, ||, and ; operators for conditional execution, validation gates, fallback chains, and safe sequential workflows in bash scripts. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: linux role: implementation scope: implementation output-format: code content-types:

  • code
  • guidance
  • do-dont
  • examples triggers: shell command chaining, && operator, || operator, semicolon in bash, conditional command execution, fallback chain, AND list OR list, short-circuit evaluation command execution archetypes:
  • tactical anti_triggers:
  • brainstorming
  • vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational related-skills: linux-shell-process-management, shell-parameter-expansion

Shell Command Chaining and Conditional Execution

Implements reliable sequential and conditional command execution patterns using && (AND-list), || (OR-list), and ; (semicolon) operators in bash scripts. Teaches how to build validation gates, fallback chains, safe cleanup sequences, and deployment pipelines that behave predictably under set -e.

TL;DR Checklist

  • Use && for dependent steps — second command runs only if the first exits 0
  • Use || for fallback/alternative paths — second command runs only if the first fails
  • Use ; when you want unconditional sequential execution, ignoring all exit codes
  • Never rely on A && B || C as a true ternary — if A succeeds and B fails, C runs unexpectedly
  • Remember that commands inside && / || chains are exempt from set -e (errexit)
  • Quote all variable expansions in chained commands: [[ -f "$file" ]] && cp "$file" "$dest"

When to Use

Use this skill when:

  • Building bash scripts that must proceed only if prerequisites are met
  • Implementing deployment pipelines where each step depends on the previous one
  • Designing fallback chains: try method A, fall back to method B, abort if both fail
  • Writing cleanup or teardown logic that must always run regardless of success/failure
  • Creating validation gates that check for tools, files, permissions before executing expensive operations

When NOT to Use

Avoid this skill for:

  • Parallel/concurrent execution — use &, jobs, fg, bg from linux-shell-process-management instead
  • Variable substitution and default values — use parameter expansion ${VAR:-default} from shell-parameter-expansion instead
  • Complex multi-path logic with many branches — use explicit if/elif/else/fi blocks for readability (chains beyond 5 operators become unreadable)
  • Subshell isolation needs — if a failure in one chain step should not affect the parent shell, use (...) subshells or dedicated functions

Core Workflow

  1. Determine Execution Dependency — Ask: "Does the next command require the previous one to succeed?"

    • Yes → &&
    • No, but I need an alternative if it fails → ||
    • No dependency at all → ; Checkpoint: Write down the expected exit codes for each step in the chain before assembling.
  2. Choose the Operator — Select based on the dependency analysis:

    • && chains abort on first failure (zero or non-zero exit)
    • || chains continue only on failure
    • ; runs everything unconditionally Checkpoint: Verify the chain handles every expected exit code path.
  3. Write and Test the Chain — Implement with proper quoting, test each operator branch in isolation:

    # Test && branch (success)
    true && echo "chain executed"  # prints
    false && echo "should not print"  # silent, exit 1
    
    # Test || branch (failure fallback)
    false || echo "fallback ran"  # prints
    true || echo "should not print"  # silent, exit 0
    
  4. Verify with set -e Interaction — Confirm commands inside && / || do not trigger errexit on failure:

    set -euo pipefail
    false && true  # does NOT abort despite `false` returning non-zero
    echo "still running"  # prints because `true` succeeded, and `false` was in an AND-list
    

Implementation Patterns

Pattern 1: Validation Gate (AND-list with &&)

A validation gate checks that all prerequisites exist before proceeding. Each && is a checkpoint — if any check fails, the chain aborts immediately without executing the main operation. This is the most common and safest chaining pattern in production scripts.

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

# Validation gate: ensure all required tools are available before deployment
validate_prerequisites() {
    local missing=0

    # Check each tool; if any fails, the && chain breaks and we print a message
    command -v docker  >/dev/null 2>&1 || { echo "ERROR: docker is not installed" >&2; missing=$((missing + 1)); }
    command -v kubectl >/dev/null 2>&1 || { echo "ERROR: kubectl is not installed" >&2; missing=$((missing + 1)); }
    command -v helm    >/dev/null 2>&1 || { echo "ERROR: helm is not installed" >&2; missing=$((missing + 1)); }

    if (( missing > 0 )); then
        echo "FATAL: $missing prerequisite(s) missing — aborting" >&2
        return 1
    fi

    echo "All prerequisites verified."
}

# Usage in a chained gate — only deploy if validation passes
validate_prerequisites && \
  docker build -t myapp:latest . && \
  docker push myapp:latest && \
  helm upgrade --install myapp ./charts/myapp && \
  echo "Deployment complete."

Why this works: If docker build fails, docker push and helm upgrade never execute. The chain stops at the first failure, preventing partially-applied deployments. Each step is verified before proceeding.

BAD vs GOOD

# ❌ BAD: No validation gate — if docker is missing, cryptic error deep in script
docker build -t myapp . && \
  docker push myapp && \
  kubectl apply -f deployment.yaml
# User gets "docker: command not found" with no context about what was supposed to happen

# ✅ GOOD: Explicit validation gate before the main chain
command -v docker >/dev/null || { echo "Install docker first"; exit 1; }
command -v kubectl >/dev/null || { echo "Install kubectl first"; exit 1; }
docker build -t myapp . && \
  docker push myapp && \
  kubectl apply -f deployment.yaml

Pattern 2: Fallback / Alternative Execution (OR-list with ||)

When a command might fail in certain environments, chain alternatives with ||. Each subsequent operator tries the next method only when the previous one has failed. This pattern is essential for cross-platform scripts and gracefully degraded operations.

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

# Fallback chain: try multiple methods to set a timezone, last resort uses UTC
setup_timezone() {
    local desired_tz="${1:-UTC}"

    # Try timedatectl (systemd systems), then tzselect (busybox/alpine), then hardcoded value
    if command -v timedatectl >/dev/null 2>&1; then
        timedatectl set-timezone "$desired_tz" 2>/dev/null && \
            echo "Timezone set via timedatectl to $desired_tz" || \
            { echo "WARN: timedatectl failed, trying fallback" >&2; }
    fi

    # Second fallback: use tzselect if available
    command -v tzselect >/dev/null 2>&1 && \
        echo "$desired_tz" | tzselect -y >/dev/null 2>&1 && \
        echo "Timezone set via tzselect to $desired_tz" || \
        { echo "WARN: tzselect failed, using hardcoded /etc/localtime link" >&2; }

    # Final fallback: direct symlink (works on nearly all Linux systems)
    ln -sf "/usr/share/zoneinfo/$desired_tz" /etc/localtime 2>/dev/null && \
        echo "Timezone set via /etc/localtime symlink to $desired_tz" || \
        { echo "FATAL: could not set timezone to $desired_tz" >&2; return 1; }
}

# Simple fallback chain for downloading a file from multiple CDN locations
download_with_fallback() {
    local url="$1"
    local dest="${2:-./output}"

    curl -fsSL "$url" -o "$dest" 2>/dev/null || \
      wget -q --no-check-certificate "$url" -O "$dest" 2>/dev/null || \
      { echo "ERROR: failed to download $url via curl or wget" >&2; return 1; }

    [[ -s "$dest" ]] || { echo "ERROR: downloaded file is empty" >&2; return 1; }
    echo "Downloaded to $dest ($(stat -c%s "$dest") bytes)"
}

Key insight: The || operator creates a cascading fallback. Each step is tried only when the previous one has failed. This is fundamentally different from && (which aborts on failure) and ; (which runs everything regardless).

BAD vs GOOD

# ❌ BAD: Falls back to nothing — if curl fails, script silently does nothing
curl -fsSL "https://example.com/file.tar.gz" -o file.tar.gz
echo "Proceeding with extraction..."  # Always runs, even if download failed!
tar -xzf file.tar.gz  # Crashes on missing file

# ✅ GOOD: Explicit fallback chain with final failure handling
curl -fsSL "https://example.com/file.tar.gz" -o file.tar.gz 2>/dev/null || \
  wget -q "https://example.com/file.tar.gz" -O file.tar.gz 2>/dev/null || \
  { echo "ERROR: could not download file" >&2; exit 1; }

[[ -f file.tar.gz ]] || { echo "ERROR: download produced no output" >&2; exit 1; }
tar -xzf file.tar.gz && echo "Extraction complete."

Pattern 3: Safe Cleanup Sequences (semicolon after &&/||)

The semicolon operator executes unconditionally, making it the correct tool for cleanup code that must always run — whether the preceding operation succeeded or failed. Place ; between a conditional chain and a cleanup step.

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

# Deploy with guaranteed cleanup regardless of outcome
deploy_application() {
    local version="${1:-latest}"
    local temp_dir
    temp_dir="$(mktemp -d)"

    echo "Deploying version $version..."

    # Main operation chain: download, extract, deploy
    # If ANY step fails, the || branch catches it and sets $? to non-zero for cleanup
    (
        curl -fsSL "https://releases.example.com/app-${version}.tar.gz" -o "${temp_dir}/app.tar.gz" && \
        tar -xzf "${temp_dir}/app.tar.gz" -C "${temp_dir}" && \
        cp -r "${temp_dir}"/app/* /opt/app/ && \
        systemctl restart app-service
    ) || {
        local exit_code=$?
        echo "WARN: deployment failed with exit code $exit_code, proceeding to cleanup" >&2
        return "$exit_code"
    }

    echo "Deployment successful."
}

# Simpler version using pure chaining (no subshell):
deploy_simple() {
    local temp_file="/tmp/deploy-tmp-$$"

    # Step 1: Download (conditionally runs cleanup on failure via ||)
    curl -fsSL "https://releases.example.com/app.tar.gz" -o "$temp_file" && \
      echo "Downloaded successfully." || \
      { echo "ERROR: download failed" >&2; }

    # Step 2: ALWAYS run cleanup — ; ignores the exit code of the previous chain
    rm -f "$temp_file"; echo "Temporary file cleaned up."

    # Combined pattern: deploy succeeds OR fails, but cleanup always runs
    curl -fsSL "https://releases.example.com/app.tar.gz" -o /opt/app/latest.tar.gz && \
      systemctl restart app || \
      { echo "Restart failed, checking status..." >&2; systemctl status app; }

    # Cleanup — semicolon ensures this runs regardless of restart outcome
    rm -f /opt/app/previous.tar.gz 2>/dev/null; echo "Old artifact removed."
}

Critical detail: The semicolon ; has the lowest precedence among chaining operators. In A && B || C; D, D runs unconditionally after the entire A && B || C expression completes — this is exactly what you want for cleanup.

BAD vs GOOD

# ❌ BAD: Cleanup uses && — only runs if deploy succeeded, leaving temp files on failure
deploy() {
    cp app.tar.gz /opt/ 2>/dev/null && \
      tar -xzf /opt/app.tar.gz -C /opt/ && \
      echo "Deployed"

    rm -f /tmp/deploy-$$  # This runs always because it's on a new line without chaining...
                         # BUT: if someone adds && here, cleanup stops running on failure!
}

# ✅ GOOD: Cleanup uses ; — runs unconditionally after the conditional chain
deploy() {
    local tmp="/tmp/deploy-$$"

    cp app.tar.gz /opt/ 2>/dev/null && \
      tar -xzf /opt/app.tar.gz -C /opt/ && \
      echo "Deployed" || \
      { echo "ERROR: deploy failed for $user" >&2; }

    # Semicolon ensures cleanup always runs, even if deploy failed
    rm -f "$tmp"; echo "Temp files cleaned."
}

Pattern 4: Multi-Step Deployment Pipeline (long AND-list chains)

Production deployment scripts use long && chains to create linear pipelines where every step must succeed before the next begins. This pattern provides automatic rollback-at-first-failure semantics without explicit error handling in each step.

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

# Production deployment pipeline — each step chained with &&
# If any step fails, the entire pipeline aborts and no further steps execute
run_deployment_pipeline() {
    local env="${1:-staging}"
    local branch="${2:-main}"
    local skip_tests=false

    echo "=== Starting deployment to $env (branch: $branch) ==="

    # Step 1: Validate environment
    [[ -f ".env.${env}" ]] || { echo "ERROR: .env.${env} not found" >&2; exit 1; }
    [[ -f "Dockerfile" ]] || { echo "ERROR: Dockerfile not found" >&2; exit 1; }

    # Step 2: Pull latest code and verify integrity
    git fetch origin "$branch" && \
      git rev-parse --verify "origin/$branch" >/dev/null 2>&1 && \
      echo "✓ Branch verified: origin/$branch"

    # Step 3: Run tests (skip only if explicitly requested)
    if [[ "$skip_tests" != "true" ]]; then
        pytest tests/ -q --tb=short && \
          echo "✓ All tests passed" || \
          { echo "✗ Tests failed — aborting deployment" >&2; exit 1; }
    else
        echo "⊘ Tests skipped (explicit request)"
    fi

    # Step 4: Build and tag Docker image
    docker build -t "myapp:${env}-$(git rev-parse --short HEAD)" . && \
      echo "✓ Image built"

    # Step 5: Push to registry
    docker push "myapp:${env}-$(git rev-parse --short HEAD)" && \
      echo "✓ Image pushed"

    # Step 6: Deploy with health check
    kubectl set image deployment/myapp myapp="myapp:${env}-$(git rev-parse --short HEAD)" && \
      kubectl rollout status deployment/myapp --timeout=120s && \
      echo "✓ Rollout complete"

    # Step 7: Run post-deployment smoke tests
    curl -sf "https://${env}.example.com/health" && \
      echo "✓ Health check passed" || \
      { echo "⚠ Health check failed, but deployment completed" >&2; }

    echo "=== Deployment to $env complete ==="
}

# Pipeline with rollback on critical failure (using function + trap for safety)
run_rollback_safe_pipeline() {
    local backup_label="pre-deploy-$(date +%Y%m%d-%H%M%S)"
    local failed=false

    # Set up rollback trigger on first pipeline step failure
    rollback() {
        if [[ "$failed" == "true" ]]; then
            echo "⚠ Rolling back to $backup_label..." >&2
            kubectl rollout undo deployment/myapp
            echo "✓ Rollback complete"
        fi
    }

    trap rollback EXIT  # Always run rollback handler on script exit

    kubectl create deployment/myapp-backup --dry-run=client -o yaml | \
      kubectl apply -f - 2>/dev/null || true

    kubectl set image deployment/myapp myapp="myapp:latest" && \
      sleep 10 && \
      curl -sf "https://example.com/health" >/dev/null && \
      failed=false || \
      failed=true
}

Why long && chains work for pipelines: Each operator acts as an implicit gate. The chain reads like a sequential specification: "do A, then B, then C, and stop if any fails." This is more compact than equivalent if/then blocks while being equally safe.

BAD vs GOOD

# ❌ BAD: Semicolons instead of && — failure in one step doesn't prevent the next
docker build -t myapp . ; docker push myapp ; kubectl apply -f deployment.yaml
# If build fails, push and apply still execute with stale image or no changes

# ✅ GOOD: AND-list chain — each step verified before proceeding
docker build -t myapp . && \
  docker push myapp && \
  kubectl apply -f deployment.yaml && \
  kubectl rollout status deployment/myapp --timeout=60s && \
  echo "Deployment verified."

Pattern 5: Atomic Check-Perform-Verify (AND-list with verification)

Atomic operations check a precondition, perform the work, then verify the result — all in a single && chain. This pattern prevents partial state changes by ensuring verification always follows the operation.

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

# Atomic file migration: verify source exists → move it → verify destination has it
atomic_move() {
    local src="$1"
    local dst="$2"

    # Precondition: source must exist and be readable
    [[ -f "$src" && -r "$src" ]] || \
      { echo "ERROR: source not found or not readable: $src" >&2; return 1; }

    # Atomic sequence: check → perform → verify
    cp -v "$src" "$dst" && \
      diff "$src" "$dst" >/dev/null 2>&1 && \
      echo "Atomic move verified: $src → $dst" || \
      { echo "ERROR: verification failed for $dst, cleaning up partial copy" >&2; rm -f "$dst"; return 1; }
}

# Atomic database migration with pre-check and post-verification
run_db_migration() {
    local migration_file="$1"

    # Pre-condition: migration file must exist and be valid SQL
    [[ -f "$migration_file" ]] || { echo "ERROR: migration not found: $migration_file" >&2; return 1; }
    grep -q "^CREATE\|^ALTER\|^INSERT\|^UPDATE\|^DELETE" "$migration_file" 2>/dev/null || \
      { echo "ERROR: migration file does not contain valid SQL statements" >&2; return 1; }

    # Atomic sequence: check → migrate → verify row count
    local before_count
    before_count="$(psql -At -c 'SELECT COUNT(*) FROM users' 2>/dev/null)" || \
      { echo "ERROR: could not query database" >&2; return 1; }

    psql -f "$migration_file" && \
      local after_count="$(psql -At -c 'SELECT COUNT(*) FROM users' 2>/dev/null)" && \
        { [[ "$after_count" != "$before_count" ]] || echo "WARN: row count unchanged (expected for CREATE/ALTER migrations)"; } && \
        echo "Migration verified: $before_count → $after_count rows" || \
        { echo "ERROR: migration may have failed — row count unchanged, review needed" >&2; return 1; }
}

# Atomic config swap with backup and verification
atomic_config_swap() {
    local new_config="$1"
    local target="/etc/myapp/config.yml"

    # Pre-check: new config must be valid YAML (syntax check)
    python3 -c "import yaml; yaml.safe_load(open('$new_config'))" 2>/dev/null && \
      echo "New config syntax OK" || \
      { echo "ERROR: new config has invalid YAML syntax" >&2; return 1; }

    # Atomic swap: backup → replace → verify → reload
    cp "$target" "${target}.bak.$(date +%Y%m%d%H%M%S)" && \
      cp "$new_config" "$target" && \
      diff "$target" "$new_config" >/dev/null 2>&1 && \
        echo "Config verified: ${#new_config} bytes, content matches" || \
        { echo "ERROR: config verification failed, restoring backup" >&2; mv "${target}.bak.*" "$target" 2>/dev/null; return 1; }

    systemctl reload myapp && \
      echo "Config reloaded successfully" || \
      { echo "ERROR: service reload failed, config swap rolled back" >&2; cp "${target}.bak."* "$target" 2>/dev/null; return 1; }
}

Why this is atomic: The check-perform-verify chain guarantees that if verification fails, the operation is undone. The && chain ensures each step succeeds before the next runs, and the final || catches verification failures for rollback.

BAD vs GOOD

# ❌ BAD: No verification — copy might fail silently (disk full, permissions)
cp /tmp/new-config.yml /etc/myapp/config.yml
systemctl reload myapp
# Config could be truncated or empty; no one would know until the app crashes

# ✅ GOOD: Check-perform-verify with rollback on failure
[[ -f /tmp/new-config.yml ]] && \
  cp /etc/myapp/config.yml /etc/myapp/config.yml.bak && \
  cp /tmp/new-config.yml /etc/myapp/config.yml && \
  diff -q /etc/myapp/config.yml /tmp/new-config.yml >/dev/null && \
    systemctl reload myapp && \
    echo "Config updated and verified" || \
    { echo "ERROR: update failed, restoring backup" >&2; mv /etc/myapp/config.yml.bak /etc/myapp/config.yml; }

Pattern 6: The A && B || C Ternary Anti-Pattern and Proper Alternatives

The expression A && B || C is often used as a ternary operator (like condition ? do_this : do_that in other languages). This is dangerous. If A succeeds but B fails, C will execute — not because the condition was false, but because the action failed. This creates subtle bugs that are extremely hard to debug.

# ❌ DANGEROUS: A && B || C as a ternary — C runs if either A or B fails!
check_file_exists "$config" && use_production_config() || load_default_config
# Problem: if check passes but use_production_config crashes, default config loads unexpectedly.
# Problem: if check fails AND use_production_config would succeed, default still loads (correct).
# The ambiguity makes this pattern unreliable for any non-trivial logic.

# ✅ SAFE alternative 1: Use explicit if/then/fi (recommended for readability)
if check_file_exists "$config"; then
    use_production_config
else
    load_default_config
fi

# ✅ SAFE alternative 2: If the action is a simple command, && alone suffices
# Only run the fallback when the primary fails (no ternary semantics needed)
command -v curl >/dev/null 2>&1 || apt-get install -y curl

# ✅ SAFE alternative 3: Use function return codes for controlled branching
run_with_fallback() {
    if ! "$@"; then
        echo "Primary command failed, running fallback..." >&2
        run_fallback_command
    fi
}
run_with_fallback my_expensive_operation --flag1 --flag2

# ✅ SAFE alternative 4: Separate the concerns into distinct chains
check_file_exists "$config" && \
  use_production_config || \
  { echo "Config not found, loading defaults" >&2; load_default_config; }
# Note: The braces {} group the fallback so it runs as one unit on failure.

The rule: Never use A && B || C as a ternary when B might fail for reasons unrelated to the condition A. Use if/then/fi instead. The only safe case for A && B || C is when B is guaranteed not to fail (e.g., a simple variable assignment or echo).


Interaction with set -e and pipefail

The interaction between chaining operators and set -e (errexit) is one of the most misunderstood aspects of bash scripting. Understanding it prevents scripts from aborting unexpectedly or continuing when they should stop.

How set -e Interacts with Each Operator

&& and || exempt their left-hand side from errexit. This is by design: if you explicitly write a conditional chain, bash assumes you are intentionally handling the failure case.

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

# ❌ Without &&/||, set -e would abort here on any non-zero exit
false  # This WOULD trigger errexit and abort the script

# ✅ With &&, the left-hand side is exempt from errexit
false && true   # Does NOT abort — `false` is in an AND-list, its failure is "handled"
echo "Still running"  # Prints because the chain result depends on `true`, not `false`

# ✅ With ||, same exemption applies
false || echo "fallback"  # Does NOT abort — `false` is in an OR-list
echo "Still running"       # Prints

The key insight: A command that returns non-zero inside && or || does NOT trigger errexit. Bash treats these as explicit error handlers. This means:

set -euo pipefail

# These are ALL safe under set -e (won't abort):
command_that_might_fail && handle_success || handle_failure
maybe_missing_command && optional_step
find /tmp -name "*.log" -delete 2>/dev/null || true

# But this WILL abort (non-zero exit outside any chain):
might_fail_always   # errexit triggers here if might_fail_always returns non-zero

Interaction with set -o pipefail

pipefail changes how pipeline exit codes are computed: the pipeline returns the exit status of the rightmost command that failed (or 0 if all succeeded). This interacts with chaining in important ways:

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

# Without pipefail: pipeline returns exit code of LAST command only
echo "hello" | grep "NOTFOUND" | true   # Returns 0 (true succeeds)
                                            # Would silently pass even though grep failed!

# With pipefail: pipeline returns exit code of the FIRST failing command
echo "hello" | grep "NOTFOUND" | true   # Returns 123 (grep's non-zero exit)
                                            # This failure propagates through && chains

# Practical impact on chaining:
grep "error" /var/log/syslog | wc -l && \
  echo "Found some errors" || \
  echo "No errors found or grep failed"

# With pipefail, if the file doesn't exist, grep returns non-zero and
# the || branch executes — this is usually the desired behavior.

Recommendation: Always use set -euo pipefail together. The -u flag catches unset variables, and pipefail ensures pipeline failures don't get hidden by a trailing true.

Exit Code Summary Table

Expression Left command fails (exit 1) Left command succeeds (exit 0)
A ; B B runs unconditionally. Chain exit = exit of B. B runs unconditionally. Chain exit = exit of B.
A && B B does NOT run. Chain exit = 1 (left's exit). Short-circuits. B runs. Chain exit = exit of B.
A || B B runs. Chain exit = exit of B. B does NOT run. Chain exit = 0 (left's exit). Short-circuits.
A && B ; C A fails → B skipped → C runs. Exit = exit of C. A succeeds → B runs → C runs. Exit = exit of C.

Constraints

MUST DO

  • Use set -euo pipefail as the first line of every non-trivial bash script
  • Quote all variable expansions in chains: [[ -f "$file" ]] && mv "$file" "$dest" (never "$file")
  • Use && for sequential dependencies where each step requires the previous one to succeed
  • Use || for fallback chains, defaulting, and error recovery paths
  • Use ; between a conditional chain (&&/||) and cleanup code that must always run
  • Group complex fallback logic in { ... } or (...) blocks for clarity and scoping
  • Test each operator branch independently before combining into long chains
  • Add echo status messages after major chain steps for observability

MUST NOT DO

  • Never use A && B || C as a ternary when B might fail — use explicit if/then/fi instead
  • Never use ; to separate conditional steps that have dependencies — use && instead
  • Never assume the right-hand side of || ran successfully — always verify fallback outcomes
  • Never put cleanup code after && — it won't run on failure. Use ; or a trap EXIT handler.
  • Never chain more than 5–6 operators on one line — break into named functions with clear step labels
  • Never use set -e without also using set -o pipefail and set -u (unbound variables are equally dangerous)

Output Template

When implementing or reviewing shell command chains, produce:

  1. Operator Choice — Which operator (&&, ||, ;) was used for each link and why
  2. Execution Flow Diagram — ASCII representation of the chain's control flow showing success/failure branches
  3. set -e Compliance — Confirm that all commands are properly placed (no non-chained commands that could trigger errexit unexpectedly)
  4. Verification Points — Where in the chain does verification occur? Is there a check-perform-verify pattern for critical operations?
  5. Cleanup Strategy — How does cleanup code execute regardless of success/failure? (semicolon, trap, or subshell)

Related Skills

Skill Purpose
linux-shell-process-management Backgrounding commands with &, job control (jobs, fg, bg), and process synchronization (wait) — complements chaining for concurrent workflows
shell-parameter-expansion Variable substitution patterns (${VAR:-default}, ${VAR:?error}) that work alongside chains for defensive scripting — use ${VAR:?check} inside validation gates

Live References

Authoritative documentation links for shell command chaining and conditional execution in bash.

Install via CLI
npx skills add https://github.com/paulpas/agent-skill-router --skill shell-command-chaining
Repository Details
star Stars 4
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator