Upload a file
  1. base64hub
  2. Base64 in Code
  3. Bash
Code

Base64 in Bash

The base64 command does the work; Bash only pipes data into it. GNU coreutils on Linux and the BSD tool on macOS take different flags. Linux outputs below come from GNU coreutils 9.7.

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

TaskGNU coreutils (Linux)macOS
Decode-d, --decode-D, --decode (recent releases also -d)
No line wrapping-w 0default
Wrap at N chars-w N (default 76)-b N
Input filebase64 filebase64 -i file or base64 < file
Output fileredirect >-o file or redirect
Ignore junk when decoding-i, --ignore-garbagenot 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:

InputResult
aGkhi, exit 0 (missing padding tolerated)
"aGVs\nbG8K"hello, exit 0 (newlines ignored)
aGl=hi, then invalid input, exit 1
ainvalid 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 base64 exists on almost every system and behaves the same on Linux and macOS. See Base64 with OpenSSL.
  • python3 -m base64 -d file and python3 -m base64 file work where coreutils are missing; the output wraps at 76 characters.
  • In Kubernetes, kubectl get secret x -o jsonpath='{.data.password}' | base64 -d is 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.

Keep reading