Упрощаем доступ к данным, выводимым из командной строки

Заметим, что работать с утилитами командной строки из сценариев WSH довольно непросто. Конечно, имеющийся в WSH объект WshShell позволяет запускать внешние программы, предоставляя для этого метод Run, однако при использовании данного подхода мы сталкиваемся с двумя проблемами. Первая из них связана с тем, что многие из утилит командной строки являются зависимыми от консоли, что может привести к их непредсказуемому поведению, если они запускаются не через тот командный процессор, который требуется. Вторая проблема еще серьезней. Дело в том, что метод Run не предоставляет никаких возможностей для вывода данных. И хотя с появлением WSH версии 5.6 в объект WshShell был добавлен метод Exec, который может возвращать данные из командной строки, он тоже имеет ряд ограничений. Прежде всего, данный метод довольно сложен в использовании, поскольку при этом вы должны обеспечивать мониторинг потоков. К тому же, если в качестве сервера сценариев используется WScript, метод Exec будет выводить видимое окно консоли, причем оно будет появляться при каждом запуске внешней команды через этот метод. Еще одно ограничение связано с тем, что компонент WSH версии 5.6 может быть установлен не везде.

Для решения этих проблем без необходимости повторного написания кода при каждом вызове консольной утилиты из WSH я использую так называемый упаковщик утилит командной строки. Благодаря данному инструменту работа с утилитами командной строки существенно упрощается. Он позволяет перехватывать выходные данные от выполняемых команд и не требует вывода видимого окна. При этом работает данный упаковщик на всех современный версиях Windows даже при отсутствии компонента WSH версии 5.6.

Данный упаковщик может использоваться в любых сценариях WSH. Он написан на VBScript, но также может применяться и в сценариях JScript, например через файл .wsf. Теперь давайте подробно рассмотрим, как работает этот инструмент: как он вызывает "правильный" командный процессор, запускает утилиту, перехватывает ее выходные данные и передает их в WSH.

Вызов корректного командного процессора

Как отмечалось выше, утилиты командной строки могут повести себя непредсказуемо, если они запущены через неподходящий командный процессор. Обычно в Windows XP, Windows 2000 и Windows NT 4.0 используется cmd.exe - полнофункциональный 32-разрядный интерфейс командной строки. Что же касается Windows Me и Windows 9x, то здесь мы работаем с command.com. Для того чтобы гарантировать, что в сценарии будет задействован корректный командный процессор, упаковщик считывает имя процессора, используемого по умолчанию, из системной переменной среды %COMSPEC%.

По завершении обработки команды необходимо, чтобы и процессор также завершил свою работу. В cmd.exe, command.com и всех остальных стандартных процессорах команд для этих целей обычно используется ключ /c. Приведенная ниже строка кода используется в упаковщике для запуска на локальном компьютере "правильного" окна командной строки, которое автоматически закроется по завершении работы вызванной утилиты:

%COMSPEC% /c cmd

Здесь cmd - команда запуска соответствующей утилиты. Если требуется запустить, скажем, утилиту ipconfig, команда запуска будет выглядеть следующим образом:

%COMSPEC% /c ipconfig

Программа ipconfig является одной из наиболее часто используемых в сценариях WSH утилит командной строки. Использование для получения IP-адреса компьютера других средств, таких как WMI, обычно приводит к необходимости написания нескольких строк кода в силу того, что в системе может быть несколько экземпляров адресов, шлюзов, масок подсетей и других важных параметров. Что же касается инструмента Ipconfig, который имеется в Windows XP, Windows 2000, Windows NT4.0, Windows ME и Windows 98 (и даже в Windows 95 при наличии установленного пакета Microsoft Windows 95 Resource Kit), то с его помощью эти данные можно получить за один шаг.

Запуск утилиты

Итак, мы заменили код запуска и закрытия командного процессора. Теперь нужно добавить инструкцию, предписывающую исполнительному механизму VBScript запустить соответствующую утилиту командной строки. Для этого можно задействовать метод Run объекта WshShell, используя следующий синтаксис:

