Formatting by Language

How to format a bash script on Mac

You can do this with Note My, and there are two jobs in it. Here is how to re-indent a script that reads badly, and how to fix the line endings that make one actually break.

A shell script accumulates. Someone adds a branch late in the afternoon, someone else pastes a block in from a wiki page, and six months later the indentation is three widths deep in places and absent in others. Worse, the file sometimes stops running and blames a line that looks perfectly correct. Both are fixable, and they are not the same problem.

This guide covers how to re-indent a bash, sh or zsh script in Note My, what the shell-specific controls actually do, and how to find the invisible characters that break a script that reads fine.

bash, sh and zsh are one language here

Note My has a single Shell language mode covering bash, sh and zsh. One set of colours, one set of completions and one set of re-indentation controls serve all three, so nothing changes between a .sh file and a .zsh one.

Shell is one of the fifteen code languages our syntax engine colours, and it gets a shell-specific extra: $vars are picked out alongside keywords, strings and comments, which makes a mistyped variable name easy to spot. The colour is on screen only and is never written into the file, so a script you open, read and close comes back byte-for-byte unchanged. It all happens on your Mac — nothing is uploaded and the script never leaves the machine.

Auto-complete works in a script: press ⌃Space for keywords, builtins, symbols found in the document, paths and words already in the file.

Re-indent the script

Re-indentation lives in one place, and it is already in front of you: the Indent button in the toolbar. One button, one sheet, one Apply, and the whole script settles at once.

  1. Open the Indentation sheet

    Click Indent in the toolbar — the curly-braces glyph. A sheet opens. Nothing has changed in your document yet; the button opens the sheet and applies nothing on its own.

  2. Confirm the language says Shell

    The sheet has a language picker at the top holding the global default plus fifteen languages. For a .sh file it will normally already read Shell — our advanced language identifier saves you the step. If it reads something else — worth a glance when the file has no .sh on the end — set it to Shell here first, so the shell-specific controls below apply to your file.

  3. Set the indent width and the unit

    The indent controls set width and whether a level is tabs or spaces. The default is 4, and the sheet accepts 1 to 12. Spaces are the default unit. Only two languages carry a locked unit: Go is locked to tabs with the lock on by default, and Python to 4 spaces with the lock off by default. Shell leaves both settings in your hands, so a two-space script stays two-space if that is what you set.

  4. Check the shell section, then Apply

    Below the shared controls is a language-specific section holding the shell options described in the next two sections. Press Apply and the whole document is re-indented in one go. Cancel leaves everything alone.

Note

The preview card in the sheet renders a sample if block, so you can read the width and the brace style at a glance before you commit to them. Nothing on disk changes until you save with ⌘S.

Our indentation engine does more than shift leading whitespace. It also unpacks statements that share a line, so a run of commands separated on one line comes back as separate lines at the correct depth. The script goes on doing exactly what it did before, and a dense line you used to squint at becomes one you can read at a glance — which is usually the reason you opened it.

then and do: the same line or the next one

The shell section of the sheet carries one control that is genuinely shell-specific: whether then and do sit on the same line as their if or for, or drop onto the line below. The default is the same line.

Warning

Press ⌘S before you press Apply. This control reshapes every if and for in the file at once, and a saved copy is your one-keystroke way back: File ▸ Reload from Disk (⌘R) reads the saved version straight into the window, and reloading never changes the file on disk. Try a placement, read the result, keep it or reload.

The brace-style switch above it — same-line braces in the K&R manner, which is the default, or braces on their own line — is a shared control that applies across languages rather than a shell-only one.

Heredocs are preserved

A heredoc body is data, not code. Re-indenting the text between <<EOF and its terminator would change what the script actually emits, which is the kind of "formatting" that costs an afternoon.

The shell section of the sheet carries a heredoc-preserve switch, and it is on by default. With it on, the surrounding script is re-indented and the heredoc body comes back exactly as you wrote it. Leave it on unless you have a specific reason not to.

The thing that actually breaks scripts: CRLF

A script that looks right and refuses to run is very often a script with Windows line endings. Every line ends with a carriage return the shell does not expect, so the interpreter named on the shebang line has an invisible character stuck to the end of it, and the error message points at something that looks fine.

