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
- What a script is and when it is worth writing one
- The shebang: the first line that changes everything
- The four ways to run a script
- Execute permissions and why you need the
./ - Anatomy of a well-formed script
- Comments that are actually useful
- Exit codes:
exit Nas a contract - Names and location inside the toolkit
daily-report.sh, version 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.
- 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:
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), asystemdservice or another user with a different shell, it will be interpreted withshand 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.
- 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.
- Execute permissions and why you need the
./
./A newly created file is not executable. Recall the x bit from lesson 02-03:
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-x755 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.
- 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.
- 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
- Exit codes:
exit N as a contract
exit N as a contractIn 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 0means "everything is fine". It is the only value that means success.exit NwithNother 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.
- 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, noscript2.sh. The name must say what it does. - The
.shextension: 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.shfor teaching clarity. - Location in
bin/: since~/veloz-ops/binhas been in yourPATHsince 01-02, any script you drop there and mark as executable automatically becomes a system command, callable from any directory and without./.
daily-report.sh, version 1
daily-report.sh, version 1The 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 0Save it, give it permissions and run it:
===================================================
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 issueGo 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 deniedand exit code126. It is, by far, everyone's first stumble. - CRLF line endings. If you edit the script on Windows, every line ends with an invisible
\rand you will see absurd messages likebash: ./daily-report.sh: /usr/bin/env bash^M: no such file or directoryorbad interpreter. Diagnose it withfile daily-report.sh(it will say with CRLF line terminators) and fix it withdos2unix daily-report.shorsed -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/shwhile using Bash syntax. On Ubuntu that isdash: it will fail on[[ ]], arrays and arithmetic. If it is Bash, say so. - Running tools with
source. Anexitinside will close your terminal and the script's variables will pollute your session.sourceis for configuration and libraries. - Editing a system script without permissions. If
nanolets you type but not save, you have lost the work. Check first withls -lor edit withsudoedit. - Naming a script the same as an existing command. A file called
testorlsin yourbin/will cause chaos. Check first withtype -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 cacheThey 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 0Remember 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
- What Is Bash?
- Setting Up Your Environment
- Basic Command Line Navigation
- Understanding the Shell
- Finding Help: man, help and --help
Module 2: Basic Bash Commands
- File and Directory Operations
- Text Processing Commands
- File Permissions and Ownership
- Redirection and Piping
- Wildcards and Path Expansion
- History and Keyboard Shortcuts
Module 3: Scripting Fundamentals
- Creating and Running a Script
- Variables and Constants
- Basic Operators
- Conditional Statements
- Arguments and User Input
- Quoting, Expansion and Substitution
Module 4: Intermediate Scripting
- Loops in Bash
- Functions in Bash
- Arrays and Associative Arrays
- String Manipulation
- The case Statement and Interactive Menus
- Arithmetic and Numeric Calculations
Module 5: Advanced Scripting Techniques
- Advanced File Operations
- Process Management
- Error Handling and Debugging
- Regular Expressions
- Advanced I/O: Descriptors and Here-Documents
- Modular Scripts and Reusable Libraries
Module 6: Working with External Tools
Module 7: Automation and Scheduling
- Cron Jobs
- Automating Tasks
- Backup and Restore Scripts
- Monitoring and Logging
- Services and Timers with systemd
- Remote Automation with SSH
Module 8: Best Practices and Optimization
- Writing Readable Code
- Optimizing Bash Scripts
- Security Considerations
- Version Control with Git
- Static Analysis with ShellCheck and shfmt
- Automated Testing with Bats
- Portability: POSIX sh versus Bashisms
