# Code Style Guide

> Based on the [Google Shell Style Guide](https://google.github.io/styleguide/shellguide.html)
> with the following project-specific adaptations:
> - **Line length**: 100 characters (Google: 80) — shell commands are naturally longer
> - **Naming**: Scoped prefixes (`gr_*`, `radp_*`, `__fw_*`) instead of flat lowercase — needed for framework-scale namespace management
> - **File extensions**: All files use `.sh` — no distinction between executables and libraries

This document describes the coding conventions and style guidelines for radp-bash-framework.

## Shell Compatibility

| Component                         | Shell     | Features                            |
|-----------------------------------|-----------|-------------------------------------|
| `init.sh`, `preflight/stage1/`    | POSIX     | `test`, `[ ]`, no `local` arrays    |
| `preflight/stage2/`, `bootstrap/` | Bash 4.3+ | `[[ ]]`, arrays, `local`, `mapfile` |

**POSIX layer constraints** (`init.sh`, `preflight/stage1/`):

- Use `[ ]` or `test`, not `[[ ]]`
- Do not use `local` (not guaranteed in POSIX)
- Do not use arrays
- Use `$(cat file)`, not `$(< file)`

**Bash layer features** (`preflight/stage2/` and beyond):

- `[[ ]]` conditional expressions
- Indexed and associative arrays
- `local` declarations
- `mapfile` / `readarray`
- `${var,,}` / `${var^^}` case conversion

## Naming Conventions

### Variables

| Prefix   | Scope       | Mutability        | Example                      |
|----------|-------------|-------------------|------------------------------|
| `gr_*`   | Global      | Readonly          | `gr_fw_root_path`            |
| `gw_*`   | Global      | Writable          | `gw_fw_run_initialized`      |
| `gwxa_*` | Global      | Writable array    | `gwxa_fw_sourced_scripts`    |
| `__fw_*` | Internal    | Private           | `__fw_config_cache`          |
| `opt_*`  | Command     | Local (auto-set)  | `opt_dry_run`, `opt_version` |
| `args_*` | Command     | Local (auto-set)  | `args_name`, `args_file`     |
| `gopt_*` | Application | Global (auto-set) | `gopt_config`, `gopt_env`    |

> `opt_*`, `args_*`, and `gopt_*` variables are auto-generated by the framework annotation system
> (`@option`, `@flag`, `@arg`, `@global`). Use them directly in command functions.

**Best practices:**

- Use `local` for function-scoped variables
- Quote variables unless intentional word splitting: `"$var"`
- Use `${var:-default}` for default values
- Use `${var:?error}` for required variables

### Functions

| Pattern              | Meaning                 | Example                    |
|----------------------|-------------------------|----------------------------|
| `radp_*`             | Public API              | `radp_log_info`            |
| `radp_nr_*`          | Nameref (pass var name) | `radp_nr_arr_merge_unique` |
| `radp_<domain>_*`    | Domain-specific         | `radp_os_get_distro_id`    |
| `*_is_*` / `*_has_*` | Boolean (returns 0/1)   | `radp_os_is_pkg_installed` |
| `__fw_*`             | Internal/private        | `__fw_bootstrap`           |

### Commands

| Pattern       | Example                  |
|---------------|--------------------------|
| File location | `commands/db/migrate.sh` |
| Function name | `cmd_db_migrate()`       |
| Invocation    | `myapp db migrate`       |

**Application global options** (`_globals.sh`):

```
commands/_globals.sh          # Define with @global annotation
                              # Same syntax as @option / @flag
                              # Variables: gopt_<name>
                              # Placement: before or after command
```

Example:

```bash
# @global -c, --config <dir> Configuration directory
# @global -e, --env <name> Environment name [default: local]
```

These options are available in all commands as `gopt_config`, `gopt_env`, etc.

## File Header

Every script and library file should begin with a shebang and a brief description:

```bash
#!/usr/bin/env bash
# =============================================================================
# Brief description of what this script/library does
# =============================================================================
```

For POSIX entry scripts (`init.sh`, `preflight/stage1/`):

```sh
#!/bin/sh
# =============================================================================
# Brief description
# =============================================================================
```

## Code Formatting

### Indentation

- Use 2 spaces for indentation
- No tabs

### Line Length

- Aim for 100 characters max
- Break long pipelines at `|`
- Break long strings with `\`

### Quoting

```bash
# Always quote variables
echo "$variable"
cp "$source" "$dest"

# Use single quotes for literal strings
grep -E '^Host '

# Use $'...' for escape sequences
echo $'line1\nline2'
```

### Conditionals

```bash
# Prefer [[ ]] over [ ] in Bash code
if [[ -n "$var" ]]; then
  # ...
fi

# Use [[ ]] for pattern matching
if [[ "$str" == *"pattern"* ]]; then
  # ...
fi

# Boolean functions return 0/1
radp_os_is_pkg_installed() {
  command -v "$1" &>/dev/null
}
```

### Functions

```bash
# Function declaration
my_function() {
  local arg1="$1"
  local arg2="${2:-default}"

  # Implementation
}

# Document complex functions
# Description of what the function does
# Args:
#   $1 - First argument description
#   $2 - Second argument description (optional)
# Returns:
#   0 on success, 1 on failure
# Outputs:
#   Writes result to stdout
```

### Pipelines

Short pipelines may be on one line. Multi-line pipelines should break at `|` with continuation
indented by 2 spaces:

```bash
# Short — one line is fine
command1 | command2

# Long — break at pipe
command1 \
  | command2 \
  | command3
```

### Loops and Control Flow

Place `; then` and `; do` on the same line as `if`/`for`/`while`:

```bash
if [[ -n "$var" ]]; then
  # ...
fi

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

while read -r line; do
  # ...
done < "$input"
```

### Case Statements

Indent alternatives by 2 spaces. Short single-line actions may include `;;` on the same line.
Multi-line actions should have `;;` on its own line:

```bash
case "$action" in
  start) start_service ;;
  stop)  stop_service ;;
  restart)
    stop_service
    start_service
    ;;
  *)
    radp_log_error "Unknown action: $action"
    return 1
    ;;
