Base64 in Bash & Shell Scripts
Bash scripting with Base64 has one major trap: echo adds a trailing newline by default, changing the encoded output. Miss this and your scripts silently produce wrong results. Here is the full guide.
Basic encode and decode
Almost every Unix-like system ships a base64 command. Encoding a string is a matter of piping text into it, and decoding is the same command with a decode flag. Here is the simplest round trip:
$ echo -n "Hello, World!" | base64 SGVsbG8sIFdvcmxkIQ== $ echo -n "SGVsbG8sIFdvcmxkIQ==" | base64 -d Hello, World!
The decode flag is -d on GNU/Linux (coreutils) and -D (capital D) on macOS, which ships the BSD version. Both also accept the long form --decode on Linux. We cover that portability headache in detail in the last section.
The base64 tool reads from standard input by default, so it composes naturally with pipes. You can also pass a filename directly as an argument, which avoids the shell entirely and sidesteps the newline issue described next:
$ base64 myfile.txt # encode a file's contents $ base64 -d encoded.txt # decode a file's contents
Base64 output contains only A-Z, a-z, 0-9, +, /, and = for padding. That makes it safe to embed in JSON, URLs (with the URL-safe variant), email headers, and shell variables without escaping problems.
The echo -n gotcha
This is the single most common Base64 bug in shell scripts. By default, echo appends a trailing newline (\n) to its output. When you pipe that into base64, the newline becomes part of the encoded data, so the result is not the encoding of your string — it is the encoding of your string plus a newline byte.
$ echo "Hello" | base64 SGVsbG8K # note the trailing "K" — that is the encoded \n $ echo -n "Hello" | base64 SGVsbG8= # this is what you actually wanted
The two outputs differ entirely, and the trailing byte is invisible when you look at the input. This bug is especially insidious when generating tokens, HMAC keys, or basic-auth headers, where a single extra byte breaks authentication and the error message gives no hint about the cause.
Never use bare echo to feed base64 in a script. Always use echo -n or, better, printf "%s" which does not append a newline and behaves consistently across shells. The -n flag for echo is itself not portable (some shells print the literal -n), so printf is the safest choice.
The robust, portable idiom is:
$ printf "%s" "Hello" | base64 SGVsbG8= # building a basic-auth header — get this wrong and login fails $ printf "%s" "user:pass" | base64 dXNlcjpwYXNz
Use printf "%s" (not just printf "Hello") so that any % or backslash characters in your data are treated literally rather than interpreted as format specifiers or escape sequences.
Encode and decode files in scripts
For binary files — images, archives, certificates — pass the path to base64 directly. There is no echo and therefore no newline trap. Encoding writes Base64 text to standard output; redirect it to a file to save it:
# encode a binary file to a text file $ base64 logo.png > logo.b64 # decode it back to the original binary $ base64 -d logo.b64 > logo_restored.png # verify the round trip is byte-for-byte identical $ cmp logo.png logo_restored.png && echo "OK: identical"
By default, GNU base64 wraps encoded output at 76 characters per line (matching the MIME standard). When you store the result in a file destined for line-oriented processing this is fine, but for single-line use cases you will want to disable wrapping with -w 0 on Linux. On BSD/macOS use -b 0, or pipe through tr -d '\n' to strip the line breaks portably.
# Linux: single-line output, no wrapping $ base64 -w 0 cert.pem > cert.b64 # portable: strip newlines after encoding $ base64 cert.pem | tr -d '\n' > cert.b64
To embed a small file inline in a script (for example a default config or a tiny icon), encode it once on the command line, paste the Base64 into your script, and decode it at runtime with a heredoc. This keeps a single self-contained file with no external dependencies.
Store Base64 in environment variables
Environment variables cannot hold raw newlines or binary data cleanly, which is exactly why Base64 is the standard transport for secrets and certificates in CI/CD systems like GitHub Actions, GitLab CI, and Kubernetes. Encode the value to a single line, store it, then decode it at the point of use.
# store a multi-line private key as a single-line env var $ export TLS_KEY=$(base64 -w 0 server.key) # later, in the same or a child process, decode it back to a file $ printf "%s" "$TLS_KEY" | base64 -d > server.key
Decoding into a shell variable uses command substitution. Remember that command substitution $(...) strips all trailing newlines from the captured output, which is usually convenient but worth knowing when you decode data that legitimately ends in a newline:
$ encoded="SGVsbG8sIFdvcmxkIQ==" $ decoded=$(printf "%s" "$encoded" | base64 -d) $ echo "$decoded" Hello, World!
Avoid the bare echo "$encoded" | base64 -d form when the variable might contain characters like leading dashes — echo could interpret them as options. Quote the variable and prefer printf "%s". Also be careful not to leak secrets into shell history or process listings; reading from a file or stdin is safer than passing secrets as command arguments.
Multi-line strings and heredoc approach
When you need to encode a block of text that spans several lines — a config snippet, a JSON payload, an SSH key — a heredoc is the cleanest approach. It feeds the literal lines to base64 on standard input without you having to escape newlines or quotes:
$ base64 -w 0 <<EOF line one line two line three EOF bGluZSBvbmUKbGluZSB0d28KbGluZSB0aHJlZQo=
The -w 0 (Linux) keeps the output on one line, which is ideal when the encoded value will land in an environment variable or a YAML field. Note that a standard heredoc adds a trailing newline after the final line, so line three is followed by \n in the encoded data. If you must avoid that trailing newline, use a here-string with printf instead:
# here-string keeps it compact for a single value $ base64 -w 0 <<< "single line value" # no trailing newline at all $ printf "%s" "exact bytes" | base64
To decode multi-line Base64 (such as MIME-wrapped output) the decoder ignores embedded newlines and whitespace automatically, so you can pipe wrapped text straight back in:
$ base64 -d <<EOF bGluZSBvbmUKbGluZSB0d28KbGluZSB0aHJlZQo= EOF line one line two line three
Portable scripts: handle GNU vs BSD base64 differences
The biggest cross-platform headache is that GNU coreutils base64 (Linux) and BSD base64 (macOS) disagree on flag names. Writing a script that runs on both requires care. The key differences:
- Decode: GNU uses
-dor--decode; BSD uses-D(capital). Confusingly, GNU treats-Das an error, and BSD treats lowercase-das a different option. - Line wrapping: GNU disables wrapping with
-w 0; BSD uses-b 0and does not understand-w. - Long options:
--decodeand--wrapexist on GNU but not on stock BSD.
The most robust portable decode trick: both implementations accept --decode only on GNU, but both accept input on stdin and both work with a small wrapper. A common pattern is to detect the platform or simply try the GNU flag and fall back:
# portable decode function
b64decode() {
if base64 --decode </dev/null >/dev/null 2>&1; then
base64 --decode # GNU / coreutils
else
base64 -D # BSD / macOS
fi
}
$ printf "%s" "SGVsbG8=" | b64decode
Hello
For line wrapping, the most portable approach is to ignore the wrap flags entirely and strip newlines yourself with tr -d '\n'. This works identically everywhere and removes any dependency on -w versus -b:
# portable single-line encode, works on Linux and macOS $ base64 secret.key | tr -d '\n'
If your scripts target a known environment (a specific Docker base image or CI runner), just use the native flags for that platform and document the assumption. The portability dance only pays off for scripts that genuinely run on both Linux and macOS — for example, developer tooling distributed to a mixed team.
One more cross-platform note: GNU base64 tolerates and ignores invalid characters with -i/--ignore-garbage, while BSD silently ignores newlines but is stricter about other junk. If you decode data that may contain stray whitespace from copy-paste, pre-clean it with tr -d '[:space:]' before decoding for predictable behavior across systems.
FAQ
Why does my base64 output differ between machines?
Usually a newline issue. echo "text" | base64 includes a trailing newline in the input; echo -n "text" | base64 does not. The two produce different Base64 output. Always use echo -n or printf "%s" in scripts.
How do I decode Base64 to a variable in Bash?
Use command substitution: decoded=$(echo "SGVsbG8=" | base64 -d) on Linux or decoded=$(echo "SGVsbG8=" | base64 -D) on macOS. Be aware that command substitution strips trailing newlines from the output.
How do I base64 encode a multi-line string in Bash?
Use a heredoc piped to base64: base64 -w 0 <<EOF followed by your lines and a closing EOF. The -w 0 flag (Linux) disables line wrapping so the output is a single line suitable for env vars.
Try Base64 encoding and decoding instantly
Paste any string or file — base64.dev auto-detects and converts it instantly.
Open base64.dev →