Shell / Bash Script Formatter

Paste a Bash script, a CI step or a Dockerfile RUN block that grew too long, and shfmt will indent it consistently. Unclosed if, for and case blocks are reported with the exact line and column.

Input

Settings

History

Load from URL

shfmt, compiled to WebAssembly

shfmt is the formatter from the mvdan/sh project, written in Go and used by the VS Code shell-format extension, pre-commit hooks and countless dotfiles repositories. This page runs it as WebAssembly, so the script you paste is parsed and printed on your own machine. Deploy scripts tend to contain hostnames, tokens and internal URLs, so keeping them off third-party servers is not a small thing.

Scripts are read with Bash grammar, which is a superset of POSIX sh, so arrays, [[ ]] tests and $(( )) arithmetic all parse whatever the shebang says. zsh-only syntax such as parameter expansion flags is the exception and is reported as an error rather than silently mangled.

Paste, format, copy

A shebang line such as #!/usr/bin/env bash or #!/bin/sh is enough for detection. Without one, choose Shell from the format menu. Ctrl/Cmd+Enter formats, Ctrl/Cmd+Shift+M switches to minified output and back, and Ctrl/Cmd+Shift+C copies whatever is in the output pane.

Indent in the toolbar sets the indentation: 2 spaces by default, 4 spaces, or real tabs. shfmt itself defaults to tabs, so choose Tabs to match a project that runs shfmt without flags.

Error messages follow shfmt’s file:line:column pattern. The file name is a stand-in: it reads script.bash when the shebang mentions bash or zsh and script.sh otherwise, so script.sh:1:6 simply means line 1, column 6 of what you pasted. The editor underlines the same spot.

Options, flag by flag

Each option corresponds to a shfmt command-line flag, which makes it easy to copy the same settings into an .editorconfig or a CI job afterwards.

  • Indent case branches (-ci, on by default) indents the start) and stop) patterns inside a case block. Turn it off for the flush style some projects use.
  • Operators start continuation lines (-bn) moves &&, || and | to the beginning of the next line when a pipeline is split across lines, so each continued line starts with the operator that joins it.
  • Space after redirects (-sr) writes > out.log and < in.txt instead of >out.log. Descriptor duplications like 2>&1 stay compact.
  • Function brace on next line (-fn) puts the opening brace of deploy() on its own line.
  • Simplify code (-s) rewrites a few things that are safe to shorten: [[ "$env" == "prod" ]] drops the quotes that [[ ]] does not need, and $(( $count + 1 )) becomes $((count + 1)).

Minify (-mn) removes comments and indentation but keeps the shebang, one statement per line and heredoc bodies exactly as written, since their contents are data.

What shfmt leaves to you

shfmt respects some layout choices instead of imposing its own. A one-line if ...; then ...; fi stays on one line, and a long command split with backslashes keeps its breaks. It never adds or removes quotes outside the [[ ]] simplification, so an unquoted $file that will break on spaces stays unquoted. That kind of bug is a job for ShellCheck, which this page does not run.

Examples

Deploy function with simplify and next-line braces

The function brace moves to its own line and the [[ ]] test loses its unneeded quotes, while the one-line for loop stays on one line.

Input
#!/usr/bin/env bash
set -euo pipefail
deploy(){ local env=${1:-staging}
if [[ "$env" == "prod" ]];then
echo "Deploying to production"
fi
for f in dist/*.js;do gzip -k "$f"&&echo "compressed $f">>build.log;done
case "$env" in prod) url=https://example.com;; *) url=https://$env.example.com;; esac
curl -fsS "$url/health"|| { echo "health check failed" >&2; exit 1; }
}
deploy "$@"
Output
#!/usr/bin/env bash
set -euo pipefail
deploy()
{
  local env=${1:-staging}
  if [[ $env == "prod" ]]; then
    echo "Deploying to production"
  fi
  for f in dist/*.js; do gzip -k "$f" && echo "compressed $f" >>build.log; done
  case "$env" in prod) url=https://example.com ;; *) url=https://$env.example.com ;; esac
  curl -fsS "$url/health" || {
    echo "health check failed" >&2
    exit 1
  }
}
deploy "$@"
Open this example in the tool

CI pipeline with operators at line starts

The && and | operators move to the front of their continuation lines and redirects get a space before the file name.

Input
#!/bin/sh
docker build \
  -t registry.example.com/shop/api:$CI_COMMIT_SHA . && \
  docker push registry.example.com/shop/api:$CI_COMMIT_SHA |
  tee push.log
echo "pushed" >>deploy.log 2>&1
Output
#!/bin/sh
docker build \
    -t registry.example.com/shop/api:$CI_COMMIT_SHA . \
    && docker push registry.example.com/shop/api:$CI_COMMIT_SHA \
    | tee push.log
echo "pushed" >> deploy.log 2>&1
Open this example in the tool

Release script minified

Comments and extra spaces are removed, while the shebang and the heredoc text survive untouched.

Input
#!/usr/bin/env bash
# build the release notes
set -e   # stop on first error
VERSION=$(git describe --tags)
echo "building $VERSION"
cat <<EOF > notes.txt
  release $VERSION
EOF
Output
#!/usr/bin/env bash
set -e
VERSION=$(git describe --tags)
echo "building $VERSION"
cat <<EOF >notes.txt
  release $VERSION
EOF
Open this example in the tool

Common errors and how to fix them

ErrorCauseFix
script.sh:1:1: `if` statement must end with `fi`An if block has no closing fi. The position points at the if that was never closed, not at the end of the file.Add fi after the last command of the block, and check that elif and else branches belong to the right if.
script.sh:1:6: reached EOF without closing quote `"`A double-quoted string was opened and never closed, so the rest of the script was swallowed into it.Close the quote. To include a literal double quote inside, escape it as ".
script.sh:1:1: `case` statement must end with `esac`A case block is missing its esac terminator.Add esac after the last pattern, and end each pattern body with ;;.
script.sh:1:1: `for` statement must end with `done`A for, while or until loop is missing done, often because a one-line loop was cut short when copied.Add done after the loop body.
script.sh:1:5: a command can only contain words and redirects; encountered `(`Usually an array assignment with spaces around =, as in x = (a b), which the shell reads as a command named x.Remove the spaces: x=(a b).
script.bash:2:18: parameter expansion flags are a zsh feature; tried parsing as bashThe script uses zsh-only syntax such as ${(f)var}. shfmt here parses everything as Bash.Format zsh-specific files with a zsh-aware tool, or rewrite the expansion in portable Bash.

Frequently asked questions

Is this the same as running shfmt locally?

Yes. It is shfmt compiled to WebAssembly, and each option maps to a command-line flag: -ci, -bn, -sr, -fn, -s and -mn. Indentation corresponds to -i.

Does it work for sh, dash and zsh scripts?

POSIX sh and dash scripts format fine, because Bash grammar covers them. zsh scripts work only if they avoid zsh-only syntax such as expansion flags and glob qualifiers.

Will formatting change what my script does?

No. shfmt only changes whitespace, line breaks and, with Simplify code, quotes inside [[ ]] and $ signs inside arithmetic, both of which are safe. Behaviour stays the same.

Does it find bugs like ShellCheck?

No. It reports syntax errors only. Unquoted variables, useless cat and other lint warnings need ShellCheck.

Why does a minified script still have line breaks?

shfmt keeps statements on separate lines and preserves heredoc bodies, because joining them could change meaning. Comments and indentation are what get removed.

Related tools