In the previous lesson you met Veloz Envíos and the veloz-ops toolkit we will build during the course. Before writing the first line of that toolkit you need a proper working environment: Bash installed and in a modern version, a comfortable terminal, your configuration files under control and the project's directory skeleton already created. This is the lesson people skip the most and the one that prevents the most trouble: a badly set up environment produces incomprehensible errors weeks later ("it works on my machine, not on the server"). Let us get it right from the start.

Contents

  1. Checking whether you already have Bash and which version
  2. Bash on Linux
  3. Bash on macOS: the version 3.2 problem
  4. Bash on Windows: WSL2 and Git Bash
  5. Opening a terminal and choosing an emulator
  6. Bash configuration files and when each one is read
  7. Basic customization: PS1, aliases and PATH
  8. Creating the ~/veloz-ops project skeleton
  9. Reloading the configuration with source
  10. Choosing an editor: nano, vim, VS Code

  1. Checking whether you already have Bash and which version

The first step, always, is to find out what we are starting from. There are two complementary ways of asking for the Bash version.

bash --version
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This command runs the bash program found in your PATH and asks it to identify itself. It is the version that would be used when launching a new script.

echo "$BASH_VERSION"
5.2.21(1)-release

Here you do not run any program: you query an internal variable of the shell you have open right now. The difference matters: if your terminal starts Zsh, bash --version will tell you which Bash is installed, but $BASH_VERSION will be empty because you are not inside Bash.

1.1 Why the version matters

Version Relevant additions Where you find it
3.2 (2007) POSIX base + indexed arrays, [[ ]] macOS by default
4.0 (2009) Associative arrays (declare -A), recursive **, &>> Old distros (CentOS 7)
4.2 (2011) declare -g, negative array indices RHEL 7
4.4 (2016) ${var@Q} (safe quoting), improved mapfile, local -n Debian 9, Ubuntu 18.04
5.0 (2019) EPOCHSECONDS, EPOCHREALTIME, BASH_ARGV0 Ubuntu 20.04, RHEL 9
5.1–5.2 (2020-2022) ${var@k}, performance improvements, PROMPT_COMMAND as an array Ubuntu 22.04/24.04, Fedora