object.Run(strCommand,
[intWindowStyle],
[bWaitOnReturn])

где strCommand соответствует команде запуска (в терминах упаковщика это команда, запускающая утилиту командной строки). Аргумент intWindowStyle определяет, какой тип окна (если таковые имеются) должен использоваться для отображения информации в процессе работы команды. В упаковщике для intWindowStyle используется значение 0, что предписывает скрыть окно. Аргумент bWaitOnReturn указывает, должен ли сценарий ожидать завершения работы запущенной команды, прежде чем продолжить выполнение. Для bWaitOnReturn в упаковщике используется значение True, соответственно, сценарий будет приостановлен до момента завершения работы команды. Таким образом, теперь наш упаковщик стал выглядеть так:

cmd = "ipconfig"
Set sh = CreateObject _("WScript.Shell")
sh.Run "%COMSPEC% /c cmd",
0, true

Перехват выводимых данных из командной строки

Теперь добавим код для перехвата выходного потока данных запущенной утилиты. Организовать захват и перенаправление данных стандартного выходного потока довольно просто. Если вы умеете создавать командные файлы или имеете опыт работы с утилитами из командной строки, то вы, вероятно, знакомы с символом ">", с помощью которого можно организовать перенаправление выходных потоков команды в файл. Однако в случае организации вывода данных от команд мы не можем просто перенаправить вывод в стандартный файл. Рассмотрим, что будет происходить в том случае, когда команда выводит данные в виде, отличном от стандартного выходного потока. Если командный процесс не может найти утилиту командной строки или в процессе ее запуска возникали проблемы, обычно утилита посылает сообщение об ошибке через канал, называемый потоком вывода ошибок команды. Если не предусмотреть перенаправление данного потока в файл, сообщение об ошибке будет утеряно.

При использовании символа перенаправления ">" можно цифрами обозначать соответствующий стандартный поток вывода данных и поток вывода ошибок. Если перед символом ">" стоит цифра 1, это указывает на поток вывода данных, если же используется цифра 2, это указывает на поток вывода ошибок. В том случае если цифра перед символом ">" отсутствует, то по умолчанию используется перенаправление для стандартного потока данных. Можно рекомендовать перенаправление обоих потоков (как данных, так и ошибок) во временные файлы, поскольку это позволяет защитить другие файлы от случайной перезаписи, и в то же время вы всегда сможете удалить временные файлы, когда они перестанут представлять для вас интерес. Что касается нашего упаковщика, то в нем реализовано перенаправление стандартного потока данных во временный файл с именем out.tmp, а всех сообщений об ошибках - в файл err.tmp. Соответствующий код приведен ниже:

%COMSPEC% /c cmd
2>err.tmp 1>out.tmp

В результате синтаксис вызова Run будет выглядеть следующим образом:

cmd = "ipconfig"
Set sh = _ CreateObject("WScript.Shell")
sh.Run "%COMSPEC% /c" & _
cmd & "2>err.tmp 1>out.tmp",
0, true

Данный код будет прекрасно работать в тех случаях, когда упаковщик используется только в одном сценарии. Если же вы планируете работать с упаковщиком из нескольких одновременно запущенных сценариев, то использование этого кода в показанном выше виде приведет к проблемам, поскольку для хранения выходных потоков команды будут использоваться одни и те же два файла - err.tmp и out.tmp. Для решения этой проблемы можно воспользоваться методом GetTempName объекта FileSystemObject, с помощью которого имена временных файлов генерируются случайным образом. Объект FileSystemObject входит в состав библиотеки Scripting Runtime Library. Сформированные таким образом имена будут начинаться с символов rad, за которыми следуют 5 выбранных случайным образом символов, которые могут принимать значения от 0 до 9 или от A до F (т.е. шестнадцатеричные цифры). В качестве примеров имен этих файлов можно привести rad35D8F.tmp или rad1986E.tmp. При создании нового имени файла метод GetTempName не выполняет проверку существования файла с таким именем. Однако диапазон возможных имен файлов при подобном алгоритме их построения составляет примерно миллион значений, поэтому вряд ли вы столкнетесь с проблемой дублирования имен. Если же это все-таки произошло, то причиной почти всегда является наличие сгенерированного в свое время сценарием временного файла, который не был корректно удален.

