Откройте Win+R → sysdm.cpl → «Дополнительно» → «Переменные среды». В верхнем блоке (пользовательские) вы увидите TEMP и TMP. В нижнем (системные) — снова TEMP и TMP. Четыре переменных, и все указывают примерно в одно и то же место. Вопрос «зачем» звучит наивно, пока не лезешь в историю — а там, как обычно у Microsoft, всё строится на трёх китах: CP/M, обратная совместимость и компромиссы между разными командами разработчиков.
Раймонд Чен, легендарный разработчик и историограф Windows, разобрал эту тему в своём блоге Old New Thing ещё в 2015 году. Я освежил её — потому что сегодня к старой дилемме TMP/TEMP добавились ещё USERPROFILE, путь к Windows и относительно свежий GetTempPath2, и это уже не «два варианта», а полноценная иерархия из четырёх уровней.
Откуда вообще взялась эта пара
Чтобы понять, откуда растут ноги, нужно отмотать ленту назад — в 1973 год.
🕰️ CP/M (1973) — операционка, на которой стоял весь ранний микрокомпьютерный мир. И в ней переменных окружения вообще не было. Если хотел указать программе, где хранить временные файлы, нужно было... патчить байты в исполняемом файле. Чен вспоминает свой WordStar: в мануале прямо было написано, какие байты в .EXE менять, чтобы настроить букву диска для temp-файлов. Бонусом шли несколько десятков байт «patch space» — место для самописных подпрограмм, например, чтобы дописать драйвер принтера.
🕰️ MS-DOS (1981) — спроектирован как машинно-переводимый клон CP/M. Цель была буквально такая: чтобы существующую программу под 8080 можно было автоматически конвертировать в программу под 8086. Это, кстати, объясняет дикие на первый взгляд особенности набора инструкций x86 — например, почему из четырёх базовых регистров AX, BX, CX, DX обращаться к памяти по вычисляемому адресу можно только через регистр BX. Просто потому, что в 8080 единственным таким регистром была пара HL, а HL → BH/BL при автотрансляции.
В MS-DOS впервые появились переменные окружения. Но первые программы под DOS были портами с CP/M, и про переменные окружения в них никто не вспоминал. Постепенно появились «нативные» программы для DOS, и они дружно начали использовать переменные для конфигурации. И вот тут, как пишет Чен, «в наступившем хаосе рынка» в качестве кандидатов на «папку для временных файлов» выкристаллизовались два варианта: TEMP и TMP. Никакого комитета по стандартизации, никакого RFC — просто кто-то из ранних авторов выбрал одно, кто-то другое, и пошло-поехало.
MS-DOS 2.0 и пайпы, которых не было
Тут есть очень изящный технический поворот, который мне как инженеру нравится больше всего.
В MS-DOS 2.0 появилась поддержка пайпов: program1 | program2. Вот только DOS — однозадачная ОС. В Unix пайп — это буфер в памяти ядра, через который параллельно работающие процессы общаются. В DOS параллельно работать ничего не может в принципе.
Решение оказалось элегантно-костыльным:
📝 Запускаем program1, перенаправляем её stdout во временный файл
⏸️ Ждём, пока она полностью завершится
📂 Запускаем program2, перенаправляем её stdin из этого временного файла
🗑️ После завершения удаляем файл
То есть «пайп» в DOS — это не пайп, а костыль через диск. Чтобы это работало, COMMAND.COM нужно было где-то создавать эти временные файлы. Авторы DOS выбрали — внимание — TEMP. Не TMP. Просто такое было решение.
Но другие программы продолжали жить своей жизнью. Чтобы угодить всем, многие проверяли обе переменные. В каком порядке — на усмотрение разработчика. Старые DISKCOPY и EDIT смотрели сначала TEMP, потом TMP.
А что Windows?
И вот здесь вступает в действие тот самый поворот, который и порождает путаницу до сих пор.
Когда писали Windows API, авторы функции GetTempFileName решили проверять переменные в обратном порядке: сначала TMP, потом TEMP. Чен в своём блоге пишет это с явным «for whatever reason» — то есть сам же признаётся, что внятной причины нет, просто так исторически сложилось. Скорее всего, разработчик из команды Windows был из лагеря «TMP» и сделал по-своему, а потом обратной совместимости ради это решение зацементировали навсегда.
Результат: внутри одной экосистемы Microsoft образовалось внутреннее противоречие — COMMAND.COM смотрит сначала на TEMP, а Win32 API через GetTempFileName/GetTempPath — сначала на TMP. Поэтому пара существует и сегодня: убрать любую — сломаются программы, которые опираются на «свою» переменную.
Современный порядок: что реально проверяет Windows сегодня
В современном Windows функция GetTempPath (и её ANSI/Unicode варианты GetTempPathA/GetTempPathW) проверяет переменные строго в таком порядке, останавливаясь на первой найденной:
🥇 TMP
🥈 TEMP
🥉 USERPROFILE
🏛️ Папка Windows (C:\Windows, как последний рубеж обороны)
Важная деталь, о которой часто забывают: функция не проверяет, существует ли путь и есть ли у процесса права на запись в него. Это полностью на совести вызывающего кода. Поэтому если у вас в TMP указан какой-нибудь D:\BackedUpDrive\Temp, а диск D отвалился — Windows честно вернёт этот путь, и приложение упадёт с access denied.
Ещё интересный нюанс: .NET-метод Path.GetTempPath() под капотом — это просто обёртка над Win32 API. На Linux он смотрит на TMPDIR, потом fallback в /tmp/. На macOS — то же самое, причём TMPDIR система устанавливает при логине пользователя автоматически в персональный каталог. То есть кросс-платформенный GetTempPath() ведёт себя сильно по-разному в зависимости от ОС.
GetTempPath2 — security-патч 2022 года
А теперь свежак, про который кратко в исходной заметке вообще не упоминается, а зря — это, пожалуй, самое важное изменение в логике temp-путей за последние 20 лет.
В относительно недавних версиях Windows появилась новая функция — GetTempPath2. Зачем? Из-за безопасности.
Проблема в следующем. Когда процесс работает под учёткой SYSTEM (то есть с максимальными привилегиями), он раньше использовал ту же папку C:\Windows\Temp, что и обычные процессы. А C:\Windows\Temp, в отличие от пользовательского %USERPROFILE%\AppData\Local\Temp, имеет довольно либеральные разрешения. Это значит, что обычный пользователь мог подсунуть туда символическую ссылку или вредоносный файл — и SYSTEM-процесс при работе с временным файлом мог напороться на path redirection attack (подмену пути). Классическая поверхность атаки на повышение привилегий.
GetTempPath2 решает это так:
🛡️ Если процесс работает под SYSTEM — функция возвращает C:\Windows\SystemTemp, ACL'нутую так, что туда могут писать только SYSTEM/администраторы
👤 Если процесс работает под обычным пользователем — GetTempPath2 ведёт себя ровно так же, как старый GetTempPath (TMP → TEMP → USERPROFILE → Windows-папка)
Microsoft теперь в документации прямо пишет: «Apps should call GetTempPath2 instead of GetTempPath». То есть для нового кода старая функция считается deprecated по соображениям безопасности.
Почему это важно для разработчика
Можно подумать: «ну дублирование переменных, ну подумаешь». На практике эта мелочь регулярно стреляет в ногу:
🐛 Кросс-софтовые разногласия. Программа А пишет временные файлы по TMP, программа Б — по TEMP. Если эти переменные указывают на разные каталоги (а это случается чаще, чем кажется, особенно когда юзер ковырял переменные руками), вы получите два разных места для temp-файлов, и автоматическая очистка одного не подметёт другое.
🐛 Утилиты очистки. Многие «cleaner»-программы чистят один путь и не трогают другой. Реальные кейсы: десятки гигабайт temp-мусора, которые годами копились в %TMP%, потому что чистильщик ходил только в %TEMP%.
🐛 Сервисы и SYSTEM-процессы. Та самая ситуация из форума: служба, запущенная под SYSTEM, ломится в C:\Windows\system32\config\systemprofile\AppData\Local\Temp\, а его, разумеется, не существует в типовой инсталляции. Если вы пишете сервис — либо явно задавайте свой temp-путь, либо используйте GetTempPath2, который для SYSTEM вернёт безопасный C:\Windows\SystemTemp.
🐛 Разная семантика в разных окружениях. В Cygwin/MSYS/WSL переменные могут вести себя ещё веселее: TMP может содержать виндовый путь, а /tmp ссылаться на линуксовое представление через монтирование. Особенно радует, когда скрипт на bash через MSYS пытается использовать $TMP, ожидая POSIX-путь.
Личное мнение: почему такие штуки никто не «исправит»
Эта история — идеальная микро-иллюстрация того, почему legacy-системы остаются legacy, даже когда у компании есть все ресурсы переписать всё «правильно». Microsoft могла бы:
🤔 Отказаться от одной из переменных — нет, сломаются миллионы старых приложений
🤔 Объединить их в одну ссылку (junction) — частично сделано, но не везде
🤔 Создать новый «правильный» API и пометить старый deprecated — собственно, это и сделали с GetTempPath2, но только по линии безопасности, не по семантике
Это, кстати, ключевая разница в подходах к обратной совместимости между Microsoft и, например, Apple. Apple раз в несколько лет ломает API без зазрения совести, и разработчики переписывают код. Microsoft за 40+ лет накопила столько legacy-софта в энтерпрайзе, что сломать TMP/TEMP — значит положить производственные пайплайны в банках, заводах и госконторах. А производственный пайплайн сборки в банке начала 2000-х никто переписывать ради красоты не будет.
И знаете, мне такой подход даже скорее нравится. Да, в коде Windows есть слой за слоем артефактов — от CP/M-патчей до Win16, от 16-битных DOS-эмуляторов до WOW64 и MSIX. Это уродливо с точки зрения чистого инженерного эстета. Но это работает. Программы 1995 года в большинстве случаев запускаются на Windows 11. Это очень редкое в IT-мире свойство, и его цена — вот эти самые TMP и TEMP, духи MS-DOS, тихо живущие в реестре современной 64-битной системы.
Заключение и пара практических советов
Если вы пишете код:
✅ Не парсите переменные руками — используйте GetTempPath2 (или его обёртки в .NET, Go, Rust, Python через ctypes)
✅ Всегда проверяйте, что путь существует и доступен на запись — Windows вам этого не гарантирует
✅ Если пишете сервис под SYSTEM — GetTempPath2, и точка
✅ Помните, что Path.GetTempPath() в кросс-платформенных приложениях ведёт себя по-разному на Windows/Linux/macOS
Если вы админ:
✅ Держите TMP и TEMP синхронными — указывающими на одну папку
✅ Регулярно чистите обе, на всякий случай (и %LOCALAPPDATA%\Temp тоже)
✅ Не трогайте SystemTemp, если не знаете точно, что делаете
История TMP против TEMP — это «Adidas против Puma, гик-версия», как метко выразился Раймонд Чен. Два бренда, основанные братьями, разругавшимися между собой; два пути к одному и тому же; и обе компании выжили, потому что рынок оказался достаточно большим. Только в случае Microsoft «рынок» — это сорок лет совместимости с миллионами приложений, и компромисс этот, судя по всему, останется навсегда.
Источники
🔗 Raymond Chen — Why are there both TMP and TEMP environment variables, and which one is right? (The Old New Thing, 17 апреля 2015): https://devblogs.microsoft.com/oldnewthing/20150417-00/?p=44213
🔗 Документация GetTempPath2W (Win32 API): https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppath2w
🔗 Документация GetTempPathW (Win32 API): https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw
🔗 .NET Path.GetTempPath Method: https://learn.microsoft.com/en-us/dotnet/api/system.io.path.gettemppath
🔗 Tim Paterson — An Inside Look at MS-DOS (исторический материал по дизайну DOS): http://www.patersontech.com/dos/byte%E2%80%93inside-look.aspx
🔗 The Old New Thing — про дизайн x86 (8086 как наследник 8080): https://devblogs.microsoft.com/oldnewthing/20040105-00/?p=41193