8

If I have a string that contains a c# string literal expression can I "expand" it at runtime

    public void TestEvaluateString()
    {
        string Dummy = EvalString( @"Contains \r\n new line");
        Debug.Assert(Dummy == "Contains \r\n new line");
    }

    private string EvalString(string input)
    {
        return "Contains \r\n new line";
    }

Like Can I convert a C# string value to an escaped string literal, but in reverse?

Community
  • 1
  • 1
Andy Morris
  • 3,393
  • 1
  • 21
  • 20
  • You know your Assert will fail, don't you? And the 2nd method does not use its parameter. – H H Jul 21 '10 at 10:17
  • 1
    Its an Nunit Assert, I've edited the code to use a debug.assert. The second method is a TDD (Test driven design) stub, the answer to the question will provide a more general solution – Andy Morris Jul 21 '10 at 10:29
  • still you present 2 unrelated functions. That last line helped me understand, the code only confused me. – H H Jul 21 '10 at 10:37

5 Answers5

11

Similar to Mikael answer but using the CSharpCodeProvider:

    public static string ParseString(string txt)
    {
        var provider = new Microsoft.CSharp.CSharpCodeProvider();
        var prms = new System.CodeDom.Compiler.CompilerParameters();
        prms.GenerateExecutable = false;
        prms.GenerateInMemory = true;
        var results = provider.CompileAssemblyFromSource(prms, @"
namespace tmp
{
    public class tmpClass
    {
        public static string GetValue()
        {
             return " + "\"" + txt + "\"" + @";
        }
    }
}");
        System.Reflection.Assembly ass = results.CompiledAssembly;
        var method = ass.GetType("tmp.tmpClass").GetMethod("GetValue");
        return method.Invoke(null, null) as string;
    }

You might be better off using a dictionary of wildcards and just replacing them in the string.

JDunkerley
  • 12,355
  • 5
  • 41
  • 45
  • Good call about using the csharp provider. I was so tuned to using eval and modified some code I already had that I missed this obvious way of doing it. – Mikael Svenson Jul 21 '10 at 11:39
  • This doesn't work for the following : *ParseString("foo\nbar")* – Gluip May 25 '11 at 06:22
  • @Gluip, Doesn't that need to be ParseString(@"foo\nbar"). If not the code inside ParseString doesnt compile. Admittedly it would better if it handled this failure better and return cleanly – JDunkerley Jun 24 '11 at 16:05
  • 2
    I can't believe that the only way do do this is to compile a DLL file on the user's end system (only to leave it in the temp directory). That's fairly slow should this code get called numerous times. +1 for the answer because it works, however I'd really like to see a better solution. – Paul Mar 07 '12 at 20:19
6

Regex.Unescape would be my method of choice.

Lingxi
  • 14,579
  • 2
  • 37
  • 93
3

Not sure if this is the simplest way, but by referencing the Microsoft.JScript namespace you can reparse it with the javascript eval function.

Here's a test for the code at the bottom

var evalToString = Evaluator.MyStr("test \\r\\n test");

This will turn the \r into a carriage return.

And the implementation

public class Evaluator
{
    public static object MyStr(string statement)
    {
        return _evaluatorType.InvokeMember(
                    "MyStr",
                    BindingFlags.InvokeMethod,
                    null,
                    _evaluator,
                    new object[] { statement }
                 );
    }

    static Evaluator()
    {
        ICodeCompiler compiler;
        compiler = new JScriptCodeProvider().CreateCompiler();

        CompilerParameters parameters;
        parameters = new CompilerParameters();
        parameters.GenerateInMemory = true;

        CompilerResults results;
        results = compiler.CompileAssemblyFromSource(parameters, _jscriptSource);

        Assembly assembly = results.CompiledAssembly;
        _evaluatorType = assembly.GetType("Evaluator.Evaluator");

        _evaluator = Activator.CreateInstance(_evaluatorType);
    }

    private static object _evaluator = null;
    private static Type _evaluatorType = null;
    private static readonly string _jscriptSource =

        @"package Evaluator
        {
           class Evaluator
           {
              public function MyStr(expr : String) : String 
              { 
                 var x;
                 eval(""x='""+expr+""';"");
                 return x;
              }
           }
        }";
}
Mikael Svenson
  • 39,181
  • 7
  • 73
  • 79
2

If you're just looking to do "simple" escape characters as defined on the Microsoft site, you can use this routine and save importing external libs:

public static class StringExtensions
{
    /* https://msdn.microsoft.com/en-us/library/aa691087(v=vs.71).aspx */
    private readonly static SortedDictionary<char, char> EscapeMap = new SortedDictionary<char, char>
    {
        { '\'', '\'' },
        { '"', '\"' },
        { '\\', '\\' },
        { '0', '\0' },
        { 'a', '\a' },
        { 'b', '\b' },
        { 'f', '\f' },
        { 'n', '\n' },
        { 'r', '\r' },
        { 't', '\t' },
        { 'v', '\v' },
    };

    public static string UnescapeSimple(this string escaped)
    {
        if (escaped == null)
            return escaped;

        var sb = new StringBuilder();

        bool inEscape = false;
        var s = 0;
        for (var i = 0; i < escaped.Length; i++)
        {
            if (!inEscape && escaped[i] ==  '\\')
            {
                inEscape = true;
                continue;
            }

            if (inEscape)
            {
                char mapChar;
                if (EscapeMap.TryGetValue(escaped[i], out mapChar))
                {
                    sb.Append(escaped.Substring(s, i - s - 1));
                    sb.Append(mapChar);

                    s = i + 1;
                }
                inEscape = false;
            }
        }

        sb.Append(escaped.Substring(s));

        return sb.ToString();
    }
}

Here's a unit test to prove it:

    [TestMethod]
    public void UnescapeSimpleTest()
    {
        var noEscapes = @"This is a test".UnescapeSimple();
        Assert.AreEqual("This is a test", noEscapes, nameof(noEscapes));

        var singleEscape = @"\n".UnescapeSimple();
        Assert.AreEqual("\n", singleEscape, nameof(singleEscape));

        var allEscape = @"\'\""\\\0\a\b\f\n\r\t\v".UnescapeSimple();
        Assert.AreEqual("\'\"\\\0\a\b\f\n\r\t\v", allEscape, nameof(allEscape));

        var textInEscapes = @"\tthis\n\ris\\a\ntest".UnescapeSimple();
        Assert.AreEqual("\tthis\n\ris\\a\ntest", textInEscapes, nameof(textInEscapes));

        var backslashNoEscapes = @"\,\h\qtest".UnescapeSimple();
        Assert.AreEqual(@"\,\h\qtest", backslashNoEscapes, nameof(backslashNoEscapes));

        var emptyStr = "".UnescapeSimple();
        Assert.AreEqual("", emptyStr, nameof(emptyStr));

        // Prove Enviroment.NewLine is "\r\n" and not "\n\r" (Windows PC)
        var newLine = @"\r\n".UnescapeSimple();
        Assert.AreEqual(Environment.NewLine, newLine, nameof(newLine));

        // Double check prior test (Windows PC)
        var newLineWrong = @"\n\r".UnescapeSimple();
        Assert.AreNotEqual(Environment.NewLine, newLineWrong, nameof(newLineWrong));
    }

Feel free to tweak the EscapeMap or rename the function UnescapeSimple (awkward I know).

Note that this solution doesn't handle Unicode escape characters or hex or octal, it just handles the simple single character ones.

Paul Cooper
  • 196
  • 2
  • 7
0

You can achieve this with a one-liner using the Microsoft.CodeAnalysis.CSharp.Scripting package.

private Task<string> EvaluateStringAsync(string input)
{
    return CSharpScript.EvaluateAsync<string>('"' + input + '"');
}

If you start including the outer quotes in the method argument, the method can be generalized to handle verbatim, interpolated, and concatenated strings too (.NET Fiddle):

private Task<string> EvaluateStringAsync(string input)
{
    return CSharpScript.EvaluateAsync<string>(input);
}

// await EvaluateStringAsync(@"$""This is a number: {40:N3}""")
// Output: "This is a number: 40.000"

This method can be slow to invoke repeatedly. If you have a large number of strings to convert, you'd be better off batching them (.NET Fiddle):

private static Task<string[]> EvaluateStringsAsync(string[] inputs)
{
    var inputsConcat = string.Concat(inputs.Select(x => $"    {x},\r\n"));
    var arrayInitializationCode = $"new[] {{\r\n{inputsConcat}}}";
    return CSharpScript.EvaluateAsync<string[]>(arrayInitializationCode);
}

As with all dynamic compilation, you need to restrict your calls to trusted input only, or take measures to protect against injection attacks.

Douglas
  • 53,759
  • 13
  • 140
  • 188