Short answer
bash script.sh runs it too.- Open a terminal in the folder that holds the file:
cd ~/scripts - Make it executable once:
chmod +x script.sh - Run it with a path in front of the name:
./script.sh
Key facts
- Make it executable
- chmod +x script.sh
- Run it from the same folder
- ./script.sh
- Run it without chmod
- bash script.sh
- Run it in your current shell
- source script.shor . ./script.sh
- /bin/sh on Debian and Ubuntu
- dashnot bash
- Exit status of the last run
- echo $?
Those three steps cover most cases. The rest of this page is about what each way of starting a script actually changes, because ./script.sh, bash script.sh, sh script.sh and source script.sh do not do the same thing, and the differences explain nearly every error people hit. Every command and output below ran on Debian 12 with bash 5.2.15 and dash 0.5.12 on 2 November 2026. Debian 13 and Ubuntu 24.04 use the same pair, bash 5.2 with dash as /bin/sh. On older releases the commands work the same way, only the CRLF error further down is worded differently.
What is a .sh file?
A .sh file is a plain text file full of shell commands, run from top to bottom as if you had typed them one after another. Linux does not care about the extension: a script called backup with no extension runs exactly like backup.sh. What decides how it runs are two other things: the execute permission and the first line, the shebang, which names the interpreter.
#!/bin/bash
echo "Hello from $0, running in bash $BASH_VERSION"file reads that first line, not the name, to tell you what you have:
$ file hello.sh
hello.sh: Bourne-Again shell script, ASCII text executableBecause a script can do anything your account can do, read one before you run it, especially a script you downloaded and especially as root. less script.sh shows it without running anything.
How to run a .sh file: make it executable and run it
Create the script with any text editor (nano hello.sh works on every server), then check its permissions. A new file has no execute bit, so running it fails even as root:
$ ls -l hello.sh
-rw-r--r-- 1 root root 64 Nov 2 03:41 hello.sh
$ ./hello.sh
bash: ./hello.sh: Permission denied
$ chmod +x hello.sh
$ ls -l hello.sh
-rwxr-xr-x 1 root root 64 Nov 2 03:41 hello.sh
$ ./hello.sh
Hello from ./hello.sh, running in bash 5.2.15(1)-releasechmod +x without a letter in front adds the execute bit for the owner, the group and everyone else, minus whatever your umask blocks (chmod(1)). With the usual umask of 022 that gives rwxr-xr-x, as above. To allow only yourself, use chmod u+x script.sh. The octal forms do the same with numbers: chmod 755 script.sh gives rwxr-xr-x, chmod 700 script.sh gives rwx------. chmod and the other everyday commands are collected in the Linux commands cheat sheet.
Why ./ is needed
Without a slash in the name, the shell looks for the command only in the folders listed in PATH, and the current folder is not one of them. So the bare name fails with exit status 127, while any path to the file works:
$ hello.sh
bash: hello.sh: command not found
$ ./hello.sh
Hello from ./hello.sh, running in bash 5.2.15(1)-release
$ /home/deploy/scripts/hello.sh
Hello from /home/deploy/scripts/hello.sh, running in bash 5.2.15(1)-releaseTo run a script by name from anywhere, put it into a folder that is on PATH: /usr/local/bin for every user, or ~/.local/bin or ~/bin for yourself (Debian and Ubuntu’s default ~/.profile adds those two when they exist, at your next login). Drop the .sh if you like; the name is only a name. How PATH works is covered in setting environment variables.
Pass arguments to the script
Words after the script name become $1, $2 and so on, $# counts them and "$@" is all of them. Quote an argument that contains spaces:
#!/bin/bash
echo "script: $0"
echo "count: $#"
echo "first: $1"
echo "second: $2"
for a in "$@"; do echo "arg: $a"; done$ ./args.sh web01 "two words"
script: ./args.sh
count: 2
first: web01
second: two words
arg: web01
arg: two wordsThat for a in "$@" line is the usual way to walk through arguments; bash for loops covers the other forms.
Run it as root or as another user
sudo ./script.sh runs it as root and sudo -u deploy ./script.sh as the user deploy, who must be able to read the file and enter its folder. On Debian and Ubuntu, sudo replaces your PATH with its own secure_path, so a command that works in your shell can be missing inside the script; exit code 127 under sudo shows the fix. Give a script root only when it needs it.
On a desktop (file manager)
In GNOME Files, the file manager of Ubuntu 24.04, right-click the script, open Properties and switch on Executable as Program. After that the right-click menu of the file offers Run as a Program. A double click still opens the script in an editor, as the GNOME help points out. These labels come from the GNOME help and the Files 46 source; we did not click through them on a desktop, and KDE Dolphin or Xfce Thunar word them differently. A terminal remains the better place to run a script, because it shows you the output and any error.
Run a .sh file without chmod: bash, sh and source
If you hand the file to an interpreter yourself, it only needs to be readable. That is convenient, but it also changes which shell runs your code and where it runs:
| What changes | ./script.sh | bash script.sh | sh script.sh | source script.sh |
|---|---|---|---|---|
| Needs the execute bit | Yes | No | No | No |
| Which shell runs it | The one in the shebang | bash | /bin/sh: dash on Debian and Ubuntu | The shell you are typing in |
| Uses the shebang line | Yes | No, it is a comment | No, it is a comment | No, it is a comment |
| Separate process | Yes | Yes | Yes | No |
| Can change your folder and variables | No | No | No | Yes |
| Runs from a noexec mount | No | Yes | Yes | Yes |
| exit inside the script | Ends the script | Ends the script | Ends the script | Ends your shell |
sh script.sh is not bash script.sh
sh ignores the #!/bin/bash line. On Debian and Ubuntu it is dash, a small POSIX shell without bash’s arrays, [[ ]] or {1..3}. A script that uses them works under ./ and bash and breaks under sh:
#!/bin/bash
names=(web db cache)
if [[ ${#names[@]} -gt 2 ]]; then echo "3 servers: ${names[*]}"; fi
echo {1..3}$ ./arr.sh
3 servers: web db cache
1 2 3
$ bash arr.sh
3 servers: web db cache
1 2 3
$ sh arr.sh
arr.sh: 2: Syntax error: "(" unexpectedThe rule: run a script the way its first line says. If it starts with #!/bin/bash, use ./script.sh or bash script.sh, never sh script.sh. Which program sits behind /bin/sh also differs: on Fedora, Rocky Linux and AlmaLinux it is bash itself, on Alpine it is BusyBox, so a script started with sh can work on one server and fail on the next.
source runs it in your current shell
./script.sh, bash and sh all start a new process. Whatever the script changes there, its working folder or an exported variable, is gone when it ends. source (or its POSIX name .) reads the file into the shell you are typing in, so the changes stay:
#!/bin/bash
cd /etc
export APP_ENV=production$ ./setenv.sh; echo "pwd=$PWD APP_ENV=${APP_ENV:-unset}"
pwd=/home/deploy/scripts APP_ENV=unset
$ source ./setenv.sh; echo "pwd=$PWD APP_ENV=${APP_ENV:-unset}"
pwd=/etc APP_ENV=productionThat is what you want for files that set up an environment, like a Python venv/bin/activate or your own .env loader, and what you do not want for a normal script. An exit in a sourced file exits your shell, so a sourced script that ends with exit 1 closes the terminal or SSH session. Two more details: source is a bash word, dash only knows ., and dash’s . searches only PATH, so . setenv.sh fails there with sh: 1: .: setenv.sh: not found. Write . ./setenv.sh and it works in both.
The shebang line: #!/bin/bash, #!/bin/sh or #!/usr/bin/env bash
When you run ./script.sh, the kernel reads the first line. If it starts with #!, the rest of the line names the program that runs the file. You can watch it pick:
$ head -1 which.sh; ./which.sh
#!/bin/sh
interpreter: /usr/bin/dash
$ bash which.sh
interpreter: /usr/bin/bash#!/bin/bashfor scripts that use bash features. bash sits at/bin/bashon every mainstream server distribution.#!/usr/bin/env bashfinds bash throughPATH. Useful on macOS or BSD, where a newer bash lives elsewhere, and forpython3ornodescripts.#!/bin/shonly for plain POSIX scripts, and then test them withdash, because that is what runs them on Debian and Ubuntu.
Two limits: the shebang must be the very first line with nothing before the #!, and Linux hands everything after the interpreter over as one single argument. So #!/usr/bin/env bash -e fails with /usr/bin/env: ‘bash -e’: No such file or directory; env itself suggests -S for that case, or write set -e on the next line. Without any shebang, bash runs the file in a new bash and dash in a new dash, but programs that start files directly, such as systemd, get Exec format error. Always write the line.
Common errors when running a .sh file
Each message below is the exact text bash 5.2 or dash printed on the test server; the CRLF row also gives the wording of bash 5.1 and 5.3, taken from the bash source. The exit status tells you the family: 126 means found but not runnable, 127 means not found.
Symptom, cause, fix
Symptom: bash: ./script.sh: Permission denied (exit 126)
- Cause
- The file has no execute bit. This happens even as root.
- Fix
- Run
chmod +x script.sh, or start it withbash script.sh. More cases in bash: Permission denied.
Symptom: Permission denied although ls -l shows -rwxr-xr-x
- Cause
- The file sits on a filesystem mounted noexec. On hardened servers that is often /tmp, /dev/shm or a separate home or data partition. Nothing on it may be executed directly.
- Fix
check the mount, then run it another way findmnt -T ./script.sh -o TARGET,OPTIONS # look for noexec bash ./script.sh # works: bash only reads the file mv script.sh ~/scripts/ && ~/scripts/script.sh
Symptom: bash: script.sh: command not found (exit 127)
- Cause
- You typed the bare name, and the current folder is not on PATH.
- Fix
- Add the path:
./script.sh. If the message names a command inside the script, see exit code 127.
Symptom: cannot execute: required file not found, /bin/bash^M: bad interpreter, or $'\r': command not found
- Cause
- The file was saved with Windows line endings (CRLF), so the shebang asks for a program called "bash\r". bash 5.2 (Debian 12 and 13, Ubuntu 24.04) says "cannot execute: required file not found" (exit 127); bash 5.1 and 5.3 say "/bin/bash^M: bad interpreter: No such file or directory" (exit 126). With #!/usr/bin/env bash the message is /usr/bin/env: 'bash\r': No such file or directory.
- Fix
detect and strip the carriage returns file script.sh # "... with CRLF line terminators" sed -i 's/\r$//' script.sh # or: dos2unix script.sh
Symptom: Syntax error: "(" unexpected, or [[: not found
- Cause
- A bash script was run with sh, which is dash on Debian and Ubuntu.
- Fix
- Run it with
./script.shorbash script.sh, and make sure the first line is#!/bin/bash.
Check a script before you run it, and trace it
$ bash -n broken.sh # parse only, run nothing
broken.sh: line 4: syntax error: unexpected end of file
$ bash -x args.sh one # print each command before it runs
+ echo 'script: args.sh'
script: args.sh
+ echo 'count: 1'
count: 1bash -n catches a missing fi or done without side effects. bash -x shows each line with its variables already filled in, which is the fastest way to see where a script goes wrong. Put set -x inside the script to trace only part of it.
Run a script at boot or on a schedule
For a script that should start with the server, a systemd unit is the clean way. The script needs its shebang and execute bit, and ExecStart= needs an absolute path (a bare name works only for files in /usr/local/bin and the other system bin folders; ./startup.sh never works), because systemd starts the file directly, without a shell:
[Unit]
Description=Run my startup script
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/startup.sh
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now startup-script.service
journalctl -u startup-script.service # the script's outputIf it fails with status=203/EXEC, systemd could not execute the file: check the path, the execute bit, the shebang and CRLF line endings, in that order. For services that keep running and restart on a crash, see how to create a systemd service.
For a schedule, use cron. Open your table with crontab -e and give the full path plus a log file, since cron has nowhere to show output:
*/15 * * * * /home/deploy/scripts/check.sh >> /home/deploy/check.log 2>&1
@reboot /home/deploy/scripts/startup.sh >> /home/deploy/startup.log 2>&1cron passes each line to /bin/sh with a minimal PATH, so a script that works in your terminal can fail there. The crontab guide shows what differs, and cron job not running works through the usual causes.
To keep your terminal free while a script runs, add &. jobs lists what runs in the background and kill %1 stops job number 1:
$ ./long.sh &
[1] 658956
$ jobs
[1]+ Running ./long.sh &
$ kill %1
[1]+ Terminated ./long.shA job started with & still belongs to your terminal session. To start a long job by hand and log out while it runs, use nohup or tmux.
Run a script on a remote server over SSH
ssh runs whatever command you add after the host instead of an interactive shell, and returns that command’s exit status:
# script already on the server
ssh deploy@203.0.113.10 '~/scripts/deploy.sh'
# script on your machine, run on the server without copying it
ssh deploy@203.0.113.10 'bash -s' < deploy.sh
# same, with arguments for the script
ssh deploy@203.0.113.10 'bash -s -- web01 "two words"' < deploy.sh
# a script that needs sudo and asks for a password: force a terminal
ssh -t deploy@203.0.113.10 'sudo /usr/local/bin/maintenance.sh'bash -s reads the script from standard input and treats everything after -- as its arguments. We ran that part locally, and $1 and $2 came out as web01 and two words; only $0 changes, to bash, because there is no file name. The remote examples follow ssh(1), they were not run against a second server.
In the bash -s form the script itself is bash’s standard input. So it cannot read your keyboard, and worse, any command inside it that reads standard input (read, ssh, an apt prompt, docker exec -i, cat) eats the following lines of the script instead. bash then runs whatever is left, or nothing. Piped in the same way locally:
$ cat rd.sh
echo start
read line
echo "read got: $line"
echo end
$ cat rd.sh | bash -s
start
endread swallowed the line after it, so its echo never ran. Give such commands </dev/null, or copy the script over first (scp) and run it as a file with the first form. Anything that should outlive the connection needs tmux or nohup on the server. Host aliases in ~/.ssh/config shorten all of these to ssh web01 '...'.
Scripts are how most people run their own servers: a deploy script, a nightly backup, a health check from cron. Our Standard KVM servers come with full root and Debian or Ubuntu images. Debian 12 and 13 and Ubuntu 24.04 carry the same bash 5.2 plus dash pair as the test host here, so the commands and messages on this page match there; older images run the same commands and only word the CRLF error differently.
How we tested
- OS image
- Debian 12
- Versions
- bash 5.2.15dash 0.5.12 (/bin/sh)coreutils 9.1
- Tested on
- Method
- Every script and output on this page run in a scratch folder: permission denied and command not found with their exit codes, chmod +x, the four ways to start a script, a bash script under sh, source changing the folder and variables, exit in a sourced file, scripts without a shebang, CRLF files under ./, bash, sh and env, a background job with jobs and kill %1, a script piped into bash -s whose read swallows the next line, a noexec mount and the /home/deploy paths in a private mount namespace, bash -n, bash -x and bash -s with arguments. The systemd, cron and remote SSH examples follow their manuals and were not run; the bash 5.1 and 5.3 CRLF wording comes from the bash source and the file manager labels from the GNOME help and the Files 46 source.
Frequently asked
How do I run a .sh file in Linux?
Open a terminal in the file's folder, make it executable once with 'chmod +x script.sh', then run it as './script.sh'. Without execute permission, 'bash script.sh' runs it too. From another folder, give the full path, such as /home/deploy/scripts/script.sh.
What is a .sh file?
A plain text file with shell commands that run from top to bottom, like a saved list of terminal commands. The .sh extension is only a convention: Linux decides how to run the file from its execute permission and its first line, the shebang (for example #!/bin/bash), not from the name.
Why do I need ./ before the script name?
A name without a slash is looked up only in the folders listed in PATH, and the current folder is not one of them, so 'script.sh' alone fails with 'command not found' (exit status 127). './script.sh' gives the shell a path. To run a script by name from anywhere, put it in /usr/local/bin or ~/.local/bin.
What is the difference between sh script.sh, bash script.sh and ./script.sh?
'./script.sh' uses the interpreter named in the shebang line and needs the execute bit. 'bash script.sh' always uses bash and 'sh script.sh' always uses /bin/sh, which is dash on Debian and Ubuntu; both ignore the shebang. A bash script with arrays or [[ ]] therefore fails under sh with errors like 'Syntax error: "(" unexpected'.
What does source script.sh do differently?
source (or '.') runs the file inside your current shell instead of a new process, so a cd or an exported variable in the script stays in effect afterwards. That suits files that set up an environment. An 'exit' in a sourced file closes your own shell or SSH session, so do not source normal scripts.