We closed Module 2 with a very concrete limit: an alias does not accept arguments, a five-filter pipeline cannot be documented or version-controlled, and nothing you type today will run tomorrow at seven in the morning without you sitting there. This lesson breaks that limit. You are going to save those lines in a file, tell the system how to run it and turn it into a full-fledged command inside ~/veloz-ops/bin. By the end you will have the first real version of daily-report.sh, the script that will grow with you throughout the module until it becomes the central tool of the Veloz Envíos toolkit.

Contents

  1. What a script is and when it is worth writing one
  2. The shebang: the first line that changes everything
  3. The four ways to run a script
  4. Execute permissions and why you need the ./
  5. Anatomy of a well-formed script
  6. Comments that are actually useful
  7. Exit codes: exit N as a contract
  8. Names and location inside the toolkit
  9. daily-report.sh, version 1

  1. What a script is and when it is worth writing one

A Bash script is a plain text file with commands, one per line, that the shell reads and runs from top to bottom. There is no compilation, no project, no dependencies: exactly the same commands you type in the terminal, saved in a file.

That simplicity is deceptive, because what the file adds is not power but permanence. Compare it with what you have done so far:

Need Typed pipeline Alias Script
Run it tomorrow without remembering it No Yes Yes
Accept arguments Yes (by rewriting it) No Yes
Several lines and logic Very awkward No Yes
Be documented and version-controlled No Barely Yes
Run on its own in the middle of the night No No Yes
Be shared with a colleague By copying text No Yes

The practical rule for deciding is simple and worth internalizing: if you are going to repeat a sequence more than two or three times, or if you need someone else to run it exactly the same way, write a script. For a one-off query the terminal is still the right tool; writing a script for something you will do only once is wasted work.

There is a third criterion, less obvious but decisive in operations: if human error is expensive, write a script. A chain of filters you type every morning will eventually contain a typo. The script types it the same way every time.

  1. The shebang: the first line that changes everything

When the Linux kernel runs a file, it needs to know which interpreter should read it. That information goes on the very first line, which starts with the characters #! (called a shebang, from sharp + bang) followed by the path to the interpreter:

#!/usr/bin/env bash

The shebang must be line 1, column 1. Not a blank line before it, not a space in front of the #. To the shell it is just another comment (it starts with #), but to the kernel it is an instruction.

There are two common forms and it is worth understanding the difference:

Form How it works Advantages Drawbacks
#!/bin/bash Runs the binary at that exact path Explicit, no middleman; immune to a tampered PATH Fails if Bash is not in /bin (macOS with Homebrew, some BSDs); always uses the system Bash, even if it is old
#!/usr/bin/env bash Runs env, which looks for bash in the PATH Portable across systems; respects versions installed by the user Depends on the PATH; does not accept extra options portably

Recommendation for this course: #!/usr/bin/env bash. It is the de facto standard in scripts that get shared, and on srv-veloz-01 it works just as well. What you must never write if you use Bash-specific features is #!/bin/sh: on Ubuntu 24.04 that link points to dash, a much more limited POSIX shell that does not understand [[ ]], arrays or many other things you will use starting with the next lesson (POSIX portability is covered in depth in 08-07).

And what if the shebang is missing? The script does not necessarily fail, and that is the dangerous part. What happens is this:

  • If you run it with bash daily-report.sh, it works: you have already said which interpreter to use.
  • If you run it with ./daily-report.sh, the kernel does not know what to do with it and the interactive shell tries to run it with a copy of itself. In your Bash terminal it will look like it works.
  • If it is run by cron (Module 7), a systemd service or another user with a different shell, it will be interpreted with sh and fail in baffling ways.

In other words: a missing shebang produces a script that works in your terminal and fails in production, which is the worst kind of failure there is. That is why the shebang is not decoration.

  1. The four ways to run a script

There are four ways to launch a script and they are not equivalent. Understanding the difference connects directly with the subshells from lesson 01-04.

bash ~/veloz-ops/bin/daily-report.sh    # 1. explicit interpreter
./daily-report.sh                       # 2. executable, giving its path
daily-report.sh                         # 3. by name, via PATH
source ~/veloz-ops/bin/daily-report.sh  # 4. source (shorthand: a dot)
. ~/veloz-ops/bin/daily-report.sh       #    equivalent to the previous one
Form Needs the x permission? Honors the shebang? Where does it run?
bash script.sh No No (it is ignored, you already chose) New Bash process (subshell)
./script.sh Yes Yes New process according to the shebang
script.sh (via PATH) Yes Yes New process according to the shebang
source script.sh No No In your current shell