Для того чтобы использовать метод GetTempName, нужно задействовать раздел кода в упаковщике, создающий экземпляр объекта FileSystemObject, а также код, который вызывает данный метод. При добавлении этих разделов кода упаковщик приобретает вид, показанный в Листинге 1.

Передача потоков вывода в WSH

После того как выходной поток был перенаправлен во временный файл, нужно передать эти данные в текстовый файл, который затем может быть считан средствами WSH. Это делается с помощью приведенной в Листинге 2 функции ProcessFile. Данная функция выполняет и еще кое-какую работу, а именно обеспечивает предотвращение возможных ошибок и выполняет некоторые служебные действия. В частности, прежде чем считывать данные из временных файлов, функция выполняет проверку их существования. Дело в том, что в редких случаях может возникать ситуация, когда временный файл теряется по причине блокирования потока вывода. Соответственно, при попытке чтения из несуществующего файла будет выдано сообщение об ошибке. Аналогично приводит к возникновению ошибки и попытка чтения из пустого файла. Причем мы не можем определить, является ли файл пустым, исходя только из его размера, поскольку даже пустой файл в формате Unicode может содержать 2 байта данных. Поэтому в рассматриваемой функции, прежде чем применять к временному файлу операцию чтения, также проверяется, не является ли этот файл пустым.

Функция ProcessFile также выполняет анализ формата временных файлов (ANSI или Unicode). Формат этих файлов может различаться в зависимости от нюансов настройки конфигурации систем. Поэтому для предотвращения появления мусора функция считывает данные в соответствующем формате. И, наконец, данная функция выполняет работы по очистке, удаляя ненужные временные файлы.

Чтобы упростить работу с упаковщиком командной строки, я создал специальный класс с именем CliWrapper, в котором объединил разделы кода из Листинга 1 и Листинга 2. Данный класс показан на Листинге 3.

Как работать с CliWrapper

Для того чтобы можно было использовать класс CliWrapper, нужно добавить в глобальный код сценария (в произвольном месте) фрагмент кода из Листинга 3, обозначенный меткой B. Я предпочитаю размещать этот фрагмент в конце сценария, поскольку это позволяет акцентировать внимание на коде сценария, а не на деталях реализации класса. После этого в начало сценария должен быть помещен код, обозначенный в Листинге 3 меткой A. После того как были объявлены утверждение Option Explicit и переменная console, из рассматриваемого класса создается объект console. Далее осуществляется запуск требуемой утилиты командной строки и отображение результатов.

В рассматриваемом примере кода, обозначенного меткой A, запускается программа Ipconfig. Чтобы организовать запуск других утилит командной строки из данного сценария, нужно просто отредактировать соответствующий аргумент метода Exec. Например, если необходимо с помощью команды Dir отыскать все файлы с расширением .ini, находящиеся в каталоге Windows и во всех его подкаталогах, то в этом случае нужно заменить оператор console.exec("ipconfig | Find ""IP Address""") на console.exec("dir %windir%*.ini /s /b"). При формировании команд запуска для различных утилит придерживайтесь приведенных ниже рекомендаций.

Следите за двойными кавычками. Неправильное использование двойных кавычек - одна из наиболее типичных ошибок при написании кода на VBScript. Дело в том, что исполнительный механизм VBScript интерпретирует двойные кавычки как окончание строки, поэтому если двойные кавычки используются для каких-то других целей (скажем, для указания пути, в описании которого содержатся пробелы), следует использовать этот символ дважды. В таком случае VBScript будет интерпретировать одну из кавычек как соответствующий символ. Таким образом, если нужно найти таблицы Microsoft Excel, размещенные, например, в папке E:My Documents, то соответствующий аргумент для метода Exec должен указываться в виде console.exec("dir ""E:My Documents*.xls"" /s /b /a-d"). Более подробно вопросы использования двойных кавычек рассматриваются в работе "Rem: Understanding Quotation Marks in VBScript," декабрь 2002, http://www.winscriptingsolutions.com, InstantDoc ID 26975.

