Bash declare Flags Explained: -i, -x, -A, -n in Detail
AI generated
$_
#!/
Bash · Variables · Scripting
Bash declare Flags
Understanding -i, -x, -A, -n and -r in detail

declare in Bash is far more than a visible declaration. Every flag, whether integer enforcement, export, associative array, name reference, or readonly, changes how a variable actually behaves on every assignment, with pitfalls that only surface during real script runs.

15 min read declare -i · -x · -A · -n · -r Bash 4.x · 5.x

1. Why declare flags are more than cosmetics

declare in Bash is far more than a way to visibly mark a variable as such. Each optional flag, such as -i, -x, -A, -n, or -r, changes the variable's actual behavior on every following assignment and every read access, not just how it appears in declare -p.

Anyone unfamiliar with these flags gives up built-in error prevention, such as automatic integer checking, and writes manual validation logic instead that Bash already provides out of the box. The following section walks through every important flag individually, shows its actual behavior with an example, and names the pitfalls that show up regularly in real scripts.

2. declare -i: integer enforcement and its side effects on arithmetic

A variable declared with declare -i automatically evaluates every assignment as an arithmetic expression rather than a string. An assignment like count="2 + 3" produces the string "2 + 3" for a normal variable, but the number 5 for a variable declared with -i, because Bash evaluates the right-hand expression through the same mechanism as an arithmetic expansion.

The side effect that surprises many: assigning a non-numeric string to a -i variable does not raise an error, it silently becomes 0. Anyone writing user input into a -i variable should therefore validate with a regular expression beforehand to confirm it is actually a number, rather than relying on Bash to raise an error.


declare -i count
count="2 + 3"
echo "$count"        # 5

count="abc"
echo "$count"        # 0, no error!

if [[ "$1" =~ ^-?[0-9]+$ ]]; then
  declare -i n="$1"
else
  echo "Invalid number: $1" >&2
  exit 1
fi

3. declare -x: exporting into the environment and visibility for subshells

declare -x marks a variable for export, identical to export VAR=value, with the difference that declare -x also combines cleanly with other flags. An exported variable lands in the environment of every child process the script starts from that point on, whether it is another Bash script, a Python program, or any external command.

A common pitfall is assuming export also works in the other direction, mirroring variables from a child process back into the calling script. That is not the case; environment variable inheritance flows exclusively from parent to child. Anyone wanting data back from a child process needs command substitution or a file, never a shared environment variable.


declare -x DEPLOY_ENV="production"

check_env() {
  # Runs in a subshell/child process, sees DEPLOY_ENV automatically
  bash -c 'echo "Child sees: $DEPLOY_ENV"'
}
check_env
# Child sees: production

4. declare -A in detail: the difference from plain indexed arrays

declare -A creates an associative array with string keys, unlike declare -a, which creates an indexed array with sequential numeric indices. The crucial difference shows up on access: array[foo] looks up the key "foo" in an associative array, while in an indexed array "foo" is evaluated as an arithmetic expression instead, which resolves to 0 for a non-numeric string.

An associative array must be declared with declare -A before the very first assignment, otherwise Bash automatically creates an indexed array, and every later assignment using a string key either fails or produces unexpected behavior. That ordering, declare -A first, then assignments, is therefore not a style choice but a strict requirement.


declare -A user
user[name]="Alice"
user[role]="admin"

declare -a list
list[foo]="value"   # "foo" evaluates to 0!
echo "${list[0]}"   # value

5. declare -n: name references as a pass-by-reference mechanism

declare -n creates a name reference, commonly called a nameref: the declared variable behaves like an alias for another variable specified by name. Reads and assignments on the nameref act directly on the referenced variable, giving Bash its only built-in way to pass variables to functions by reference instead of by copy.

The biggest pitfall with namerefs is a name collision: if a function's local nameref shares its name with a variable the caller passes in, a self-reference results and Bash aborts with an error. Good style therefore gives nameref names inside functions a consistent, distinct prefix that does not appear anywhere else in the script.


set_value() {
  local -n ref="$1"
  ref="$2"
}

my_var="old"
set_value my_var "new"
echo "$my_var"   # new

6. declare -r: readonly and when variables should stay immutable

declare -r marks a variable as read-only, identical to readonly VAR=value. Every later attempt to change the value results in an error and, without set -e, an error message on stderr while the script keeps running, which is easily missed if return values are not checked consistently.

Readonly fits anything that should not change after initialization: paths, constants, configuration values passed on the command line. The protection holds for the entire remaining runtime of the script and cannot be undone, not even with another declare, which makes readonly the right tool when accidental overwriting is a real risk.

7. Combining flags: -ir, -ax and typical pitfalls when using several at once

Several flags can be combined in a single declare call, for example declare -ir for a read-only integer constant or declare -ax for an exported integer variable. The order of the letters does not matter, declare -ri and declare -ir behave identically, because Bash applies the flags independently as a bitmask on the variable.

