Bash Variables and Default Values Made Easy (Part 14 of 34)






11 Bash Variables and Default Values Made Easy | LinuxTeck




Learning bash variables types explained with examples is the foundation every Linux scripter needs before writing anything serious. The first bash script I attempted to run produced an error that made absolutely no sense to me. It turned out I had written name = "LinuxTeck" with spaces around the equals sign, exactly the way you would in Python or many other programming languages. Bash does not care how other languages work. That single extra space makes Bash interpret the variable name as a command, and the resulting error message gives you very little clue about what actually went wrong.

Every bash script you write depends on understanding bash variable types explained with examples. Variables are the foundation of automation, user input handling, conditions, loops, and script logic. That is why it is important to understand the six common types of bash variables, the three most common mistakes beginners make, and which variable misuse can silently break your entire script without producing a clear error message.

This guide is for anyone who has opened a bash script, seen $1 or $HOME and wondered what is actually going on. It covers everything from the simplest assignment to production-level techniques that most tutorials skip entirely.

Why Variables Matter More Than You Think

Variables are not just storage boxes. They control how your scripts behave across different machines, users, and environments. Get them wrong and:

  • A filename with spaces silently deletes the wrong files
  • An unexported variable vanishes the moment a function runs
  • A configuration value gets overwritten mid-script with no warning
  • Your systemd service starts blind because it cannot see your shell variables

Every one of those failures is covered here, with the fix alongside it.

If you are just getting started with scripting in general, it helps to first get comfortable with what bash scripting actually is before going deeper into variables.



#01

What Bash Actually Does With Your Variables (And Why Types Matter)

Before touching any syntax, it helps to understand what bash is doing under the hood. When bash runs a script, it reads it line by line. Whenever it sees a dollar sign followed by a word, it swaps that token out for the value stored in that variable. This is called parameter substitution, or variable expansion. The actual replacement happens before the command even runs.

So when you write:

bash
LinuxTeck.com
# Bash reads this, finds $name, swaps it out before echo ever runs
name="LinuxTeck"
echo "Hello $name"

# To see every variable currently defined in your shell session
set

# To remove a variable entirely
unset name
echo "$name" # prints nothing — variable is gone

OUTPUT
Hello LinuxTeck
(set output shows all variables — truncated here for readability)

Concept: Bash Is Untyped by Default

Bash stores everything as a string. It only interprets a value as a number when you use arithmetic syntax like $(( )), and only as an array when you access it with index notation. The declare command lets you opt into stricter typing when you need it. You will see this in Section 06.

The set command is genuinely useful during debugging. Run it at any point in a script and you get a full snapshot of every variable in your current shell session. Most beginners do not know it exists, but it saves a lot of guesswork. Learn more about your shell scripting environment setup to understand what is already loaded before your script even starts.



#02

User-Defined Variables: The Rules, the Syntax, and the Mistakes

User-defined variables are the ones you create yourself inside a script or session. They are simple to define, but bash has a few rules that catch people off guard.

Naming rules: variable names can only contain letters, numbers, and underscores. They cannot start with a number. They are case sensitive, so myvar and MyVar and MYVAR are three different things. By convention, keep user-defined variables lowercase and system/environment variables uppercase. This prevents accidental collisions with variables the shell already uses.

bash
LinuxTeck.com
#!/bin/bash

# Valid variable names
server_name="web01"
port=8080
_private="hidden"
release2026="v5.1"

# Accessing values
echo "$server_name"
echo "$port"

# Using curly braces for clarity (recommended in strings)
echo "Server: ${server_name}"

# Invalid — causes error
# 2server=web (starts with number)
# server-name=x (hyphen not allowed)

OUTPUT
web01
8080
Server: web01

Common Mistake: Spaces Around the Equals Sign

This is the single most common bash variable error, and the error message makes no sense when you see it for the first time:

name = "LinuxTeck" gives you: bash: name: command not found

Bash sees the space and reads name as a command, = and "LinuxTeck" as arguments to that command. The correct form is name="LinuxTeck" with no spaces anywhere around the equals sign. No exceptions. This trips up anyone coming from Python, JavaScript, or basically any other language where spacing around = is harmless.

Another common trap: forgetting to quote variables when you use them. If filename="my report.txt" and you run rm $filename without quotes, bash splits it on the space and tries to delete two files called my and report.txt. Always use rm "$filename". The quotes tell bash to treat the whole value as a single unit. This matters everywhere variables are referenced, not just with rm. Check the bash hello world guide for a safe first script to practice this in.



#03

Local, Global, and the export Gateway

Scope is where most variable confusion happens. Not just for beginners. The rules in bash are different enough from other languages that even experienced programmers get caught out.