The first three create a child process. Lesson 01-04 comes back to life here: the variables the script defines die with it, and a cd inside the script does not change your current directory. That is exactly what you want from a tool: that it does its job and does not touch your session.

source is the radical exception: it creates no new process, it runs the lines of the file inside your shell. That is why source ~/.bashrc applies the aliases to the current session (lesson 01-02), and that is why source is the correct way to load a configuration file or a function library — the pattern you will use with ~/veloz-ops/etc/veloz-ops.conf and that is formalized in 05-06. And for that very same reason you must not use source to run a tool: if the script does exit 1, with source you will be closing your own terminal.

  1. Execute permissions and why you need the ./

A newly created file is not executable. Recall the x bit from lesson 02-03:

ls -l ~/veloz-ops/bin/daily-report.sh
-rw-rw-r-- 1 joan joan 412 Aug  3 12:04 /home/joan/veloz-ops/bin/daily-report.sh

Without x in the permissions, ./daily-report.sh answers Permission denied. It is fixed with:

chmod +x ~/veloz-ops/bin/daily-report.sh   # quick, respects the umask
chmod 755 ~/veloz-ops/bin/daily-report.sh  # explicit: rwxr-xr-x

755 is the canonical permission for a script in bin/: the owner can read, write and execute; everyone else can read and execute. If the script contained secrets you would use 700, but the good practice is for secrets to live in etc/veloz-ops.conf with 600 permissions, not in the code.

That leaves the question of the ./. When you type a bare name, Bash looks for it only in the PATH directories, and the current directory is not in the PATH (nor should it be: if it were, a malicious file called ls in a shared directory would run instead of the real ls). The ./ turns the name into an explicit relative path — "the file with that name, in this directory" — and disables the PATH lookup. It is a security measure, not a syntactic whim.

  1. Anatomy of a well-formed script

A professional script has a recognizable structure. This is the skeleton you will use from now on:

#!/usr/bin/env bash
#
# daily-report.sh - Daily activity summary for Veloz Envíos
#
# Purpose    : Count the errors in app.log and group shipments by status.
# Author     : Joan Costa <[email protected]>
# Created    : 2026-08-03
# Usage      : daily-report.sh
# Output     : Report on stdout.
# Exit codes : 0 success | 1 runtime error
#

# --- Body -------------------------------------------------------------
echo "hello from the toolkit"

exit 0    # --- Exit ---

The four parts, in order: shebang (line 1, no exceptions); comment header with what it does, who wrote it, how it is used and what it returns — six months from now, that header will be you explaining to yourself why this file exists; body, with the commands grouped into visually separated blocks; and a final exit with the explicit code. That structure is the convention anyone who opens the file expects, and in 08-01 we will extend it with finer readability criteria.

  1. Comments that are actually useful

Everything after a # up to the end of the line is ignored (except the shebang on line 1), both on its own line and at the end of a command. Bash has no block comments; to comment out several lines you put a # on each one, something any editor does with a shortcut.

The classic beginner's mistake is commenting what the command does, which you can already read in the command itself. What is valuable is commenting why:

# BAD: count the errors
grep -c ERROR /var/log/veloz/app.log

# GOOD: veloz-api logs 500s as ERROR; this count feeds the daily
#       alert to support. Threshold agreed with the business: 50 errors/day.
grep -c ERROR /var/log/veloz/app.log

  1. Exit codes: exit N as a contract

In lesson 01-04 you met $?. Now it is your turn to produce it. When your script finishes, it returns a number between 0 and 255 that is its only way of communicating with whoever called it: another script, cron, systemd or a CI pipeline.

  • exit 0 means "everything is fine". It is the only value that means success.
  • exit N with N other than 0 means error, and the specific number indicates which error.

If you omit exit, the script returns the code of the last command executed, which is almost never what you want to communicate. Be explicit. Some codes have an agreed meaning on the system:

Code Meaning
0 Success
1 Generic error
2 Incorrect command usage (missing arguments, unknown option)
3 Specific errors that you define: e.g. the data file is missing
126 The file exists but is not executable (the x is missing)
127 Command not found (it is not in the PATH, or the shebang points to a nonexistent interpreter)
130 Terminated with Ctrl-C (128 + signal 2)

Codes 126 and 127 are your two best diagnostic clues in this lesson: 126 is "you are missing chmod +x" and 127 is "the name or the shebang is wrong".

This contract is what lets you write daily-report.sh && send-email.sh, with the && from lesson 03-03: the email is sent only if the report finished properly.

  1. Names and location inside the toolkit

Conventions we will apply in ~/veloz-ops/bin throughout the course:

  • Lowercase and hyphens, descriptive name: daily-report.sh, rotate-logs.sh. No spaces, no accents, no uppercase, no script2.sh. The name must say what it does.
  • The .sh extension: useful while you are learning, because it identifies the language and turns on your editor's highlighting. In mature tools it is usually dropped (system commands do not carry it); we will keep .sh for teaching clarity.
  • Location in bin/: since ~/veloz-ops/bin has been in your PATH since 01-02, any script you drop there and mark as executable automatically becomes a system command, callable from any directory and without ./.

  1. daily-report.sh, version 1

The moment has arrived. We are going to move into a file the pipelines you were already writing by hand in Module 2.

Open the file with nano ~/veloz-ops/bin/daily-report.sh and type:

#!/usr/bin/env bash
#
# daily-report.sh - Daily activity summary for Veloz Envíos
#
# Purpose    : Count the ERROR entries in app.log and group shipments by status.
# Author     : Joan Costa <[email protected]>
# Created    : 2026-08-03
# Usage      : daily-report.sh
# Exit codes : 0 success
#

echo "==================================================="
echo "  DAILY REPORT - VELOZ ENVIOS"
date +"  Generated: %F %T"
echo "==================================================="

# --- Errors logged by the application --------------------------------
echo
echo "-- Errors in app.log --"
grep -c ERROR /var/log/veloz/app.log

# --- Shipments by status ----------------------------------------------
echo
echo "-- Shipments by status --"
tail -n +2 /srv/veloz/data/shipments.csv | cut -d, -f5 | sort | uniq -c | sort -rn

exit 0

Save it, give it permissions and run it:

chmod 755 ~/veloz-ops/bin/daily-report.sh
daily-report.sh
===================================================
  DAILY REPORT - VELOZ ENVIOS
  Generated: 2026-08-03 12:11:47
===================================================

-- Errors in app.log --
37

-- Shipments by status --
    981 delivered
    148 in_transit
     118 issue

Go over what just happened. You did not invoke it with ./ or with the full path: you typed daily-report.sh on its own, from any directory, because ~/veloz-ops/bin is in the PATH and the file has the x bit. You have just created a new command on srv-veloz-01.

Notice tail -n +2 too, which discards the CSV header line: without it, the word status would show up as if it were one more status. And notice that the script writes to stdout, redirecting nothing, which is deliberate: whoever calls it will decide whether to see it on screen, save it with > report.txt or do both with tee. A well-built script does not decide for its caller.

This script still has an obvious weakness: the paths and the numbers are hardcoded, scattered around the file. If shipments.csv moves, you have to search and replace. That is exactly the problem the next lesson solves.

Common Mistakes and Tips

  • Forgetting chmod +x. Symptom: Permission denied and exit code 126. It is, by far, everyone's first stumble.
  • CRLF line endings. If you edit the script on Windows, every line ends with an invisible \r and you will see absurd messages like bash: ./daily-report.sh: /usr/bin/env bash^M: no such file or directory or bad interpreter. Diagnose it with file daily-report.sh (it will say with CRLF line terminators) and fix it with dos2unix daily-report.sh or sed -i 's/\r$//' daily-report.sh.
  • A space or blank line before the shebang. It stops being a shebang and becomes just another comment.
  • Writing #!/bin/sh while using Bash syntax. On Ubuntu that is dash: it will fail on [[ ]], arrays and arithmetic. If it is Bash, say so.
  • Running tools with source. An exit inside will close your terminal and the script's variables will pollute your session. source is for configuration and libraries.
  • Editing a system script without permissions. If nano lets you type but not save, you have lost the work. Check first with ls -l or edit with sudoedit.
  • Naming a script the same as an existing command. A file called test or ls in your bin/ will cause chaos. Check first with type -a name (lesson 01-04).

