Writing your first BASH script correctly: understanding the shebang line and the difference between #!/bin/bash and /usr/bin/env bash, setting execution permission with chmod +x, and dissecting the three ways to run a script — as a program, as an interpreter argument, and as a source.

In episode 1 we dissected the history — that BASH was born from the Bourne Shell, became the de facto standard, and sits among sh, zsh, and high-level languages — so in this episode we finally write our first real script. Episode 2 is the point where theory turns into practice: you'll write a file, make it executable, and run it through three different ways whose behavior is not the same.
The topics of this episode look simple — a first line called the shebang, one chmod +x command, and three ways to run a script. But don't be fooled: misunderstandings here are the source of many of the mysteries that frustrate beginners. Why does a script work with bash script.sh but fail with ./script.sh? Why do variables set inside a script "disappear" the moment the script ends? Why do files created on Windows error weirdly on Linux? All the answers are in this episode.
Every good BASH script opens with the two characters #! on the first line. This combination is called a shebang (or hashbang). It's not a comment — it's an instruction to the kernel: "run this file with the interpreter I point to."
#!/bin/bash
echo "Hello, BASH Scripting!"When you type ./script.sh in the terminal, the kernel reads the first two bytes #!, finds the path /bin/bash, and hands the entire file to BASH for execution. Without a shebang, the system doesn't know which interpreter to use — and you'll get a Permission denied error or have to invoke the interpreter manually.
An analogy: the shebang is like the "Attach to relevant department" label on a letter. The kernel is the front desk that reads the label and forwards the letter (the script) to the right desk (the BASH interpreter). Without the label, the letter doesn't know where to go.
#!/bin/bash vs #!/usr/bin/env bashTwo most common variants:
| Shebang | How it works | When to use |
|---|---|---|
#!/bin/bash | Invokes BASH directly from the absolute path /bin/bash | Most common on servers; deterministic |
#!/usr/bin/env bash | Looks up bash from $PATH | More portable on non-Linux systems (BSD, macOS with Homebrew) |
Why two options? On Linux, /bin/bash almost always exists. But on other systems, BASH can be located somewhere else — for example /opt/homebrew/bin/bash on macOS with Homebrew. #!/usr/bin/env bash solves that by finding bash wherever $PATH points.
The trade-off: env uses $PATH, so if $PATH is unusual (for example, the very minimal environment in cron), env bash can point to the wrong location. For this series — and for the majority of Linux server scripts — #!/bin/bash is the right, safe, and deterministic choice.
Note
Whether you use #!/bin/bash or #!/usr/bin/env bash, what matters is: always on the first line, with no leading space, and no spaces inside the path. Small mistakes here produce confusing errors — we cover them in the pitfalls section.
hello.shLet's write our first script. Create a file named hello.sh in your practice folder:
#!/bin/bash
echo "Hello, BASH Scripting!"
echo "Tanggal hari ini: $(date +%A)"
echo "Sistem ini bernama: $(hostname)"bash hello.shHello, BASH Scripting!
Tanggal hari ini: Minggu
Sistem ini bernama: arman-vmNotice: $(date +%A) and $(hostname) are command substitutions — the command's output is inserted into the string. We won't dissect them now (there's a dedicated episode), but look at how easy it is to weave command output into text — this is BASH's power that you'll keep using.
chmod +x: Granting the Permission to Become a ProgramRunning with bash hello.sh doesn't need execution permission — you're invoking the interpreter manually. But to run ./hello.sh, the file must have execution permission. This is where chmod +x comes in:
chmod +x hello.sh
./hello.shHello, BASH Scripting!Now you can check the difference in the file's metadata:
ls -l hello.sh-rwxr-xr-x 1 arman arman 89 Agu 2 10:00 hello.shThe -rwxr-xr-x notation means: owner can read-write-execute (rwx), group and others can read-execute (r-x). It's the execution permission (x) that allows the kernel to invoke the file as a program. Without chmod +x, ./hello.sh will refuse with Permission denied.
Tip
chmod +x adds execution permission for all categories (user, group, others). For personal scripts, chmod u+x hello.sh (owner only) is enough. Permission details — including octal modes like 755 and 644 — are covered in depth in the Learn Linux series' permission episode, but the principle is: a script needs x to be executed as a program.
This is the most important part of this episode — and the most common source of confusion. There are three ways to run a script, and they are not the same:
| Method | Syntax | New process? | Environment? |
|---|---|---|---|
| Program | ./hello.sh | Yes, new subshell | Inherits environment, changes don't come back |
| Interpreter argument | bash hello.sh | Yes, new subshell | Same as above |
| Source | source hello.sh or . hello.sh | No — runs in the current shell | Variable changes persist in the shell |
Let's dissect each one with an analogy: imagine the script is a sheet of instructions.
./hello.sh — you hand the instruction sheet to a new employee (a subshell process). The employee completes the task at their own desk, then goes home. Anything they wrote on their whiteboard (variables) is gone when they leave.
bash hello.sh — same as above, but you choose the interpreter manually instead of letting the shebang decide. Because of that, it runs even if the file doesn't have the x permission.
source hello.sh (or its shorthand . hello.sh) — you read out the instructions yourself at your desk, line by line, in the same shell. Anything you write on your own whiteboard (variables) stays there after you finish.
This difference is decisive. Let's prove it with an experiment.
exportCreate two small scripts:
#!/bin/bash
export MESSAGE="Dari dalam skrip"
echo "MESSAGE di dalam skrip: $MESSAGE"bash setenv.sh
echo "setelah bash setenv.sh : [$MESSAGE]"
. setenv.sh
echo "setelah source setenv.sh : [$MESSAGE]"MESSAGE di dalam skrip: Dari dalam skrip
MESSAGE di dalam skrip: Dari dalam skrip
setelah bash setenv.sh : []
setelah source setenv.sh : [Dari dalam skrip]Notice the results:
bash setenv.sh, the script runs in a subshell — the MESSAGE variable is created in that subshell and is gone the moment the script finishes. $MESSAGE in the main shell stays empty.source setenv.sh, the script runs in your own shell — the MESSAGE variable persists and is visible afterward.This is why configuration files (like .bashrc) are sourced, not run as programs: you want their changes to stick to the current shell, not vanish in a subshell.
Important
The execution mode must match your goal. Want a script to modify your shell's environment (aliases, variables, functions)? Use source. Want the script to run independently and isolated (backup, deployment, cron)? Use ./script.sh or bash script.sh. Using the wrong method is the root of "mysteriously disappearing variables" — not a bug, but a mistake in choosing the execution mode.
| Need | Right mode | Real-world example |
|---|---|---|
| Running an automation task | ./script.sh | ./deploy.sh, ./backup.sh |
| Running without execution permission | bash script.sh | bash setup.sh on someone else's system |
| Loading configuration into the shell | source / . | . ~/.bashrc, source env.local |
| Running a script that sets variables for a session | source | source venv/bin/activate |
The rule of thumb sysadmins hold onto: default to ./script.sh (respecting the shebang and execution permission); use source only when you truly want to affect the current shell.
Now that you know the core difference, let's formalize it. When you run ./script.sh or bash script.sh, the main shell creates a fork — a child process (subshell) that inherits the environment (exported variables) but executes its instructions in its own space. Any change inside it — variables, cd, export — doesn't affect the parent.
Conversely, source doesn't create a new process. The script executes line by line inside the current shell, exactly as if you'd typed it yourself. A cd inside the script will change your shell's directory; an export will apply to all subsequent commands.
cat > whereami.sh <<'EOF'
#!/bin/bash
cd /tmp
echo "Sekarang di: $PWD"
EOF
bash whereami.sh
echo "Shell utama masih di : $PWD"Sekarang di: /tmp
Shell utama masih di : /home/arman/labIf you replace bash whereami.sh with source whereami.sh, the main shell moves to /tmp too. This experiment makes the subshell vs source difference tangible.
Forgetting chmod +x and typing ./script.sh. The Permission denied error doesn't mean the script is wrong — the file just isn't executable yet. The fix is chmod +x script.sh. This is why bash script.sh "works" while ./script.sh doesn't.
A shebang with a space. #! /bin/bash (with a space after #!) isn't supported by all systems. Write it without a space: #!/bin/bash.
A shebang that isn't on the first line. The shebang must be the first line. If there's an empty line or a comment above it, the kernel treats the file as a plain text script — and it may get run with the wrong shell.
CRLF from Windows (the trickiest one). Files created in Notepad/Windows editors end with \r\n, while Linux uses \n. As a result, the first line becomes #!/bin/bash\r — the interpreter is "not found" and every line errors with $'\r': command not found. Fix it with:
dos2unix skrip.sh
# atau tanpa dos2unix:
sed -i 's/\r$//' skrip.shRunning a script that sets the environment with ./ when you wanted source. As we proved: an export inside a script run as a program is gone the moment it finishes. If you want the variables to persist, use source.
Typing bash script.sh when the shebang is #!/usr/bin/env bash and the content uses BASH features. That's fine as long as BASH is what's invoked — but this habit can mask interpreter problems on other systems. Get used to invoking via ./script.sh so the shebang does the work.
cat -A skrip.sh#!/bin/bash^M$
echo "Halo"^M$The ^M at the end of lines is the CRLF trace from Windows — the clearest visual clue why a script errors "weirdly".
Warning
When a script errors with $'\r': command not found or the interpreter is "not found" even though the shebang looks correct, CRLF should be your first suspicion. Run cat -A script.sh and look for ^M. This is one of the most common bugs that hits every beginner — and now you know how to beat it.
In episode 2 you wrote your first BASH script and understood three foundations that will stay with you forever: the shebang as the interpreter's identity (with the deterministic #!/bin/bash vs the portable #!/usr/bin/env bash), chmod +x as the key to running a script as a program, and three execution modes — ./script.sh and bash script.sh, which run in an isolated subshell, and source/., which runs in the current shell and makes changes persist.
The core takeaways:
#!/bin/bash for pure BASH.chmod +x is needed for ./script.sh to run; bash script.sh doesn't need execution permission../script.sh and bash script.sh run in a subshell — variable changes don't persist.source script.sh runs in the current shell — variables, cd, and export persist.$'\r' / interpreter not found errors = the file has CRLF line endings from Windows.Now you know how to write and run scripts. In episode 3 we'll enter the BASH language itself: an introduction to and explanation of variables & environment variables — naming rules, the difference between $VAR and ${VAR}, export to pass variables to child processes, built-in variables like $PATH, $HOME, $USER, and building scripts that read the environment correctly. That's where you start "talking" to BASH, not just commanding it. See you there!