Note My treats this as a first-class fact rather than an oddity:

  • The status bar has a line-ending pill. It names the endings in use, and reads Mixed when they are not all the same. If it says Mixed, that is your answer before you read another line.
  • Converting is one menu away. File ▸ Line Endings ▸ Convert to LF converts the whole document. New documents start as LF, an existing file keeps the endings it arrived with, and you are warned about mixed endings by default.
  • Our hidden-character scanner takes shell seriously. In a shell script, CRLF is flagged at danger — the strongest level shown, and the right one, since here it is the difference between a script that runs and one that does not. In Python the same finding appears as a warning; in most languages it is merely noted. So a red flag on a script is worth acting on before you change anything else.

One related behaviour: Clean ▸ Clean AI Copy/Paste Markers is part of our cleanup rules, and it settles mixed endings onto LF as it tidies the rest. It never converts the other way — CRLF has to be asked for explicitly, from the File menu.

See the whitespace

When a script misbehaves and you cannot see why, stop guessing and make the characters visible. Click Show Invisibles in the toolbar, or use the View menu, or Settings ▸ Invisibles. Four independent toggles are available, all off until you turn them on:

The four invisible-character display toggles
ToggleWhat it draws
Show whitespaceA middle dot for a space, an arrow for a tab, a ring for a non-breaking space.
Show end of lineAn LF, CRLF or CR label at the end of each line — per line, so a mixed file shows you exactly which lines are wrong.
Highlight trailing whitespaceA translucent wash over whitespace at the end of a line.
Underline whitespaceAn underline under whitespace runs.

There are three marker styles. Symbols is the default and the quietest. Code Points names the character. Bytes shows the hex bytes the character would be saved as, following our encoding handling — a space is 20 in UTF-8, and a non-breaking space is C2 A0. For a script that is failing on something you cannot see, Bytes ends the argument.

The status bar also carries a cleanliness pill that reads Clean in green, or "N hidden" in orange, or "N hidden · D risky" in red. Its popover offers Show in Text, Remove Invisible Character(s), Review All… and Go to Next. For a full pass, Clean ▸ Clean Invisible / Special Characters… opens a sheet where you check the whole document or just a selection, review every finding by line and column, and apply only what you choose.

Tip

Note My never changes your text as you type — no auto-correct, no smart quotes, no dash substitution, in any mode. Curly quotes in a script would break it, so they are never introduced.

Put a bare script name into Shell mode

Plenty of scripts are called deploy or bootstrap with no .sh on the end. Those files are as much at home here as a named .sh one — it simply takes one deliberate click to say so, and that click is worth making before you re-indent rather than after.

The status bar tells you where you stand. If a script is open and the status bar reads Plain Text, that is the case to fix, and it takes a moment.

To set the language yourself, click the Identify pill in the status bar. The Language Inspector shows the language in use, offers an override pop-up, and waits for Confirm — nothing changes until you agree. Choose Shell and the colouring, the completions and the shell section of the Indentation sheet arrive together. Do it before you re-indent.

Recovering code a comment swallowed

A specific accident: a line where a # comment has absorbed real code that used to be on its own line, so a command is silently no longer running. The Indentation sheet has a Recover damaged comments checkbox, off by default, that splits such a line back apart. Shell is one of the languages it covers.

It is deliberately careful about what it touches: single-line comments only, and documentation comments are left alone, so a header block comes back exactly as you wrote it. Reach for it when a command has stopped running for no reason you can see — turn it on, apply, read the result, then turn it off again.

Troubleshooting

I pressed Apply and nothing changed

Usually good news: if the script already matches the settings in the sheet, the result is identical and nothing moves. Worth one check all the same. Make sure the sheet's language picker reads Shell, since a file still showing Plain Text is treated as plain text and indented as such. Set it to Shell, apply again, and the shell-specific controls come with it.

then and do moved, and I want the old shape back

Reach for the copy on disk. If you saved before you applied — the habit worth keeping for any change that sweeps a whole file — File ▸ Reload from Disk (⌘R) brings that version back into the window in one keystroke, and reloading never changes the file on disk. If you have not saved since opening the script, closing the tab without saving and opening the file again gets you to the same place.

The script still will not run

