| |

Bash Scripting 02 ๐Ÿง Variables & Types โ€” Declaring strings, integers, and read-only values

โฑ๏ธ Estimated read: 8 min | |

The previous chapter covered the script execution mechanics. This chapter covers the storage of the data: the variables. In Bash, the variables are untyped by default. A variable can hold the string, the number, or the array, and the shell infers the type from the context. The declare builtin (and its synonyms, typeset and local) is the mechanism for enforcing the type. The critical distinction is between the untyped assignment (VAR=value), which is always the string, and the typed declaration (declare -i, declare -a, declare -r), which adds the semantics. The read-only variable (declare -r) is the constant, and the assignment after the declaration fails. The integer variable (declare -i) performs the arithmetic evaluation on the assignment, and the string value that is not numeric becomes the 0.

Key point: The declare -i treats the variable as the integer and evaluates the arithmetic expressions on the assignment. The declare -r makes the variable read-only and prevents the reassignment. The declare -a declares the indexed array. The declare -A declares the associative array. The declare -x exports the variable to the child processes. The local inside the function limits the scope to the function. The readonly command is the synonym for the declare -r. The unset removes the variable (except the read-only).


Why variable types require a specific mental model

The untyped problem. The VAR=5 assigns the string "5", not the number 5. The VAR+1 is the string concatenation "51", not the arithmetic. The $((VAR + 1)) is the arithmetic because the $(( )) parses the value. The declare -i VAR=5 followed by the VAR+=1 produces 6 because the integer attribute makes the += the arithmetic addition .

The read-only problem. The readonly VAR=value prevents the reassignment. The second assignment fails with the error: “readonly variable.” The unset of the read-only variable fails. The read-only attribute is the permanent for the shell session. The declare -r is the same. The read-only variable is useful for the configuration constants and the paths that must not be overwritten .

The scope problem. The variable is the global by default. The function that assigns the variable without the local overwrites the caller’s value. The local VAR=value inside the function limits the scope to the function. The local is only valid inside the function. The declare inside the function without the local creates the global. The local -r combines the scope and the read-only .

The type attribute problem. The declare attributes are the sticky. The declare -i VAR sets the integer attribute. The VAR=abc becomes the 0 (the invalid integer). The declare -l VAR lowercases the value on the assignment. The declare -u VAR uppercases. The declare -a creates the array. The declare -A creates the associative array. The attributes are the added to the variable, and the declare +i removes the integer attribute .

The export problem. The export VAR makes the variable visible to the child processes. The child process cannot modify the parent’s variable. The declare -x combines the declaration and the export. The export -n removes the export. The environment variable is the exported variable .

The trade-off. The untyped variable is the flexible and the simple. The typed variable is the safer and the self-documenting. The declare -i prevents the string concatenation bugs. The declare -r prevents the accidental overwrites. The practice is the explicit type when the semantics matter.


a. The untyped variables and the declare

The untyped variable is the string.

#!/bin/bash
VAR=5
VAR+=1
echo "$VAR"
# 51 (the string concatenation, not the arithmetic)

VAR2=$((5 + 1))
echo "$VAR2"
# 6 (the arithmetic via the $(( )))

The VAR+=1 is the string append. The $((5 + 1)) is the arithmetic. The untyped variable requires the explicit arithmetic context.

The declare -i makes the variable the integer.

#!/bin/bash
declare -i COUNT=5
COUNT+=1
echo "$COUNT"
# 6 (the arithmetic addition, not the append)

declare -i BAD="abc"
echo "$BAD"
# 0 (the invalid integer becomes the 0)

The declare -i evaluates the arithmetic on the assignment. The COUNT+=1 is the 6. The non-numeric value becomes the 0.


b. The read-only and the exported variables

The readonly prevents the reassignment.

#!/bin/bash
readonly CONFIG_PATH="/etc/myapp/config.conf"
echo "$CONFIG_PATH"
# /etc/myapp/config.conf