Exercises

Exercise 1 — Diagnosing a script that will not start. A colleague has created /home/juan/veloz-ops/bin/summary.sh, but running summary.sh gives bash: summary.sh: command not found, and ./summary.sh gives bash: ./summary.sh: Permission denied. Explain both messages and give the commands that fix it.

Exercise 2 — Your second script. Create ~/veloz-ops/bin/server-status.sh with a complete header, showing the server name, the uptime, the free disk space and the three IPs that have accessed the most according to access.log. It must end with exit 0 and be callable by name from any directory.

Exercise 3 — Choosing the way to run it. For each case, state which of the four forms is the right one and why: (a) running the daily report from cron at 07:00; (b) loading the variables from ~/veloz-ops/etc/veloz-ops.conf into your session; (c) testing a freshly downloaded script without giving it execute permissions; (d) a script that must change your terminal's current directory.

Solutions

Solution to Exercise 1

chmod 755 ~/veloz-ops/bin/summary.sh     # gives the missing x bit
echo $PATH | tr ':' '\n' | grep veloz    # checks that bin/ is in the PATH
hash -r                                  # refreshes Bash's path cache

They are two different failures, and that is why there are two different messages. command not found (code 127) means Bash walked the PATH and found no file called summary.sh: either the directory is not in the PATH, or Bash has an old version cached (hence hash -r). Permission denied (code 126) is the other problem: with ./ it does find the file, but it is missing the x bit. It is worth fixing both, because fixing only the permissions would leave the script uncallable by name.

Solution to Exercise 2

#!/usr/bin/env bash
#
# server-status.sh - Summarized status of srv-veloz-01
#
# Purpose    : Quick health view of the server and of the traffic to veloz-api.
# Author     : Joan Costa <[email protected]>
# Usage      : server-status.sh
# Exit codes : 0 success
#

echo "-- Server --"; hostname; uptime

echo; echo "-- Disk --"
df -h /srv /var/log

echo; echo "-- Top 3 IPs in access.log --"
cut -d' ' -f1 /var/log/veloz/access.log | sort | uniq -c | sort -rn | head -3

exit 0

Remember to finish it off with chmod 755 ~/veloz-ops/bin/server-status.sh before calling it by name. Notice that df -h /srv /var/log limits the output to the partitions that matter instead of listing every filesystem: an operational report should answer a question, not dump data.

Solution to Exercise 3

Case Correct form Reason
(a) From cron at 07:00 Absolute executable path: /home/joan/veloz-ops/bin/daily-report.sh cron starts with a minimal PATH and without your ~/.bashrc; never assume your PATH exists there (Module 7)
(b) Loading veloz-ops.conf source ~/veloz-ops/etc/veloz-ops.conf The variables must stay in your shell; a subshell would die with them
(c) Testing without giving permissions bash script.sh It does not require the x bit and, on top of that, it lets you add bash -x to see every line before trusting it
(d) Changing the current directory source (or better, a function) A child process cannot change its parent's directory; it is a limitation of the system, not of Bash (lesson 01-04)

Case (d) deserves a nuance: a script that needs to modify your session is usually a sign that what you want is not a script but a shell function, and those arrive in 04-02.

Conclusion

You have made the jump from command to program. You know what a script is and when it is not worth writing one; you write the right shebang and you understand why its absence produces failures that only show up in production; you can tell the four ways of running apart and you know that source is a category of its own because it creates no subshell; you set permissions with chmod 755 and you understand that the ./ is a security defense; you structure the file with header, body and exit; you comment the why instead of the what; and you return exit codes that turn your script into a piece composable with &&.

Above all, you have daily-report.sh working in ~/veloz-ops/bin and callable by name from anywhere on srv-veloz-01. It is a real command, though still rigid: /var/log/veloz/app.log and /srv/veloz/data/shipments.csv are written literally in the middle of the file, and the error threshold you want to watch is nowhere to be found.

In lesson 03-02 we give it memory. You will learn to store values in variables, to fix paths and thresholds as constants at the top of the file with readonly, and to capture a command's output into a variable with $(...). From then on, changing a path will mean modifying one line, and daily-report.sh will start to look like a program.

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