esac
```

### Command Substitution

Use `$(command)` instead of backticks:

```bash
# Correct
local output
output=$(my_command)

# Incorrect — backticks are harder to read and nest
local output=`my_command`
```

### Arithmetic

Use `(( ))` for arithmetic and `$(( ))` for arithmetic expansion. Do not use `let`, `expr`,
or `$[ ]`:

```bash
# Correct
(( count++ ))
result=$(( width * height ))

# Incorrect
let count++
result=$(expr "$width" \* "$height")
```

## Logging

Use framework logging functions instead of `echo`:

```bash
# Correct
radp_log_debug "Processing file: $file"
radp_log_info "Server started on port $port"
radp_log_warn "Config file not found, using defaults"
radp_log_error "Failed to connect to database"

# Incorrect
echo "Processing file: $file"
echo "ERROR: Failed to connect"
```

## Error Handling

```bash
# Check command success
if ! command -v yq &>/dev/null; then
  radp_log_error "yq not found"
  return 1
fi

# Use set -e with caution (prefer explicit checks)
# set -e can cause unexpected exits

# Handle cleanup with traps
cleanup() {
  rm -f "$temp_file"
}
trap cleanup EXIT
```

### STDOUT vs STDERR

Error messages must go to STDERR. The framework's `radp_log_error` and `radp_log_warn` already
do this. When writing manual error output, use `>&2`:

```bash
echo "Error: file not found" >&2
```

### PIPESTATUS

When using pipelines, check individual command exit codes via `PIPESTATUS` if any stage
can fail independently:

```bash
cmd1 | cmd2 | cmd3
local -a statuses=("${PIPESTATUS[@]}")
if [[ "${statuses[0]}" -ne 0 ]]; then
  radp_log_error "cmd1 failed"
fi
```

### Separate `local` Declaration from Assignment

When assigning command output to a local variable, split the declaration and assignment. `local`
always returns 0, which masks the real exit code:

```bash
# Correct — $? reflects the command's exit code
local output
output=$(my_command)

# Incorrect — $? is always 0 because local succeeded
local output=$(my_command)
```

## Practices

### No `eval`

Do not use `eval`. It makes input injection trivial and code hard to reason about.

### Pipes to `while`

A `while` loop in a pipeline runs in a subshell, so variable assignments inside it are lost.
Use process substitution or `readarray` instead:

```bash
# Correct — variables persist after the loop
while IFS= read -r line; do
  (( count++ ))
done < <(some_command)

# Correct — capture all lines into an array
readarray -t lines < <(some_command)

# Incorrect — count is always 0 after the loop
some_command | while IFS= read -r line; do
  (( count++ ))
done
```

### No Aliases in Scripts

Use functions instead of aliases. Aliases are not expanded in non-interactive shells by default
and are harder to compose and test.

### Wildcard Expansion Safety

Use `./*` instead of `*` when globbing to avoid filenames starting with `-` being interpreted
as options:

```bash
# Correct
rm -f ./*.tmp

# Risky — a file named "-rf" would be interpreted as a flag
rm -f *.tmp
```

### `main()` Pattern

Scripts that contain multiple functions should define a `main()` function and call it at the end.
This keeps global scope clean and makes the entry point explicit:

```bash
main() {
  parse_args "$@"
  do_work
}

main "$@"
```

> **Note:** This pattern applies to standalone scripts. Framework library files (sourced, not
> executed) do not need a `main()` function.

## ShellCheck

Preserve existing ShellCheck directives:

```bash
# shellcheck source=./lib.sh
source "./lib.sh"

# shellcheck disable=SC2034  # Variable used by sourced script
local __result="value"
```

Run ShellCheck before committing:

```bash
shellcheck src/main/shell/**/*.sh
```

## YAML Configuration Keys

| Style        | Usage                                             |
|--------------|---------------------------------------------------|
| `dash-case`  | Framework config keys: `banner-mode`, `log-level` |
| `snake_case` | Plugin options (match Vagrant): `auto_update`     |

## Testing

Tests use [BATS](https://github.com/bats-core/bats-core):

```bash
# Run all tests
bats src/test/shell/

# Run specific test file
bats src/test/shell/toolkit_core.bats
```

See [src/test/shell/README.md](../../src/test/shell/README.md) for writing tests.

## See Also

- [Architecture](./architecture.md) - Framework architecture
- [API Reference](../reference/api.md) - Complete function reference