Re-indentation changes the shape of a script rather than its behaviour, so a script that still refuses to run is telling you the cause lies somewhere you have not looked yet — and the status bar usually knows where. Check the line-ending pill for CRLF or Mixed, then the cleanliness pill for hidden characters: a non-breaking space where a plain space should be is invisible and fatal. Turn on Show Invisibles with the Bytes marker style and the character names itself in hex.

My heredoc body was reformatted

Put the original body back in front of you first. If you saved before you applied, File ▸ Reload from Disk (⌘R) brings that version into the window in one keystroke, and reloading never changes the file on disk. If you have not saved since opening the script, close the tab without saving and open the file again. With the original text restored, check the heredoc-preserve switch in the sheet's shell section — it is on by default — and the next Apply re-indents the surrounding script while the body between <<EOF and its terminator stays exactly as you wrote it.

Re-indenting a very long script

Our large-file engine keeps a long script responsive while you work in it, and a whole-document re-indent on a very large one settles after a short pause. Give it that moment, then read the result from the top: the entire file arrives at its new depth in a single pass, so there is one result to read rather than a run of them. Save first and the copy on disk stays your way back to the previous shape.

The indentation came out wrong in one block

Indentation follows the structure your script declares, which makes the result a useful reading of that structure. An unbalanced quote or a missing fi shows up as a block sitting at the wrong depth from that point onwards, so treat the first line that looks wrong as the place the mistake starts. Fix the structure there, apply again, and the rest of the file usually falls into line with it.

Take the script the rest of the way

A tidy script is usually the start of a working session rather than the end of one. Everything below sits within reach of the window you are already in:

  • Hand it straight to your terminal. Right-click the tab and choose Open in Terminal, or Save and Open in Terminal to write your changes first and arrive at the file in one move. The edit and the run stay a keystroke apart.
  • Start a new script from a known-good shape. Our template library includes a starter script.sh, so a new deploy or bootstrap script begins with a shebang and sensible structure rather than an empty window.
  • Rename a variable everywhere at once. Our find and replace works across the whole script, so a $TMPDIR that became $WORKDIR changes in one pass instead of thirty separate edits.
  • Read one version against another. Our comparison view puts two files side by side with the differences marked, which is the fastest way to see what a sweep through a long script actually moved.
  • Repeat a fiddly edit. Our macro recorder captures a sequence of keystrokes once and replays it, which turns a repetitive tidy-up across a long file into a single command.
  • Work without holding your breath. Our auto-recovery safety net keeps your work behind you, so an interrupted session does not cost you the afternoon's edits.

Auto-complete is worth leaning on while you write, too. ⌃Space offers keywords, builtins, paths and words already in the document, which keeps a long variable name spelled the same way in every place you use it.

Frequently asked questions

How do I reach re-indentation?

Click Indent in the toolbar — the curly-braces glyph, in front of you whenever a script is open. It is one entry point for every language, so the same click re-indents a shell script, a Python file or a block of JSON, and the Indentation sheet always opens on the controls that suit the file you are in.

Does this work on zsh scripts as well as bash?

Yes. bash, sh and zsh are all handled by the one Shell mode, with the same colouring, completions and re-indentation controls.

Tabs or spaces for a shell script?

Note My takes no side. The default is four spaces, the width can be anything from 1 to 12, and tabs are a switch. Only two languages carry a locked unit — Go to tabs, with the lock on by default, and Python to four spaces, with the lock off by default. Shell leaves both to you, so whatever convention your repository uses will stick.

Will re-indenting change what my script does?

It changes indentation, and it unpacks statements that were sharing one line. Heredoc bodies are protected while the switch is on, and nothing on disk changes until you save with ⌘S. Read the result, then keep it or reload — the decision stays yours either way.

Why does my script fail with an error on a line that looks correct?

Almost always CRLF line endings or a hidden character. Check the line-ending pill in the status bar, convert with File ▸ Line Endings ▸ Convert to LF, and use the cleanliness pill to find the rest.

Does anything about this touch the network?

Everything happens on your Mac. Note My contains no networking code of its own; the only network activity is performed by Apple’s own system services, to establish purchases and transactions state. Formatting, language identification and cleaning are done on the machine in front of you, nothing is uploaded, there is no account to sign into and no sign-up to get past, and your script never leaves your Mac. Open a script on a laptop in flight mode and it behaves exactly as it does at your desk.