Пишем утилиту xd. Часть II

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

Напомним, в предыдущей статье мы сделали практически все, что нужно: в частности, мы создали универсальную функцию, которая может работать с любым диапазоном данных, а также сформировали структуры данных, которые обслуживают функцию. Но, то что было создано, удобно лишь в библиотечном использовании, а не в пользовательском применении. Этот момент можно исправить в несколько этапов. Первым этапом можно назвать создание специальной функции-помощника, которая будет принимать не подготовленные аргументы в виде структур данных, а строковые аргументы, такие как если бы они поступили от пользователя.

Функцию-помощника можно описать следующим образом:

auto xd(Range)(Range range, string xfmt, string ofmt, bool repeated = false, bool reversed = false)
{
	XDFORMAT chunkFormat;
	// Default values from xd man
	chunkFormat.width = WIDTH.DWORD;
	chunkFormat.style = STYLE.HEXADECIMAL;
	chunkFormat.order = ORDER.LITTLE;
	
	// Default address style
	STYLE offsetFormat = STYLE.DECIMAL;
		
	if (xfmt.length >= 1)
	{
		auto firstChar = xfmt[0];
		
		switch (firstChar)
		{
			case '1', 'b':
				chunkFormat.width = WIDTH.BYTE;
				break;
			case '2', 'w':
				chunkFormat.width = WIDTH.WORD;
				break;
			case '4', 'l':
				chunkFormat.width = WIDTH.DWORD;
				break;
			case '8', 'v':
				chunkFormat.width = WIDTH.QWORD;
				break;
			case 'c':
				chunkFormat.width   = WIDTH.BYTE;
				chunkFormat.isASCII = true;
				break;
			default:
				break;
		}
		
		if (xfmt.length >= 2)
		{
			char secondChar = xfmt[1];
		
			if ((firstChar == 'b') || (firstChar == 'w') || (firstChar == 'l') || (firstChar == 'v'))
			{
				if (xfmt.length >= 3)
				{
					secondChar = xfmt[2];
				}
			}
					
			switch (secondChar)
			{
				case 'o':
					chunkFormat.style = STYLE.OCTAL;
					break;
				case 'd':
					chunkFormat.style = STYLE.DECIMAL;
					break;
				case 'x':
					chunkFormat.style = STYLE.HEXADECIMAL;
					break;
				default:
					break;
			}
		}
	}
		
	if (ofmt.length >= 1)
	{
		switch (ofmt[0])
		{
			case 'o':
				offsetFormat = STYLE.OCTAL;
				break;
			case 'd':
				offsetFormat = STYLE.DECIMAL;
				break;
			case 'x':
				offsetFormat = STYLE.HEXADECIMAL;
				break;
			default:
				break;
		}
	}
	
	if ((chunkFormat.width == WIDTH.DWORD) && (reversed))
	{
		chunkFormat.order = ORDER.BIG;
	}
	
	writeln(chunkFormat);
	xd(range, chunkFormat, offsetFormat, repeated);
}

Данная функция работает аналогично той, что была в первой части статьи, только в качестве аргументов для формата вывода кусков и адреса используются обычные строковые аргументы. Также для предоставления некоторых возможностей – в частности, для вывода повторений кусков с помощью символа “*”, используются булевы флаги.

В принципе, в самой функции-помощнике уже сделаны необходимые проверки, а также уже сформирован интерфейс, который можно использовать для того, чтобы организовать взаимодействие с пользователем, однако, все равно нужно делать захват переданных из командной строки аргументов. Получение данных из командной строки мы решили сделать в соответствии с духом минимализма, не используя сторонних библиотек и даже не привлекая стандартной. Сделана обработка способом типичным для программ на C/C++: в функцию main обычно передается строковый массив аргументов и был сделан ручной разбор этого строкового массива, так как этот массив имеет весьма специфическую структуру.

Таким образом, для завершения построения xd требуется только написать функцию main, которая выглядит так:

void main(string[] args)
{	
	if (args.length < 2)
	{
		writeln("xd [-u] [-r] [-s] [-a{odx}] [-c|{b1w2l4v8}{odx}] ... file ...\n");
	}
	else
	{
		string xfmt, afmt;
		bool repeated, reversed;
		string[] files;
		
		foreach (arg; args[1..$])
		{
			if (arg.length >= 2)
			{
				if (arg[0] == '-')
				{
					switch (arg)
					{
						case "-u":
							break;
						case "-r":
							repeated = true;
							break;
						case "-s":
							reversed = true;
							break;
						case "-c":
							xfmt = "c";
							break;
						default:
							if (arg.length == 3)
							{
								if (arg[1] == 'a')
								{
									afmt = to!string(arg[2]);
								}
								else
								{
									xfmt = arg[1..$];
								}
							}
							break;
					}
				}
				else
				{
					files ~= arg;
				}
			}
		}
		
		import std.file : exists, read;
		
		foreach (f; files)
		{
			if (f.exists)
			{
				auto buf = cast(ubyte[]) read(f);
				xd(buf, xfmt, afmt, repeated, reversed);
			}
		}
	}
}

Работает это все достаточно просто: если количество строк в массиве аргументов командной строки меньше 2 (что означает, что не были переданы минимально необходимые параметры – формат и файл для обработки), то выдается справка по использованию утилиты. Если же аргументов больше 2, то у массива аргументов убирается первый элемент (т.к. самый первый аргумент – это как правило имя программы) и дальше идет поэлементная обработка. Сама обработка проверяет начинается ли элемент с дефиса (что означает, что нам попалась опция командной строки) и если да, то проверяется полученный аргумент на соответствие списку опций xd. Если опция была найдена и соответствует одной из опций, то она кладется в соответствующую переменную, что используется далее функцией-помощником. Также, нетрудно заметить, что отдельно обрабатывается случай, когда есть опция для формата адреса (-a) и для списка файлов (это если не подошла ни одна опций, но аргументы еще остались).

Таким образом, получается полностью функциональная копия xd из Plan 9, но насколько нам удалось понять с меньшим количеством кода: оригинальная версия xd из Plan 9 на C имеет 463 строки, а наша версия – 364 строки! Конечно, ничего особенного это не означает, кроме того, что версия на D чуть проще для понимания, но она может требовать некоторых доработок. Также, нами не были произведены какие-либо бенчмарки обоих версий, так как мы не ставили перед собой целей именно побить в чем-либо оригинальную версию. Стоит заметить, что несмотря на отсутствие проверок производительности, наша версия xd вполне готова к использованию.