For this course we will work on Bash 5.x, which is what srv-veloz-01 ships (Ubuntu 24.04 LTS). The two points where the version will really bite you are:

  • Associative arrays (declare -A, Bash's "dictionaries"). We will use them in Module 4 to count shipments per city. They do not exist before Bash 4.
  • ${var@Q}, which returns a variable's value safely quoted so you can reuse it. It is very handy for debugging and logging, and it requires Bash 4.4 or later.

If your version is below 4.4, read on: the macOS section explains how to upgrade.

  1. Bash on Linux

This is the simplest case: practically every distribution ships Bash installed and as the user's default shell. If for some reason it were missing:

# Debian, Ubuntu and derivatives
sudo apt update && sudo apt install bash

# Fedora, RHEL, Rocky, AlmaLinux
sudo dnf install bash

# Arch Linux
sudo pacman -S bash

# Alpine Linux (containers)
apk add bash

An important detail for the Alpine case: Docker images based on Alpine do not ship Bash, only BusyBox's ash. If a Veloz Envíos Dockerfile uses RUN bash script.sh on Alpine without installing it, it will fail with bash: not found. It is a classic.

To find out which shell is assigned to you as login shell:

echo "$SHELL"
/bin/bash

Beware of a frequent confusion: $SHELL does not tell you the shell you are running right now, but the one configured in /etc/passwd as your login shell. If you start Bash from inside Zsh, $SHELL will still say /bin/zsh. We will see the reliable way of knowing where you are in 01-04.

To change your login shell permanently:

chsh -s /bin/bash

The change takes effect in the next session, not the current one.

  1. Bash on macOS: the version 3.2 problem

macOS includes Bash, but frozen at version 3.2 from 2007. The reason is not technical but a licensing one: from Bash 4 onwards, the GNU project moved to the GPLv3 license, which Apple does not incorporate into its system. On top of that, since Catalina the default shell for new accounts is Zsh.

Check your situation:

/bin/bash --version
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

If you are going to follow this course from macOS, install a modern Bash with Homebrew:

# 1. Install Homebrew if you do not have it (see brew.sh)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 2. Install a modern Bash
brew install bash

# 3. Check where it ended up
brew --prefix bash
/opt/homebrew/opt/bash

Homebrew does not replace the system /bin/bash (macOS protects it): it installs a new one in /opt/homebrew/bin/bash (Apple Silicon) or /usr/local/bin/bash (Intel). To make that the one in use:

# Register the new shell as valid
echo "$(brew --prefix)/bin/bash" | sudo tee -a /etc/shells

# Set it as the login shell
chsh -s "$(brew --prefix)/bin/bash"
Path Version What it is
/bin/bash 3.2 Apple's. Cannot be touched.
/opt/homebrew/bin/bash 5.x The modern one, for your daily work
/bin/zsh 5.x The default macOS shell since Catalina

In your scripts, always use #!/usr/bin/env bash instead of #!/bin/bash: the first form looks for Bash in the PATH and will find the modern one, whereas the second points rigidly at Apple's 3.2.

  1. Bash on Windows: WSL2 and Git Bash

Windows does not ship Bash, but there are two routes with very different purposes.

4.1 WSL2 (recommended)

WSL2 (Windows Subsystem for Linux 2) runs a real Linux kernel inside Windows. It is not an emulation: it is a lightweight virtual machine that integrates seamlessly. It is the closest thing to working on srv-veloz-01 that you can have on a Windows laptop.

From PowerShell as administrator:

wsl --install -d Ubuntu-24.04

After rebooting and creating your Linux user, you have a complete Ubuntu with Bash 5.2. Useful commands from PowerShell:

wsl --list --verbose      # see installed distributions and their state
wsl --set-default Ubuntu-24.04
wsl --shutdown            # restart the subsystem if it gets stuck

Inside WSL, your Windows drives appear under /mnt/c, /mnt/d, and so on.

ls /mnt/c/Users

A performance tip that saves a lot of grief: keep your projects inside the Linux filesystem (/home/your_user/...), not in /mnt/c/.... Access across filesystems is slow and Linux permissions are not represented correctly on NTFS.

4.2 Git Bash

Git for Windows includes Git Bash: a Bash compiled for Windows with a minimal set of Unix utilities (ls, grep, sed, ssh). It is convenient for using Git and running simple scripts, but it is not Linux: there is no systemd, no package manager, many tools are missing and some behaviors differ.

Criterion WSL2 Git Bash
Real Linux kernel Yes No
Package manager (apt) Yes No
systemd, cron Yes No
Installation Requires enabling the feature Simple installer
Fidelity to srv-veloz-01 High Low
Recommended for this course Yes Only as a stopgap

From Module 7 onwards (cron, systemd) Git Bash simply will not do. If you work on Windows, install WSL2.

  1. Opening a terminal and choosing an emulator

How you open a terminal depends on the system:

System How to open it
Ubuntu/GNOME Ctrl + Alt + T, or search for "Terminal"
KDE Search for "Konsole"
macOS Cmd + Space → "Terminal" or "iTerm"
Windows + WSL Open "Windows Terminal" and pick the Ubuntu tab

Common emulators and why you might choose one over another:

  • GNOME Terminal / Konsole: the ones your desktop ships. More than enough to get started.
  • Windows Terminal: mandatory on Windows; it handles PowerShell, CMD and WSL tabs in the same window, with good color and font support.
  • iTerm2 (macOS): pane splitting, history search, profiles. The standard among Mac professionals.
  • Alacritty / WezTerm / Kitty: GPU-accelerated emulators, very fast with huge log output.
  • The VS Code integrated terminal: convenient because you get editor and shell in the same window.

Remember what you saw in 01-01: the emulator is not the shell. Switching from Alacritty to Kitty does not change a single comma of your Bash.

  1. Bash configuration files and when each one is read

This is where most of the confusion lives, and it is worth genuinely understanding. Bash reads different files depending on how it was started, and there are two independent axes.

6.1 The two axes: login and interactive

  • Login shell: the first one started when you authenticate. It happens when you log in over SSH to srv-veloz-01, when you sign in on a text console, or when you run bash --login.
  • Interactive shell: it has a prompt and waits for you to type. Opening a terminal tab on the desktop starts an interactive shell that is not a login shell.
  • Non-interactive shell: it runs a script and finishes. There is no prompt and nobody in front of it. That is the case of a script launched by cron.

6.2 Table of what is read and when

Situation Files Bash reads Typical example
Interactive login /etc/profile, then the first one that exists of ~/.bash_profile, ~/.bash_login, ~/.profile Logging in over SSH to srv-veloz-01
Interactive non-login /etc/bash.bashrc and ~/.bashrc Opening a terminal tab
Non-interactive (script) None of the above; only the file named in $BASH_ENV, if it is defined A script launched by cron
Closing a login session ~/.bash_logout Leaving the SSH session
graph TD
    A[Bash starts] --> B{Is it a login shell?}
    B -->|Yes| C["/etc/profile → ~/.bash_profile<br/>(or ~/.bash_login, or ~/.profile)"]
    B -->|No| D{Is it interactive?}
    D -->|Yes| E["/etc/bash.bashrc → ~/.bashrc"]
    D -->|No| F["Nothing, except $BASH_ENV"]
    C --> G[Prompt ready]
    E --> G

6.3 The practical consequence

The classic problem follows from the table: if you put your aliases in ~/.bash_profile, they will not show up when you open a terminal tab (which is non-login), and if you put them only in ~/.bashrc, they will not show up when you log in over SSH (which is login). The universally adopted solution is to have ~/.bash_profile load ~/.bashrc:

# ~/.bash_profile
# If the shell is interactive, also load the ~/.bashrc configuration
if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

Line-by-line breakdown:

  • if [ -f ~/.bashrc ]; then checks whether a regular file called ~/.bashrc exists. The -f option means "it is a regular file". Conditionals are studied in depth in 03-04.
  • The lone dot . is a synonym of source: it runs the contents of that file in the current shell, as if you had typed it.
  • fi closes the if (it is if spelled backwards, a Bash convention).

On Ubuntu, ~/.profile already ships this logic out of the box. If you create your own ~/.bash_profile, bear in mind that ~/.profile will stop being read, because Bash only reads the first of the three that it finds.

The golden rule, and what you should remember:

What you want to configure Where to put it
Aliases, functions, PS1, interactive options ~/.bashrc
Environment variables (PATH, EDITOR) that programs must inherit ~/.profile or ~/.bash_profile
Configuration common to every user on the server /etc/profile.d/*.sh

  1. Basic customization: PS1, aliases and PATH

7.1 The PS1 prompt

PS1 is the variable that defines the look of the prompt. Bash supports a set of escape sequences of its own:

Sequence Meaning
\u User name
\h Short machine name
\w Full current directory (with ~)
\W Just the name of the current directory
\$ # if you are root, $ otherwise
\t Time in HH:MM:SS format
PS1='\u@\h:\w\$ '
joan@srv-veloz-01:~/veloz-ops$

A somewhat more informative prompt with color, designed for working on servers:

PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ '

Here \e[32m turns on green, \e[34m blue and \e[0m restores the default color. The \\[ and \\] delimiters are essential: they tell Bash that what is inside takes up no space on screen. If you leave them out, the shell will miscalculate the length of the line and you will see the text overwriting itself when you use the history.

A very useful professional tip: use different colors on your laptop and on the production server. A red prompt on srv-veloz-01 is a constant visual reminder of where you are typing, and it prevents the classic rm run on the wrong machine.

7.2 Aliases

An alias is a shortcut: a word that Bash replaces with something else before running it.

alias ll='ls -lh'
alias veloz-logs='cd /var/log/veloz'
alias ops='cd ~/veloz-ops'

Now typing ll is the same as typing ls -lh. Things you need to know:

  • Do not leave spaces around the =: alias ll = 'ls -lh' is an error.
  • Aliases only work in interactive shells. A script does not see them; that is why scripts use functions (Module 4), not aliases.
  • alias with no arguments lists everything defined.
  • To bypass an alias temporarily, prefix it with a backslash: \ls runs the real ls.
alias
alias ll='ls -lh'
alias ops='cd ~/veloz-ops'
alias veloz-logs='cd /var/log/veloz'

7.3 The PATH variable

PATH is a colon-separated list of directories where Bash looks for programs when you type a command name.

echo "$PATH"
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

When you type date, Bash walks those directories from left to right and runs the first match. We will pick the full lookup mechanism apart in lesson 01-04.

To add a directory of your own:

export PATH="$HOME/veloz-ops/bin:$PATH"

Three details that matter:

  • export makes the variable inherited by the programs you launch from this shell. Without export it would only exist in the current shell.
  • Putting $HOME/veloz-ops/bin first means your scripts take priority. Putting it last ($PATH:$HOME/veloz-ops/bin) is more conservative, because it prevents a script of yours called ls from shadowing the system one.
  • Never include . (the current directory) in the PATH. It is a classic security risk: all it would take is for someone to leave a file called ls in a shared directory for you to run it unintentionally. We will cover this in 08-03.

  1. Creating the ~/veloz-ops project skeleton

The time has come to create the structure that will accompany us throughout the course. Each directory has a clear purpose, mirroring the convention of any Unix package:

Directory Contents Future example
bin/ Executable scripts, the ones invoked by name veloz-report, veloz-backup
lib/ Reusable functions that other scripts load with source log.sh, csv.sh
etc/ Configuration files veloz-ops.conf
logs/ Output generated by our own scripts report-2026-08-03.log

Create it with a single command:

mkdir -p ~/veloz-ops/{bin,lib,etc,logs}

Let us analyze this line, because it contains two new ideas:

  • mkdir creates directories. The -p option means "also create any parent directories needed, and do not complain if they already exist". Without -p, the command would fail if ~/veloz-ops did not exist yet.
  • {bin,lib,etc,logs} is a brace expansion: Bash turns it into four paths before running anything. In other words, mkdir actually receives four arguments. This expansion is studied in 02-05.

Check the result:

ls -R ~/veloz-ops
/home/joan/veloz-ops:
bin  etc  lib  logs

/home/joan/veloz-ops/bin:

/home/joan/veloz-ops/etc:

/home/joan/veloz-ops/lib:

/home/joan/veloz-ops/logs:

Now add bin/ to the PATH permanently. Open ~/.bashrc with your editor and append at the end:

# --- veloz-ops toolkit ---
export PATH="$HOME/veloz-ops/bin:$PATH"
alias ops='cd ~/veloz-ops'
alias veloz-logs='cd /var/log/veloz'

Notice that $HOME is used and not ~. Inside double quotes, the tilde is not expanded, so "~/veloz-ops/bin" would create an invalid literal path. $HOME always works. This is a very common mistake and a hard one to spot.

  1. Reloading the configuration with source

You have edited ~/.bashrc, but your open shell does not find out on its own: it read that file at startup and never looks at it again. You have two options: close and reopen the terminal, or reload it.

source ~/.bashrc

Or, equivalently and more portably:

. ~/.bashrc

source runs the contents of the file in the current shell, so the variables and aliases it defines become available here and now. This is different from running the file as a script: in that case Bash would start a child process, define the variables there and lose them when it finished. That difference — fundamental, and responsible for countless wasted hours — will fully click in lesson 01-04, when we talk about subshells.

Check that it worked:

echo "$PATH" | tr ':' '\n' | head -3
/home/joan/veloz-ops/bin
/usr/local/sbin
/usr/local/bin

This command splits the PATH on the colons and shows the first three entries; tr and pipes are explained in Module 2, here we are just using it to read comfortably.

A warning: if you run source ~/.bashrc several times, the line export PATH="$HOME/veloz-ops/bin:$PATH" will add the directory over and over. It is not serious, but it is messy. A defensive version:

# Add only if it is not already present
case ":$PATH:" in
    *":$HOME/veloz-ops/bin:"*) ;;
    *) export PATH="$HOME/veloz-ops/bin:$PATH" ;;
esac

The case statement appears in 04-05; keep it as a recipe for now.

  1. Choosing an editor: nano, vim, VS Code

You are going to write a lot of text files. Pick a tool and learn at least the basics of it.

Editor Learning curve When to use it
nano Very low Quick edits on a server; it is everywhere
vim High When you work a lot over SSH; extremely powerful once mastered
VS Code Low Local development, with highlighting, ShellCheck and debugging
micro Low A modern alternative to nano, with desktop-style shortcuts

10.1 nano

nano ~/.bashrc

The shortcuts appear at the bottom; ^ means Ctrl. The three essential ones: Ctrl+O saves, Ctrl+X exits, Ctrl+W searches.

10.2 vim

vim ~/.bashrc

Vim is modal, and that is where its initial difficulty lies. Bare survival:

  • You start in normal mode (characters are commands, not text).
  • i enters insert mode so you can type.
  • Esc returns to normal mode.
  • :w saves, :q exits, :wq saves and exits, :q! exits discarding changes.

Even if you do not adopt it, learn that minimum: on a freshly installed server it may be the only editor available.

10.3 VS Code with WSL

If you work on Windows with WSL2, install Microsoft's WSL extension. Then, from your Ubuntu terminal:

code ~/veloz-ops

VS Code will open in Windows but work inside the Linux filesystem, with its integrated terminal pointing at Bash. It is the best combination of comfort and fidelity to the server environment.

Recommended extensions for this course:

  • ShellCheck: flags shell errors as you type (we will cover it thoroughly in 08-05).
  • shell-format: formats scripts automatically.
  • Bash Debug: lets you step through execution.

Finally, set your preferred editor so that other tools (such as crontab -e, which we will use in Module 7) respect it. In ~/.profile:

export EDITOR=nano
export VISUAL=nano

Common Mistakes and Tips

  • Putting aliases in ~/.bash_profile. They will not show up when you open a terminal tab, because that is a non-login session. Aliases and PS1 belong in ~/.bashrc.
  • Creating a ~/.bash_profile and "losing" the ~/.profile configuration. Bash reads only the first of ~/.bash_profile, ~/.bash_login, ~/.profile. If you create the first, the third stops being read.
  • Writing export PATH="~/veloz-ops/bin:$PATH". The tilde is not expanded inside double quotes. Use $HOME.
  • Forgetting $PATH when reassigning it. Writing export PATH="$HOME/veloz-ops/bin" leaves the system with no access to ls, grep or anything else. If it happens to you, the current session becomes unusable but it is fixed by opening a new terminal and correcting the file; to rescue the broken session you can use export PATH=/usr/bin:/bin temporarily.
  • Spaces in alias ll = 'ls -lh'. In Bash, assignments do not allow spaces around the =.
  • Editing ~/.bashrc and expecting the change to apply by itself. Reload with source ~/.bashrc or open a new terminal.
  • Tip: before touching ~/.bashrc, make a copy (cp ~/.bashrc ~/.bashrc.bak). A syntax error in there can leave you with a terminal that prints errors on every startup.
  • Tip: if you manage several servers, keep your dotfiles in a Git repository. We will see how in 08-04.

Exercises

Exercise 1: Diagnosing your environment

Find out and note down, on your own machine:

  1. The installed Bash version and whether it supports associative arrays (you need 4.0 or later).
  2. Your configured login shell.
  3. How many directories your PATH has.
  4. Whether ~/.bashrc, ~/.bash_profile and ~/.profile exist.

Exercise 2: Setting up the veloz-ops environment

Get your environment ready for the course:

  1. Create ~/veloz-ops with the bin, lib, etc and logs subdirectories in a single command.
  2. Add ~/veloz-ops/bin to the front of the PATH permanently.
  3. Define an ops alias that takes you to ~/veloz-ops.
  4. Change your prompt so that it shows user, machine and current directory.
  5. Apply the changes without closing the terminal and check that everything works.

Exercise 3: Reasoning about the startup files

A colleague at Veloz Envíos defines export API_TOKEN="abc123" only in their ~/.bashrc. They then schedule a cron script that needs that variable, and the script fails saying API_TOKEN is empty.

  1. Explain why it fails.
  2. Propose two different solutions.

Solutions

Solution to Exercise 1

bash --version | head -1
echo "$BASH_VERSION"
echo "$SHELL"
echo "$PATH" | tr ':' '\n' | wc -l
ls -la ~/.bashrc ~/.bash_profile ~/.profile
GNU bash, version 5.2.21(1)-release (x86_64-pc-linux-gnu)
5.2.21(1)-release
/bin/bash
6
-rw-r--r-- 1 joan joan 3771 Aug  3 09:12 /home/joan/.bashrc
ls: cannot access '/home/joan/.bash_profile': No such file or directory
-rw-r--r-- 1 joan joan  807 Aug  3 09:12 /home/joan/.profile

Interpretation: there is Bash 5.2, well above the 4.0 needed for associative arrays; the login shell is Bash; the PATH has 6 entries (tr turns the : into newlines and wc -l counts lines); and ~/.bashrc and ~/.profile exist, but not ~/.bash_profile, which is the standard Ubuntu configuration.

Solution to Exercise 2

# 1. Directory structure
mkdir -p ~/veloz-ops/{bin,lib,etc,logs}

# 2, 3 and 4. Persistent configuration
cat >> ~/.bashrc << 'EOF'

# --- veloz-ops toolkit ---
export PATH="$HOME/veloz-ops/bin:$PATH"
alias ops='cd ~/veloz-ops'
PS1='\u@\h:\w\$ '
EOF

# 5. Apply and verify
source ~/.bashrc
echo "$PATH" | tr ':' '\n' | head -1
alias ops
ls ~/veloz-ops
/home/joan/veloz-ops/bin
alias ops='cd ~/veloz-ops'
bin  etc  lib  logs

The cat >> file << 'EOF' ... EOF construct is a here-document: it appends to the end of the file all the text up to the EOF marker. The single quotes around 'EOF' matter: they stop $HOME and $PATH from being expanded now instead of being written literally into the .bashrc. Here-documents are studied in 05-05; you can also simply open the file with nano ~/.bashrc and type the lines by hand.

Solution to Exercise 3

  1. It fails because cron scripts run in a non-interactive, non-login shell. As we saw in the table, in that mode Bash reads neither ~/.bashrc nor ~/.bash_profile. In fact, ~/.bashrc on Ubuntu starts with a check that aborts its execution if the shell is not interactive. Therefore API_TOKEN is never defined in cron's environment.

  2. Two valid solutions:

    • Define the variable inside the script itself or in a configuration file that the script loads explicitly, for example ~/veloz-ops/etc/veloz-ops.conf with a source at the top. This is the cleanest one and the one we will use in the course: the script does not depend on the environment of whoever launches it.
    • Define it in the crontab itself, since cron accepts assignments before the task lines (API_TOKEN=abc123). It works, but it mixes secrets with task scheduling.

    As a variant, BASH_ENV could be used pointing at an environment file, but that is uncommon and fragile. In general, the lesson to take away is: a script must not depend on the user's interactive configuration. We will see this applied in 07-01.

Conclusion

Your environment is now ready: you know which Bash version you have and why it matters, how to get a modern Bash on Linux, macOS and Windows, and — the most valuable thing in the medium term — which configuration file is read in each situation, which is the root of most "it does not work for me" cases. On top of that, ~/veloz-ops already exists with its four directories, its bin is in the PATH and you have aliases and a prompt to your taste.

With the ground prepared, it is time to learn how to move around it. In the next lesson, Basic Command Line Navigation, you will walk the Linux directory tree, understand the anatomy of a command and explore the real Veloz Envíos infrastructure for the first time: /var/log/veloz, /srv/veloz/data and your own ~/veloz-ops.

Bash Programming Course

Module 1: Introduction to Bash

Module 2: Basic Bash Commands

Module 3: Scripting Fundamentals

Module 4: Intermediate Scripting

Module 5: Advanced Scripting Techniques

Module 6: Working with External Tools

Module 7: Automation and Scheduling

Module 8: Best Practices and Optimization

Module 9: Real-World Projects

© Copyright 2026. All rights reserved