Предусматривайте фильтрацию выводимых командой данных. В некоторых случаях утилиты командной строки предоставляют существенно больше информации, чем это необходимо. В этой ситуации наиболее простым решением будет передача соответствующих данных в команду Find, в результате выполнения которой будут возвращены только те строки, которые содержат заданную вами строку. В команде Find строка поиска должна быть заключена в двойные кавычки, следовательно, как обсуждалось выше, при задании строкового значения необходимо использовать два символа двойных кавычек. Во фрагменте кода, обозначенного меткой A на Листинге 3, показан пример передачи потока вывода данных утилиты Ipconfig в команду Find, которая затем выполняет поиск строки IP Address. В данном случае команда Find используется с ключом /i, для того чтобы в ходе поиска игнорировался регистр.

Кроме команды Find также можно использовать имеющиеся в VBScript функции для работы со строками. К ним относятся такие функции как Left, Right, Mid, Split и Join, которые могут оказаться исключительно полезными при обработке консольных данных.

Справочная система (Help). Практически каждая уважающая себя утилита командной строки имеет ключ для вызова встроенной справочной системы - чаще всего это /? или /h, с помощью которого можно получить доступ к необходимой справочной информации. Помимо этого в системах Windows XP и Windows 2000 можно вызвать Help по работе с командной строкой. Для этого в окне командной строки нужно запустить следующую команду:

HH ntcmds.chm

В добавление к сказанному выше следует отметить, что класс CliWrapper содержит в себе пару полезных механизмов. В нем имеется свойство Command, используя которое можно просматривать команду в том же виде, в котором ее видит Windows. В тех случаях, когда что-либо не работает должным образом, можно просмотреть, как в точности выглядит запускаемая команда. Для этого используется следующий оператор:

WScript. Echo console.command

Если необходимо организовать вывод ошибок сценария в виде эха, используйте приведенную ниже команду:

WScript. Echo console.stderr

И последнее. Если требуется повторно задействовать объект console, нужно просто вызвать метод console.exec с новой строкой запуска команды. При этом нет необходимости добавлять еще один оператор New, поскольку объект console при каждом вызове автоматически стирает выведенные ранее данные.

Правильный инструмент для работы

?С помощью класса CliWrapper можно существенно упростить процедуру получения данных от консольных приложений, поскольку при этом от пользователя скрываются все сложности деталей реализации. Однако мы не затронули один важный вопрос: а правильно ли то, что эти процедуры скрыты? Очевидно, что разработчики WSH рассуждали именно так, поскольку в WSH версии 5.6 появившийся у объекта WScript.Shell метод Exec обеспечивает некоторую поддержку недостающей функциональности.

Утилиты командной строки имеют массу достоинств. Во-первых, это эффективный и проверенный инструмент. Накладные расходы, связанные с использованием оболочки командной строки, крайне невелики, а утилиты командной строки обычно используют только малую часть несложных вызовов API. А поскольку при работе с утилитами через упаковщик нет необходимости в механизмах отображения и прокрутки данных, то выполняются они быстрее, чем в стандартном режиме.

Во-вторых, эти утилиты весьма долговечны. Многие из них используются на протяжении почти 20 лет, соответственно, за это время практически все недочеты были выявлены и устранены. Вообще говоря, к задачам сценариев администрирования относятся принятие прямых решений и фильтрация, а получаемые при этом данные должны выбираться из наиболее значимых параметров системы. В то же время большинство COM-объектов, с которыми можно работать в сценариях, разрабатывается, в первую очередь, для использования программистами и поэтому часто требует написания большого объема кода, для того чтобы заставить их работать так, как нужно.

Разумеется, прежде чем принимать решение о целесообразности использования той или иной утилиты командной строки в сценариях WSH, следует сначала ознакомиться с работой этой утилиты. А после того как это будет сделано, вы сами убедитесь, что во многих случаях утилиты командной строки оказываются наилучшим инструментом для решения поставленной задачи.


