---
applyTo: '**/*.sh'
description: 'Bash script authoring conventions'
---

# Bash Script Instructions

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.

<!-- <template-script-structure> -->
```bash
#!/usr/bin/env bash
#
# script-name.sh
# Brief description of what this script does

set -euo pipefail

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

main "$@"
```
<!-- </template-script-structure> -->

### Shebang

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

### Strict Mode

Enable strict error handling at the top of every script:

```bash
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

## Copyright Headers

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.

<!-- <example-copyright-header> -->
```bash
#!/usr/bin/env bash
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: MIT
#
# script-name.sh
# Brief description of script purpose

set -euo pipefail
```
<!-- </example-copyright-header> -->

## 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

```bash
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:

```bash
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

```bash
if [[ "${ENVIRONMENT}" == "prod" ]]; then
  echo "Production environment"
fi

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

## Variables and Naming

### Naming Conventions

| Type                  | Convention                       | Example                  |
|-----------------------|----------------------------------|--------------------------|
| Environment variables | UPPER_SNAKE_CASE                 | `RESOURCE_GROUP_NAME`    |
| Constants             | UPPER_SNAKE_CASE with `readonly` | `readonly MAX_RETRIES=3` |
| Local variables       | lower_snake_case                 | `local file_path`        |
| Function names        | lower_snake_case                 | `validate_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

```bash
# 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:

```bash
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.

```bash
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:

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

### Command Validation

Check for required commands before use:

```bash
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

```bash
# 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:

```bash
## 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:

```bash
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:

```bash
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:

```bash
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:

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

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

### File Permissions

Set appropriate permissions for sensitive files:

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

### Checksum Verification

Verify downloaded files before execution:

```bash
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:

```bash
shellcheck script.sh
```

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

```bash
# 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

```bash
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:

```bash
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[@]}"
```
