1 (изменено: Rumata, 2011-06-20 05:39:49)

Тема: JScript: расширенная функция поиска и фильтрации файлов по шаблону

Выношу на обсуждение коллег функцию поиска файлов.

Функция понимает следующие параметры, задаваемые в виде объекта-списка ключей:
-- path - строка, определяет каталог, где поиск поиска
-- pattern - строка/массив строк шаблонов поиска
-- included - строка/массив строк шаблонов для фильтрации требуемых файлов
-- excluded - строка/массив строк шаблонов для "отбраковки" ненужных файлов
-- fiolders - логическое, задает поиск каталогов вместо файлов
-- recursive -- логическое, задает поиск в глубину для всех подкатлогов данного каталога
-- filter - функция, которая вызывается для более тонкой фильтрации файлов. Функция принимает имя файла и возвращает true если файл следует сохранить в результирующем файле, иначе - false
-- codepage - строка, при необходимости задает другую кодовую страницу для команды DOS. Идея заимствована из поста #12 коллеги wisgest в теме JS: Быстрый поиск файлов с заданными расширениями

Все параметры необязательны. При вызове функции без параметров по умолчанию производится поиск всех файлов в текущем каталоге.

Функция поиска реализована в виде статичного метода объекта FileSystem. При реализации функции использовались некоторые идеи из вики-статьи http://en.wikipedia.org/wiki/Glob_(programming).



var FileSystem = FileSystem || {};

FileSystem.find = function(options)
{
	options = options || {};

	// Normalize a path
	var path = (new ActiveXObject('Scripting.FileSystemObject')).GetAbsolutePathName(options.path);
	if ( path.slice(-2) != ':\\' ) {
		path += '\\';
	}

	// Set auxiliary parameters:
	// f - find folders only
	var f;
	if ( options.folders ) {
		f = ' /ad ';
	} else {
		f = ' /a-d ';
	}

	// p - path to an object of FS
	// s - recursive search
	// b - left border of a pattern
	var p;
	var s;
	var b;
	if ( options.recursive ) {
		p = '';
		s = ' /s ';
		b = ' \\\\';
	} else {
		p = path;
		s = '';
		b = ' ^';
	}

	// Construct the shell command that would be executed
	var cmd = '';
	if ( options.codepage ) {
		cmd += 'cmd /c chcp ' + options.codepage + ' && ';
	}
	cmd += 'cmd /c dir /b ' + f + s + '"' + path + [].concat(options.pattern || '*').join('" "' + path) + '"';
	if ( options.included ) {
		cmd += ' | findstr /i /e "' + b + FileSystem.wildcard2regex(options.included, true, true).join(b) + '"';
	}
	if ( options.excluded ) {
		cmd += ' | findstr /v /i /e "' + b + FileSystem.wildcard2regex(options.excluded, true, true).join(b) + '"';
	}

	// Storing the command for debugging reasons
	arguments.callee.cmd = cmd;

	// Perform the shell command ...
	var sh = new ActiveXObject('WScript.Shell');
	var ex = sh.Exec(cmd);

	// ... and collect each string from the output to the resulting array
	var result = [];
	while ( ex.Status == 0 ) {
		if ( ex.StdOut.AtEndOfStream ) {
			break;
		}
		result.push(p + ex.StdOut.ReadLine());
	}
	while ( ! ex.StdOut.AtEndOfStream ) {
		result.push(p + ex.StdOut.ReadLine());
	}

	if ( typeof options.filter != 'function' ) {
		return result;
	}

	// Perform the filtering function
	var filtered = [];
	for (var i = 0; i < result.length; i++) {
		if ( options.filter(result[i], i, result) ) {
			filtered.push(result[i]);
		}
	}
	return filtered;
};

FileSystem.wildcard2regex = function(wildcard, skipRegexp, strictly)
{
	var convert = arguments.callee[strictly ? 'strictly' : 'std'];

	var result = [];
	if ( Object.prototype.toString.call(wildcard) == '[object String]' ) {
		result.push(convert(wildcard));
	} else {
		for (var i = 0; i < wildcard.length; i++) {
			result.push(convert(wildcard[i]));
		}
	}

	return skipRegexp 
		? result 
		: new RegExp('^(' + result.join('|') + ')$', 'i');
};

FileSystem.wildcard2regex.strictly = function(wildcard)
{
	var result = wildcard
		.replace(/([\^\$\\\/\|\.\+\!\[\]\(\)\{\}])/g, '\\$1')
		.replace(/\?/g, '[^\\\\]?')
		.replace(/\*/g, '[^\\\\]*')
		;
	return result;
};