By default, any variable you define in a script is global to that script. Every function in the script can read and change it. That sounds convenient but it causes real problems in larger scripts when a function accidentally overwrites a value it was not supposed to touch.

bash
LinuxTeck.com
#!/bin/bash

deploy_env="production" # global variable

check_env() {
local tmp_status="checking" # local — only inside this function
echo "Status: ${tmp_status}"
echo "Environment: ${deploy_env}"
}

check_env
echo "After function: ${tmp_status}" # empty — local is gone
echo "Global still: ${deploy_env}"

OUTPUT
Status: checking
Environment: production
After function:
Global still: production

Now here is the part that confuses almost everyone: the difference between a script-global variable and an environment variable that child processes can actually see.

When you run a script, bash launches a child process. Variables defined in your current shell are not automatically passed into that child. You have to use export to make that happen. Export tells bash to include the variable in the environment of any child process this shell starts from now on.

bash
LinuxTeck.com
# Without export — child process cannot see it
app_mode="debug"
bash -c 'echo $app_mode' # prints nothing

# With export — child process gets a copy
export app_mode="debug"
bash -c 'echo $app_mode' # prints: debug

# Export is one-way only
# Changes inside a child script do NOT flow back up to the parent

OUTPUT
debug

Distro Note: Where to Make Variables Persist

For variables that should survive a reboot or be available in every new terminal session, add them to the right file depending on your system:

  • Ubuntu / Debian: add export MYVAR="value" to ~/.bashrc (interactive shells) or ~/.bash_profile (login shells). Ubuntu 22.04+ runs Bash 5.1 by default.
  • Rocky Linux / RHEL 9: also ships Bash 5.1. For system-wide variables available to all users, place a file in /etc/profile.d/ rather than editing ~/.bashrc. That is the RHEL-family convention.
  • macOS users: note that macOS ships Bash 3.2 by default. Associative arrays (declare -A) require Bash 4+. Scripts written on macOS that use associative arrays will fail on macOS without installing a newer bash via Homebrew.

Production Warning: systemd Cannot See Your Shell Variables

This one breaks things silently. Environment variables you set in ~/.bashrc or exported in your terminal are not inherited by systemd services. systemd does not load your user shell at all when starting a service.

Fix: add your variables to the service unit file using EnvironmentFile=:

[Service]
EnvironmentFile=/etc/myapp/env.conf

Inside that file, one variable per line: APP_MODE=production. No export keyword, no quotes needed. This is the correct way to pass configuration into systemd-managed services.

For a practical look at how this applies to scheduled scripts and automated tasks, see the guide on cron command usage in Linux where environment scope matters in the same way.



#04

Environment Variables: The Ones the Shell Already Set for You

Before your script runs a single line, bash has already loaded dozens of variables. These are environment variables, and they hold information about the current user, the system, the shell version, and the paths where programs live. You use them constantly, usually without realising it.

bash
LinuxTeck.com
# View all environment variables
printenv

# Or use env
env

# Check specific ones
echo $HOME
echo $USER
echo $PATH
echo $PWD
echo $BASH_VERSION
echo $LANG
echo $HOSTNAME

OUTPUT
/home/adminuser
adminuser
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/home/adminuser
5.1.16(1)-release
en_US.UTF-8
web01.linuxteck.com

These variables are yours to use directly in scripts. If your script needs to know the home directory of whoever runs it, $HOME is the right way to get it. Hardcoding /home/john breaks the moment someone else runs the same script.

Production Technique: Lock Configuration With readonly

In any script that touches production systems, declare your core configuration variables as readonly at the top. This means if any part of the script accidentally tries to overwrite them, bash throws an error immediately instead of continuing silently with wrong values.

readonly DB_HOST="db01.internal"

declare -r MAX_RETRIES=3

Both forms do the same thing. readonly is slightly more readable; declare -r fits neatly alongside other declare flags. Either way, an attempt to reassign the variable will stop your script with an error, which is exactly what you want in production.

Understanding how PATH is structured is closely related to this, since it is itself a variable that controls everything bash can find and run. The guide on bash PATH and command lookup covers exactly how that works.



#05

Special Variables: The Hidden Power of $?, $$, $@, and the Rest

Special variables are set and managed by bash itself. You cannot assign to most of them. They give you real-time information about what is happening as your script runs: which arguments came in, whether the last command succeeded, and what process is running. These are the variables that separate functional scripts from fragile ones.