A typical pitfall arises when -r is set too early: declare -ir MAX=10 followed by a later attempt to recompute MAX for a different run fails, because readonly is already active. In scripts computing constants dependent on command-line arguments, -r should therefore only be set once the final value is known, not already at the first declaration.


declare -ir MAX_RETRIES=5
declare -ax WORKER_COUNT=4

echo "$MAX_RETRIES / $WORKER_COUNT"

MAX_RETRIES=10   # error: readonly variable

8. Scope: declare inside functions, local variables, and the -g flag

Called inside a function, declare makes a variable local to the function by default, exactly like local, with the one exception that declare can additionally be combined with -i, -A, -n, or -x. After the function returns, such a variable no longer exists, regardless of whether a global variable with the same name exists.

Anyone who deliberately wants to create or overwrite a global variable inside a function, instead of producing a local copy, needs the -g flag: declare -g -A cache. Without -g, the same call inside a function would create a new, function-local variable that shadows the intended global variable, a bug that often only surfaces on the second function call, when the expected state suddenly disappears.

9. Typical mistakes with declare flags and how to find them

The most common mistakes with declare flags come not from wrong syntax but from wrong assumptions about behavior: that -i raises an error on invalid numbers, that declare -A is automatically inferred for later assignments, or that a nameref and its referenced variable can be changed independently of each other. All three assumptions are wrong and lead to bugs that only show up late in a script run.

ShellCheck already catches many of these pitfalls statically, such as a missing declare -A before associative array use, but it does not catch every runtime bug like the silent 0 conversion with -i. Anyone using declare flags in production should therefore write dedicated tests alongside ShellCheck that cover exactly the edge cases shown here: invalid numbers, nameref name collisions, and a forgotten -g inside functions.

Flag Meaning Typical pitfall Combinable with
-i Integer enforcement, arithmetic evaluation Invalid values silently become 0, no error -r, -x, -g
-x Export into the environment for child processes Only works parent to child, never back -i, -r, -g
-A Associative array with string keys Must be declared before the first assignment -r, -g
-n Name reference, pass-by-reference Name collision with caller variable aborts commonly local inside functions
-r Readonly, write-protected for the runtime Set too early blocks later recomputation -i, -x, -A

Mironsoft

Shell automation, DevOps tooling and deployment infrastructure

Shell scripts that hold up in production?

We review existing Bash scripts, spot fragile patterns and replace them with robust Bash patterns: complete error handling, logging and safe parallelization for your deployment stack.

Code Review

ShellCheck analysis and manual review for critical Bash pattern violations.

Refactoring

Retrofitting error handling, logging and safe file operations.

CI Integration

Wiring ShellCheck and BATS into pipelines and building regression tests.

10. Summary

Bash declare Flags: The Essentials at a Glance

-i integer

Forces arithmetic evaluation on every assignment; invalid values become 0 without an error message.

-x export

Makes a variable visible in the environment of every child process, but only works from parent to child.

-n nameref

Enables pass-by-reference for functions; name collisions with the target variable must be avoided.

-g for global scope

Required inside functions to deliberately create a global variable instead of a local one.

11. FAQ: Bash declare Flags: The Essentials at a Glance

1What happens if I assign an invalid number to a declare -i variable?
Bash silently assigns the value 0 without raising an error. Input should therefore be validated with a regular expression before being written into an integer variable.
2Does declare -x retroactively affect already-running child processes?
No. Export only affects child processes started after the declaration. Already-running processes copied their environment before that point and never see later changes.
3Does declare -A need to precede every assignment or just the first use?
declare -A must precede the very first assignment. Without it, Bash automatically creates an indexed array in which string keys evaluate to 0.
4How do I pass a variable by reference into a function?
With declare -n or local -n inside the function, followed by the target variable's name as the argument. Reads and assignments on the nameref then act directly on the referenced variable.
5What happens when a nameref name collides with the target variable?
Bash detects the self-reference and aborts with an error. Local nameref names inside functions should therefore always carry a distinct prefix that does not appear elsewhere in the script.
6Can a variable protected with declare -r be made writable again later?
No, the write protection holds for the rest of the script's runtime and cannot be lifted by another declare call. A new process or subshell invocation is the only way around it.
7Why do I need declare -g if variables in Bash are global by default?
Only outside functions are variables automatically global. Inside a function, declare without -g creates a function-local variable that shadows a global variable of the same name instead of changing it.
8Can I combine several declare flags at once?
Yes, for example declare -ir for a read-only integer or declare -ax for an exported integer variable. The order of the letters does not matter.
9Does ShellCheck catch mistakes with declare flags?
ShellCheck catches some static issues like a missing declare -A before array use, but it does not catch every runtime bug such as the silent 0 conversion for invalid integer values.
10Is declare -i faster than plain string variables for arithmetic?
The speed difference is negligible in practice. The real benefit of declare -i lies in automatic arithmetic evaluation on every assignment, not in performance.