CONFIG_PATH="/tmp/override"
# bash: CONFIG_PATH: readonly variable

unset CONFIG_PATH
# bash: unset: CONFIG_PATH: cannot unset: readonly variable

The readonly is the same as the declare -r. The assignment fails. The unset fails. The variable is the permanent for the session.

The export makes the variable visible to the child processes.

#!/bin/bash
export APP_ENV="production"
bash -c 'echo "$APP_ENV"'
# production

LOCAL_VAR="local"
bash -c 'echo "$LOCAL_VAR"'
# (empty, not exported)

The export passes the variable to the child. The unexported variable is the invisible to the child. The export -n removes the export.


c. The arrays and the associative arrays

The indexed array.

#!/bin/bash
declare -a SERVERS=("web01" "web02" "db01")
echo "${SERVERS[0]}"
# web01

echo "${SERVERS[@]}"
# web01 web02 db01

echo "${#SERVERS[@]}"
# 3

SERVERS+=("cache01")
echo "${SERVERS[3]}"
# cache01

The declare -a creates the indexed array. The SERVERS[0] is the first element. The ${SERVERS[@]} is all the elements. The ${#SERVERS[@]} is the count. The += appends.

The associative array.

#!/bin/bash
declare -A PORTS
PORTS[http]=80
PORTS[https]=443
PORTS[ssh]=22

echo "${PORTS[https]}"
# 443

for key in "${!PORTS[@]}"; do
  echo "$key โ†’ ${PORTS[$key]}"
done
# http โ†’ 80
# https โ†’ 443
# ssh โ†’ 22

The declare -A creates the associative array. The PORTS[https]=443 is the key-value. The ${!PORTS[@]} is the keys. The ${PORTS[$key]} is the value.


Complete Example Session

# ============================================
# PART 1: THE UNTYPED VARIABLE
# ============================================

VAR=5
VAR+=1
echo "$VAR"
# 51

# ============================================
# PART 2: THE INTEGER VARIABLE
# ============================================

declare -i COUNT=5
COUNT+=1
echo "$COUNT"
# 6

# ============================================
# PART 3: THE INVALID INTEGER
# ============================================

declare -i BAD="abc"
echo "$BAD"
# 0

# ============================================
# PART 4: THE READ-ONLY
# ============================================

readonly CONFIG="/etc/app.conf"
echo "$CONFIG"
# /etc/app.conf

CONFIG="/tmp/x"
# bash: CONFIG: readonly variable

# ============================================
# PART 5: THE EXPORT
# ============================================

export APP_ENV="prod"
bash -c 'echo $APP_ENV'
# prod

# ============================================
# PART 6: THE UNEXPORTED
# ============================================

LOCAL_VAR="local"
bash -c 'echo $LOCAL_VAR'
# (empty)

# ============================================
# PART 7: THE INDEXED ARRAY
# ============================================

declare -a SERVERS=("web01" "web02")
SERVERS+=("db01")
echo "${SERVERS[@]}"
# web01 web02 db01

# ============================================
# PART 8: THE ASSOCIATIVE ARRAY
# ============================================

declare -A PORTS
PORTS[http]=80
PORTS[https]=443
echo "${PORTS[https]}"
# 443

# ============================================
# PART 9: THE LOCAL
# ============================================

my_func() {
    local VAR="local"
    echo "$VAR"
}
my_func
# local

echo "$VAR"
# (empty, the global is not set)

The nine parts trace the variables: the untyped, the integer, the invalid integer, the read-only, the export, the unexported, the indexed array, the associative array, and the local.


Quick Reference

The declare Options

OptionPurpose
-iThe integer
-rThe read-only
-aThe indexed array
-AThe associative array
-xThe export
-lThe lowercase
-uThe uppercase
-pThe print the declaration

The Read-Only

CommandEffect
readonly VAR=valueThe read-only
declare -r VAR=valueThe same
unset VARFails if the read-only

The Array Syntax

SyntaxPurpose
${arr[0]}The first element
${arr[@]}All the elements
${#arr[@]}The count
${!arr[@]}The keys (associative)
arr+=(item)The append

The Scope

CommandScope
VAR=valueThe global
local VAR=valueThe function
declare -x VARThe exported

Best Practices

โœ… Do This:

declare -i COUNT=0
<!-- Use the integer for the counters -->             # โœ…
readonly CONFIG="/etc/app.conf"
<!-- Use the read-only for the constants -->         # โœ…
local TEMP="$1"
<!-- Use the local inside the functions -->          # โœ…

โŒ Don’t Do This:

COUNT=0
COUNT+=1
<!-- The string concatenation, not the arithmetic --> # โŒ
# Reuse the read-only variable
<!-- The assignment fails -->                        # โŒ
# Assign the global inside the function
<!-- Overwrites the caller's value -->               # โŒ

Common Pitfalls

PitfallWhy It HappensFix
The += appends instead of addsThe untyped variabledeclare -i
The invalid integer becomes 0The declare -i coercionValidate before the assignment
The read-only unset failsThe read-only attributeThe unset is not allowed
The child does not see the variableNot exportedexport VAR
The function overwrites the globalNo locallocal VAR

Real-World Examples

1. The Integer Counter

declare -i COUNT=0
COUNT+=1

2. The Read-Only Path

readonly LOG_DIR="/var/log/app"

3. The Exported Environment

export APP_ENV="prod"

4. The Indexed Array

declare -a SERVERS=("web01" "web02")

5. The Associative Array

declare -A PORTS=([http]=80 [https]=443)

6. The Array Iteration

for s in "${SERVERS[@]}"; do echo "$s"; done

7. The Local Scope

my_func() { local VAR="local"; }

8. The Unset

unset TEMP

9. The Print the Declaration

declare -p COUNT

10. The Remove the Attribute

declare +i COUNT

Visual

The Type Attributes

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  VAR="5"                                     โ”‚
โ”‚  โ†’ The string "5"                            โ”‚
โ”‚  โ†’ VAR+=1 โ†’ "51"                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  declare -i VAR=5                            โ”‚
โ”‚  โ†’ The integer 5                             โ”‚
โ”‚  โ†’ VAR+=1 โ†’ 6                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Variable Scope

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Global shell                                โ”‚
โ”‚  โ”œโ”€โ”€ GLOBAL="g" (visible)                    โ”‚
โ”‚  โ””โ”€โ”€ my_func()                               โ”‚
โ”‚      โ”œโ”€โ”€ local LOCAL="l"                     โ”‚
โ”‚      โ””โ”€โ”€ GLOBAL="modified" (overwrites)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  The local is discarded on the return        โ”‚
โ”‚  The global is modified if no local          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
The untypedVAR=value (the string)
The integerdeclare -i VAR
The read-onlydeclare -r or readonly
The indexed arraydeclare -a
The associative arraydeclare -A
The exportdeclare -x or export
The locallocal VAR
The list the declarationsdeclare -p

Key takeaways:

  • The untyped variable is the string. The VAR+=1 appends, not adds. The arithmetic requires the $(( )) or the declare -i.
  • The declare -i evaluates the arithmetic. The COUNT+=1 becomes the 6. The invalid value becomes the 0. The integer attribute is the sticky.
  • The readonly prevents the reassignment. The unset also fails. The read-only is the constant for the session. The declare -r is the same.
  • The export passes the variable to the child. The unexported is the invisible. The declare -x is the combined declaration and export.
  • The arrays are the indexed (-a) and the associative (-A). The ${arr[@]} is all the elements. The ${!arr[@]} is the keys. The += appends.

Remember: The declare is the type. The readonly is the constant. The export is the environment. The local is the scope. The array is the collection. The exam tests the variable semantics, and the explicit type prevents the string concatenation bugs.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!