Running Scripts on Linux & Windows

Native Linux & macOS

On Linux and macOS a Bash script is just a text file with two extra markings: a shebang on the first line and the execute permission on the file. There is no compiler and no project file — that is the whole setup.

The Shebang

The first line, #!/usr/bin/env bash, tells the kernel which interpreter should read the file when you run it as a program. The kernel reads the two magic bytes #!, then executes the named program with your script as its first argument.

#!/usr/bin/env bash
# env looks up bash on your PATH — portable across Linux, macOS and WSL.

echo "Hello from $(uname -s)"

Two forms exist, and the difference matters:

ShebangMeaningWhen to use
#!/bin/bashRun this exact fileLinux where bash is always at /bin/bash
#!/usr/bin/env bashFind bash on PATHPortable; macOS and WSL
#!/bin/shPOSIX shell (may be dash)Scripts that avoid bashisms
The shebang only applies when the script is executed. If you run bash script.sh or source script.sh, the shebang line is treated as a comment.

Permissions & chmod

A newly created text file is readable but not executable. Grant execute permission, then run it with a path so the kernel — not your current shell — decides the interpreter.

chmod +x hello.sh      # add execute for owner/group/other as defined by umask
./hello.sh             # the shebang is honoured: kernel launches /usr/bin/env bash
ls -l hello.sh         # -rwxr-xr-x  -> the x bits are now set

If you forget the leading ./, the shell searches your PATH instead of the current directory and reports "command not found". That is a safety feature: it stops a stray ls in the current directory from shadowing /bin/ls.



Running on Windows

Windows has no native /bin/bash and the NTFS filesystem has no Unix permission bits, so the mechanisms differ. You have four realistic options.

WSL2

Windows Subsystem for Linux 2 runs a real Linux kernel in a lightweight virtual machine. This is the closest thing to native Linux and the recommended path for serious work.

wsl --install -d Ubuntu      # run once from an admin PowerShell/cmd
wsl                          # enter the Ubuntu environment
cd /mnt/c/Users/me/project   # your C: drive is mounted under /mnt/c
bash hello.sh

Inside WSL the filesystem is case-sensitive, chmod works, and #!/bin/bash behaves exactly as on a server. Store scripts that must be Unix-like under the Linux home directory (~/) rather than under /mnt/c, because cross-filesystem I/O is slow and loses permission metadata.

Git Bash

Installing Git for Windows also installs Git Bash: a MSYS2-based Bash that runs natively on Windows without a VM. It is perfect for quick scripts and for this track's exercises.

bash --version          # GNU bash, version 5.x ... x86_64-pc-msys
cd /c/Users/me/project  # the C: drive is mounted as /c
./hello.sh              # chmod is emulated; bash reads the shebang anyway

Git Bash translates paths for you, but the emulation has edges: some Linux tools are GNU ports, /proc is partial, and native Windows programs expect Windows-style paths.

MSYS2 & Cygwin

MSYS2 is the modern packaging of the MSYS environment (also used by Git Bash) with a real package manager, pacman. Cygwin is the older, more complete POSIX emulation layer. Both give you Bash plus most GNU utilities; Cygwin is heavier but closer to Linux semantics.



Anatomy of a Script

Every well-behaved script follows the same skeleton. Learn it now and reuse it forever.

#!/usr/bin/env bash
set -euo pipefail          # strict mode — explained in the robustness lesson

readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

main() {
  echo "script dir: $SCRIPT_DIR"
  echo "args: $# -> $*"
}

main "$@"                  # pass every argument through to main

Exit Status

Every command returns a number from 0 to 255. 0 means success; anything else means failure. The shell stores the last one in $?. This single convention is what lets &&, if, and pipelines work.

grep -q root /etc/passwd    # -q: quiet, only the status
echo "$?"                    # 0 if a match was found, 1 otherwise

exit 2                      # your script reports 2 to whoever called it

Passing Arguments

Positional parameters $1, $2 … hold the arguments; $0 is the script name; $# counts them; $@ expands to all of them as separate quoted words.

#!/usr/bin/env bash
echo "name:  $0"
echo "first: ${1:-<none>}"    # default value if the argument is missing
echo "count: $#"
printf 'arg: %s\n' "$@"       # one line per argument, spaces preserved

Editors & Line Endings

Use an editor with Bash syntax highlighting — VS Code (with the ShellCheck extension), Vim, or Emacs — and always save with LF line endings.

A carriage return (\r) from Windows CRLF line endings hides at the end of the shebang. The kernel then looks for an interpreter literally named bash\r and fails with bad interpreter: No such file or directory. Fix it with dos2unix script.sh, or in Git with git config --global core.autocrlf input.
file hello.sh         # look for "CRLF" — should say "ASCII text"
sed -i 's/\r$//' hello.sh   # strip carriage returns in place on Linux/Git Bash

With a shebang, executable permission (or an emulated equivalent), and LF endings you can run the same script on a Linux server, in a container, under WSL, and in Git Bash on Windows.