Листинг 1. Код для вызова командного процессора и запуска утилиты
cmd = "ipconfig"
Set sh = CreateObject("WScript.Shell")
Set fso = CreateObject("Scripting.FileSystemObject")
outfile = fso.GetTempName
errfile = fso.GetTempName
sh.Run "%COMSPEC% /c " & cmd & " 2>" & errfile & _
	" 1>" & outfile, 0, true

Листинг 2. Функция ProcessFile
Function ProcessFile(filepath)
НАЧАЛО КОММЕНТАРИЯ LINE
	' Перед открытием файла проверяем, существует ли он
КОНЕЦ КОММЕНТАРИЯ LINE
	If fso.FileExists(filepath) Then
НАЧАЛО КОММЕНТАРИЯ LINE
		' Открываем для чтения (1), не создаем (false),
		' и используем по умолчанию режим ANSI/Unicode (-2).
КОНЕЦ КОММЕНТАРИЯ LINE
		With fso.OpenTextFile(filepath, 1, false, -2)
НАЧАЛО КОММЕНТАРИЯ LINE
			' Если при открытии файла не был сразу достигнут его конец
			'(что означает, что файл пустой), считываем все его содержимое.
			' Это позволяет корректно обнаруживать пустые файлы Unicode.
КОНЕЦ КОММЕНТАРИЯ LINE
			If .AtКонецOfStream <> true Then ProcessFile = .ReadAll
НАЧАЛО КОММЕНТАРИЯ LINE
			' Выполняем очистку - закрываем файл и удаляем его.
КОНЕЦ КОММЕНТАРИЯ LINE
			.Close
			fso.DeleteFile(filepath)
		Конец With
	Конец If	
Конец Function

Листинг 3. CliWrapper.vbs
?НАЧАЛО МЕТКИ A
Option Explicit
Dim console
Set console = new cliwrapper
WScript. Echo console.exec("ipconfig | Find ""IP Address""")
? КОНЕЦ МЕТКИ A
? НАЧАЛО МЕТКИ B
Class CliWrapper
	public stdout, stderr, command
	private sh, fso, syncmode, outfile, errfile
	private ForReading, TristateUseDefault, DoNotCreateFile
	
	Public Function Exec(sCmd)
НАЧАЛО КОММЕНТАРИЯ LINE
		' Очистка старых файлов stdout и stderr.
КОНЕЦ КОММЕНТАРИЯ LINE
		stdout = vbNullString: stderr = vbNullString
НАЧАЛО КОММЕНТАРИЯ LINE
		'В целях отладки сохраняем значение sCmd
КОНЕЦ КОММЕНТАРИЯ LINE
		command = sh.ExpandEnvironmentStrings(sCmd)
		sh.Run "%COMSPEC% /c " & sCmd & " 2>" & errfile _
			& " 1>" & outfile, 0, syncmode
		stderr = ProcessFile(errfile)
		stdout = ProcessFile(outfile)
		Exec = stdout
	Конец Function
	
	Private Sub class_initialize
		Set sh = CreateObject("WScript.Shell")
		Set fso = CreateObject("Scripting.FileSystemObject")
		ForReading = 1:	TristateUseDefault = -2
		DoNotCreateFile = false
		outfile = fso.GetTempName
		errfile = fso.GetTempName
		syncmode = true
	Конец Sub
	Private Function ProcessFile(filepath)
НАЧАЛО КОММЕНТАРИЯ LINE
		' Находим файл по данному пути и считываем его содержимое
КОНЕЦ КОММЕНТАРИЯ LINE
		If fso.FileExists(filepath) Then
			With fso.OpenTextFile(filepath, ForReading, _
				false, TristateUseDefault)
				If .AtКонецOfStream <> true Then
					ProcessFile = .ReadAll
				Конец If
				.Close
				fso.DeleteFile(filepath)
			Конец With
		Конец If	
	Конец Function
Конец Class
? КОНЕЦ МЕТКИ B