Bash: Getting Started with the Basic Concepts

Bash may look intimidating, but it is simply a program waiting for commands. Learn how Bash works, finds commands, handles files and patterns, and combines commands using pipes, redirection, variables, conditions, and more.

Bash: Getting Started with the Basic Concepts

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:

  • bash is the program
  • -c tells 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.

CharacterMeaning
;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:

  1. cd ~
  2. 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:

  1. Find a word in a file.
  2. If it's found, print the file.
  3. 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
  • if
  • grep
  • 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.

Read next

Bash Comments: Understanding the # Character

Learn how comments work in Bash, why they're essential for writing readable scripts, and the best practices every beginner should know. This guide covers single-line comments, inline comments, common mistakes, and the special #!/bin/bash exception.

Bash !: Inverting a Command's Exit Status

Learn how Bash's ! operator reverses a command's exit status and simplifies if statements and loops. This beginner-friendly guide explains exit codes, practical examples, common mistakes, and best practices for cleaner shell scripts.