Guides

How to run a .sh file in Linux: chmod +x, ./script.sh, bash, sh and source

Run a .sh file in Linux: chmod +x, then ./script.sh, or bash script.sh without chmod. What sh and source change, and fixes for Permission denied and CRLF.

Published ~10 min read
On this page

Short answer

Make the file executable once, then run it with a path in front of its name. Without execute permission, bash script.sh runs it too.
  1. Open a terminal in the folder that holds the file: cd ~/scripts
  2. Make it executable once: chmod +x script.sh
  3. 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.

hello.sh
#!/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:

Debian 12
$ file hello.sh
hello.sh: Bourne-Again shell script, ASCII text executable

Because 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:

Debian 12 · bash
$ 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)-release

chmod +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:

Debian 12 · bash
$ 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)-release

To 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:

args.sh
#!/bin/bash
echo "script: $0"
echo "count:  $#"
echo "first:  $1"
echo "second: $2"
for a in "$@"; do echo "arg: $a"; done
Debian 12 · bash
$ ./args.sh web01 "two words"
script: ./args.sh
count:  2
first:  web01
second: two words
arg: web01
arg: two words

That 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.shbash script.shsh script.shsource script.sh
Needs the execute bitYesNoNoNo
Which shell runs itThe one in the shebangbash/bin/sh: dash on Debian and UbuntuThe shell you are typing in
Uses the shebang lineYesNo, it is a commentNo, it is a commentNo, it is a comment
Separate processYesYesYesNo
Can change your folder and variablesNoNoNoYes
Runs from a noexec mountNoYesYesYes
exit inside the scriptEnds the scriptEnds the scriptEnds the scriptEnds your shell
Each row checked on Debian 12 (bash 5.2.15, dash 0.5.12) on 2 Nov 2026.

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:

arr.sh
#!/bin/bash
names=(web db cache)
if [[ ${#names[@]} -gt 2 ]]; then echo "3 servers: ${names[*]}"; fi
echo {1..3}
Debian 12
$ ./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: "(" unexpected

The 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:

setenv.sh
#!/bin/bash
cd /etc
export APP_ENV=production
Debian 12 · bash
$ ./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=production

That 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:

Debian 12
$ head -1 which.sh; ./which.sh
#!/bin/sh
interpreter: /usr/bin/dash
$ bash which.sh
interpreter: /usr/bin/bash
  • #!/bin/bash for scripts that use bash features. bash sits at /bin/bash on every mainstream server distribution.
  • #!/usr/bin/env bash finds bash through PATH. Useful on macOS or BSD, where a newer bash lives elsewhere, and for python3 or node scripts.
  • #!/bin/sh only for plain POSIX scripts, and then test them with dash, 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

  1. 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 with bash script.sh. More cases in bash: Permission denied.
  2. 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
  3. 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.
  4. 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
  5. 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.sh or bash script.sh, and make sure the first line is #!/bin/bash.

Check a script before you run it, and trace it

Debian 12
$ 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:  1

bash -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:

/etc/systemd/system/startup-script.service
[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.target
enable it
sudo systemctl daemon-reload
sudo systemctl enable --now startup-script.service
journalctl -u startup-script.service      # the script's output

If 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:

crontab -e
*/15 * * * * /home/deploy/scripts/check.sh >> /home/deploy/check.log 2>&1
@reboot /home/deploy/scripts/startup.sh >> /home/deploy/startup.log 2>&1

cron 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:

Debian 12 · bash
$ ./long.sh &
[1] 658956
$ jobs
[1]+  Running                 ./long.sh &
$ kill %1
[1]+  Terminated              ./long.sh

A 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:

from your own machine
# 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:

Debian 12 · bash
$ cat rd.sh
echo start
read line
echo "read got: $line"
echo end
$ cat rd.sh | bash -s
start
end

read 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.

Related articles

All guides