Short answer: the echo command prints its arguments followed by a newline: echo "Hello, $USER". In bash, -n leaves the newline off and -e turns on escapes such as \n and \t. Quote anything that holds a variable, write to a file with > or >>, and use printf in scripts that must behave the same under every shell.
echo "Hello, $USER" # print text and a variable
echo -n "no newline" # leave the trailing newline off
echo -e "col1\tcol2\nnext line" # interpret \t and \n (bash)
echo "server=web1" >> app.conf # append a line to a file
printf '%s\n' "$value" # the portable way in scriptsThe examples and outputs below were run on Debian 12 in bash 5.2.15 and in dash 0.5.12, which is /bin/sh on Debian and Ubuntu, plus the standalone /bin/echo from GNU coreutils 9.1. The sudo case, the [ -t 1 ] check and the zsh behaviour follow the bash, sudo and zsh documentation.
echo cheat sheet
| Task | Command |
|---|---|
| Print text | echo "Deploy finished" |
| Print a variable | echo "$HOME" |
| Print without the newline | echo -n "Password: " |
| Interpret \n, \t and colours (bash) | echo -e "a\tb\nc" |
| Write a file, replacing it | echo "line" > file.txt |
| Append to a file | echo "line" >> file.txt |
| Write to a root-owned file | echo "line" | sudo tee -a /etc/file |
| Print an error message to stderr | echo "error: failed" >&2 |
| Show PATH one entry per line | echo "$PATH" | tr ':' '\n' |
| Exit code of the last command | echo $? |
| Expand a range | echo {1..5} |
| Print a literal $ | echo 'Price: $5' |
| Portable replacement | printf '%s\n' "$var" |
echo options: -n, -e and -E
$ echo -n "no newline"; echo "<"
no newline<
$ echo "a\tb\nc"
a\tb\nc
$ echo -e "a\tb\nc"
a b
cBash’s built-in echo knows three options. -n suppresses the trailing newline, which is how you put a prompt and the answer on the same line. -e enables backslash escapes; without it bash prints them literally. -E switches them off again and is the default. Options can be combined, as in echo -ne.
| Escape | Meaning |
|---|---|
| \n | New line |
| \t | Tab |
| \\ | A literal backslash |
| \r | Carriage return: go back to the start of the line |
| \c | Stop here: print nothing after it, not even the newline |
| \e or \033 | Escape character, the start of a colour code |
| \0NNN | Byte with octal value NNN, e.g. \0101 is A |
| \xHH | Byte with hex value HH (bash, not dash) |
Printing -n or --help itself
Bash’s echo has no end-of-options marker, so there is no way to make it print the bare string -n: echo "-n" prints nothing at all, and echo -- -n prints -- -n. printf '%s\n' -n prints it correctly. The reverse surprise: echo --help in bash prints the word --help, because type -a echo shows the builtin comes first; the help text belongs to /bin/echo --help, the separate GNU program.
Quoting and variables
$ name="VMHeaven user"
$ echo "Hello, $name"
Hello, VMHeaven user
$ echo 'Hello, $name'
Hello, $name
$ msg="a b c"
$ echo $msg
a b c
$ echo "$msg"
a b cDouble quotes expand variables and $(command); single quotes print everything literally. Without quotes, the shell splits the value on whitespace and passes echo separate words, which it joins with single spaces, so the extra spaces vanish. It also expands wildcards inside the value:
$ pat="*.log"
$ echo $pat
app.log
$ echo "$pat"
*.log
$ file=report
$ echo "$file_2026.txt"
.txt
$ echo "${file}_2026.txt"
report_2026.txtThe unquoted $pat was replaced by the file names in the current directory. In "$file_2026.txt" the shell looked for a variable called file_2026, found none and printed an empty string; braces mark where the name ends. Three more quoting cases:
- An apostrophe in the text: use double quotes,
echo "It's done", or close and reopen single quotes:echo 'It'\''s done'. - A dollar sign:
echo "Price: \$5"or single quotes. - An exclamation mark: in an interactive bash,
echo "hi!there"fails withbash: !there: event not found, because!starts history expansion. Single quotes avoid it; scripts are not affected, since history expansion is off there.
echo $PATH and other variables
$ echo "$PATH"
/usr/local/bin:/usr/bin:/bin
$ echo "$PATH" | tr ':' '\n'
/usr/local/bin
/usr/bin
/binecho $PATH is the quickest way to see where the shell looks for commands, and the second form makes a long PATH readable. The same goes for echo $HOME, echo $SHELL or any variable you just set. echo $? prints the exit status of the previous command: 0 for success, anything else for an error. echo "${VAR:-default}" prints a fallback when the variable is unset or empty. Setting variables so that they stick, for your user or for a service, is covered in how to set an environment variable in Linux.
Writing to a file with echo
$ echo "first" > out.txt
$ echo "second" >> out.txt
$ cat out.txt
first
second> creates the file or empties an existing one first; >> appends. echo always adds a newline unless you pass -n, and that newline is real data: echo "x" > f.txt writes 2 bytes, echo -n "x" > g.txt writes 1. It matters as soon as the text is hashed or encoded:
$ echo "secret" | base64
c2VjcmV0Cg==
$ printf '%s' "secret" | base64
c2VjcmV0The Cg== at the end is the encoded newline. Build a Basic auth header or a token this way and the server sees a different value, which is a classic cause of 401 Unauthorized. For messages meant for errors rather than output, send them to standard error with echo "error: disk full" >&2, so they still show when the normal output is redirected to a file. Every redirection operator is explained in redirecting stdout and stderr.
Why sudo echo > file fails, and tee
sudo echo "vm.swappiness=10" > /etc/sysctl.d/99-swappiness.conf # Permission denied
echo "vm.swappiness=10" | sudo tee /etc/sysctl.d/99-swappiness.conf > /dev/null # works
echo "vm.vfs_cache_pressure=50" | sudo tee -a /etc/sysctl.d/99-swappiness.conf > /dev/null # appendThe > is handled by your own shell, as your user, before sudo starts; sudo only raises the privileges of echo, which does not need them. With tee, the program that opens the file is the one running as root. The trailing > /dev/null stops tee from also printing the line back to you. The full explanation is in the sudo tee section of the redirection guide.
Coloured output with echo -e
echo -e "\e[31mERROR\e[0m disk almost full"
echo -e "\e[1;32mOK\e[0m backup finished"
printf '\033[33m%s\033[0m\n' "warning: low memory"\e[ starts an ANSI escape sequence, the number picks the colour and m ends it; \e[0m resets, so always end with it or the rest of the terminal stays red. Piped through cat -v, the first line shows the real bytes: ^[[31mERROR^[[0m.
| Code | Effect |
|---|---|
| 0 | Reset everything |
| 1 | Bold |
| 31 / 32 / 33 | Red / green / yellow text |
| 34 / 35 / 36 | Blue / magenta / cyan text |
| 41 / 42 | Red / green background |
Colour codes end up as junk in log files. In a script, colour only when the output is a terminal: [ -t 1 ] && echo -e "\e[32mOK\e[0m" || echo "OK".
echo in sh, bash and zsh: why scripts use printf
echo is one of the least portable commands there is. The same lines gave different output in the two shells on the same Debian 12 machine:
| Command | bash 5.2 | dash 0.5.12 |
|---|---|---|
echo "a\tb" | a\tb (literal) | a, tab, b (escape interpreted) |
echo -e "a\tb" | a, tab, b | -e a, tab, b (prints the -e) |
echo -E "a\tb" | a\tb | -E a, tab, b |
echo -e "\x41" | A | -e \x41 |
echo -n "x" | x, no newline | x, no newline |
So a script with #!/bin/sh that uses echo -e prints a stray -e on Debian and Ubuntu, and the same script works under bash. Cron runs its lines with /bin/sh too. zsh, the default shell on macOS, interprets escapes in plain echo without -e, per its manual. In bash itself, shopt -s xpg_echo makes echo interpret escapes by default (checked).
printf is specified much more tightly by POSIX, so %s, %d, \n and octal escapes gave the same output in bash and dash. It does not add a newline by itself, and its format string repeats for extra arguments:
$ printf '%s\n' "one" "two"
one
two
$ printf 'Name: %-10s|\n' "web1"
Name: web1 |
$ printf '%d items, %05.1f%%\n' 3 3.14159
3 items, 003.1%
$ printf '%s=%s\n' a 1 b 2
a=1
b=2%s prints a string unchanged, so backslashes in user data stay literal; use %b when you want escapes in the argument interpreted. For colours in a script that may run under sh, use the octal form \033: it worked in both shells, while \x escapes did not work in dash.
echo on a server
- Logging from a cron job or script:
echo "$(date -u +%FT%TZ) backup done" >> /var/log/backup.logputs a timestamp in front of each line. In a crontab line, every%must be written as\%; the crontab guide explains why. - Adding a line to a config: check it is not there yet, so a second run does not add it twice:
grep -qxs 'vm.swappiness=10' /etc/sysctl.d/99-swappiness.conf || echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-swappiness.conf. The swap guide explains what the value does. - Printing in
~/.bashrc: a welcome message there breaks SFTP logins with “Received message too long” unless it only runs in interactive shells, as described in transferring files to a VPS.
Debian and Ubuntu on a KVM VPS give you bash as the login shell and dash as /bin/sh, the same pair the examples here ran in on Debian 12, with full root for the sudo tee cases. More everyday commands are in the Linux commands cheat sheet.
Frequently asked
What is the difference between echo -n and echo -e?
echo -n prints the text without the trailing newline. echo -e makes bash interpret backslash escapes such as \n for a new line and \t for a tab. They combine as echo -ne. Under /bin/sh on Debian and Ubuntu, which is dash, -e is not an option and is printed as text.
Why does sudo echo into a file say Permission denied?
In sudo echo text > /etc/file, the redirection is done by your own shell, as your user, before sudo runs; only echo gets root rights, and it never opens the file. Use echo text | sudo tee /etc/file > /dev/null instead, or tee -a to append.
Should I use echo or printf in a shell script?
printf. echo behaves differently in bash, dash and zsh, especially with -e, -n and backslashes, while POSIX specifies printf much more tightly. printf '%s\n' "$var" prints a variable safely, whatever it contains.
Why does echo add a newline that breaks base64 or a hash?
echo always ends its output with a newline, and that byte becomes part of the encoded value: echo "secret" | base64 gives c2VjcmV0Cg== instead of c2VjcmV0. Use printf '%s' "secret" | base64, or echo -n in bash.