Linux Tutorial / Write Your First Bash Script with Variables, Conditions, and Loops

A shell script stores commands and control flow in a reusable program. In Bash, quote variable expansions, validate inputs, and check exit status while building and testing a small script.

This lesson creates a first program stored in a file. Review shell execution flow for variables and operators, and Bash errors and safe file operations for failure, path, and concurrency limits.

Choose Bash explicitly and prepare a workspace

This lesson targets Bash on Ubuntu and Rocky Linux. Features such as [[ ... ]] and arrays are not portable to every sh, so do not run a Bash script with sh script.sh. Check bash --version, work in a new directory, and save shell files with LF line endings.

lab=$(mktemp -d "$HOME/linux-script-lab.XXXXXX")
cd "$lab"
mkdir "sample files"
printf "one\n" > "sample files/alpha.txt"
printf "two\n" > "sample files/beta.txt"

Create the complete runnable script

The following block creates list-files.sh in the current directory. The script validates one directory argument and prints its regular-file names. It does not modify the files, making it suitable for experimenting with variables, branches, and loops.

cat > list-files.sh <<'EOF'
#!/usr/bin/env bash
if (( $# != 1 )); then
  printf "Usage: %s DIRECTORY\n" "$0" >&2
  exit 2
fi
directory=$1
if [[ ! -d "$directory" ]]; then
  printf "Not a directory: %s\n" "$directory" >&2
  exit 1
fi
count=0
for path in "$directory"/*; do
  [[ -f "$path" ]] || continue
  printf "%s\n" "${path##*/}"
  count=$((count + 1))
done
printf "files=%s\n" "$count"
EOF
bash -n list-files.sh
bash list-files.sh "sample files"

Expect alpha.txt, beta.txt, and files=2; name ordering can depend on locale. bash -n checks syntax only, not runtime paths or permissions. The initial #! line is a shebang that selects an interpreter, not a shell prompt.

Understand variables and quoting

Assignment uses no spaces around the equals sign: directory=$1. Quoting "$directory" passes a path containing spaces as one argument. Single quotes generally suppress expansion; double quotes allow parameter expansion while preventing word splitting and pathname expansion. There is no need to reinterpret user input using eval.

In "$directory"/*, the directory portion is protected while the asterisk performs pathname expansion. Under default Bash settings, an unmatched pattern may remain literal, so the -f test skips it. The pattern excludes dotfiles and does not recurse. A symbolic link to a regular file can satisfy -f.

Read arguments, conditions, loops, and status

Syntax Meaning
$# / $1 Argument count / first argument
[[ -d ... ]] Test whether the target is a directory
for ...; do ...; done Apply a body to each expanded item
${path##*/} Remove the prefix through the last slash
exit 0 / 1 / 2 Success / invalid target / usage error in this example

If the final printf succeeds, the script finishes with status 0. The meanings of 1 and 2 are conventions chosen by this script, not a universal classification for every command. $? holds the immediately previous command’s status, so inspect it before running another command.

bash list-files.sh
printf "exit=%s\n" "$?"
bash list-files.sh missing-directory
printf "exit=%s\n" "$?"
chmod u+x list-files.sh
./list-files.sh "sample files"

Change one condition at a time

  1. Run the script against the original directory and confirm a count of 2.
  2. Add gamma.txt and confirm the count changes to 3.
  3. Remove only the added file and confirm the count returns to 2.
  4. Test an empty directory for files=0 and a missing path for an error and status 1.

If errors mention bash\r or ^M after editing on Windows, inspect line endings. bash FILE reads the script directly; ./FILE needs executable permission and a valid interpreter line. Check your current directory before cleaning up the lab.

If a Windows editor shows CRLF, change it to LF and save. In VS Code, choose CRLF → LF in the status bar. With GNU sed, the following also makes a backup and converts this exercise’s list-files.sh.

cp -i -- list-files.sh list-files.sh.crlf-backup
sed -i 's/\r$//' list-files.sh
bash -n list-files.sh
bash list-files.sh "sample files"

A syntax check should exit 0 without output; with only the original two example files, expect files=2. To undo the conversion, inspect the backup and restore it with cp -i -- list-files.sh.crlf-backup list-files.sh.

Official references