Config Transformation Tool: Parameters support

    • .NET
    • C#
    • CodePlex
    • XDT
    • XML-Document-Transform
    • Config Transformation Tool
  • modified:
  • reading: 3 minutes

Couple of weeks ago I wrote about my small utility Config Transformation Tool, which I wrote with base of web.config transformation task. In those moments I was thinking about opportunity to pass parameters to transform file, to tool can replaces parameters in transformation file with special values. Yesterday I resolved this issue. From now I use class Microsoft.Web.Publishing.Tasks.XmlTransformation which works with strings and XmlDocuments instead of files. I had two tasks: (a) I need method which will replace parameters on values, (b) I need method which will be parse command line and create dictionary of parameters.

ParametersTask

Let’s solve first task. I decided to use next syntax for parameters: {Parameter_Name:Default_Value}, where default value is optional. Rules for replace:

  1. If parameter defined with value – tool will use this value;
  2. If parameter not set, but value by default defined – tool will use default value;
  3. If parameter not set and default value not set – tool will leave this as is.

I didn’t want to solve this issue with RegEx or string.Replace methods, because if parameters will be many, execution of this task can take a long period. So I wanted to write method which will handle all parameters in string in one pass. Also I thought that maybe will need to use symbols ‘{’, ‘}’ in string, so I need way to escape these symbols. I decided to use combinations ‘\}’, “\{”, and if you want to use ‘\’ you should use combination ‘\\’. Ok, so class ParametersTask has one field _parameters with type IDictionary<string,string>, where keys are names of parameters, and values are values of parameters. Main method ApplyParameters:

```csharp 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(); } ```

In the while cycle we read parameter or just content of file. First if check that next char is end of parameter’s definition, second if check that next char is start of parameter’s definition. Next if escape special combinations “\{”, “\}” or “\\”. Of course it is not a full “Recursive descent parser”, but it looks good, and it is working with next tests:

```csharp [Test] public void ApplyParameters_Sample() { const string ExpectedResult = @" "; const string Source = @" "; ParametersTask task = new ParametersTask(); task.AddParameters(new Dictionary { {"CustomParameter1", "Value CustomParameter1"}, {"TrueValueParameter", "False"}, {"CustomParameter2", "Value CustomParameter2"} }); var result = task.ApplyParameters(Source); Assert.AreEqual(ExpectedResult, result); } [Test] public void WithoutParameters() { const string Source = @" "; ParametersTask task = new ParametersTask(); var result = task.ApplyParameters(Source); Assert.AreEqual(Source, result); } [Test] public void WithoutParameters_But_With_Default_Values() { const string ExpectedResult = @" "; const string Source = @" "; 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 = @" "; const string Source = @" "; ParametersTask task = new ParametersTask(); var result = task.ApplyParameters(Source); Assert.AreEqual(ExpectedResult, result); } [Test] public void Apply_With_Escaped_Brackets() { const string ExpectedResult = @" "; const string Source = @" "; 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 = @" "; const string Source = @" "; 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 = @" "; const string Source = @"{Parameter1:Defa\{ultva\}lue}"" value=""\{TestParameter:Test\}"" />"; ParametersTask task = new ParametersTask(); var result = task.ApplyParameters(Source); Assert.AreEqual(ExpectedResult, result); } ```

ParametersParser

Second issue – tool should parse parameters from command line. I decided to use a way which use MsBuild tool, or very similar to it. Parameters should be separated by ‘;’, name and value of parameter should be separated by ‘:’, if parameter’s value has space or ‘;’ you can quote it, also you can use ‘\”’ and ‘\\’ for escape symbols ‘”’ and ‘\’. Realization:

```csharp /// /// Parse string of parameters /// public static class ParametersParser { private readonly static ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); /// /// Parse string of parameters 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 \. /// /// String of parameters /// Dicrionary of parameters, where keys are names and values are values of parameters. /// Can be null if is empty or null. public static IDictionary ReadParameters(string parametersString) { if (string.IsNullOrWhiteSpace(parametersString)) return null; Dictionary parameters = new Dictionary(); 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 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()); } } } ```

Very simple. In while cycle we read name of parameter of value of parameter. Of course you can solve this issue with split methods, but I decided to handle this string in one pass too. Some tests for this method:

```csharp /// /// Check simple parameters command line /// [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"]); } /// /// Check parameters command line when one of parameter has semi in value string /// [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"]); } /// /// Check that if command line has semicon at end parameters will be loaded /// [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"]); } /// /// Check that value of parameter can contain escaped quotes /// [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"]); } ```

Result

Ok, so let’s look on example. Source file (s.config):

```xml ```

Transform file, it contains two parameters, one is Parameter1, value of which is optional (default value is there), and parameter Test3Value:

```xml ```

Call tool from command line:

```bash ctt s:s.config t:t.config d:d.config p:Parameter1:True;Test3Value:"c:\Program Files\Test" ```

As expected d.config:

```xml ```

Tool has one more argument fpt – you can use this argument, if transform file contains parameters with default values and in command line you don’t set values for these parameters, so if you set this argument tool will set default values, in other way tool will not execute ParametersTask, because you don’t set list of parameters.

Summary

Maybe you can find a lot of bugs, so please if you see something or will see – just tell me. I will glad to get from you some advises for this tool. Source code and binary you can download from project site at CodePlex: http://ctt.codeplex.com, last version Config Transformation Tool v1.1.

See Also