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.
export, is the entire difference between a value your shell keeps to itself and a value every child process inherits.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.
export a variable, every program your script launches inherits a copy of it, and on Linux the value sits in plain text in that process's environment. Anyone who can read /proc/<process-id>/environ for a running process, or who can run ps eww, can read an exported password or API (Application Programming Interface) token straight out of it. Reach for export for ordinary configuration like a region name or a log level. For a secret, hand it to the one program that needs it through a file with tight permissions or through standard input, so it never becomes an environment variable the whole process tree can see.count=$(ls /etc | wc -l), then echo 'Found $count files' with single quotes. What does that echo print?api_key='abc123' without using export, then launches an external tool as a child process. Can that tool read api_key from its environment?dir="/srv/app data" then tar -czf out.tgz $dir, and /srv really holds a directory named 'app data'. What goes wrong?Try this
Work through “Shell variables versus environment variables” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: an exported secret leaks to every child process. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.