Bash Script Instructions

June 26, 2026 ยท View on GitHub

These instructions define conventions for authoring Bash scripts in this repository. Scripts follow Bash 5.x conventions with strict error handling and ShellCheck compliance.

Script Structure

Scripts follow a consistent structure with shebang, header comment, strict mode, and a main function pattern.

#!/usr/bin/env bash
#
# script-name.sh
# Brief description of what this script does

set -euo pipefail

main() {
  # Script logic here
  echo "Executing..."
}

main "$@"

Shebang

Use #!/usr/bin/env bash for portability across systems.

Strict Mode

Enable strict error handling at the top of every script:

set -euo pipefail

This configuration:

  • -e: Exits immediately on command failure
  • -u: Treats unset variables as errors
  • -o pipefail: Propagates pipeline failures

Main Function Pattern

Encapsulate script logic in a main() function called at the end. This pattern:

  • Ensures all functions are defined before use
  • Supports sourcing scripts for testing
  • Provides clear entry point

Every .sh file requires a copyright header immediately after the shebang line.

Two required lines:

  • # Copyright (c) Microsoft Corporation.
  • # SPDX-License-Identifier: MIT

Placement: after #!/usr/bin/env bash, before any other content.

CI validates copyright headers through the repository's copyright validation script, if one is configured. Check package.json for a copyright validation command.

#!/usr/bin/env bash
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
#
# script-name.sh
# Brief description of script purpose

set -euo pipefail

Formatting and Style

Indentation and Line Length

  • Use 2 spaces for indentation, never tabs
  • Limit lines to 80 characters when practical
  • Long commands use backslash continuation
az resource show \
  --resource-group "$RESOURCE_GROUP_NAME" \
  --name "$RESOURCE_NAME" \
  --query id \
  --output tsv

Control Structures

Place then and do on the same line as their control keyword:

if [[ -n "${VAR:-}" ]]; then
  echo "Variable is set"
fi

for item in "${items[@]}"; do
  process "$item"
done

Conditionals and Tests

  • Use [[ ... ]] instead of [ ... ] or test
  • Use (( ... )) for arithmetic operations
if [[ "${ENVIRONMENT}" == "prod" ]]; then
  echo "Production environment"
fi

if (( count > 10 )); then
  echo "Count exceeds threshold"
fi

Variables and Naming

Naming Conventions

TypeConventionExample
Environment variablesUPPER_SNAKE_CASERESOURCE_GROUP_NAME
ConstantsUPPER_SNAKE_CASE with readonlyreadonly MAX_RETRIES=3
Local variableslower_snake_caselocal file_path
Function nameslower_snake_casevalidate_input()

Variable Expansion

  • Use braces for clarity: "${var}" over "$var"
  • Quote all variable expansions unless word splitting is intentional
  • Use command substitution with $(): never backticks
# Variable with default
ENVIRONMENT="${ENVIRONMENT:-dev}"

# Required variable check
if [[ -z "${REQUIRED_VAR:-}" ]]; then
  echo "ERROR: REQUIRED_VAR must be set" >&2
  exit 1
fi

Arrays

Use arrays for lists of elements:

declare -a files=("file1.txt" "file2.txt" "file3.txt")

for file in "${files[@]}"; do
  process "$file"
done

Functions

Define functions before use. Use local for function-scoped variables.

log() {
  local message="\$1"
  printf "========== %s ==========\n" "$message"
}

err() {
  local message="\$1"
  printf "ERROR: %s\n" "$message" >&2
  exit 1
}

validate_input() {
  local input="\$1"
  if [[ -z "${input}" ]]; then
    err "Input cannot be empty"
  fi
}

Error Handling

Error Functions

Implement consistent error reporting:

err() {
  printf "ERROR: %s\n" "\$1" >&2
  exit 1
}

Command Validation

Check for required commands before use:

if ! command -v "az" &>/dev/null; then
  err "'az' command is required but not installed"
fi

Error Visibility

Allow commands to fail naturally with their native error messages. Avoid redirecting stderr to /dev/null unless errors are genuinely irrelevant. Let tools display their built-in error information.

Comments

Keep comments minimal. Add them only when logic requires explanation:

  • Complex regex patterns
  • Non-obvious conditionals
  • Workarounds with context
# Match semantic version pattern: major.minor.patch
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  echo "Valid version"
fi

Document environment variables at the top of scripts that require them:

## Required Environment Variables:
# ENVIRONMENT       - Target environment (dev, prod)
# RESOURCE_GROUP    - Azure resource group name

## Optional Environment Variables:
# DEBUG             - Enable verbose output when set

Usage Functions

Scripts with arguments include a usage function:

usage() {
  echo "Usage: ${0##*/} [OPTIONS]"
  echo ""
  echo "Options:"
  echo "  --help, -h    Show this help message"
  echo "  --verbose     Enable verbose output"
  exit 1
}

Argument Parsing

Use case statements for argument handling:

while [[ $# -gt 0 ]]; do
  case "\$1" in
    --verbose)
      VERBOSE=true
      shift
      ;;
    --output)
      if [[ -z "${2:-}" || "\$2" == --* ]]; then
        echo "Error: --output requires an argument" >&2
        usage
      fi
      OUTPUT_FILE="\$2"
      shift 2
      ;;
    --help|-h)
      usage
      ;;
    *)
      echo "Unknown option: \$1" >&2
      usage
      ;;
  esac
done

File Operations

Create directories safely and handle paths properly:

mkdir -p "$(dirname "$OUTPUT_FILE")"

if [[ -f "${config_file}" ]]; then
  source "${config_file}"
fi

Security Practices

Variable Quoting

Quote variables to prevent word splitting and command injection:

# Correct
rm -f "${temp_file}"
grep "${pattern}" "${file}"

# Avoid
rm -f $temp_file
grep $pattern $file

File Permissions

Set appropriate permissions for sensitive files:

chmod 0600 "${HOME}/.kube/config"

Checksum Verification

Verify downloaded files before execution:

EXPECTED_SHA256="abc123..."
if ! echo "${EXPECTED_SHA256} ${downloaded_file}" | sha256sum -c --quiet -; then
  echo "ERROR: Checksum verification failed" >&2
  rm "${downloaded_file}"
  exit 1
fi

ShellCheck Compliance

All scripts pass ShellCheck validation. Use the VS Code problems panel or run ShellCheck directly:

shellcheck script.sh

When a specific rule needs suppression, add a directive with justification:

# shellcheck disable=SC2034  # Variable used by sourced script
EXPORTED_CONFIG="value"

Azure CLI Patterns

When working with Azure CLI commands:

Output Handling

  • Use --output tsv for single values in scripts
  • TSV output returns empty strings for null values (not the string "null")
  • Use --query with JMESPath for filtering results
resource_id=$(az resource show \
  --resource-group "$RESOURCE_GROUP" \
  --name "$RESOURCE_NAME" \
  --resource-type "Microsoft.Storage/storageAccounts" \
  --query id \
  --output tsv)

if [[ -z "${resource_id}" ]]; then
  err "Resource not found"
fi

Conditional Command Arguments

Build commands with arrays when arguments are conditional:

az_cmd=("az" "connectedk8s" "connect"
  "--name" "$RESOURCE_NAME"
  "--resource-group" "$RESOURCE_GROUP"
)

if [[ "${AUTO_UPGRADE:-true}" == "false" ]]; then
  az_cmd+=("--disable-auto-upgrade")
fi

"${az_cmd[@]}"