Variable What It Contains Practical Use
$0 Name of the script Error messages, logging
$1 to $9 Positional arguments (1st to 9th) Reading user input to script
${10}, ${11}... Arguments beyond the 9th Requires curly braces
$# Total number of arguments passed Validate argument count
$@ All arguments as separate items Safe to loop over
$* All arguments as one string Rarely what you want in loops
$? Exit status of last command (0=ok) Error checking after commands
$$ PID of current shell/script Unique temp file names
$! PID of last background process Track background jobs
bash
LinuxTeck.com
#!/bin/bash

echo "Script name: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "Total args: $#"
echo "All args: $@"
echo "This PID: $$"

# Check if the last command worked
ls /nonexistent 2>/dev/null
echo "Exit status: $?" # non-zero means it failed

ls /tmp
echo "Exit status: $?" # 0 means success

OUTPUT
Script name: ./myscript.sh
First arg: web01
Second arg: production
Total args: 2
All args: web01 production
This PID: 14382
Exit status: 2
Exit status: 0

The $* vs $@ Distinction (Gets People Every Time)

$* and $@ both mean "all arguments." The difference only shows up when you put them in double quotes inside a loop, and that difference matters a lot.

"$*" treats all arguments as one single string. If argument two contains a space, it gets merged with the others. Your loop gets one iteration instead of separate ones.

"$@" treats each argument as its own item, preserving spaces inside individual arguments. Always use "$@" when looping over arguments. There is almost no situation in practice where "$*" is the right choice in a loop.

Example: for arg in "$@"; do echo "$arg"; done handles "my file.txt" as one item. The same loop with "$*" does not.

Exit codes tie directly into how you write reliable scripts. For more on that, the bash exit codes and error handling guide picks up exactly where this section leaves off.



#06

Arrays and Typed Variables: When Strings Are Not Enough

Sometimes you need to store a list of values, not just one. That is where arrays come in. Bash supports two kinds: indexed arrays (numbered like a list) and associative arrays (key-value pairs like a dictionary). You also have declare flags to add integer or readonly constraints.

bash
LinuxTeck.com
#!/bin/bash

# Indexed array — list of servers to check
declare -a servers=("web01" "web02" "db01" "cache01")

# Access by index (starts at 0)
echo ${servers[0]} # web01
echo ${servers[2]} # db01

# Length of array
echo "Total: ${#servers[@]}"

# Loop over every server
for server in "${servers[@]}"; do
echo "Checking: $server"
done

# Associative array — requires Bash 4+ (not macOS default bash)
declare -A service_ports
service_ports["nginx"]=80
service_ports["ssh"]=22
service_ports["mysql"]=3306

# Access by key
echo "nginx port: ${service_ports[nginx]}"

# Integer variable — arithmetic happens automatically
declare -i retry_count=0
retry_count=retry_count+1
echo "Retry count: $retry_count"

OUTPUT
web01
db01
Total: 4
Checking: web01
Checking: web02
Checking: db01
Checking: cache01
nginx port: 80
Retry count: 1

Note: declare -A Needs Bash 4 or Later

Associative arrays with declare -A were introduced in Bash 4. Ubuntu 22.04 and Rocky Linux 9 both ship Bash 5.1, so they work fine. However, macOS ships with Bash 3.2 by default. If you write scripts with associative arrays on Linux and then try to run them on a Mac, they will fail. Either add a version check at the top of your script or document that Bash 4+ is required.

Arrays are particularly useful in automation scripts. See the Linux bash scripting automation guide for real patterns using arrays in production-style scripts.



#07

A Real Script That Uses All Six Variable Types Together

Reading about variable types in isolation is one thing. Seeing them work together in a script that does something real is another. This is a deployment pre-flight check script. It uses user-defined, environment, special, positional, readonly, and array variables, all in one place. Run it like: bash preflight.sh web01 production

bash
LinuxTeck.com
#!/bin/bash

# readonly: lock config values at the top
readonly SCRIPT_NAME="preflight-check"
readonly MIN_DISK_GB=5

# positional: arguments passed at runtime
target_server=$1
deploy_env=$2

# default values if args missing (parameter expansion)
: ${target_server:=localhost}
: ${deploy_env:=staging}

# environment: use what the shell already knows
running_user=$USER
start_dir=$PWD

# array: services that must be running
declare -a required_services=("nginx" "sshd" "firewalld")

echo "=== $SCRIPT_NAME ==="
echo "Run by: $running_user"
echo "Target: $target_server"
echo "Env: $deploy_env"

for svc in "${required_services[@]}"; do
systemctl is-active --quiet "$svc"
if [ $? -eq 0 ]; then
echo "[OK] $svc is running"
else
echo "[FAIL] $svc is NOT running"
fi
done

echo "Pre-flight done. PID was: $$"

