Введение
Мы все любим писать серьезные вещи на серьезных языках. Шаблоны, C++, Reflection, Perl и многое другое – вот то, что мы любим, то, чему посвящаем длинные сообщения в форумах, то, что снится нам по ночам.
Однако в нашей повседневной деятельности встречаются и вещи, которые
не так интересны и интеллектуальны. Мы не очень любим говорить об этом,
делаем вид, что Это – грязно, нечистоплотно и недостойно нашего
внимания. Однако, приходит день, приходит час и перст Судьбы находит
нас – нам надо написать еще один батничек… Иногда это запускалка для
построения проекта, которая должна при ошибке компиляции скопировать
логи на сетевой диск, иногда – запуск обновления исходных текстов из
SVN. Иногда – что-нибудь еще.
К чему я это все? А к тому, что поговорим мы о полезных хитростях
при написании файлов сценариев на встроенном командном языке Windows. К
счастью, это занятие не является доминирующим в профессиональной
деятельности автора, так что я не обязуюсь заполнить абсолютно все
пробелы в данной области. Кроме того, рожденный ползать летать не
может, и из cmd.exe, увы, не получится ни /usr/bin/perl, ни даже
/bin/sh. Так что, все нижеприведенное – просто некоторые интересные
факты из жизни файлов с расширением bat, на которые автор обратил
внимание во время решения различных практических задач автоматизации.
Наш урок будет построен по сугубо практическому принципу, известному
в народе как Cookbook. Иными словами, я не буду вдаваться в
синтаксические и семантические дебри командного языка Windows, а лишь
продемонстрирую его возможности (хотел написать «мощь», но все-таки
передумал). Именно поэтому большинство следующих заголовков будет
начинаться со слова «Как». Впрочем, для полноты по ходу развития
событий будут даваться подробные комментарии, в том числе и по языковым
конструкциям.
ПРЕДУПРЕЖДЕНИЕ
Практически
все описанные здесь рецепты подойдут только для Windows 2000 и старше.
Bat-язык Windows 9x, к счастью, можно считать почившим, так что здесь
он не рассматривается. Более того, диалекты cmd.exe операционных систем
Windows 2000, Windows XP и Windows Server 2003 также немного
различаются. Все приведенное ниже создано и проверено на компьютере под
управлением операционной системы Windows XP. За подробной информацией
по различиям в реализации той или иной команды обращайтесь к [1].
|
Как экранировать символ?
В командном языке Windows существует некоторый набор символов с
высоким приоритетом, которые всегда трактуются как спецсимволы. К ним,
в частности, относятся:
- Операторы перенаправления ввода-вывода <, >, >>.
- Оператор конвейера |.
- Операторы объединения команд ||, & и &&.
- Оператор разыменования переменной %…%.
В случае если символ, относящийся к одному из таких операторов,
должен быть включен в вашу команду в его литеральном смысле, вас ждут
определенные неожиданности. Например, при выполнении вот такой строки
echo The ratio should be up to 10%.
|
символ процента будет «съеден» интерпретатором, который решит, что
это попытка вывода значения какой-то переменной. В случае со знаком
процента решение довольно хорошо известно и состоит в удвоении этого
символа:
echo The ratio should be up to 10%%.
|
после чего все заработает так, как надо. Однако в других случаях все
менее очевидно. Рассмотрим командный сценарий, который генерирует
незатейливый HTML-файл:
@echo off
set OUTPUTFILE=%1
echo <html> >%OUTPUTFILE%
echo <head> >>%OUTPUTFILE%
echo <title>This is a greeting page</title> >>%OUTPUTFILE%
echo </head> >>%OUTPUTFILE%
echo <body> >>%OUTPUTFILE%
echo Hello World! >>%OUTPUTFILE%
echo </body> >>%OUTPUTFILE%
echo </html> >>%OUTPUTFILE%
|
К сожалению, при попытке запуска этого "чуда инженерного разума" нас постигнет неудача в виде сообщения об ошибке
> was unexpected at this time.
|
Оно и понятно: командный интерпретатор не в силах разобраться, где
его просят вывести на экран символ HTML-тега, а где перенаправить
вывод. В нормальных языках программирования эта проблема обычно
решается обрамлением строковых литералов кавычками. Отчасти это
помогает и в bat-файлах. Но лишь отчасти. Выполнение строки
echo "<html>" >%OUTPUTFILE%
|
приведет к тому, что в выходной файл будут записаны и сами кавычки. Это явно не совсем то, что требуется.
К счастью, есть один малоизвестный способ, позволяющий добиться
требуемого результата. Символ ^ позволяет экранировать любой другой
символ с безусловным приоритетом. Таким образом, вышеприведенный пример
генерации HTML может быть успешно записан так:
@echo off
set OUTPUTFILE=%1
echo ^<html^> >%OUTPUTFILE%
echo ^<head^> >>%OUTPUTFILE%
echo ^<title^>This is a greeting page^</title^> >>%OUTPUTFILE%
echo ^</head^> >>%OUTPUTFILE%
echo ^<body^> >>%OUTPUTFILE%
echo Hello World! >>%OUTPUTFILE%
echo ^</body^> >>%OUTPUTFILE%
echo ^</html^> >>%OUTPUTFILE%
|
Таким же способом можно экранировать любой другой специальный
символ. Очевидно, можно экранировать и сам ^. Не очень эстетично, зато
дешево и практично. Слово «надежно» я пропустил умышленно…
Как перенести длинную строку?
Совет по поводу экранирующего символа ^ имеет еще одно применение:
перенос строк. Я (как и многие из вас, наверное) люблю, чтобы любой
исходный текст, который я пишу, выглядел красиво – даже *.bat-файлы.
Одним из обязательных условий красоты и удобочитаемости кода для меня
является его ширина: все строки должны умещаться в 78 столбцов. Можно
поспорить по поводу числа 78, но в одном я непреклонен – ограничение на
ширину текста кода должно быть, иначе это не код, а макароны.
Так вот долгое время *.bat-файлы портили мне жизнь тем, что иногда
приходилось писать длинную строку – например, вызов какой-нибудь другой
программы с кучей опций, и я не знал, что с этим делать. Происходило
это нечасто, но всегда было неприятно. Но, к счастью, моя жизнь
изменилась с тех пор, как я открыл для себя Супер-Символ ^:
packagebin.exe --recursive-search=yes --files-mask=exe,dll,pdb,obj ^
--archive-type=zip --archive-level=max --deliver-method=ftp ^
--deliver-target=ftp://ftp.site.com
|
Помните лишь, что чудо-символ должен быть последним в строке – скажите «Нет!» концевым пробелам.
Как определить имя каталога, в котором находится запущенный командный файл?
Иногда сценарию надо знать полный путь к себе самому и/или к
каталогу, в котором он находится. Это может понадобиться по разным
причинам. Например, он должен достать из системы контроля версий
исходники в каталог <script-dir>/src рядом с собой. Или,
запускаются тесты из каталога <script-dir>/tests, и перед их
запуском надо добавить каталог <script-dir>/bin в переменную PATH.
Можно, конечно, рассчитывать на то, что командный файл был вызван из
того же каталога, где он находится, и тогда в качестве вышеупомянутого
<script-dir> можно использовать переменную окружения %CD% -
полный путь к текущему каталогу. Однако любые допущения в нашем деле
недопустимы (хороший каламбур, однако!). Поэтому приведу более надежное
решение.
Прежде всего, вспоминаем, что переменная %0 в bat-файле
соответствует нулевому аргументу командной строки, т.е. имени самого
файла. После этого читаем скудную документацию для команды call:
и обнаруживаем, что при использовании нумерованных переменных %0-%9 можно использовать некоторые модификаторы:
%~1 - разворачивает %1, удаляя кавычки (")
%~f1 - разворачивает %1 в полный квалифицированный путь
%~d1 - разворачивает %1 в букву диска
%~p1 - разворачивает %1 в путь
%~n1 - разворачивает %1 в имя файла
%~x1 - разворачивает %1 в расширение файла
%~s1 - развернутый путь будет содержать только короткие имена
%~a1 - разворачивает %1 в атрибуты файла
%~t1 - разворачивает %1 в дату/время создания файла
%~z1 - разворачивает %1 в размер файла
%~$PATH:1 - Ищет в каталогах, перечисленных в переменной среды PATH,
и разворачивает %1 в полное квалифицированное имя
первого совпадения. Если имя перменной среды
не определено, или если файл не найден, этот
модификатор вернет пустую строку
|
и, более того:
Модификаторы можно объединять для получения сложных результатов:
%~dp1 - разворачивает %1 в букву диска и путь
%~nx1 - разворачивает %1 в имя файла с расширением
%~dp$PATH:1 – ищет %1 в каталогах, перечисленных в переменной
среды PATH, и разворачивает в букву диска
и путь к первому найденному файлу.
%~ftza1 - разворачивает %1 в строку, подобную DIR
|
Таким образом, правильным будет использовать в качестве тега <script-dir> сочетание %~dp0, которое будет раскрыто в полный путь к каталогу, где находится сценарий. Например,
"%~dp0\packagebin.exe" --recursive-search=yes --files-mask=exe,dll,pdb,obj ^
--archive-type=zip --archive-level=max --deliver-method=ftp ^
--deliver-target=ftp://ftp.site.com --deliver-source="%~dp0\bin"
|
Обратите внимание на использование кавычек – потенциально каталог
может иметь в своем пути пробел. Кавычки избавят от проблем в этом
случае.
ПРЕДУПРЕЖДЕНИЕ
Опасайтесь
бездумного применения команды cd %~dp0 без проверки результата
выполнения. Теоретически, эта команда должна сменить текущий каталог на
каталог, в котором расположен командный файл. Как правило, это
работает. Однако возможны неожиданности. Однажды был написан простой
командный сценарий, задача которого была просто удалить все каталоги
рядом с собой. В «свою» директорию он переходил как раз через cd %~dp0.
Все было проверено на локальной машине – работало замечательно. После
этого сценарий был помещен на файл-сервер, где ему и полагалось быть. Я
зашел с помощью Far в сетевой каталог, и для контрольной проверки решил
запустить файл еще раз. Дальнейшее словно в тумане. cmd.exe правильно
определил местонахождение bat-файла: \\servername\sharename\directory.
Однако при попытке сделать туда cd, он сказал, что UNC-пути в качестве
текущих каталогов не поддерживаются и лучше он сменит текущий каталог
на C:\WINDOWS… Это было действительно мудрое решение… Часть сценария,
отвечавшая за удаление всех каталогов, сработала отлично – хорошо, что
я успел вовремя остановить это безумие.
В тот день я узнал, что такое System Restore…
|
Как получить короткое (8.3) имя файла?
«А зачем? – спросите вы – Ведь мы живем в мире Интернета,
Web-сервисов и NTFS с длинными именами файлов». Это действительно так,
но иногда встречаются программы, которые отчаянно сопротивляются
прогрессу, и в частности, не любят имен файлов и полных путей с
пробелами. Одной из таких программ, кстати, является утилита build.exe
из Windows DDK… В таких ситуациях спасает использование короткого,
«беспробельного» DOS-имени для файла.
ПРЕДУПРЕЖДЕНИЕ
Доступ
к файлу по короткому имени может быть не всегда возможен. На файловой
системе NTFS создание коротких псевдонимов для файлов может быть
отключено путем установки в единицу значения
«NtfsDisable8dot3NameCreation» в ключе реестра
«HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\FileSystem».
|
Итак, все же (в предположении, что надругательства над NTFS не было)
– как? Внимательный читатель должен был заметить в предыдущем разделе,
что при обращении к переменным %0 - %9 можно использовать префикс
%~s1 - expanded path contains short names only
|
который нам как раз мог бы помочь. Но есть засада – все эти полезные
префиксы нельзя использовать с произвольной переменной окружения, а
присваивание переменным %0 - %9 не поддерживается. К счастью,
описываемые префиксы можно еще использовать с переменными цикла for, и
это дает нам способ достичь требуемого результата. Например, вот так
можно получить 8.3-путь к “Program Files”:
for /d %%i in ("%PROGRAMFILES%") do (
set PROGRAMFILESSHORT=%%~si
)
echo 8.3-имя для каталога "%PROGRAMFILES%" -^> "%PROGRAMFILESSHORT%"
|
Этот и другие модификаторы можно использовать и с любой другой формой цикла for, подробнее о которых можно узнать из:
Как перенаправить стандартный вывод в файл?
Плоха та короткая программа, которая не стремится стать большой. К
сожалению, это правило применимо и к командным файлам Windows тоже –
иногда bat-файлы вырастают до довольно больших размеров. Если при этом
результат выполняемых команд должен журналироваться, то все становится
совсем плохо – почти каждая строка имеет хвостик типа
echo Cleaning up the target directory >>%LOGFILE%
...
echo The target directory has been cleaned >>%LOGFILE%
|
Гораздо проще было бы перенаправить стандартный вывод в файл, чтобы
все команды echo и вообще, все, что выводится на экран, автоматически
попадали в журнальный файл. Сделать это можно следующим образом
(рассмотрим на знакомом примере генерации HTML-файла):
@echo off
set OUTPUT=out.html
if "%STDOUT_REDIRECTED%" == "" (
set STDOUT_REDIRECTED=yes
cmd.exe /c %0 %* >%OUTPUT%
exit /b %ERRORLEVEL%
)
echo ^<html^>
echo ^<head^>
echo ^<title^>This is a greeting page^</title^>
echo ^</head^>
echo ^<body^>
echo Hello World!
echo ^</body^>
echo ^</html^>
|
Здесь делается то же, что и раньше, но с перенаправлением
стандартного вывода в файл out.html. Делается это простым способом –
перезапуском сценарием самого себя. Сначала проверяется, не установлена
ли переменная окружения STDOUT_REDIRECTED. Если она установлена,
значит, сценарий уже перезапущен с перенаправленным выводом и можно
просто продолжить работу. Если же переменная не установлена, то мы ее
устанавливаем и перезапускаем скрипт (cmd.exe /c %0) с
таким же набором параметров, что и исходная команда (%*) и
перенаправленным в файл стандартным выводом (>%OUTPUT%). После
завершения выполнения «перенаправленной» команды выходим.
Такое «единовременное» перенаправление имеет и еще один неочевидный
плюс: файл открывается и закрывается только один раз, и всем командам и
дочерним процессам передается дескриптор уже открытого файла.
Во-первых, это чуть-чуть улучшит производительность (жизнь удалась –
сроду бы не подумал, что буду когда-нибудь писать о производительности
в bat-файлах). Во-вторых, это поможет избежать проблемы с
невозможностью открыть файл для записи. Такое может случиться, если
после выполнения одной из команд останется «висеть» какой-нибудь
процесс. Он будет держать дескриптор интересующего нас файла и
перенаправление вывода в этот файл для всех последующих команд
провалится. Проблема может показаться надуманной, но однажды она украла
у меня 2 часа жизни…
Как сложить два числа?
Краткий ответ – смотри:
Длинный ответ таков. В bat-файлах можно производить довольно-таки
продвинутые вычисления – продвинутые не в сравнении с другими языками,
а в сравнении с отсутствием возможности что-либо вычислить вообще.
Вычисление осуществляется командой set, если она выполняется с ключом
/a. Поддерживается практически полный набор операторов языка C, включая
шестнадцатеричный модификатор 0x. Переменные окружения в выражении не
обязательно заключать в знаки процента – все, что не является числом,
считается переменной. Подробнее – все-таки в man set, тьфу, то есть в
set /?. А здесь напоследок – просто несколько примеров.
@echo off
set ARG=1
remset /a RESULT=ARG + 2
echo %RESULT%
rem rem
set /a RESULT="ARG << 2"
echo %RESULT%
rem set /a RESULT=0x1234 + 0x6786
echo %RESULT%
rem
|
А можно создать в bat-файле функцию?
Да, можно. Более того, иногда даже нужно. Правда, функциями это
можно назвать условно. Есть особый синтаксис команды call, который
позволяет перейти на метку в этом же bat-файле с запоминанием места,
откуда был произведен этот вызов:
Возврат из функции производится командой:
exit /b [опциональный код возврата]
|
Ключ /b здесь очень важен: без него будет произведен выход не из функции, а из сценария вообще.
За подробностями обращайтесь к:
Что интересно, команда call с таким синтаксисом поддерживает
рекурсивные вызовы с автоматическим созданием нового фрейма для
переменных аргументов %0-%9. Иногда это может быть полезным. Вот
классический пример рекурсивного подсчета факториала на командном языке:
@echo off
call :factorial %1
echo %RESULT%
exit
remremremremrem:factorial
if %1 == 0 (
set RESULT=1
exit /b
)
if %1 == 1 (
set RESULT=1
exit /b
)
set /a PARAM=%1 - 1
call :factorial %PARAM%
set /a RESULT=%1 * %RESULT%
exit /b
|
Пример работы:
> factorial.bat 10
3628800
|
Как можно избежать использования goto?
Любой хоть сколько-то осмысленный *.bat-файл длиной больше 50 строк является ярким лозунгом в поддержку работы Дейкстры «О вреде оператора goto».
Мешанина из переходов вперед и назад действительно является кодом
«только для записи». Можно ли что-то предпринять по этому поводу?
На самом деле можно. Как правило, большинство меток и переходов
используются для организации ветвлений при проверке условий, т.е.
банальных if-then-else блоков. В оригинале, bat-язык поддерживал только
одну команду в блоке then, что автоматически приводило к идиомам вида:
if condition goto :THEN
remremgoto IF_END
:THEN
remrem:IF_END
|
Но к счастью, командный интерпретатор cmd.exe современных ОС Windows
2000 и старше поддерживает блоки команд в конструкциях ветвления, что
устраняет необходимость применения меток. Блоки команд заключаются в
круглые скобки. Выглядит это так (имитируя C/C++ indentation style):
if condition (
rem rem) else (
rem rem)
|
Конкретный пример использования:
@echo off
set BUILDMODE=%1
if "%BUILDMODE%" == "" (
echo FAIL: Аргумент является обязательным ^(--debug, --release^)
exit /b 1
)
remset BUILDMODE=%BUILDMODE:-=%
if "%BUILDMODE%" == "debug" (
echo INFO: Устанавливаем debug-режим окружения
set CCFLAGS=/Od /MDd /Z7
) else (
echo INFO: Устанавливаем release-режим окружения
set CCFLAGS=/O2 /MD
)
|
На мой взгляд, с этим уже вполне можно жить. Но, как всегда, жизнь
не так проста, как кажется. Есть одна проблема. Переменные,
использующиеся в блоках then и else, раскрываются перед началом
выполнения этих блоков, а не в процессе выполнения. В приведенном
примере это не вызывает никаких проблем, однако в следующем вызовет:
if "%BUILDMODE%" == "debug" (
echo INFO: Устанавливаем debug-режим окружения
set OPTFLAGS=/Od
set CCFLAGS=%OPTFLAGS% /MDd /Z7
) else (
echo INFO: Устанавливаем release-режим окружения
set OPTFLAGS=/O2
set CCFLAGS=%OPTFLAGS% /MD
)
|
Загвоздка в том, что в обоих блоках подстановка переменной OPTFLAGS
произойдет до того, как она будет изменена в процессе выполнения этого
блока. Соответственно, в CCFLAGS будет подставлено то значение, которое
OPTFLAGS имела на момент начала выполнения данного if-блока.
Решается эта проблема путем использования отложенного раскрытия
переменных. Переменные, заключенные в !…! вместо %…%, будут раскрыты в
их значения только в момент непосредственного использования. Данный
режим по умолчанию отключен. Включить его можно либо использованием
ключа /V:ON при вызове cmd.exe, либо использованием команды
setlocal enabledelayedexpansion
|
в тексте самого bat-файла. Второй способ мне представляется более
удобным – не очень здорово требовать от кого-то запуска твоего сценария
с определенным параметром.
С учетом сказанного предыдущий «неправильный» пример может быть исправлен так:
setlocal enabledelayedexpansion
rem
if "%BUILDMODE%" == "debug" (
echo INFO: Setting up debug mode environment
set OPTFLAGS=/Od
set CCFLAGS=!OPTFLAGS! /MDd /Z7
) else (
echo INFO: Setting up release mode environment
set OPTFLAGS=/O2
set CCFLAGS=!OPTFLAGS! /MD
)
|
Вот теперь это почти полноценный if-then-else блок. Почти, потому
что если в одной из команд echo у вас встретится закрывающая круглая
скобка, то вам необходимо заэкранировать ее символом ^, иначе
синтаксический анализатор путается…
Но в любом случае, это гораздо лучше безумного количества меток и переходов.
Как обработать текстовый файл?
Иногда в командном файле необходимо получить доступ к содержимому
некоторого текстового файла и некоторым образом это содержимое
обработать. Например, прочитать файл настроек программы.
Для привнесения еще большей конкретики в процесс изучения зададимся целью прочитать файл с настройками следующего содержания:
buildmode=release
compiler=cl.exe
arch=x86
|
Ничего сверхъестественного – простой key=value формат с возможностью
вставки Unix-style комментариев. Помочь в чтении и обработке этого
файла нам сможет команда for. Ее дополнительные опции позволяют задать
и разделители, и символ начала комментария, и кое-что еще. Вот
командный файл, который выполняет поставленную задачу:
@echo off
remremremcall :read_settings %~dp0\settings.txt || exit /b 1
remecho Build mode : %BUILDMODE%
echo Compiler : %COMPILER%
echo Architecture: %ARCH%
remexit /b 0
remremremrem:read_settings
set SETTINGSFILE=%1
remif not exist %SETTINGSFILE% (
echo FAIL: Файл с настройками отсутствует
exit /b 1
)
remremremremremremremremremrem
for /f "eol=# delims== tokens=1,2" %%i in (%SETTINGSFILE%) do (
rem rem rem set %%i=%%j
)
exit /b 0
|
Обильные комментарии должны помочь легко разобраться, что к чему. За подробностями, как обычно, отошлю к:
Кстати, возможности команды for не ограничиваются чтением из файла. Возможно также чтение вывода другой команды. Например, так:
@echo off
for /f "tokens=* usebackq" %%i in (`cmd.exe /c ver`) do (
set VERSION=%%i
)
echo %VERSION%
|
Особенно меня умиляет наличие опции “usebackq”, которая делает
синтаксис отдаленно похожим на юниксовый. И в стенах царства Билла есть
граждане, скучающие по /bin/sh и пытающиеся хоть как-то скрасить
существование свое и окружающих. Следующий совет это также косвенно
подтверждает.
Что это за упомянутые ранее операторы объединения команд?
Это операторы &, && и ||. Они практически совсем не
освещены в документации, но полезны в повседневности. Они позволяют
объединять несколько команд в одну, т.е. примерно так:
command1 & command2
command1 && command2
command1 || command2
|
Форма этих операторов весьма соответствует их содержанию. В случае,
пожалуй, наименее полезного оператора & вторая команда будет просто
выполнена после первой, т.е. это равносильно простой записи:
Оператор && гарантирует, что вторая команда будет выполнена
только, если первая была выполнена успешно, т.е. с нулевым кодом
возврата (он же %errorlevel%). Такие конструкции очень популярны в мире
shell-сценариев Unix. Например:
Я был приятно удивлен, узнав, что cmd.exe тоже умеет выполнять такие
конструкции. Это безопаснее и правильнее, нежели простое
последовательное выполнение этих команд, и короче и проще, чем строгая
проверка и обработка кодов возврата. Очень удобно при написании на
скорую руку. Не менее полезен иногда и оператор ||. Суть его тоже
логична – выполнить вторую команду, если первая дала сбой. Часто
встречается в таких идиомах:
Если перейти в каталог sources не удастся, то будет произведен выход
с кодом ошибки 1. Если же первая команда отработает нормально, то
вторая выполнена не будет. Например, такая простейшая защита помогла бы
в случае с cd по UNC-адресу, описанному ранее.
Можно ли написать на bat-языке серьезную программу?
Пожалуй, нет. Серьезная программа должна все-таки выглядеть
серьезно. А все написанное на командном языке Windows таковым назвать
можно лишь с о-о-о-чень большой натяжкой. Так что для решения более
сложных задач автоматизации лучше все-таки взять что-нибудь более
функциональное:
- Perl
- Python
- Ruby
- JScript / VBScript
Последние, кстати, присутствуют в Windows 2000/XP по умолчанию (с
некоторыми функциональными различиями) и в целом могут считаться
заменой *.bat языку. Однако сдается мне, что *.bat-файлы проживут еще
очень долго.
Дай Бог, чтобы я ошибся…
Материалы по теме
- Windows Batch Files – Commands Reference.
- Windows 2000. Команды: Карманный справочник. Э. Фриш. – М.: Мир, 2003.
|