FileSystem.wildcard2regex.std = function(wildcard)
{
	var result = wildcard
		.replace(/([\^\$\\\/\|\.\+\!\[\]\(\)\{\}])/g, '\\$1')
		.replace(/\?/g, '.?')
		.replace(/\*/g, '.*?')
		;
	return result;
};

.
Примеры использования.

1. Поиск всех файлов в текущем каталоге


var f = FileSystem.find();

.
2. Поиск заданных файлов в указанном каталоге
вариант 1


var options = {
	path: 'C:\\Windows\\System32', 

	pattern: 'z*'
};

var f = FileSystem.find(options);

.
вариант 2


var options = {
	path: 'C:\\Windows\\System32', 

	pattern: '*', 
	included: 'z*'
};

var f = FileSystem.find(options);

.
вариант 3


var options = {
	path: 'C:\\Windows\\System32', 

	filter: function(filename) { return filename.match(/z[^\\]*]$/i); }
};

var f = FileSystem.find(options);

.
3. Поиск всех подкаталогов в указанном каталоге и фильтрация с помощью функции


var options = {
	path: 'C:\\Windows\\System32', 

	folders: true, 
	recursive: true, 

	filter: function(filename) { return filename.match(/z[^\\]*$/i); }
};

var f = FileSystem.find(options);

.
Объемное послесловие
В WSH для получения списка файлов/каталогов по заданному шаблону (или нескольким шаблонам) можно воспользоваться одним из трех способов:
1. ActiveX - например, JScript: Поиск файлов/каталогов по шаблону
2. WQL-запрос к классу CIM_DataFile (подробности здесь http://www.script-coding.com/WMI_FileSystem.html)
3. чтение стандартного вывода внешней команды, например, способ коллеги JSman - JScript: поиск файлов по расширению

Из всех трех способов третий обладает следующими преимуществами перед остальными:
1. наибольшая скорость исполнения
2. достаточно гибкая фильтрация необходимых данных по шаблону
3. лаконичность алгоритма
4. простота использования при получении списка объектов любой глубины вложенности

Тем не менее, этот способ имеет несколько существенных недостатков:
1. ограничение буфера вывода в 4 КБ. Это проблема решается с помощью метода, описанного в теме JScript: Проблема с WshScriptExec.Exec, пост №#22, "расширенный" алгоритм
2. алгоритм основан на чтении вывода встроенной команды DIR, который отличается при указании опции /S - поиск файлов из указанного каталога и всех его подкаталогов (поиск в глубину). Проблема заключается в том, что команда DIR /B /S возвращает список файлов, включая путь до каждого из них, в то время как DIR /B - только имена файлов без пути к ним. Для "нормализации" списка требуется дополнительное действие по добавлению префикса (путь к файлу) во втором случае. Но решая проблему 1 можно решить и проблему 2.
3. Для фильтрации определенной группы файлов используется внешняя команда FINDSTR, которая отфильтровывает требуемые строки входного потока. Однако, принимая во внимание проблему 2, необходимо учитывать различия вывода команды DIR в обоих случаях.
4. Видимо может возникнуть проблема читабельности полученного текста в случае использования другой кодовой страницы (см. описание параметра codepage в начале поста)

В предложенном алгоритме все указанные проблемы были учтены и решены. Текущая реализация не использует сторонние либо внешние компоненты, классы, библиотеки, однако использует удобную (на мой взгляд) функцию конвертации символов-джокеров (wildcards * и ?) в правильное регулярное выражение. Суть этой вспомогательной функции в том, что она принимает на входе строку/массив строк, содержащие джокеры и возвращает правильное регулярное выражение, например:

var re = FileSystem.wildcard2regex(['*.js', '*.vbs']); // по умолчанию вернет /^(.*?\.js|.*?\.vbs)$/i
( 2 * b ) || ! ( 2 * b )

2

Re: JScript: расширенная функция поиска и фильтрации файлов по шаблону

ограничение буфера вывода в 4 КБ

При сохранении вывода в файл ограничения не заметил.

Вообще, говоря о коде, представленном выше, скажу, что у меня лично идеология другая в настоящий момент. Не устраивает возврат массива строк. Так, недавно работав над одним проектом, где много строчек кода, у меня начало появляться сообщение, что Wscript не хватает памяти (раньше не встречал такого:-) ). Слишком много памяти было захламлено массивом строк. Я подумал, что просто хранить такую кучу в оперативке не имеет смысла, так как мне нужен каждый элемент массива, но только в подходящее время и для определенной цели. Решением проблемы явилось использование замыканий:

function Callback(f)
{
    if (/abc/.test(f)) OperateWithFile(f);
}

FindFiles(What, isDeep, Callback);

Более лаконично все-таки, не правда ли?

3 (изменено: Rumata, 2011-06-29 12:49:36)

Re: JScript: расширенная функция поиска и фильтрации файлов по шаблону

JSman
Совершенно верно - ограничение в 4КБ наблюдается только при работе с потоком. Callback предусмотрен в коде данной функции. Возможно, для большей гибкости, имеет смысл модифицировать код так чтобы предложенная функция поиска возвращала массив или обрабатывала "каждый элемент массива, но только в подходящее время и для определенной цели" без сохранения в массив.

JSman пишет:

Более лаконично все-таки, не правда ли?

"Лаконично" - вызов основной функции? Да. Но это хорошо до тех пор пока количество аргументов не более 3-4. При большем количестве передаваемых параметров, передаваемых через отдельные аргументы функции, теряется лаконичность.

( 2 * b ) || ! ( 2 * b )

4

Re: JScript: расширенная функция поиска и фильтрации файлов по шаблону

4. Видимо может возникнуть проблема читабельности полученного текста в случае использования другой кодовой страницы (см. описание параметра codepage в начале поста)

Кстати, кроме 1251 можно использовать 65001 (utf)

5

Re: JScript: расширенная функция поиска и фильтрации файлов по шаблону

JSman пишет:

... нужен каждый элемент массива, но только в подходящее время и для определенной цели...

Очень ценное замечание. Действительно, если каждый элемент необходим только здесь и сейчас, то незачем держать память под массив. Модифицировал код с учетом это. Также научил обрабатывать несколько путей поиска. Далее - описание и код функции.

Функция понимает следующие параметры, задаваемые в виде объекта-списка ключей:
-- path - строка/массив строк определяет каталоги, в которых будет произведен поиск
-- pattern - строка/массив строк определяет шаблоны поиска
-- included - строка/массив строк определяет шаблоны для фильтрации требуемых файлов/каталогов
-- excluded - строка/массив строк определяет шаблоны для "отбраковки" ненужный файлов/каталогов
-- filter - функция, вызываемая для настраиваемой фильтрации файлов/каталогов. Функция принимает имя файла и возвращает true если файл следует сохранить в результирующем файле, иначе - false
-- each - функция используется для выполнения заданных действий для каждого найденного объекта.
-- folders - логическое, задает поиск каталогов вместо файлов
-- recursive -- логическое, задает поиск в глубину для всех подкаталогов данного каталога
-- codepage - строка, при необходимости задает другую кодовую страницу для команды DOS 'CHCP'. Идея заимствована из поста #12 коллеги wisgest в теме JS: Быстрый поиск файлов с заданными расширениями

Все параметры необязательны. При вызове функции без параметров по умолчанию производится поиск всех файлов в текущем каталоге. По умолчанию функция возвращает массив найденных файлов/каталогов. Если будет указан параметр each, то будет вызвана эта функция, заданная параметров each для каждого найденного элемента. А функция FileSystem.find вернет количество найденных элементов. При реализации функции использовались некоторые идеи из вики-статьи http://en.wikipedia.org/wiki/Glob_(programming).


var FileSystem = FileSystem || {};

