Encode and decode a string
printf '%s' 'héllo wörld' | base64 # aMOpbGxvIHfDtnJsZA==
printf '%s' 'aMOpbGxvIHfDtnJsZA==' | base64 -d # héllo wörld
base64 encodes bytes, so the result follows your terminal’s encoding. On any modern Linux or macOS setup that is UTF-8.
The classic mistake is echo, which appends a newline:
echo 'héllo wörld' | base64 # aMOpbGxvIHfDtnJsZAo= (note the "o=")
echo -n hi | base64 # aGk=
echo -n works in Bash but not in every sh; printf '%s' is portable. A here-string (base64 <<< "text") also adds a newline. If someone sends you Base64 that ends in Cg== or o=, it probably contains a trailing newline; paste it into the Base64 decoder to check.
GNU (Linux) vs BSD (macOS) flags
| Task | GNU coreutils (Linux) | macOS |
|---|---|---|
| Decode | -d, --decode | -D, --decode (recent releases also -d) |
| No line wrapping | -w 0 | default |
| Wrap at N chars | -w N (default 76) | -b N |
| Input file | base64 file | base64 -i file or base64 < file |
| Output file | redirect > | -o file or redirect |
| Ignore junk when decoding | -i, --ignore-garbage | not available |
The two -i flags mean different things: on macOS it names the input file, on Linux it ignores garbage. Portable scripts should read from stdin and write to stdout:
base64 < in.bin | tr -d '\n' > out.b64 # same output on Linux and macOS
base64 --decode < out.b64 > in-copy.bin
GNU base64 wraps at 76 characters by default, which breaks headers and environment variables:
head -c 60 /dev/zero | tr '\0' x | base64 -w 0
# eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4
Without -w 0 the first line is 76 characters and the rest goes to a second line. If you need MIME-style lines, the default wrapping is what you want.
URL-safe Base64
GNU coreutils 8.31 and newer include basenc, which supports Base64URL. It keeps the padding:
printf '%s' '<<???>>' | basenc --base64url # PDw_Pz8-Pg==
printf '%s' 'PDw_Pz8-Pg' | basenc --base64url -d # <<???>>
For macOS or older Linux, translate with tr:
printf '%s' '<<???>>' | base64 | tr '+/' '-_' | tr -d '=' # PDw_Pz8-Pg
Decoding needs the reverse translation and, for strict decoders, the padding back:
s=PDw_Pz8-Pg
s=$(printf '%s' "$s" | tr -- '-_' '+/')
while [ $(( ${#s} % 4 )) -ne 0 ]; do s="$s="; done
printf '%s' "$s" | base64 -d # <<???>>
The -- stops tr from reading -_ as an option. That is the same transformation JWTs need; the JWT decoder handles it for you. More in Base64 vs Base64URL.
Encode and decode files
base64 -w 0 hello.txt # aGVsbG8K
base64 -d <<< 'aGVsbG8K' > copy.txt
base64 -d payload.b64 > payload.pdf
base64 streams, so file size does not matter; it reads and writes in blocks. To build a data URI for an image:
printf 'data:image/png;base64,%s' "$(base64 -w 0 logo.png)"
For an HTTP Basic Auth header:
printf 'Basic %s' "$(printf '%s' 'user:pass' | base64)" # Basic dXNlcjpwYXNz
The Basic Auth header generator does the same without the shell history entry containing your password.
Validation, errors and exit codes
GNU base64 -d exits with status 1 and prints base64: invalid input on stderr when it finds a bad character. It still writes everything decoded up to that point, so a failed decode can leave a partial file behind:
printf 'aGk=!!' | base64 -d # prints "hi", then: base64: invalid input
echo $? # 1 (without a pipe after it)
printf 'aGk=!!' | base64 -d -i # hi (garbage ignored, exit 0)
Results with coreutils 9.7:
| Input | Result |
|---|---|
aGk | hi, exit 0 (missing padding tolerated) |
"aGVs\nbG8K" | hello, exit 0 (newlines ignored) |
aGl= | hi, then invalid input, exit 1 |
a | invalid input, exit 1 |
PDw_Pz8-Pg== | partial output, invalid input, exit 1 |
Use the exit status to validate, and decode to a temporary file so a failure does not overwrite good data:
if printf '%s' "$b64" | base64 -d > /tmp/out.bin 2>/dev/null; then
mv /tmp/out.bin result.bin
else
echo "invalid Base64" >&2
fi
In a pipeline, $? is the status of the last command. Add set -o pipefail if base64 -d is not last. When a string from a log or email will not decode, Repair Base64 strips quotes, whitespace and prefixes; the errors guide explains each failure.
Other tools on the command line
openssl base64exists on almost every system and behaves the same on Linux and macOS. See Base64 with OpenSSL.python3 -m base64 -d fileandpython3 -m base64 filework where coreutils are missing; the output wraps at 76 characters.- In Kubernetes,
kubectl get secret x -o jsonpath='{.data.password}' | base64 -dis the usual pattern, or use the Kubernetes secret decoder.
FAQ
Why does echo hello | base64 give a different result than online tools?
echo adds a newline, so you encode hello\n (aGVsbG8K) instead of hello (aGVsbG8=). Use printf '%s' hello or echo -n hello.
How do I stop base64 from wrapping lines?
On Linux, base64 -w 0. macOS does not wrap by default. For a command that behaves the same on both, pipe through tr -d '\n'.
What does 'base64: invalid input' mean?
GNU base64 -d hit a character outside the alphabet or broken padding. It prints the bytes it decoded so far, then the error, and exits with status 1. Common culprits are URL-safe - and _, quotes, and data: prefixes.
Is base64 -d on macOS the same as on Linux?
Older macOS releases only knew -D (or --decode); recent ones accept -d too. Use --decode in scripts that must run on both.