Декодирование Base64 в Bash: полное руководство
Время от времени в вашем терминале оказываются строка из букв, цифр и изредка попадающихся +, / или =, и вам нужно вернуть настоящие данные. JWT, вставленный в тикет, файл .b64, приложенный к письму в поддержку, секрет Kubernetes, который читается как каша из букв, PNG, прячущийся внутри HTML-тега data:. Это полевой справочник по возврату байтов в Bash и shell с помощью инструментов, которые у вас почти наверняка уже установлены.
Формат в одном дыхании: Base64 переписывает каждые три байта сырых данных в четыре символа из 64-символьного алфавита (A-Z, a-z, 0-9, плюс + и /), а если количество байтов не кратно трём, добавляет в конце один или два знака =. Декодирование - это направление сжатия этой сделки: четыре символа на входе, три байта на выходе. Главная страница этого сайта разбирает формат пошагово, так что в этой статье мы занимаемся тем, чем по праву должны заниматься, shell-стороной работы.
А вот и подвох: команды base64 в единственном экземпляре не существует. Этим именем пользуются программа на C от GNU, переписанная на Rust версия, апплет BusyBox, реликт BSD и утилита OpenSSL, флаги которой перекрываются по-настоящему опасно. Все они сходятся в алфавите, но не всегда сходятся во мнении о том, как выглядит сломанный ввод, и именно из-за этой разницы скрипты погибают. Так что первый шаг - понять, кто отвечает, когда вы вводите base64.
Узнайте, с каким декодером вы имеете дело
Одна команда расскажет вам почти всю историю:
base64 --version
В зависимости от машины перед вами один из этих вариантов:
| Что вы видите | Что у вас есть | Флаг декодирования |
|---|---|---|
base64 (GNU coreutils) 9.x |
Классическая реализация на C, по-прежнему стандартная на большинстве дистрибутивов Linux | -d или --decode |
base64 (uutils coreutils) 0.8.x |
Переписанные на Rust coreutils, стандартный пользовательский уровень в актуальных выпусках Ubuntu | -d (и, необычно, работает и -D) |
| Текст использования в стиле BSD, без флага версии | BSD base64 на macOS и BSD-системах, происходящий от старой утилиты bintrans |
-D (у этого семейства строчный -d означает отладку, а не декодирование) |
BusyBox v1.x |
Универсальный бинарник Alpine Linux и встраиваемых систем | -d |
Обратите внимание, чего нет в этой таблице: OpenSSL. openssl base64 - совсем другое животное, и его флаг -d означает расшифровку, а не декодирование. Этот один-единственный флаг виноват в большем числе незаметных пустых файлов вывода, чем любая другая привычка, о которой идёт речь в статье, так что познакомимся с ним как следует в разделе про запасные варианты.
Если ваш дистрибутив ставит несколько семейств бок о бок (актуальный Ubuntu так и делает), ещё пара команд покажет полную картину:
command -v base64
base64 --version 2>&1 | head -1
Четыре строки-однострочника, которые покрывают большинство дней
Декодируем строку из стандартного ввода. Это тот приём, который вы будете применять тысячу раз, а printf не даёт shell украшать ваш payload:
printf '%s' "SGVsbG8sIFdvcmxkIQ==" | base64 -d
Декодируем файл. Каждая серьёзная реализация принимает аргумент FILE, и это самый чистый способ держать данные вдали от механизма кавычек shell:
base64 -d payload.b64 > payload.bin
Декодируем из here-string. Here-string добавляет завершающий символ новой строки, но все декодеры считают переводы строк игнорируемым пробельным символом, поэтому для небольших блоков это совершенно безопасно:
base64 -d <<< "SGVsbG8sIFdvcmxkIQ=="
Декодируем сложенный в несколько строк блок через heredoc. Если закрыть разделитель одинарными кавычками, shell не будет интерпретировать ничего внутри:
base64 -d <<'EOF'
SGVs
bG8s
IFdv
cmxk
IQ==
EOF
Все четыре команды выдают Hello, World!. На macOS и BSD замените -d на -D во всех примерах выше, остальной синтаксис идентичен.
Небрежный ввод - это норма
Base64, с которым вы встречаетесь в дикой природе, почти никогда не бывает одной чистой строкой. Он приходит сложенным по 76 символов (конвенция MIME) или по 64 символа (конвенция PEM), выгруженным из Windows с окончаниями строк CRLF или скопированным из чат-окна с лишними пробелами посередине. Хорошая новость: декодеру всё равно, где стоят переводы строк, главное, чтобы это были настоящие переводы строк.
Универсальное лекарство от любого стиля переноса - смыть переводы строк перед декодированием:
tr -d '\r\n' < blob.b64 | base64 -d
Возврат каретки - особый случай. Перевод строки - допустимый ввод, а вот \r для строгих декодеров не перевод строки. Блок, побывавший в Windows-системе, споткнёт GNU-декодер: тот напечатает частичный результат и завершится ошибкой. Лечение - сначала убрать возвраты каретки:
printf 'SGVs\r\nbG8s\r\n' | tr -d '\r' | base64 -d
Если вы видите фрагмент вроде Hel, за которым следует ошибка, значит, под ногами у вас CRLF-блок. Та же самая промывка, tr -d '\r\n', перед декодированием - переносимая привычка для любого ввода, который вы не создали сами.
Для по-настоящему испорченного ввода GNU и uutils предлагают флаг -i (--ignore-garbage), который пропускает символы вне алфавита и декодирует, что может:
printf 'SGVs!bG8' | base64 -di
Эта команда печатает Hello. Прежде чем сделать из -i привычку по умолчанию, узнайте, почему стандарт предостерегает от него: в RFC 4648, раздел 3.3, сказано, что реализации должны отклонять данные, содержащие символы вне алфавита, потому что пропущенные символы можно использовать как скрытый канал, через который провозят данные, не появляющиеся в декодированном результате. Снаряжайтесь -i, когда вставка из документа привела с собой пунктуацию, а не когда вы проверяете данные, которым доверяете.
Вот как три основных декодера на самом деле ведут себя на граничных случаях, на инструментарии 2026 года (uutils 0.8.x, GNU coreutils 9.7, BusyBox 1.37):
| Ввод | uutils 0.8.x | GNU 9.7 | BusyBox 1.37 |
|---|---|---|---|
SGVsbG8sIFdvcmxkIQ==, чистый |
Hello, World!, код выхода 0 |
Hello, World!, код выхода 0 |
Hello, World!, код выхода 0 |
SGVsbG8s, восемь символов, без заполнения |
Hello,, код выхода 0 |
Hello,, код выхода 0 |
Hello,, код выхода 0 |
Zg, два символа, без заполнения |
f, код выхода 0 |
f, код выхода 0 |
ошибка, «truncated input» |
SGV, три символа, без заполнения |
ошибка, без вывода | напечатано He, затем ошибка |
ошибка, «truncated input» |
| строки с переносами CRLF | декодируется без проблем, код выхода 0 | частичный вывод, затем ошибка | декодируется без проблем, код выхода 0 |
SGVs!bG8, лишняя пунктуация |
ошибка (с -i: Hello) |
частичный вывод (с -i: Hello) |
частичный вывод (флага -i нет вообще) |
SGV=, неканонические свободные биты |
ошибка, без вывода | напечатано He, затем ошибка |
He, код выхода 0 |
TQ==junk, мусор после заполнения |
код выхода 0, продолжает декодировать junk |
код выхода 0, продолжает декодировать junk |
напечатано M, затем «truncated input» (код выхода 1) |
Три вывода из этой таблицы. Во-первых, универсального правила для хвостов без заполнения нет: BusyBox требует, чтобы длина была кратна четырём, тогда как GNU и uutils принимают допустимые остатки, но только когда все оставшиеся биты равны нулю (поэтому Zg проходит, а SGV - нет). Во-вторых, GNU и BusyBox записывают уже декодированные байты до того, как упасть, так что скрипт, который перенаправляет вывод в файл, а потом проверяет код выхода, с удовольствием сохранит наполовину декодированный файл. Всегда проверяйте код завершения и относитесь с подозрением к любому файлу, оставленному неудачным декодированием. В-третьих, последняя строка таблицы: uutils и GNU продолжают декодировать junk после == и завершаются с кодом 0, потому что им не говорит никто, что поток должен был закончиться; BusyBox - исключение: он печатает единственный байт перед заполнением, а затем падает с ошибкой усечённого ввода. Если хвостовой мусор для вас важен, проверяйте форму ввода, прежде чем доверять выводу.
base64url: алфавит токенов и URL
Раздел 5 RFC 4648 определяет второй диалект: та же самая 6-битная арифметика, только - и _ заменяют + и /, а заполнение выброшено, потому что URL редко нуждается в том, чтобы сообщать точную длину в байтах. RFC говорит об этом прямо: эту кодировку «не следует считать той же, что и base64-кодировка». Если вы когда-нибудь смотрели на JWT, вы уже встречались с этим диалектом, потому что его сегменты - это base64url без заполнения.
Рецепт в shell - двуступенчатая подмена: сначала переведите URL-безопасные символы обратно в стандартных собратьев, затем декодируйте:
printf '%s' "_k-C" | tr '_-' '/+' | base64 -d | xxd -p
На выходе получается fe4f82, три сырых байта, которые просто оказывались в URL-костюме. Подмена позиционная, поэтому направление важно: кодирование идёт tr '+/' '-_', декодирование - tr '_-' '/+'. Если перепутать, ошибки не будет, просто тихо получаются другие байты, а это худший сорт багов.
Теперь ловушка, в которую попадают те, кто обращается с base64url как с обычным Base64. Заполнение в этом диалекте необязательно, а сегмент с длиной, дающей остаток три от деления на четыре, - это ровно та форма, которую пристально осматривают строгие декодеры. Надёжный приём - сначала восстановить недостающие символы =, для чего нужна небольшая функция:
b64url_decode () {
local s=$1
local n
n=$(printf '%s' "$s" | wc -c | tr -d '[:space:]')
while [ $(( n % 4 )) -ne 0 ]; do
s="${s}="
n=$(( n + 1 ))
done
printf '%s' "$s" | tr '_-' '/+' | base64 -d
}
b64url_decode "eyJzdWIiOiJob21lciJ9"
Последняя строка печатает {"sub":"homer"}, незатейливый JSON-объект, который веб-сервер подписал или запечатал минуту назад. Сегмент с длиной, дающей остаток один от деления на четыре, некорректен сам по себе, и никакое количество заполнения его не спасёт, так что отказ функции обрабатывать этот случай - её особенность.
В GNU coreutils есть и родной путь: basenc, старший брат base64, понимает этот диалект напрямую:
printf '%s' "Zg==" | basenc --base64url -d
Это печатает f, тот единственный байт, который прятался в двух символах. Одно предупреждение, прежде чем строить на этом конвейеры: GNU basenc этой эпохи даже декодирует base64url-ввод без заполнения (голый Zg) без единого возмущения, тогда как у uutils basenc по-прежнему требует сначала восстановить заполнение, как показывает пример выше. Маленькая функция выше работает везде, и именно поэтому это переносимый выбор.
Открываем JWT
JSON Web Token - это, согласно RFC 7515, три base64url-сегмента, склеенные точками. Два первых - обычный JSON (заголовок и утверждения payload), поэтому они декодируются сразу в читаемый текст. Третий - подпись, сырой бинарный дайджест, так что оставьте его в покое: декодирование даст вам байты подписи, а не сообщение.
token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiJ9.abc123"
b64url_decode "$(printf '%s' "$token" | cut -d. -f2)"
Вывод - {"sub":"42"}, утверждение subject, без какого-либо сервера. Читайте утверждения с ясной головой о том, что декодирование есть, а что нет: оно раскрывает, но не проверяет. Подпись молчит, пока сервер, держащий ключ, не пересчитает её, а это работа для openssl dgst (статья о кодировании показывает весь танец чеканки), не для этой. Частая ошибка в поле - принять декодированный payload за доказательство того, что токен валиден; злоумышленник может чеканить вручную неподписанные или подписанные слабо токены, и декодер охотно прочитает все их.
Файлы, байты и стена переменных
Декодирование выдаёт вам сырые байты. Они могут складываться в английское предложение, а могут оказаться серединой PNG, разделяемой библиотеки или zip-архива. Файл - единственное место в shell-скрипте, где байты абсолютно безопасны, поэтому стандартный рабочий процесс - декодировать в файл, а потом сравнивать:
base64 -d photo.b64 > photo.png
Когда оригинал лежит рядом, побайтовое сравнение - единственное доказательство, которое имеет значение:
cmp photo.png photo.png.orig && echo "byte-for-byte identical"
Когда оригинал далеко, сравнивайте контрольные суммы:
sha256sum expected.bin
base64 -d blob.b64 | sha256sum
Два совпавших хеша - и ваше декодирование доказуемо точно, что лучше любого количества визуального осмотра в просмотрщике изображений или шестнадцатеричного дампа.
Переменные shell - другое дело, и они образуют стену по двум причинам. Подстановка команд $(...) срезает все завершающие переводы строк из вывода и вообще не может удержать байт NUL: bash печатает предупреждение и молча выбрасывает их. Payload из трёх байтов 41 00 42 демонстрирует обе проблемы одним показом:
printf 'QQBC' | base64 -d > out.bin
xxd out.bin
Файл держит все три байта (41 00 42). Путь через переменную - нет:
v=$(printf 'QQBC' | base64 -d)
printf '%s' "$v" | wc -c
Вы получите 2, с предупреждением в стандартном выводе об ошибках про проигнорированный байт NUL. Вывод короткий: если данные могут быть бинарными, декодируйте их в файл, осматривайте через xxd и никогда не прогоняйте через переменную.
Где Base64 прячется в реальной работе
Когда декодирование становится привычным, вы начинаете замечать формат везде. Вот уголки реальной shell-работы, где он выплывает, и точный приём для каждого.
Секреты Kubernetes. Каждое поле под .data в секрете - это Base64, и официальные документы подчёркивают, что это кодирование, а не шифрование. Прочитать его обратно - классический однострочник:
kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d; echo
Бинарные патчи git. Когда diff касается бинарных файлов, git diff --binary выводит блок GIT binary patch. Не тянитесь здесь за base64 -d: строки в этом блоке - собственное base85-подобное кодирование git (каждая строка начинается со символа длины, A-Z или a-z, за которым идёт base85-данные), а не Base64, и обычный декодер задохнётся на них. Правильный инструмент - владелец формата:
git diff --binary | grep -a -A2 'GIT binary patch'
Подайте diff в git apply или git am и позвольте им заняться распаковкой.
Data URI. Изображение, встроенное в HTML или CSS, выглядит как data:image/png;base64,iVBOR.... Отрежьте всё до запятой с запятой, смойте переводы строк, декодируйте - и файл у вас в руках:
cut -d, -f2- icon.uri | tr -d '\r\n' | base64 -d > icon.png
PEM-броня. Сертификаты и закрытые ключи обворачивают свой Base64 в рамочные строки, которые вообще не Base64. Отберите бронированный блок, выбросьте две рамочные строки и декодируйте до сырого DER-бинарника:
awk '/BEGIN CERTIFICATE/{f=1;next} /END CERTIFICATE/{f=0} f' cert.pem | tr -d '\r\n' | base64 -d > cert.der
Подставьте вместо CERTIFICATE строку PRIVATE KEY или какую метку несёт ваш файл; форма та же.
MIME-почта. Любая часть письма с Content-Transfer-Encoding: base64 сложаается по 76 символов с окончаниями строк CRLF, потому что так предписывает RFC 2045. Переносимая связка - промывка плюс декодирование:
tr -d '\r\n' < attachment.b64 | base64 -d > attachment.bin
Вставки из буфера обмена. Текст, скопированный из браузера, чат-окна или документа, приезжает с лишними пробелами и завершающим переводом строки. Пробел - не буква алфавита, поэтому строгие декодеры отклоняют такую вставку; стандартное спасение - сначала выбросить пробелы:
tr -d ' \r\n' < pasted.b64 | base64 -d
Сначала байты: кодировки и Unicode
Самая повторяющаяся ошибка в работе с Base64 - думать символами, тогда как формат знает только байты. base64 -d выдаёт вам сырые байты, и станут ли они читаемым текстом - решение того, кто будет читать их следующим. Это решение - кодировка, и оно происходит после декодирования, никогда внутри него.
UTF-8 - базовое допущение и обычно правильное. Слово café в UTF-8 - это пять байтов, и декодирование - одно удовольствие:
printf 'Y2Fmw6k=' | base64 -d | xxd -p
Получается 636166c3a9: caf плюс два UTF-8-байта c3 a9 для буквы é. Старые системы, правда, отдадут вам байты Latin-1 (ISO-8859-1), где та же буква - одиночный байт e9. Декодировать такой блок и выдать его как есть в UTF-8-терминал - получить искажённый символ; лечение - переинтерпретировать байты через iconv, прежде чем что-то другое их увидит:
base64 -d latin1.b64 | iconv -f ISO-8859-1 -t UTF-8 > utf8.txt
Несколько фактов на уровне байтов, которые экономят реальное время отладки:
- UTF-8 BOM - это три байта
ef bb bf, которые кодируются в77u/. Обратите внимание на/, враждебный URL символ: именно от таких вещей и существует base64url. Если в декодированном файле «невидимый мусор в начале», посмотрите первые три байта черезxxd. - Эмодзи вроде 😀 - это четыре UTF-8-байта, которые становятся восемью Base64-символами,
8J+YgA==.+внутри - стандартный алфавит, работающий ровно по замыслу; в одежде base64url он становится8J-YgA. - Некорректные UTF-8-последовательности декодируются как байты прекрасно, а затем отображаются мусором или символами замены. Это не ошибка Base64; декодирование сделало свою работу.
xxdилиhexdump -Cпокажет, что это за байты на самом деле. - Локаль вашего терминала решает, как он отрисовывает эти байты. «Терминал показывает мусор» - это заявление об отображении, а не о данных. Байты не изменились в пути.
Большие куски: потоки, разбиение и скорость
Base64 - потоковый формат, и декодер работает как настоящий потоковый конвейер: файл на 10 ГБ никогда не лежит в памяти, он просто течёт сквозь него. Это делает сторону декодирования на больших размерах почти скучной, а это ровно то, что вам и нужно.
Когда большой кусок разбили на части для передачи (лимит письма, размер вложения в тикете, сообщение в мессенджере), сборка - это просто cat в правильном порядке, за которым следует обычная промывка:
cat part_* | tr -d '\r\n' | base64 -d > big.bin
Держите в голове одну модель размеров: закодированная форма всегда примерно на треть больше оригинала, четыре символа на три байта. Так что когда вам говорят, что файл .b64 должен быть того же размера, что и то, что он прячет, они неправы, и теперь вы можете назвать им точную разницу: payload в 300 МБ прибывает примерно 400 МБ текста.
Скорость на практике не является проблемой. Это управляемые таблицей циклы по обычной памяти, и на современной машине кусок в 200 МБ декодируется за примерно десятую долю секунды с GNU, а даже самая медленная из распространённых реализаций (BusyBox) лишь в несколько раз медленнее, и всё равно с запасом ниже двух секунд. Потребление памяти остаётся плоским, каким бы большим ни был ввод, потому что ничего не буферизуется.
Когда на машине нет base64
Большинство систем имеет хотя бы один из инструментов выше, а у большинства их несколько. Если на вашей платформе coreutils нет вовсе (урезанный контейнер, необычное специализированное устройство), пути установки выглядят так:
| Платформа | Как получить | Примечания |
|---|---|---|
| Debian / Ubuntu | предустановлен; apt install coreutils, если вырезан |
в актуальных выпусках Ubuntu по умолчанию семейство uutils (с 25.10), GNU-близнец доступен как gnubase64; Debian 13 по-прежнему ставит GNU coreutils по умолчанию |
| RHEL / Fedora | dnf install coreutils |
предустановлен практически на каждом образе |
| Alpine | apk add busybox (обычно уже есть) |
busybox base64 -d; флага -i нет |
| macOS | встроен; brew install coreutils для GNU-версии |
BSD-флаги: -D для декодирования, -b для ширины строки; brew даёт gbase64 |
А если пакетный менеджер вообще не вариант, универсальные запасные варианты ниже все читают из стандартного ввода и пишут байты в стандартный вывод, так что встают в те же конвейеры:
openssl base64 -d -A -a < in.b64 > out.bin
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' < in.b64 > out.bin
python3 -c 'import base64, sys; sys.stdout.buffer.write(base64.b64decode(sys.stdin.buffer.read()))' < in.b64 > out.bin
На BusyBox-системе, где апплет base64 вырезан из сборки, старая гвардия всё ещё работает: uuencode -m производит MIME Base64, а его братишка читает его обратно:
busybox uudecode -o out.bin in.uu
Ловушки, которые съедают целые полдни
Всё, что ниже, - это поведение, с которым вы встретитесь в поле, собранное в одном месте:
| Ловушка | Что происходит | Как починить |
|---|---|---|
openssl base64 -d в одиночку |
тихо ничего не делает и завершается с кодом 0, потому что -d там означает Decrypt |
openssl base64 -d -A -a, и результат в ноль байт считать ошибкой |
Декодирование на macOS с -d |
BSD base64 отклоняет флаг (или трактует его как отладку) | -D, либо поставить coreutils ради gbase64 |
| Окончания строк CRLF во вводе | GNU печатает частичный результат, затем падает; uutils и BusyBox принимают | сначала tr -d '\r\n', всегда для внешнего ввода |
| Хвосты без заполнения | BusyBox отклоняет любой остаток; GNU и uutils принимают только канонические хвосты | перед декодированием восстановить недостающее заполнение = |
Мусор после заполнения, например TQ==junk |
uutils и GNU продолжают декодировать и завершаются с кодом 0; BusyBox ошибается | проверять форму ввода, прежде чем доверять выводу |
Неканонические свободные биты, например SGV= |
uutils и GNU отклоняют (GNU после частичного вывода); BusyBox декодирует всё равно | ввод испорчен; перегенерировать кодирование выше по цепочке |
| Чтение декодированного вывода в переменную | $(...) срезает завершающие переводы строк и вообще не держит байты NUL |
декодировать в файл, осматривать через xxd |
-i на недоверенном вводе |
повреждение превращается в тихий успех; пропущенные символы могут нести скрытые данные | декодировать строго, прочесть ошибку, исправить источник |
| Перепутанные алфавиты | декодирование base64url как стандартного (или наоборот) даёт неверные байты или ошибку | знать формат до декодирования; подмена - tr '_-' '/+' |
| Отношение к декодированному секрету как к секрету | одна команда отменяет его; в RFC зафиксированы реальные инциденты утечки учётных данных | настоящее шифрование, а не кодирование |
Строке про OpenSSL положен отдельный абзац, потому что отказ настолько тихий. В OpenSSL 3.x у отдельного приложения -d - общий вариант «decrypt», а обработка Base64 - отдельный режим, который выбирается через -a. Так что openssl base64 -d читает ваш ввод, ничего не делает, ничего не печатает и завершается с кодом 0. Рабочее декодирование - openssl base64 -d -A -a, где -A говорит, что ввод - одна непрерывная строка. Если вам приходится использовать OpenSSL для декодирования, считайте вывод в ноль байт ошибкой, каждый единственный раз.
Жёсткая рутина
Привычки, которые не дают Base64 взять над вами верх:
- Падайте громко. Запускайте скрипты с
set -euo pipefailи проверяйте коды выхода. Все распространённые декодеры завершаются с кодом 1 на плохом вводе; OpenSSL - тот громкий, который затих, так что для него дополнительно проверяйте, что вывод не пуст. - Доказывайте круговой путь.
cmpилиsha256sumмежду ожидаемыми и восстановленными байтами - единственное доказательство, которое имеет значение. Никогда не осматривайте бинарник глазами. - Бинарное - в файлы, никогда в переменные. Завершающие переводы строк и байты NUL - оба жертвы подстановки команд.
- Промывайте внешний ввод.
tr -d ' \r\n'перед тем, как декодировать всё, что пересекло границу платформ. - Будьте строгими по умолчанию. Держите
-iвыключенным, пока не увидите, что именно было не так; строгий сбой говорит вам о месте, либеральный - о чём-либо. - Называйте алфавит. Стандартный Base64 и base64url - разные кодировки по RFC 4648. Декодируйте JWT как base64url, вложение письма - как стандартный, и никогда не подменяйте символы, не зная почему.
- Никогда не печатайте то, что только что декодировали. Весь смысл формата в мире секретов - невидимость, а весь смысл лога - видимость. Эти две цели не смешиваются.
Небольшой переносимый shim на вопрос «какой флаг хочет эта машина»:
case "$(base64 --version 2>&1 | head -2)" in
*uutils*|*GNU*|*BusyBox*) b64d () { base64 -d; } ;;
*) b64d () { base64 -D; } ;;
esac
b64d < in.b64 > out.bin
Оператор case ловит семейства GNU, uutils и BusyBox по их баннеру версии - все три принимают строчный флаг - и откатывается к BSD-флагу на всём остальном, что есть ровно четырёхсторонний раскол реального мира: GNU, uutils, BusyBox, BSD.
Как shell научился декодировать
Формат старый, а команда, которую вы печатаете, - нет. Короткая хронология shell-стороны истории:
- 1980, Беркли. Мэри Энн Хортон пишет
uuencodeиuudecodeв Калифорнийском университете в Беркли, чтобы провозить бинарные файлы (обычно сжатые) через почту. Имя означает «от Unix к Unix», безопасное кодирование для перемещения файлов между Unix-системами, которые могут не делить кодировку. Десятилетиями shell-пользователи тянулись именно за этим, а не за Base64. - Эра модемного доступа. Самые ранние кодировки по основанию едут на той же самой проблеме: uuencode на UNIX, BinHex на TRS-80 и Apple II, за ними с шагом позади - Macintosh, каждая из них предполагает только символы, которые может напечатать собственный терминал.
- 1993. MIME стандартизует Base64 для почты (RFC 1521, позже RFC 2045), с переносом строк по 76 символов, который и по сей день определяет поведение
base64по умолчанию. - 2003 и октябрь 2006. RFC 3548 приводит семейство в порядок, а RFC 4648 заменяет его, добавив алфавиты и правило строгого декодирования, которое эта статья постоянно цитирует, включая base64url.
- 15 августа 2006. coreutils 6.0 добавляет саму команду
base64, а файл NEWS откровенно благодарит её как «функцию кодирования и декодирования base64 (RFC 3548)». До этой даты shell-пользователи на Linux тянулись заopenssl base64,uuencode -m, Perl или Python, оттого так много старых скриптов предполагают, что OpenSSL - единственный вариант в городе. - OS X 10.7. macOS выпускает собственный
base64, BSD-версию с флагом-Dи без переноса строк по умолчанию, и именно отсюда берётся раскол-dпротив-D. - Март 2024. coreutils 9.5 ослабляет декодер: заполнение при декодировании больше не обязательно, а кодировки с ненулевыми свободными битами теперь диагностируются как повреждение, а не принимаются молча.
- 2025. Переписанные на Rust coreutils (uutils) становятся стандартными в актуальных выпусках Ubuntu. То же имя команды, те же флаги, новый движок со собственными мнениями о границах, вроде принятия
-Dкак алиаса.
Любопытства, которые стоит оставить себе
- Команда моложе формата. Base64 в почте с 1993 года, но команда
base64появилась только в 2006. Тринадцать лет shell-скрипты делали эту работу другими инструментами, и их отпечатки до сих пор видны повсюду. - Окаменелость в тексте справки. Утилита uutils
base64до сих пор описывает свой алфавит в справке как «RFC 3548», вышедший на пенсию предшественник RFC 4648, тогда как её GNU-собратья уже ссылаются на действующий стандарт. Крошечная окаменелость, заметная только если читать справку. - Самая дорогая тихая операция-пустышка в наборе инструментов.
openssl base64 -dничего не делает и завершается с кодом 0. Час отладки, пропал, а проверка кода выхода прошла. - BusyBox сохраняет свободные биты. Он с удовольствием декодирует
SGV=, чьи оставшиеся биты ненулевые и потому неканонические, тогда как его GNU-родственник устраивает сцен на том же вводе. Один RFC, разные нервы. - Имя формата истинно на каждой машине.
printf 'base64' | base64даётYmFzZTY0на GNU, uutils, BusyBox и OpenSSL в равной степени. Это истинно с 2006 года и всегда будет так. - Base85 - это не Base64. Блоки «binary patch» у git выглядят как Base64 для необученного глаза, но строки с префиксом длины - это base85-подобный диалект собственного почерка. Проходимец, который обходится людям крюком grep-и-декоди.
- Одиннадцать символов, шестьдесят четыре бита. ID видео на YouTube - это 11-символьная base64url-строка, 64-битное число в URL-одежде, оттого оно и появляется в URL без единого процентного знака.
- Декодеры не сходятся на одном символе. Одна-единственная строка вроде
SGV=раскалывает поле на три лагеря, как показывает таблица поведения выше. Если декодирование падает на «очевидно валидном» вводе, вы, вероятно, стоите на линии свободных битов, и кодировка с самого начала не была канонической.
Так что в следующий раз, когда в терминале окажутся строка из букв, цифр, плюсов и слэшей, вы будете знать всю историю: какой декодер следит, какой алфавит говорит, где прячутся переводы строк и как именно вернуть байты, целыми и доказанными побайтово, не потеряв часа на возврат каретки. А когда работа указывает в обратную сторону, в упаковку собственного бинарника в текстовый конверт для дороги, связанная статья о Base64-кодировании, ссылка на неё ниже, покрывает этот ритуал с той же глубиной.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в Bash: полное руководство