FileSystem.find = function(options)
{
	// Process a list of paths
	if ( Object.prototype.toString.call(options.path) == '[object Array]' ) {
		var opts = {};
		for (var p in options) {
			if ( ! options.hasOwnProperty(p) ) {
				continue;
			}
			opts[p] = options[p];
		}

		// Prepare the storage of commands for debugging reasons
		arguments.callee.cmd = [];

		// Collect all results for each path
		var result;

		var each;
		if ( typeof options.each == 'function' ) {
			result = 0;
			each = function(v)
			{
				result += v;
			};
		} else {
			result = [];
			each = function(v)
			{
				result = result.concat(v);
			};
		}

		for (var i = 0; i < options.path.length; i++) {
			opts.path = options.path[i];
			each(arguments.callee(opts));
		}
		return result;
	}

	options = options || {};

	// Normalize a path
	var path = (new ActiveXObject('Scripting.FileSystemObject')).GetAbsolutePathName(options.path);
	if ( path.slice(-2) != ':\\' ) {
		path += '\\';
	}

	// Set auxiliary parameters:
	// f - find folders only
	var f;
	if ( options.folders ) {
		f = ' /ad ';
	} else {
		f = ' /a-d ';
	}

	// p - path to an object of FS
	// s - recursive search
	// b - left border of a pattern
	var p;
	var s;
	var b;
	if ( options.recursive ) {
		p = '';
		s = ' /s ';
		b = ' \\\\';
	} else {
		p = path;
		s = '';
		b = ' ^';
	}

	// Construct the shell command that would be executed
	var cmd = '';
	if ( options.codepage ) {
		cmd += 'cmd /c chcp ' + options.codepage + ' && ';
	}
	cmd += 'cmd /c dir /b ' + f + s + '"' + path + [].concat(options.pattern || '*').join('" "' + path) + '"';
	if ( options.included ) {
		cmd += ' | findstr /i /e "' + b + FileSystem.wildcard2regex(options.included, true, true).join(b) + '"';
	}
	if ( options.excluded ) {
		cmd += ' | findstr /v /i /e "' + b + FileSystem.wildcard2regex(options.excluded, true, true).join(b) + '"';
	}

	// Store the command for debugging reasons
	if ( Object.prototype.toString.call(arguments.callee.cmd) == '[object Array]' ) {
		arguments.callee.cmd.push(cmd);
	} else {
		arguments.callee.cmd = cmd;
	}

	// Perform the shell command ...
	var sh = new ActiveXObject('WScript.Shell');
	var ex = sh.Exec(cmd);

	// ... and collect each string from the output to the resulting array
	var result;

	var each;
	if ( typeof options.each == 'function' ) {
		result = 0;
		each = function(v)
		{
			result++;
			options.each(v);
		};
	}else {
		result = [];
		each = function(v)
		{
			result.push(v);
		};
	}

	var filter;
	if ( typeof options.filter == 'function' ) {
		filter = function(v)
		{
			if ( options.filter(v, options) ) {
				each(v);
			}
		};
	} else {
		filter = each;
	}

	while ( ex.Status == 0 ) {
		if ( ex.StdOut.AtEndOfStream ) {
			break;
		}
		filter(p + ex.StdOut.ReadLine());
	}
	while ( ! ex.StdOut.AtEndOfStream ) {
		filter(p + ex.StdOut.ReadLine());
	}

	return result;
};

FileSystem.wildcard2regex = function(wildcard, skipRegexp, strictly)
{
	var convert = arguments.callee[strictly ? 'strictly' : 'std'];

	var result = [];
	if ( Object.prototype.toString.call(wildcard) == '[object String]' ) {
		result.push(convert(wildcard));
	} else {
		for (var i = 0; i < wildcard.length; i++) {
			result.push(convert(wildcard[i]));
		}
	}

	return skipRegexp 
		? result 
		: new RegExp('^(' + result.join('|') + ')$', 'i');
};

FileSystem.wildcard2regex.strictly = function(wildcard)
{
	var result = wildcard
		.replace(/([\^\$\\\/\|\.\+\!\[\]\(\)\{\}])/g, '\\$1')
		.replace(/\?/g, '[^\\\\]?')
		.replace(/\*/g, '[^\\\\]*')
		;
	return result;
};

FileSystem.wildcard2regex.std = function(wildcard)
{
	var result = wildcard
		.replace(/([\^\$\\\/\|\.\+\!\[\]\(\)\{\}])/g, '\\$1')
		.replace(/\?/g, '.?')
		.replace(/\*/g, '.*?')
		;
	return result;
};

Примеры использования 1-3 - в первом сообщении темы. Здесь - примеры, показывающие новые возможности

4. Поиск всех файлов z* во всех подкаталогах в заданном каталоге C:\Windows\System32 и вывод сообщения "Найден файл: ТАКОЙ-ТО"


var options = {
	// путь поиска
	path: 'C:\\Windows\\System32', 

	// искать во всех подкаталогах
	recursive: true, 
 
	// только файлы, имена которых соответствуют шаблону z*
	filter: function(filename) { return filename.match(/\\z[^\\]*$/i); }, 

	// для каждого найденного файла вывести сообщение
	each: function(filename) { WScript.Echo('Найден файл: ' + filename); }
};

var f = FileSystem.find(options);

// вывести количество найденных файлов
WScript.Echo('Найдено файлов: ' + f);
( 2 * b ) || ! ( 2 * b )