Describe why you have to use an array instead string variable, like the example
Using a Variable to Pass Terminal Commands as Arguments in Bash
Level: Intermediate
Category: Bash Scripting
Topic: Commands, Arguments, Arrays, Quoting, and Word Splitting
Introduction
When writing Bash scripts, it is very common to build a command in one place and execute it later.
At first, it may seem natural to store the entire command inside a normal string variable:
MY_COMMAND="sudo -u admin mysqldump -p mydatabase"
and later execute it like this:
$MY_COMMAND
For very simple commands, this may appear to work.
However, storing a command and all its arguments in a string is usually the wrong approach in Bash.
The safer and more reliable solution is to use an array:
MY_COMMAND=(sudo -u admin mysqldump -p mydatabase)
and execute it like this:
"${MY_COMMAND[@]}"
The difference may look small, but it is extremely important.
The reason is that a terminal command is not really one large piece of text.
A command is a collection of separate arguments.
Understanding this distinction helps avoid many common Bash bugs related to:
- spaces
- filenames
- paths
- quotes
- special characters
- dynamically generated options
- command wrappers
- database commands
- backup scripts
- automation scripts
This article explains why Bash arrays are the correct tool for storing commands and their arguments.
1. A Command Is Not Just a String
Consider the following command:
sudo -u admin mysqldump -p mydatabase
Visually, we see a line of text.
But the operating system does not receive that command as one single string.
Conceptually, Bash divides it into separate elements:
Argument 0: sudo
Argument 1: -u
Argument 2: admin
Argument 3: mysqldump
Argument 4: -p
Argument 5: mydatabase
The first element is the program to execute:
sudo
The remaining elements are arguments passed to that program.
We can visualize the command as:
sudo
│
├── -u
├── admin
├── mysqldump
├── -p
└── mydatabase
This distinction is fundamental.
Bash does not think of a command as:
"sudo -u admin mysqldump -p mydatabase"
It ultimately needs something closer to:
["sudo", "-u", "admin", "mysqldump", "-p", "mydatabase"]
That is exactly the kind of structure a Bash array can represent.
2. The Tempting Approach: Using a String Variable
A beginner may write:
MY_COMMAND="sudo -u admin mysqldump -p mydatabase"
and then execute:
$MY_COMMAND
In this particular example, Bash may execute the command successfully.
That can create the impression that storing commands inside strings is perfectly acceptable.
The problem becomes visible when arguments contain spaces or other characters that Bash must preserve.
For example:
MY_COMMAND="cp /srv/files/report.txt /srv/backup files/"
The intended destination might be:
/srv/backup files/
But Bash can split the string into something like:
cp
/srv/files/report.txt
/srv/backup
files/
Instead of passing one destination argument, Bash passes two.
The command no longer means what we intended.
3. Why String Variables Are Problematic
The main issue is called word splitting.
Consider:
MY_COMMAND="printf %s Hello World"
Executing:
$MY_COMMAND
causes Bash to expand the variable and then split the resulting text into words.
Conceptually:
printf
%s
Hello
World
But perhaps we wanted:
printf
%s
Hello World
Those are different argument lists.
The problem becomes even more obvious when working with paths.
Consider:
BACKUP_DIR="/srv/database backups"
and:
MY_COMMAND="mkdir -p $BACKUP_DIR"
Now execute:
$MY_COMMAND
Bash may interpret it as:
mkdir
-p
/srv/database
backups
instead of:
mkdir
-p
/srv/database backups
The argument boundary has been lost.
4. Why Adding Quotes Inside the String Does Not Solve the Problem
A common attempt to fix the problem is this:
MY_COMMAND='mkdir -p "/srv/database backups"'
It looks correct.
If you typed the following directly into the terminal:
mkdir -p "/srv/database backups"
Bash would correctly treat the path as one argument.
But when the quotes are stored inside a variable, they do not behave the same way.
This is an important Bash concept:
Quotes that appear as the result of variable expansion do not automatically become shell syntax again.
For example:
MY_COMMAND='mkdir -p "/srv/database backups"'
$MY_COMMAND
The quotation marks inside MY_COMMAND are now just characters contained in the expanded value.
They do not reliably reconstruct the original shell parsing process.
This is one of the reasons why trying to store shell syntax inside strings quickly becomes complicated.
5. The Correct Approach: Use a Bash Array
Instead of storing the entire command inside one string, store each command element as an individual array element.
For example:
MY_COMMAND=(sudo -u admin mysqldump -p mydatabase)
Now the array contains:
MY_COMMAND[0] = sudo
MY_COMMAND[1] = -u
MY_COMMAND[2] = admin
MY_COMMAND[3] = mysqldump
MY_COMMAND[4] = -p
MY_COMMAND[5] = mydatabase
This structure matches how commands actually work.
Execute the command with:
"${MY_COMMAND[@]}"
This is the important pattern:
COMMAND=(program argument1 argument2 argument3)
"${COMMAND[@]}"
Each array element becomes exactly one command argument.
6. Why "${ARRAY[@]}" Is So Important
The syntax:
"${MY_COMMAND[@]}"
means:
Expand every array element individually while preserving each element as a separate argument.
For example:
MY_COMMAND=(
mkdir
-p
"/srv/database backups"
)
The array contains three elements:
mkdir
-p
/srv/database backups
Executing:
"${MY_COMMAND[@]}"
is effectively equivalent to writing:
mkdir -p "/srv/database backups"
The space inside:
/srv/database backups
does not cause another split because that entire path is already one array element.
7. A Practical Example
Suppose we are writing a backup script.
We want to run:
sudo -u dbadmin mysqldump -p inventory
We could define:
BACKUP_COMMAND=(
sudo
-u
dbadmin
mysqldump
-p
inventory
)
Then execute:
"${BACKUP_COMMAND[@]}"
This is equivalent to typing:
sudo -u dbadmin mysqldump -p inventory
The advantage is that we can now build or modify the command safely.
8. Adding Arguments Dynamically
Arrays become especially useful when the command changes depending on script conditions.
Suppose we start with:
MYSQLDUMP_COMMAND=(
mysqldump
-p
inventory
)
Later, we decide whether to include additional options:
if [[ "$COMPRESS_MODE" == "full" ]]; then
MYSQLDUMP_COMMAND+=(--single-transaction)
fi
We can add another option:
MYSQLDUMP_COMMAND+=(--routines)
Now the array might contain:
mysqldump
-p
inventory
--single-transaction
--routines
Then:
"${MYSQLDUMP_COMMAND[@]}"
executes the complete command.
This is much cleaner than continuously concatenating strings.
9. Building a More Realistic Database Backup Command
Consider:
DATABASE_NAME="inventory"
DATABASE_USER="backupuser"
OUTPUT_FILE="/srv/database backups/inventory.sql"
DUMP_COMMAND=(
mysqldump
--user="$DATABASE_USER"
-p
--single-transaction
"$DATABASE_NAME"
)
We can inspect it:
printf '%q ' "${DUMP_COMMAND[@]}"
printf '\n'
Possible output:
mysqldump --user=backupuser -p --single-transaction inventory
Then redirect its output:
"${DUMP_COMMAND[@]}" > "$OUTPUT_FILE"
Notice that the redirection:
> "$OUTPUT_FILE"
is not placed inside the array.
Why?
Because > is shell syntax.
It is not an ordinary argument passed to mysqldump.
This distinction is very important.
10. Commands and Shell Operators Are Different Things
Consider:
mysqldump inventory > backup.sql
The command arguments are approximately:
mysqldump
inventory
But:
>
is interpreted by Bash itself.
It tells Bash:
Redirect standard output to a file.
Therefore, this is normally correct:
COMMAND=(mysqldump inventory)
"${COMMAND[@]}" > backup.sql
This is not equivalent:
COMMAND=(mysqldump inventory ">" backup.sql)
Executing:
"${COMMAND[@]}"
would pass the literal arguments:
>
backup.sql
to mysqldump.
Bash would not treat > as a redirection operator because it is already an array element produced during expansion.
The same principle applies to shell syntax such as:
>
>>
<
|
&&
||
;
These operators generally belong outside the command array.
11. Pipelines Need Special Treatment Too
Suppose we want:
mysqldump inventory | gzip > inventory.sql.gz
Do not try to create one giant command array containing the pipe:
COMMAND=(
mysqldump
inventory
"|"
gzip
)
That does not create a pipeline.
Instead, use separate commands:
DUMP_COMMAND=(
mysqldump
inventory
)
GZIP_COMMAND=(
gzip
)
Then:
"${DUMP_COMMAND[@]}" | "${GZIP_COMMAND[@]}" > inventory.sql.gz
This makes the responsibilities clear:
- the arrays represent commands and arguments
- Bash handles the pipeline
- Bash handles the output redirection
12. Another Example: File Synchronization
Suppose we want to execute:
rsync -av --delete "/srv/web files/" "/mnt/backup/web files/"
Using a string:
COMMAND='rsync -av --delete "/srv/web files/" "/mnt/backup/web files/"'
creates parsing problems if we later try:
$COMMAND
Using an array is straightforward:
COMMAND=(
rsync
-av
--delete
"/srv/web files/"
"/mnt/backup/web files/"
)
Execute:
"${COMMAND[@]}"
Each path remains one argument.
13. Arrays Also Work Well With Variables
Suppose:
SOURCE_DIR="/srv/application files"
DESTINATION_DIR="/mnt/nightly backup"
Build the command:
RSYNC_COMMAND=(
rsync
-av
--delete
"$SOURCE_DIR/"
"$DESTINATION_DIR/"
)
Then:
"${RSYNC_COMMAND[@]}"
Bash preserves:
/srv/application files/
and:
/mnt/nightly backup/
as individual arguments.
This works because array elements can contain whitespace without losing their boundaries.
14. Quoting Still Matters When Creating the Array
Arrays solve argument-boundary problems, but you still need to quote variable expansions correctly when creating array elements.
Correct:
SOURCE_DIR="/srv/application files"
COMMAND=(
rsync
-av
"$SOURCE_DIR"
)
Potentially problematic:
COMMAND=(
rsync
-av
$SOURCE_DIR
)
If:
SOURCE_DIR="/srv/application files"
then the unquoted expansion can become two array elements:
/srv/application
files
Instead of one.
Therefore, use:
"$SOURCE_DIR"
when the value should remain one argument.
15. Inspecting the Array
When debugging command arrays, it is useful to inspect each element individually.
Example:
COMMAND=(
rsync
-av
"/srv/application files/"
"/mnt/nightly backup/"
)
Print each element:
for arg in "${COMMAND[@]}"; do
printf '<%s>\n' "$arg"
done
Output:
<rsync>
<-av>
</srv/application files/>
</mnt/nightly backup/>
This makes it very easy to see exactly what the command contains.
16. Using printf '%q' to Debug Commands
Another useful Bash debugging technique is:
printf '%q ' "${COMMAND[@]}"
printf '\n'
For example:
COMMAND=(
rsync
-av
"/srv/application files/"
"/mnt/nightly backup/"
)
printf '%q ' "${COMMAND[@]}"
printf '\n'
You may see something similar to:
rsync -av /srv/application\ files/ /mnt/nightly\ backup/
The %q format prints each argument in a form that could be safely reused by a shell.
This is particularly useful when debugging scripts that dynamically construct commands.
17. Why "${ARRAY[@]}" and "${ARRAY[*]}" Are Different
Bash provides two commonly seen forms:
"${ARRAY[@]}"
and:
"${ARRAY[*]}"
They are not equivalent.
Suppose:
ARRAY=(
"first argument"
"second argument"
)
Using:
"${ARRAY[@]}"
produces two separate arguments:
first argument
second argument
That is normally what we want when executing commands.
Using:
"${ARRAY[*]}"
produces one combined string:
first argument second argument
The original argument boundaries are lost.
Therefore, for command execution, the standard pattern is:
"${COMMAND[@]}"
not:
"${COMMAND[*]}"
18. A Simple Demonstration
Create this function:
show_arguments() {
local counter=0
for argument in "$@"; do
printf 'Argument %d: <%s>\n' "$counter" "$argument"
((counter++))
done
}
Now define an array:
COMMAND=(
show_arguments
"hello world"
"database backup"
"file.txt"
)
Execute:
"${COMMAND[@]}"
Output:
Argument 0: <hello world>
Argument 1: <database backup>
Argument 2: <file.txt>
The spaces are preserved because each value is one array element.
19. What Happens With a String Instead?
Now try:
COMMAND='show_arguments "hello world" "database backup" file.txt'
and execute:
$COMMAND
Bash does not reconstruct the original quoting in the way we might expect.
Instead of naturally recovering:
hello world
database backup
file.txt
the string is subjected to shell expansion and word splitting.
This is why a string is not a reliable representation of an argument list.
20. The Fundamental Difference
This is the most important idea in the entire article.
A string stores:
characters
An array stores:
elements
For commands, those elements can correspond directly to arguments.
Consider:
COMMAND_STRING="cp file.txt /srv/backup files/"
There is nothing inside the string that Bash can safely use later to guarantee which spaces were intended to separate arguments and which spaces were part of an argument.
With an array:
COMMAND_ARRAY=(
cp
file.txt
"/srv/backup files/"
)
the structure is explicit:
Element 0: cp
Element 1: file.txt
Element 2: /srv/backup files/
No guessing is required.
21. Thinking in Terms of argv
At a lower level, programs on Unix-like systems receive arguments as something conceptually similar to an array.
A C program traditionally starts with:
int main(int argc, char *argv[])
The command:
cp source.txt "/srv/backup files/"
might conceptually produce:
argv[0] = cp
argv[1] = source.txt
argv[2] = /srv/backup files/
Bash arrays are useful because they allow us to construct something that closely resembles this argument list before executing the program.
For example:
COMMAND=(
cp
source.txt
"/srv/backup files/"
)
Then:
"${COMMAND[@]}"
preserves exactly those argument boundaries.
22. Why eval Is Usually Not the Solution
When developers discover that a string containing quotes does not execute correctly, they sometimes reach for eval.
For example:
COMMAND='cp source.txt "/srv/backup files/"'
eval "$COMMAND"
This causes Bash to parse the generated text again as shell code.
It may appear to solve the quoting problem.
However, eval introduces another much more serious problem:
Data can become executable shell code.
Consider:
DESTINATION="$USER_INPUT"
COMMAND="cp source.txt $DESTINATION"
eval "$COMMAND"
If the input contains shell syntax, Bash may execute something the script author never intended.
This can create command injection vulnerabilities.
For most situations where you simply need to store a command and its arguments, you do not need eval.
Use an array.
COMMAND=(
cp
source.txt
"$DESTINATION"
)
"${COMMAND[@]}"
Now DESTINATION remains data.
It does not suddenly become shell syntax.
23. Example of Why Arrays Are Safer
Suppose a script receives a filename:
FILE_NAME="$1"
Then builds:
COMMAND=(
cat
"$FILE_NAME"
)
and executes:
"${COMMAND[@]}"
If the filename is:
monthly report.txt
the command receives one argument:
monthly report.txt
Even if the filename contains characters that look like shell syntax, they remain part of the argument.
That is exactly what we want.
24. Commands Passed to Functions
Arrays are also useful when a function needs to execute a command supplied by another part of the script.
A very useful pattern is:
run_command() {
"$@"
}
Then call:
run_command rsync -av "/srv/web files/" "/mnt/backup/"
Inside the function:
"$@"
means:
Execute all arguments passed to the function while preserving their individual boundaries.
This follows the same principle as:
"${ARRAY[@]}"
25. A More Practical Command Runner
Consider:
run_command() {
printf 'Running: '
printf '%q ' "$@"
printf '\n'
"$@"
}
Now:
run_command \
rsync \
-av \
"/srv/application files/" \
"/mnt/nightly backup/"
The function first displays something similar to:
Running: rsync -av /srv/application\ files/ /mnt/nightly\ backup/
Then executes the command safely.
This pattern is extremely useful in:
- deployment scripts
- backup scripts
- maintenance scripts
- CI/CD scripts
- system administration tools
26. Passing an Existing Command Array to a Function
We can also define:
BACKUP_COMMAND=(
rsync
-av
--delete
"/srv/application files/"
"/mnt/nightly backup/"
)
Then:
run_command "${BACKUP_COMMAND[@]}"
Inside run_command, the argument boundaries remain preserved.
This allows commands to move safely through different layers of a Bash script.
27. Conditional Command Options
Arrays are particularly convenient when options are optional.
Suppose:
RSYNC_COMMAND=(
rsync
-av
)
Then:
if [[ "$DELETE_OLD_FILES" == "yes" ]]; then
RSYNC_COMMAND+=(--delete)
fi
Another condition:
if [[ "$DRY_RUN" == "yes" ]]; then
RSYNC_COMMAND+=(--dry-run)
fi
Finally:
RSYNC_COMMAND+=(
"$SOURCE_DIR/"
"$DESTINATION_DIR/"
)
Execute:
"${RSYNC_COMMAND[@]}"
Depending on the conditions, Bash may execute:
rsync -av --delete --dry-run "/srv/application files/" "/mnt/nightly backup/"
or simply:
rsync -av "/srv/application files/" "/mnt/nightly backup/"
This is much easier to maintain than building a command string.
28. Compare String Concatenation With Array Construction
Using strings, developers sometimes write:
COMMAND="rsync -av"
if [[ "$DELETE_OLD_FILES" == "yes" ]]; then
COMMAND="$COMMAND --delete"
fi
COMMAND="$COMMAND $SOURCE_DIR $DESTINATION_DIR"
$COMMAND
This depends heavily on word splitting.
If either path contains spaces, the command may break.
Using an array:
COMMAND=(
rsync
-av
)
if [[ "$DELETE_OLD_FILES" == "yes" ]]; then
COMMAND+=(--delete)
fi
COMMAND+=(
"$SOURCE_DIR"
"$DESTINATION_DIR"
)
"${COMMAND[@]}"
The structure is explicit and predictable.
29. Empty Optional Arguments
Arrays also make optional arguments easier to manage correctly.
Suppose we want to optionally enable verbose mode.
A poor approach is:
VERBOSE_OPTION=""
if [[ "$VERBOSE" == "yes" ]]; then
VERBOSE_OPTION="--verbose"
fi
COMMAND=(
rsync
"$VERBOSE_OPTION"
"$SOURCE"
"$DESTINATION"
)
If VERBOSE_OPTION is empty, the program may receive an empty argument:
""
A cleaner pattern is to add the argument only when it is needed:
COMMAND=(rsync)
if [[ "$VERBOSE" == "yes" ]]; then
COMMAND+=(--verbose)
fi
COMMAND+=(
"$SOURCE"
"$DESTINATION"
)
Now no unnecessary empty argument exists.
30. Long Commands Become Easier to Read
Arrays also improve readability.
Instead of:
COMMAND="mysqldump --single-transaction --routines --triggers --events --user=$DATABASE_USER -p $DATABASE_NAME"
we can write:
COMMAND=(
mysqldump
--single-transaction
--routines
--triggers
--events
--user="$DATABASE_USER"
-p
"$DATABASE_NAME"
)
This makes every option visible.
It is also easier to:
- add options
- remove options
- comment sections
- inspect arguments
- debug the command
- build it conditionally
31. Comments Can Explain Individual Options
For complex scripts, the command can be constructed in sections:
DUMP_COMMAND=(
mysqldump
# Authentication
--user="$DATABASE_USER"
-p
# Backup consistency
--single-transaction
# Database objects
--routines
--triggers
--events
"$DATABASE_NAME"
)
Although excessive comments should be avoided, this structure can make maintenance easier in production automation scripts.
32. One Important Exception: Commands That Require a Shell
Sometimes what we want to execute is not merely a program plus arguments.
For example:
grep "ERROR" application.log | sort | uniq -c > errors.txt
This includes shell operators:
|
>
If you genuinely need to store an entire shell expression for later evaluation, the problem is different.
You may need an explicit shell invocation such as:
bash -c 'grep "ERROR" application.log | sort | uniq -c > errors.txt'
But this should not be confused with ordinary command storage.
For normal program execution:
command arg1 arg2 arg3
use an array.
For shell syntax involving:
- pipelines
- redirections
- compound commands
- shell variables
- shell loops
- command substitution
you should generally write that shell structure explicitly rather than hiding it inside a variable.
33. Environment Variable Assignments Are Another Special Case
Consider:
DEBUG=1 python3 application.py
The:
DEBUG=1
part is an environment assignment interpreted by Bash.
If we attempt:
COMMAND=(
DEBUG=1
python3
application.py
)
"${COMMAND[@]}"
Bash will try to execute a command literally named:
DEBUG=1
which is not what we want.
Instead, we can use the env command:
COMMAND=(
env
DEBUG=1
python3
application.py
)
"${COMMAND[@]}"
Now env receives:
DEBUG=1
and launches:
python3 application.py
with that environment variable set.
This is another good example of understanding the difference between shell syntax and command arguments.
34. Command Paths Stored in Variables
There is nothing wrong with storing only a program path in a normal scalar variable.
For example:
MYSQLDUMP_BIN="/usr/bin/mysqldump"
Then:
"$MYSQLDUMP_BIN" --single-transaction mydatabase
This is safe because the variable represents a single command name or path, not an entire command line.
Similarly:
PYTHON_BIN="/usr/bin/python3"
"$PYTHON_BIN" application.py
The important distinction is:
One variable = one argument
versus:
One string variable = many arguments
The second case is where arrays become important.
35. A Good General Rule
When designing Bash scripts, use this rule:
If the variable represents one value, use a normal variable.
For example:
DATABASE="inventory"
BACKUP_DIR="/srv/database backups"
MYSQLDUMP_BIN="/usr/bin/mysqldump"
But:
If the variable represents a command plus multiple arguments, use an array.
For example:
MYSQLDUMP_COMMAND=(
"$MYSQLDUMP_BIN"
--single-transaction
-p
"$DATABASE"
)
Execute:
"${MYSQLDUMP_COMMAND[@]}"
36. A Complete Example
Here is a small backup script demonstrating the pattern:
#!/usr/bin/env bash
set -euo pipefail
DATABASE_NAME="inventory"
DATABASE_USER="backupuser"
BACKUP_DIR="/srv/database backups"
BACKUP_FILE="${BACKUP_DIR}/${DATABASE_NAME}.sql"
mkdir -p "$BACKUP_DIR"
DUMP_COMMAND=(
mysqldump
--user="$DATABASE_USER"
-p
--single-transaction
--routines
--triggers
"$DATABASE_NAME"
)
printf 'Running command: '
printf '%q ' "${DUMP_COMMAND[@]}"
printf '\n'
"${DUMP_COMMAND[@]}" > "$BACKUP_FILE"
printf 'Backup created: %s\n' "$BACKUP_FILE"
Notice how the responsibilities are separated.
The array contains only the executable and its arguments:
DUMP_COMMAND=(
mysqldump
--user="$DATABASE_USER"
-p
--single-transaction
--routines
--triggers
"$DATABASE_NAME"
)
The output redirection remains shell syntax:
"${DUMP_COMMAND[@]}" > "$BACKUP_FILE"
And the backup path can safely contain spaces:
BACKUP_DIR="/srv/database backups"
because it is always quoted appropriately.
37. What the Shell Is Really Doing
When Bash sees:
"${DUMP_COMMAND[@]}" > "$BACKUP_FILE"
you can mentally separate the operation into two parts.
First, Bash prepares the command arguments:
mysqldump
--user=backupuser
-p
--single-transaction
--routines
--triggers
inventory
Then Bash handles the redirection:
stdout → /srv/database backups/inventory.sql
Finally, Bash starts mysqldump with the prepared argument list.
Thinking about commands this way makes many Bash concepts easier to understand.
38. Common Mistakes
Mistake 1: Storing the entire command in a string
Avoid:
COMMAND="rsync -av $SOURCE $DESTINATION"
Prefer:
COMMAND=(
rsync
-av
"$SOURCE"
"$DESTINATION"
)
Mistake 2: Executing the array without "${ARRAY[@]}"
Avoid:
$COMMAND
for an array.
Use:
"${COMMAND[@]}"
Mistake 3: Using "${ARRAY[*]}"
Avoid:
"${COMMAND[*]}"
because it combines the array elements into one string.
Use:
"${COMMAND[@]}"
Mistake 4: Putting shell operators inside the array
Avoid:
COMMAND=(
mysqldump
inventory
">"
backup.sql
)
Use:
COMMAND=(
mysqldump
inventory
)
"${COMMAND[@]}" > backup.sql
Mistake 5: Using eval unnecessarily
Avoid:
eval "$COMMAND"
when the objective is simply to execute a program with arguments.
Prefer an array:
COMMAND=(
program
"$ARGUMENT"
)
"${COMMAND[@]}"
39. String Versus Array: Side-by-Side Comparison
Consider a source directory containing spaces:
SOURCE="/srv/application files"
DESTINATION="/mnt/nightly backup"
String approach
COMMAND="rsync -av $SOURCE $DESTINATION"
$COMMAND
Potential interpretation:
rsync
-av
/srv/application
files
/mnt/nightly
backup
The intended paths are destroyed.
Array approach
COMMAND=(
rsync
-av
"$SOURCE"
"$DESTINATION"
)
"${COMMAND[@]}"
Interpretation:
rsync
-av
/srv/application files
/mnt/nightly backup
Exactly four arguments are passed.
That is the behavior we intended.
40. A Useful Mental Model
When writing:
COMMAND="rsync -av /source /destination"
think:
I am storing text.
When writing:
COMMAND=(
rsync
-av
/source
/destination
)
think:
I am storing an argument list.
That difference explains almost everything.
41. Best-Practice Pattern
For Bash scripts, a robust pattern is:
COMMAND=(
program
option1
option2
"$VARIABLE_ARGUMENT"
)
"${COMMAND[@]}"
For example:
BACKUP_COMMAND=(
rsync
-av
--delete
"$SOURCE_DIR/"
"$BACKUP_DIR/"
)
"${BACKUP_COMMAND[@]}"
If the command needs redirection:
"${BACKUP_COMMAND[@]}" > "$LOG_FILE"
If it needs a pipeline:
"${FIRST_COMMAND[@]}" | "${SECOND_COMMAND[@]}"
If optional arguments are required:
COMMAND=(program)
if [[ "$ENABLE_FEATURE" == "yes" ]]; then
COMMAND+=(--feature)
fi
COMMAND+=("$TARGET")
"${COMMAND[@]}"
This pattern scales very well from small scripts to more complex automation.
Conclusion
The most important lesson is simple:
A shell command is not fundamentally a single string. It is a command name followed by a list of arguments.
A normal Bash variable stores text:
COMMAND="sudo -u admin mysqldump -p mydatabase"
A Bash array preserves the structure of the command:
COMMAND=(
sudo
-u
admin
mysqldump
-p
mydatabase
)
And the correct way to execute it is:
"${COMMAND[@]}"
Using arrays provides several important benefits:
- argument boundaries are preserved
- paths containing spaces work correctly
- quoting becomes predictable
- optional parameters are easier to add
- commands are easier to read
- commands are easier to debug
evalis usually unnecessary- the risk of command-injection bugs is reduced
- complex automation scripts become easier to maintain
The key pattern to remember is:
COMMAND=(
program
argument1
argument2
"$argument_with_possible_spaces"
)
"${COMMAND[@]}"
Once you begin thinking about Bash commands as lists of arguments rather than strings of text, command construction becomes significantly easier to reason about and much more reliable.