Variables, quoting & command substitution
Store values, capture command output, quote from day one.
A variable in Bash (the Bourne Again Shell, the program that reads what you type and runs it) is a labeled box. You put a value in the box once, give the box a name, and every command after that reads the value by name instead of you retyping it. Store a server address in a box called `host`, and twenty later commands can point at it without one repeated keystroke. That sounds harmless, and mostly it is. In operations work it is also where a real share of outages begins, because the rules for filling and reading these boxes are stricter than they look, and Bash rarely warns you when you break one. This lesson covers the four moves you make every day: putting a value in a box, reading it back safely, telling the two kinds of quotes apart, and capturing the output of a command into a box of its own.
Put a value in a box: watch the spaces
To fill a box, write the name, an equals sign, then the value, with nothing touching the equals sign on either side. `greeting="hello"` works. `greeting = "hello"` does not, and the reason catches almost everyone the first time. In most languages you have seen, spaces around an equals sign are only there for readability. Bash reads a space differently. To Bash, a space is the wall between a command and its arguments, so the instant it sees `greeting` followed by a space it decides `greeting` is a program you want to run, and `=` and `"hello"` are arguments you are passing to it. No program named `greeting` exists, so you get a puzzling error about a command not being found.
The first line stored the value and printed nothing, which is exactly right; assignments are silent. The second line read the box back and printed `hello`. The third line, with its stray spaces, was treated as an attempt to run a command called `greeting`. Keep the equals sign tight against the name and the value, and this whole family of errors never appears.
Read it back: $VAR and ${VAR}
To read a box, put a dollar sign in front of its name: `$greeting`. The dollar sign tells Bash to look up that name and paste in its value, a step called expansion. The plain form is fine most of the time. The braced form, `${greeting}`, does the same lookup but marks exactly where the name ends, which starts to matter the moment you want to glue a variable directly onto more text.
`$env_server` failed without a sound. Bash went looking for a box named `env_server`, found none, and pasted in nothing, so the first line printed blank. `${env}_server` told Bash the name is `env` and the literal text `_server` comes after it, giving you `prod_server`. When you are unsure where a name ends, use the braces; two characters buy you complete clarity.
Single quotes stay literal, double quotes expand
Quotes in Bash are not decoration, and the two kinds are not interchangeable. They are two different switches. Double quotes leave expansion on: anything with a dollar sign inside them gets looked up and replaced by its value. Single quotes turn expansion fully off: whatever sits between them arrives as the exact characters you typed, dollar signs included. This one difference decides whether `$user` becomes `alice` or stays the literal text `$user`.
Same word, same variable, opposite results. The double-quoted line looked up `user` and printed `alice`. The single-quoted line printed the dollar sign and the name as plain text, because single quotes told Bash to keep its hands off. Single quotes earn their place often: a password that contains a `$`, a snippet of code you are handing to another tool, any string where a literal dollar sign is the whole point.
Why you write "$VAR", not $VAR
Build this habit from your first day: wrap a variable read in double quotes by default, `"$file"`, not bare `$file`. The reason is a behavior called word splitting. After Bash pastes a value in, it runs a pair of scissors over the result and cuts it into separate arguments at every space, tab, or newline it finds. If the value holds no spaces, you never notice. The first day it does, one argument quietly becomes several, and a command aimed at a single thing acts on several things instead.
Read that failure closely. `touch "$file"` created one file whose name contains a space, and modern `ls` prints such names inside quotes so the boundary is visible. `rm $file`, left unquoted, reached `rm` as two separate words, `q3` and `report.txt`, so it tried to delete two files that do not exist and left the real one untouched. Here it failed out loud. Put two real files named `q3` and `report.txt` next to it and the unquoted line would have deleted the wrong ones with a clean exit code and no complaint. For anyone doing DevSecOps (Development, Security, and Operations) work, that gap between what a line looks like and what it actually does is where a hostile filename or an unexpected value turns an ordinary script into a weapon. Closing it costs one pair of quotes.
Command substitution: capture a command's output
Some values you can type yourself. Others you only learn by asking another program: today's date, the name of this machine, how many files sit in a folder. Command substitution is the way you ask and keep the answer. Write `$(command)`, and Bash runs the command, collects everything it sends to standard output (the normal output stream, usually your screen), trims the trailing newline off the end, and hands that text back as a value you can store in a box like any other.
Three commands, three captured values. `date +%F` printed the date in year-month-day form, `hostname` printed the machine's name, and `whoami` printed the account the shell is running as. Each value landed in a box, and the final `echo` stitched them into one line. Stamping who ran something, on which host, and when is a daily operations pattern: it is how log lines, backup filenames, and audit records get their context.
#!/usr/bin/env bashconf_count=$(find /etc -maxdepth 1 -name '*.conf' | wc -l)echo "$(whoami) on $(hostname): $conf_count .conf files in /etc"
This script counts before it reports. `find /etc -maxdepth 1 -name '*.conf'` lists the configuration files sitting directly inside `/etc`, and piping that list into `wc -l` (word count, told to count lines) collapses it to a single number. That number, captured in `conf_count`, drops into the report line beside two more substitutions run inline. You can nest `$(...)` right inside a double-quoted string and it still expands, because double quotes keep expansion switched on.
Shell variables versus environment variables
Every box so far has been a shell variable: it lives inside the one shell that created it and disappears when that shell closes. Its quiet limit is that child processes cannot see it. When your script runs another program, that program starts as a child process (a separate running program that your shell launches), and it inherits only the boxes you have marked for sharing. Marking a box for sharing is the entire job of `export`. An exported box is an environment variable, and every child the script starts receives its own copy.
The first child saw an empty box, because `token` was still a plain shell variable that stayed private to the parent. After `export token`, the second child got a copy and printed the value. That is the whole mechanism behind familiar names like `PATH` and `HOME`: a parent shell exports them once, and every program launched underneath inherits them. Programs read their configuration from the environment for exactly this reason, because it flows downhill to children with nobody passing it along by hand.
That downhill flow is convenient, and with secrets it is dangerous. An exported value does not reach only the one program you meant to give it to; it reaches every child the script starts, including tools you did not write and cannot inspect.