If you have spent most of your time using a graphical desktop, the command line can initially look a little intimidating.
You open a terminal and are greeted by something like:
dolpa@computer:~$
And then... nothing.
The computer is waiting for you.
There are no buttons to click, no menus to open, and no obvious indication of what you're supposed to do next.
But the command line is not something to be afraid of. In fact, once you understand a few basic concepts, Bash becomes surprisingly logical.
This article is intended as an introduction to those concepts. Experienced Linux and Unix users probably won't discover anything revolutionary here, but if you're just starting to explore Bash, these fundamentals will make everything that follows much easier to understand.
We'll look at what Bash actually is, how it starts, how it finds commands, how filenames are expanded, what special characters mean, how pipes and redirections work, and how several commands can be combined into surprisingly powerful one-liners.
And don't worry—there will be plenty of examples.
First of All: Bash Is a Program
Most people think of Bash as "the command line."
Technically, that's not quite correct.
Bash is a program - a shell.
A shell is a program that provides an interface between you and the operating system. It reads commands, interprets them, and executes programs according to those commands.
When you open a terminal, you are normally starting a shell.
Bash can also be started directly:
bash
You can even ask Bash to execute a command:
bash -c 'echo "Hello from Bash"'
The basic form of the Bash command is:
bash [options] [arguments]
For example:
bash -c 'echo Hello'
Here:
bashis the program-ctells Bash to execute a command supplied as an argument'echo Hello'is the command Bash should execute
The important thing to understand is that the terminal itself isn't Bash.
Your terminal emulator is simply the application that provides the window.
Inside that terminal window, Bash, or another shell such as Zsh, Fish, Dash, or Ksh—may be running.
Bash Has Input and Output
Like many other Unix programs, Bash works with standard input and output.
The three standard streams are:
- stdin — standard input
- stdout — standard output
- stderr — standard error
By default, your keyboard is connected to standard input, while the terminal displays standard output and standard error.
This concept becomes extremely important later because Bash allows us to redirect these streams.
For example:
ls > files.txt
Instead of displaying the output of ls on the screen, Bash sends it to files.txt.
And:
cat files.txt
displays the contents again.
This ability to connect programs together is one of the things that makes the Unix command line so powerful.
Starting Bash
Bash has many command-line options.
You can see them with:
bash --help
You can also read the full manual:
man bash
There are far more options than we need to cover in an introductory article, but a few are particularly useful for understanding how Bash works.
-c Execute a Command
bash -c 'echo Hello'
Bash reads the command from the following string and executes it.
This is particularly useful when another program needs to start Bash with a specific command.
-i Interactive Shell
bash -i
The -i option tells Bash to behave as an interactive shell.
An interactive shell expects you to type commands and normally displays a prompt.
-l Login Shell
bash -l
This starts Bash as a login shell.
Login shells have special startup behavior, which we'll discuss shortly.
-s Read Commands from Standard Input
bash -s
This tells Bash to read commands from standard input.
For example:
echo 'echo Hello' | bash -s
Output:
Hello
The first command generates Bash code, and the second Bash process reads and executes it.
--noprofile
bash --noprofile
This prevents Bash from reading the system-wide or user login startup files normally associated with login shells.
This can be useful when troubleshooting shell configuration problems.
--norc
bash --norc
This prevents Bash from reading the user's interactive startup file, normally:
~/.bashrc
This is especially useful when your .bashrc contains a mistake that makes starting a normal interactive shell difficult.
What Happens When Bash Starts?
Bash doesn't necessarily start with a completely empty environment.
Depending on how it was started, it reads configuration files.
This is why two users can have completely different Bash environments even when they're running the same operating system.
Things such as:
- aliases
- functions
- environment variables
- shell options
- prompt configuration
- PATH modifications
can all be configured during shell startup.
Login Shell Startup Files
For a typical Bash login shell, Bash looks for startup files such as:
/etc/profile
and then user-specific files such as:
~/.bash_profile
~/.bash_login
~/.profile
Bash uses the first applicable file from the user's list.
For example, if ~/.bash_profile exists, Bash normally uses it rather than continuing to ~/.bash_login or ~/.profile.
What Is .bashrc?
The file:
~/.bashrc
is commonly used for interactive non-login shells.
For example, it is often where people configure:
alias ll='ls -lah'
or:
export PATH="$PATH:$HOME/bin"
or customize their prompt.
A typical desktop Linux installation may start a terminal that launches a non-login interactive Bash shell, causing .bashrc to be loaded.
This distinction between login and non-login shells can be confusing at first, but it becomes important once you start customizing Bash.
Filenames and Metacharacters
Now we get to one of the most important concepts in Bash.
Suppose you type:
ls *.txt
You probably already know that this displays text files.
But ls itself isn't expanding *.txt.
Bash expands it before ls is executed.
If the directory contains:
notes.txt
report.txt
test.txt
photo.jpg
then Bash effectively turns:
ls *.txt
into:
ls notes.txt report.txt test.txt
This process is called pathname expansion, commonly known as globbing.
The * Wildcard
The * character matches zero or more characters.
For example:
ls *.txt
matches:
notes.txt
report.txt
.txt
It can also appear in the middle:
ls report*.txt
This could match:
report.txt
report1.txt
report-final.txt
report-2026.txt
It can also be used to match directories:
ls /var/log/*
The ? Wildcard
The question mark matches exactly one character.
For example:
ls file?.txt
could match:
file1.txt
file2.txt
fileA.txt
but not:
file10.txt
file.txt
fileABC.txt
because those don't contain exactly one character where ? appears.
Character Classes: [abc]
Square brackets allow you to specify a set of possible characters.
For example:
ls file[123].txt
matches:
file1.txt
file2.txt
file3.txt
It does not match:
file4.txt
Character Ranges
You can specify ranges.
For example:
ls file[0-9].txt
matches a single digit.
You can also use:
[a-z]
[A-Z]
[0-9]
For example:
ls [A-Z]*.txt
could find files whose names begin with an uppercase letter.
Negated Character Classes
Bash also allows you to specify characters that should not match.
For example:
[!0-9]
means a character that isn't a digit.
So:
ls file[!0-9].txt
could match:
fileA.txt
fileX.txt
but not:
file1.txt
The Tilde ~
The tilde is another extremely useful Bash expansion.
A plain:
~
represents the current user's home directory.
For example:
cd ~
is equivalent to:
cd /home/dolpa
assuming /home/dolpa is your home directory.
You don't need to know the actual path.
Another User's Home Directory
You can also write:
~john
to refer to John's home directory, assuming that user exists.
For example:
ls ~john
could list the contents of John's home directory.
~+ — Current Directory
Bash also provides:
~+
which represents the current working directory.
It corresponds to:
$PWD
For example:
echo ~+
and:
echo "$PWD"
normally produce the same location.
~- — Previous Directory
Similarly:
~-
refers to the previous working directory, corresponding to:
$OLDPWD
This can be particularly useful when moving between two directories.
For example:
cd /tmp
cd /var
echo ~-
The result will be the directory you were previously in.
Brace Expansion
Bash has another incredibly useful feature called brace expansion.
This is different from filename expansion.
The basic form is:
{start..end}
It allows Bash to generate a sequence of words before executing the command.
Simple Brace Expansion
Try:
echo {1,2,3,4}
Output:
1 2 3 4
You can also combine it with surrounding text:
echo hi{1,2,3,4}there
Output:
hi1there hi2there hi3there hi4there
Notice that hi and there aren't separate arguments. Bash generates four complete words.
Generating Multiple Names
Brace expansion becomes particularly useful when creating files or directories.
For example:
mkdir {photos,documents,backup}
creates:
photos
documents
backup
You could also write:
touch file{1,2,3,4}.txt
which creates:
file1.txt
file2.txt
file3.txt
file4.txt
This is much faster than typing every filename manually.
Numeric Ranges
Bash can generate numeric ranges:
echo {1..10}
Output:
1 2 3 4 5 6 7 8 9 10
You can use the same technique with commands.
For example:
mkdir chapter{1..10}
creates ten directories:
chapter1
chapter2
chapter3
...
chapter10
Specifying a Step
You can also specify the increment:
echo {1..10..2}
Output:
1 3 5 7 9
The syntax is:
{start..end..increment}
So:
{1..10..2}
means:
Start at 1, finish at 10, and increase by 2 each time.
Zero-Padded Numbers
Brace expansion can also generate zero-padded numbers.
For example:
echo {01..10}
Output:
01 02 03 04 05 06 07 08 09 10
This is particularly useful when creating numbered files:
touch image_{01..20}.jpg
Result:
image_01.jpg
image_02.jpg
...
image_20.jpg
Combining Brace Expansion and Wildcards
These features can also be combined.
For example:
ls {ch,app}?
Bash expands the braces first:
ls ch? app?
The ? then matches one character.
This could match:
ch1
ch2
app1
app2
The distinction between brace expansion and filename globbing is important.
Brace expansion generates text.
Globbing then matches filenames against patterns.
Special Characters in Bash
Bash uses many characters for special purposes.
This is both a blessing and a curse.
The good news is that these characters let us write powerful commands.
The bad news is that beginners sometimes type one in the wrong place and suddenly Bash appears to have developed a personality.
Here are some of the most important ones.
| Character | Meaning |
|---|---|
; | Separate commands |
& | Run a command in the background |
() | Group commands in a subshell |
{} | Group commands in the current shell / brace expansion |
| | Pipe output into another command |
< | Redirect standard input |
> | Redirect standard output |
>> | Append standard output |
' | Single quotes |
" | Double quotes |
` | Command substitution |
$ | Variable expansion and other expansions |
# | Comment |
! | Negate exit status / history expansion |
* | Wildcard |
? | Single-character wildcard |
[] | Character class |
We'll examine the most important ones now.
Separating Commands with ;
You can put multiple commands on one line.
For example:
cd ~; ls
Bash executes:
cd ~ls
The semicolon separates the commands.
Another example:
date; who; pwd
This executes all three commands one after another.
Running Commands in the Background
Add & to the end of a command:
long_command &
Bash starts the command and immediately gives you the prompt back.
For example:
sleep 30 &
The sleep command runs in the background while you can continue working.
This is particularly useful for programs that take a long time to complete.
Pipes
The pipe:
|
is one of the most important concepts in Unix.
It connects the standard output of one command to the standard input of another.
For example:
ls | less
The output of ls becomes the input of less.
Another classic example:
cat file.txt | grep "error"
The contents of file.txt are passed to grep, which searches for the word error.
Even better, when a command already accepts filenames, you often don't need cat:
grep "error" file.txt
But the pipe concept remains fundamental.
A More Interesting Pipeline
Suppose we want to see the number of lines in a file.
We can use:
cat file.txt | wc -l
The first command produces text.
The pipe sends that text to wc.
The -l option tells wc to count lines.
The result is the number of lines.
Pipes allow small, simple programs to be combined into much more powerful operations.
Input and Output Redirection
Bash lets us redirect standard input and output.
For example:
ls > files.txt
The > operator sends standard output into a file.
If the file already exists, it is normally overwritten.
To append instead:
ls >> files.txt
This adds the output to the end of the file.
Command Substitution
Bash can execute one command and use its output as part of another command.
Historically, this was written using backticks:
echo "Today is `date`"
A modern and much clearer form is:
echo "Today is $(date)"
For example:
Today is Sun Aug 9 21:10:00 IDT 2026
The command inside $() runs first, and its output is inserted into the surrounding command.
Why $() Is Better Than Backticks
You may still encounter:
`command`
in old scripts.
It works, but modern Bash code normally uses:
$(command)
The latter is easier to read and can be nested.
For example:
echo "$(date +%Y)-$(date +%m)-$(date +%d)"
The older backtick syntax becomes difficult to read when commands are nested.
Variable Expansion
The $ character is also used to access variables.
For example:
NAME="Pavel"
echo "$NAME"
Output:
Pavel
Bash sees $NAME and replaces it with the value stored in the variable.
This is called parameter expansion.
Why Quotes Matter
Quotes are extremely important in Bash.
Consider:
NAME="John Smith"
Now:
echo $NAME
will normally work because echo accepts multiple arguments.
But consider:
mkdir $NAME
Bash may interpret this as two arguments:
John
Smith
and therefore create two directories.
Instead, use:
mkdir "$NAME"
Now the complete value is treated as one argument.
This is why you will frequently see variables written as:
"$VARIABLE"
rather than:
$VARIABLE
Double Quotes
Inside double quotes, most text is treated literally, but certain Bash expansions still occur.
For example:
NAME="Pavel"
echo "Hello, $NAME"
Output:
Hello, Pavel
The $NAME is still expanded.
Command substitution also works:
echo "The date is $(date)"
Single Quotes
Single quotes are even stricter.
Everything inside single quotes is treated literally.
For example:
NAME="Pavel"
echo '$NAME'
Output:
$NAME
Bash does not expand the variable.
This is extremely useful when you want Bash to treat special characters as ordinary text.
The Backslash
The backslash:
\
can be used to escape a special character.
For example:
echo "\$NAME"
prints:
$NAME
rather than the value of NAME.
Similarly:
echo "\"Hello\""
allows literal double quotes to appear inside a double-quoted string.
Comments
The # character begins a comment.
For example:
# This is a comment
echo "Hello"
Bash ignores the comment.
Comments are useful for explaining your code.
There is one important exception:
#!/bin/bash
This is the shebang, which tells the operating system which interpreter should be used to execute the script.
Grouping Commands
Bash provides two important ways of grouping commands.
Using parentheses:
(command1; command2)
runs the commands in a subshell.
Using braces:
{ command1; command2; }
groups commands in the current shell.
This difference becomes important when commands modify the shell environment.
For example:
(cd /tmp; pwd)
pwd
The cd happens inside the subshell.
Your original directory doesn't change.
Compare that with:
cd /tmp
pwd
which changes the current shell's directory.
Conditional Execution with &&
The && operator means:
Execute the second command only if the first command succeeds.
For example:
mkdir backup && echo "Backup directory created"
If mkdir succeeds, the message is printed.
If it fails, echo is not executed.
This is extremely common in shell scripts.
Conditional Execution with ||
The || operator works in the opposite direction.
It means:
Execute the second command only if the first command fails.
For example:
grep "admin" users.txt || echo "User not found"
If grep finds the text, nothing is printed.
If grep fails to find it, the message is displayed.
This is one of the simplest ways to implement "success/failure" logic on the command line.
Negating a Command with !
The ! operator reverses a command's exit status.
For example:
! grep "admin" users.txt
If grep succeeds, ! makes the overall result a failure.
If grep fails, ! makes the overall result a success.
This is especially useful in if and while statements.
For example:
if ! grep "admin" users.txt
then
echo "Admin user not found"
fi
Putting Everything Together
Now we can combine several Bash concepts.
Suppose we want to:
- Find a word in a file.
- If it's found, print the file.
- Otherwise, display an error.
We could write:
grep "XX" file && lp file
The grep command searches for XX.
If it succeeds, lp file runs.
If it fails, nothing happens.
We could also handle the failure:
grep "XX" file && lp file || echo "XX was not found"
This demonstrates how Bash commands can be chained together to create surprisingly powerful operations.
Redirecting Output and Running in the Background
Here's another example from the original article:
nroff file > file.txt &
Several things happen here.
First:
nroff file
processes the file.
Then:
>
redirects the output into:
file.txt
Finally:
&
runs the entire operation in the background.
One short command therefore combines:
- command execution
- output redirection
- background processing
This is one of the reasons Bash commands can look strange to beginners at first.
Once you understand the individual pieces, however, they become much easier to read.
Running Commands Sequentially
This:
date; who; pwd
runs all three commands.
The commands execute in order:
date
↓
who
↓
pwd
The semicolon doesn't care whether the previous command succeeded or failed.
If you need the second command to run only after successful completion of the first, use && instead:
command1 && command2
That's an important distinction.
Sending Multiple Commands to a Log File
We can group commands and redirect their combined output:
(date; who; pwd) > log
The parentheses create a group.
The > redirects the group's output to log.
The result is a log file containing information from all three commands.
This is a very useful technique for simple diagnostic scripts.
A Classic Pipeline
Here's another example:
sort file | pr -3 | lp
This is a pipeline containing three programs.
The data flows like this:
file
↓
sort
↓
pr -3
↓
lp
The first program sorts the input.
The second formats it into three columns.
The final program sends the result to the printer.
This is the Unix philosophy in action:
Build small programs that do one thing well, then connect them together.
Bash Is More Than a Command Launcher
At this point, you can start to see that Bash isn't simply a program that waits for you to type commands.
It is a language for combining programs.
Bash provides:
- variables
- conditions
- loops
- functions
- command substitution
- pathname expansion
- brace expansion
- pipes
- redirection
- background processes
- error handling
- scripting
And all of these features can be combined.
That's what gives the command line its power.
A Small Example Script
Let's put several of the concepts together.
#!/bin/bash
# Directory containing log files
LOG_DIR="$HOME/logs"
# Create the directory if it doesn't exist
mkdir -p "$LOG_DIR"
# Save system information
(date; who; pwd) > "$LOG_DIR/system.log"
# Look for errors
if grep "error" "$LOG_DIR/system.log"
then
echo "Errors found."
else
echo "No errors found."
fi
This small script already uses many Bash concepts:
- a shebang
- comments
- variables
$HOME- quotes
- command grouping
- redirection
ifgrep- exit status
And we've only scratched the surface.
The Most Important Thing to Remember
Don't try to memorize every Bash character at once.
Instead, learn to recognize the building blocks.
When you see:
command1 | command2
think:
The output of command 1 becomes the input of command 2.
When you see:
command1 && command2
think:
Run command 2 only if command 1 succeeds.
When you see:
command1 || command2
think:
Run command 2 only if command 1 fails.
When you see:
command &
think:
Run this in the background.
When you see:
$(command)
think:
Run this command and insert its output here.
When you see:
"$VARIABLE"
think:
Expand this variable while preserving it as one argument.
And when you see:
*.txt
think:
Bash will expand this into matching filenames before executing the command.
Once these patterns become familiar, Bash commands stop looking like mysterious strings of punctuation.
They start looking like a small language.
Conclusion
The command line can look complicated because Bash gives special meaning to many characters.
But those characters aren't random.
Each one provides a specific piece of functionality, and once you understand the individual pieces, you can start combining them.
We began with the simple idea that Bash is a program that reads commands and executes them. From there, we looked at startup files, filename patterns, tilde expansion, brace expansion, quoting, variables, pipes, redirection, command substitution, background execution, grouping, and conditional execution.
These are the foundations you'll use again and again throughout your Bash journey.
The most important lesson isn't to memorize every syntax rule. It's to understand how Bash processes a command before the command is actually executed.
Bash expands variables, expands braces, expands filenames, performs command substitutions, handles redirections, connects pipelines, and finally executes the resulting commands.
Once you understand that process, Bash becomes much less mysterious.
And that's the point of this series: not to make you memorize hundreds of commands, but to make the terminal feel less like a frightening black box and more like a tool you actually understand.
The more you use it, the more you'll discover that a surprisingly large amount of work can be accomplished with a few simple commands—and a handful of very clever symbols.