Using a Variable to Pass Terminal Commands as Arguments in Bash

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
  • eval is 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.