OUTPUT
=== preflight-check ===
Run by: adminuser
Target: web01
Env: production
[OK] nginx is running
[OK] sshd is running
[FAIL] firewalld is NOT running
Pre-flight done. PID was: 18274

Notice the : ${target_server:=localhost} lines. That is parameter expansion with a default value. If $1 was not passed when the script was called, the variable gets set to localhost automatically instead of staying empty and breaking things downstream. This pattern appears constantly in real scripts and barely gets a mention in most tutorials. For more scripting patterns like this, the interactive shell scripts guide has practical examples you can adapt directly.



#08

Quick Reference: All Variable Types at a Glance

This table covers bash variables types explained with examples in one scannable reference. It covers all six variable types, the syntax, scope, and a working example for each one.

Type How to Define Scope Example Notes
User-defined name=value Current script port=8080 No spaces around =
Local local name=value Function only local tmp="x" Inside functions only
Environment export name=value Current + child processes export APP=prod One-way to children
Readonly readonly name=value or declare -r Current script readonly MAX=10 Cannot be overwritten
Special Set by bash automatically Automatic $?, $@, $$, $0 Cannot assign to most
Array (indexed) declare -a arr=(a b c) Current script ${arr[0]} Index starts at 0
Array (associative) declare -A map Current script ${map[key]} Bash 4+ required
Integer declare -i num=0 Current script num=num+5 Auto-evaluates math



FAQ

Questions I Get Asked About This All the Time

What is the difference between $* and $@ in bash?

Both give you all the arguments passed to a script. The difference only matters when you quote them inside a loop. "$@" keeps each argument as a separate item, even if it contains spaces. "$*" merges everything into one string. In almost every real use case, you want "$@". Use it by default unless you have a specific reason to treat all arguments as a single string.

Why does myvar = "hello" fail with "command not found"?

Because bash interprets the space before the equals sign as a separator between a command name and its arguments. It sees myvar as a command you want to run, and then = and "hello" as arguments to that command. Since myvar is not an actual command, you get "command not found." The fix is simple: remove all spaces. Write it as myvar="hello" with nothing on either side of the equals sign.

How do I make a bash variable available to all scripts, not just the current one?

Add it to your shell startup file with export. On Ubuntu and most Debian systems, put it in ~/.bashrc. On Rocky Linux and RHEL, you can use ~/.bashrc for personal use or drop a file into /etc/profile.d/ for system-wide availability. After adding it, either start a new terminal or run source ~/.bashrc to load the change immediately. Remember that this still will not make it available to systemd services. For those, you need EnvironmentFile= in your unit file.

Why can my systemd service not see my shell variables?

systemd does not load your user shell environment when it starts a service. The variables in your ~/.bashrc are never read. To pass variables to a systemd service, create a plain text file (for example /etc/myapp/env.conf) with one variable per line in the format KEY=value, then add EnvironmentFile=/etc/myapp/env.conf to the [Service] block of your unit file. Reload and restart the service after making the change.

What is the difference between $15 and ${15}?

When you use $15, bash reads it as $1 followed by the literal character 5. So instead of getting the 15th argument, you get the first argument with a 5 appended to it. To access any argument beyond the 9th, you must wrap the number in curly braces: ${15}. This tells bash the whole number is the argument index. It is a subtle gotcha that produces wrong output with no error message, which makes it hard to spot.

Can I use a variable before I have defined it?

Yes, bash will not stop you. An undefined variable expands to an empty string by default, which means your commands will silently get blank values instead of failing loudly. This is how subtle bugs sneak into scripts. You can change this behaviour by adding set -u at the top of your script. With that option active, bash will throw an error and stop if you try to use a variable that has not been set yet. It is worth adding to any script that runs in production.

Learn more about reliable scripting practices at the Linux shell scripting interview questions guide.



END

You Now Know What Most Tutorials Skip

Getting comfortable with bash variables types explained with examples is genuinely one of the highest-return things you can do early in your Linux learning. The six types covered here, from simple user-defined variables through to readonly configuration guards and associative arrays, show up in every script worth reading. The mistakes covered here, spaces around equals signs, unquoted variables, wrong use of $*, and the systemd environment gap, are the same ones that cause real failures in real environments.

From here, the natural next step is understanding how bash makes decisions based on variable values. The bash if-elif-else statement guide picks up directly from where variables leave off.

Related Articles

LinuxTeck - A Complete Linux Learning Blog
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.


About Sharon J

Sharon J is a Linux System Administrator with strong expertise in server and system management. She turns real-world experience into practical Linux guides on Linux Teck.

View all posts by Sharon J →

Leave a Reply

Your email address will not be published.

L