Config Transformation Tool: Теперь поддерживаются параметры
- modified:
- reading: 9 minutes
Пару недель назад я писал про небольшую утилиту Config Transformation Tool, которую я создал на базе задачи трансформирования web.config файла. В тот момент у меня сразу же возникла идея, что было бы неплохо еще иметь возможность указывать места в файле-трансформере, вместо которых можно было бы подставлять значения при помощи этой утилиты. И вот, сегодня я готов объявить, что мне удалось решить эту задачу. Сначала хотелось бы поблагодарить AlexBar, за то, что он порекомендовал посмотреть глубже при помощи .Net Reflector в недры библиотеки Microsoft.WebApplication.Build.Tasks.Dll, и отыскать там класс Microsoft.Web.Publishing.Tasks.XmlTransformation, который умеет выполнять XML-Document-Transform для строк. Мне это очень сильно упростило реализацию. Чтобы утилита смогла поддерживать параметры мне предстояло решить две задачи: (а) уметь пробегаться по файлу и подставлять значения вместо параметров, (б) уметь парсить командную строку на предмет передачи параметров со значениями.
ParametersTask
Итак, сначала опишу, как я решал первую задачу. Синтаксис для параметров я решил сделать таким: {Имя_параметра:значение_по_умолчанию}, где значение по умолчанию это необязательный параметр. Правила для замены следующие:
- если указан параметр со значением, то использовать это значение;
- если параметр не указан, но указано значение по умолчанию, то подставлять его;
- если параметр не указан, и значение по умолчанию так же не указано, то оставить все как есть.
Перед тем как решать данную задачу я так же подумал о том, что RegEx или простыми string.Replace тут лучше не увлекаться, так как, если параметров будет много, то такая задача может выполняться очень долго. Потому я решил обойтись одним проходом по строке-трансформере, и, соответственно, за этот один проход подставить значения параметров. Так же я подумал о том, что в строке-трансформере могут применяться символы ‘{‘, ‘}’ не только для моих параметров, а для того, чтобы моя утилита их игнорировала нужно использовать комбинации “\}”, “\{“, ну и, соответственно, для самого символа ‘\’ так же используем комбинацию “\\”. Итак, класс ParametersTask имеет одно поле _parameters с типом IDictionary<string, string>, где ключи – это имена параметров, а значения – значения этих параметров. Основной метод ApplyParameters:
public string ApplyParameters(string sourceString)
{ StringBuilder result = new StringBuilder();
int index = 0;
char[] source = sourceString.ToCharArray();
bool fParameterRead = false;
StringBuilder parameter = new StringBuilder();
while (index < source.Length)
{ // If parameter read, read it and replace it
if (fParameterRead && source[index] == '}')
{
var s = parameter.ToString(); int colonIndex = parameter.ToString().IndexOf(':');
var parameterName = colonIndex > 0 ? s.Substring(0, colonIndex) : s; var parameterDefaultValue = colonIndex > 0 ? s.Substring(colonIndex + 1, s.Length - colonIndex - 1) : null;
string parameterValue = null;
if (_parameters != null && _parameters.ContainsKey(parameterName))
parameterValue = _parameters[parameterName];
// Put "value" or "default value" or "string which was here"
result.Append(parameterValue ?? parameterDefaultValue ?? "{" + parameter + "}");
fParameterRead = false;
index++; continue;
} if (source[index] == '{')
{ fParameterRead = true;
parameter = new StringBuilder();
index++;
} // Check is this escape \{ \} \\
else if (source[index] == '\\')
{
var nextIndex = index + 1; if (nextIndex < source.Length)
{
var nextChar = source[nextIndex]; if (nextChar == '}' || nextChar == '{' || nextChar == '\\')
{
index++;
}
}
}
if (fParameterRead)
parameter.Append(source[index]); else
result.Append(source[index]);
index++;
}
return result.ToString();
}
Основная идея метода в том, что в цикле мы читаем либо параметр, либо просто содержание. В методе первый if на то, что это окончание параметра, второй if на то, что это начало параметра. Следующий пропускает специальные комбинации “\{”, “\}” или “\\”. Это, конечно, не полноценный “нисходящий разбор”, но, вроде выглядит ничего, и он отлично отрабатывает на следующих тестах:
[Test]public void ApplyParameters_Sample()
{ const string ExpectedResult =
@"
<value key=""Value CustomParameter1"" value=""False"" />
<value key=""Test2"" value=""Value CustomParameter2"" />
<value key=""Test3"" value=""False"" />";
const string Source =
@"
<value key=""{CustomParameter1:Default value}"" value=""{TrueValueParameter:True}"" />
<value key=""Test2"" value=""{CustomParameter2:Default value of CustomParameter2}"" />
<value key=""Test3"" value=""{TrueValueParameter:True}"" />";
ParametersTask task = new ParametersTask();
task.AddParameters(new Dictionary<string, string>
{ {"CustomParameter1", "Value CustomParameter1"},
{"TrueValueParameter", "False"},
{"CustomParameter2", "Value CustomParameter2"}
});
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void WithoutParameters()
{ const string Source =
@"
<value key=""{CustomParameter1}"" value=""{TrueValueParameter}"" />
<value key=""Test2"" value=""{CustomParameter2}"" />
<value key=""Test3"" value=""{TrueValueParameter}"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(Source, result);
}
[Test]public void WithoutParameters_But_With_Default_Values()
{ const string ExpectedResult =
@"
<value key=""Default value"" value=""True"" />
<value key=""Test2"" value=""Default value of CustomParameter2"" />
<value key=""Test3"" value=""False"" />";
const string Source =
@"
<value key=""{CustomParameter1:Default value}"" value=""{TrueValueParameter:True}"" />
<value key=""Test2"" value=""{CustomParameter2:Default value of CustomParameter2}"" />
<value key=""Test3"" value=""{TrueValueParameter:False}"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void Apply_With_Double_Colon_In_Definition()
{ const string ExpectedResult =
@"
<value key=""Default:value"" value=""Val"" />";
const string Source =
@"
<value key=""{Parameter1:Default:value}"" value=""Val"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void Apply_With_Escaped_Brackets()
{ const string ExpectedResult =
@"
<value key=""Default:value"" value=""{TestParameter:Test}"" />";
const string Source =
@"
<value key=""{Parameter1:Default:value}"" value=""\{TestParameter:Test\}"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void Apply_With_Escaped_Brackets_In_Default_Value()
{ const string ExpectedResult =
@"
<value key=""Defa{ultva}lue"" value=""{TestParameter:Test}"" />";
const string Source =
@"
<value key=""{Parameter1:Defa\{ultva\}lue}"" value=""\{TestParameter:Test\}"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void Apply_With_Parameter_At_End_Of_String()
{ const string ExpectedResult =
@"
<value key=""Defa{ultva}lue"" value=""Test";
const string Source =
@"
<value key=""{Parameter1:Defa\{ultva\}lue}"" value=""{TestParameter:Test}";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
[Test]public void Apply_With_Parameter_At_Start_Of_String()
{ const string ExpectedResult =
@"Defa{ultva}lue"" value=""{TestParameter:Test}"" />";
const string Source =
@"{Parameter1:Defa\{ultva\}lue}"" value=""\{TestParameter:Test\}"" />";
ParametersTask task = new ParametersTask();
var result = task.ApplyParameters(Source);
Assert.AreEqual(ExpectedResult, result);
}
ParametersParser
Вторая задача – это уметь распарсить параметры из командной строки. Реализовать эту функциональность я решил способом, который используется в MsBuild.exe, ну или очень похожим на него. Параметры должны быть разделены точкой с запятой ‘;’, имя параметра и значение должно разделять двоеточие ‘:’, если значение параметра включает в себя пробелы или точку запятой, то это значение лучше заключить в кавычки, и так же есть возможность использовать ‘\”’, ‘\\’. Реализация ниже:
/// <summary>
/// Parse string of parameters
/// </summary>
public static class ParametersParser
{ private readonly static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Parse string of parameters <paramref name="parametersString"/> separated by semi ';'.
/// Value should be separated from name by colon ':'.
/// If value has spaces or semi you can use quotes for value.
/// You can escape symbols '\' and '"' with \.
/// </summary>
/// <param name="parametersString">String of parameters</param>
/// <returns>Dicrionary of parameters, where keys are names and values are values of parameters.
/// Can be null if <paramref name="parametersString"/> is empty or null.</returns>
public static IDictionary<string, string> ReadParameters(string parametersString)
{ if (string.IsNullOrWhiteSpace(parametersString)) return null;
Dictionary<string, string> parameters = new Dictionary<string, string>();
var source = parametersString.ToCharArray();
int index = 0;
bool fParameterNameRead = true;
bool fForceParameterValueRead = false;
StringBuilder parameterName = new StringBuilder();
StringBuilder parameterValue = new StringBuilder();
while (index < source.Length)
{ if (fParameterNameRead && source[index] == ':')
{ fParameterNameRead = false;
index++;
if (index < source.Length && source[index] == '"')
{ fForceParameterValueRead = true;
index++;
}
continue;
}
if ((!fForceParameterValueRead && source[index] == ';')
|| (fForceParameterValueRead && source[index] == '"' && ((index + 1) == source.Length || source[index + 1] == ';')))
{
AddParameter(parameters, parameterName, parameterValue);
index++; if (fForceParameterValueRead)
index++;
parameterName.Clear();
parameterValue.Clear(); fParameterNameRead = true;
fForceParameterValueRead = false;
continue;
}
// Check is this escape \{ \} \\
if (source[index] == '\\')
{
var nextIndex = index + 1; if (nextIndex < source.Length)
{
var nextChar = source[nextIndex]; if (nextChar == '"' || nextChar == '\\')
{
index++;
}
}
}
if (fParameterNameRead)
{
parameterName.Append(source[index]);
} else
{
parameterValue.Append(source[index]);
}
index++;
}
AddParameter(parameters, parameterName, parameterValue);
if (Log.IsDebugEnabled)
{ foreach (var parameter in parameters)
{ Log.DebugFormat("Parameter Name: '{0}', Value: '{1}'", parameter.Key, parameter.Value);
}
}
return parameters;
}
private static void AddParameter(Dictionary<string, string> parameters, StringBuilder parameterName, StringBuilder parameterValue)
{
var name = parameterName.ToString(); if (!string.IsNullOrWhiteSpace(name))
{ if (parameters.ContainsKey(name))
parameters.Remove(name);
parameters.Add(name, parameterValue.ToString());
}
}
}
Тут все проще чем в предыдущий раз. Мы в цикле либо читаем имя параметра, либо его значение. Конечно, можно было бы сделать все попроще при помощи Split функций, но я решил и тут все сделать правильно за один проход. Итого, несколько тестов для данного метода:
/// <summary>
/// Check simple parameters command line
/// </summary>
[Test]public void Sample()
{ const string parametersLine = "Parameter1:Value1;Parameter2:121.232";
var parameters = ParametersParser.ReadParameters(parametersLine);
Assert.AreEqual("Value1", parameters["Parameter1"]);
Assert.AreEqual("121.232", parameters["Parameter2"]);
}
/// <summary>
/// Check parameters command line when one of parameter has semi in value string
/// </summary>
[Test]public void String_With_Semicolon_In_Value()
{ const string parametersLine = "Parameter1:Value1;Parameter2:\"121;232\"";
var parameters = ParametersParser.ReadParameters(parametersLine);
Assert.AreEqual("Value1", parameters["Parameter1"]);
Assert.AreEqual("121;232", parameters["Parameter2"]);
}
/// <summary>
/// Check that if command line has semicon at end parameters will be loaded
/// </summary>
[Test]public void String_With_Semicolon_At_End()
{ const string parametersLine = "Parameter1:Value1;Parameter2:\"121.232\";";
var parameters = ParametersParser.ReadParameters(parametersLine);
Assert.AreEqual("Value1", parameters["Parameter1"]);
Assert.AreEqual("121.232", parameters["Parameter2"]);
}
/// <summary>
/// Check that value of parameter can contain escaped quotes
/// </summary>
[Test]public void String_With_Values_With_Quotes()
{ const string parametersLine = @"Parameter1:Value1;Parameter2:""12\""1.2\""32"";";
var parameters = ParametersParser.ReadParameters(parametersLine);
Assert.AreEqual("Value1", parameters["Parameter1"]);
Assert.AreEqual("12\"1.2\"32", parameters["Parameter2"]);
}
Результат
В итоге теперь можно при помощи утилиты проработать такой пример. Исходный файл (s.config):
<?xml version="1.0"?>
<configuration>
<custom>
<groups>
<group name="TestGroup1">
<values>
<value key="Test1" value="False" />
<value key="Test2" value="600" />
</values>
</group>
<group name="TestGroup2">
<values>
<value key="Test3" value="C:\Test\" />
</values>
</group>
</groups>
</custom>
</configuration>
Теперь файл трансформации (t.config), в нем описано два параметра, один Parameter1, значение которого указывать необязательно, и параметр с именем Test3Value:
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<custom>
<groups>
<group name="TestGroup1">
<values>
<value key="Test2" value="601" xdt:Transform="Replace" xdt:Locator="Match(key)" />
<value key="Test1" value="{Parameter1:True5665}" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</values>
</group>
<group name="TestGroup2">
<values>
<value key="Test3" value="{Test3Value}" xdt:Transform="Replace" xdt:Locator="Match(key)" />
</values>
</group>
</groups>
</custom>
</configuration>
Вызываем утилиту:
ctt s:s.config t:t.config d:d.config p:Parameter1:True;Test3Value:"c:\Program Files\Test"
Как и следовало ожидать получаем d.config:
<?xml version="1.0"?>
<configuration>
<custom>
<groups>
<group name="TestGroup1">
<values>
<value key="Test1" value="True" />
<value key="Test2" value="601" />
</values>
</group>
<group name="TestGroup2">
<values>
<value key="Test3" value="c:\Program Files\Test" />
</values>
</group>
</groups>
</custom>
</configuration>
К аргументам утилиты еще добавил fpt – этот параметр следует применять, когда в файле-трансформере находятся параметры со значениями по умолчанию, а в командной строке вызова утилиты значения параметров не указываете, тогда под действием этого параметра утилиты пробежится и все-таки проставит значения по-умолчанию. Без параметра утилита не вызывает задача подстановки параметров, если в командной строке параметры со значениями не указаны.
Итог
Скорее всего есть очень много проблемных мест и недочетов, если видите что-то, либо знаете коварный тест, где точно не проработает – отпишитесь, пожалуйста, в комментариях, постараюсь все допилить. Ну и буду рад любым замечаниям и предложениям. Исходники и собранную версию можно скачать на сайте проекта на CodePlex: http://ctt.codeplex.com, последняя версия Config Transformation Tool v1.1.