65.9K
CodeProject 正在变化。 阅读更多。
Home

csppg: 一个用于……的 C# 预处理器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2021 年 10 月 28 日

MIT

3分钟阅读

viewsIcon

8793

downloadIcon

147

这是一个小型模板引擎构建工具,我用它来使我的代码生成项目更易于维护。

引言

我尝试使用 T4 模板。我想,“嘿,这听起来是个好主意!”直到它开始向我的项目中添加引用,并让我搜索谷歌,试图弄清楚如何从命令行调用它。

我不需要那么麻烦。 KISS(保持简单,愚蠢)。一切都应该尽可能简单,而不是更简单。 输入 csppg。 这是一个快速而粗糙的工具,允许你使用 <#/#><%/%> 分隔符和 C# 代码编写一些输入模板,然后它会生成一个 C# 文件,其中包含一个类,该类包含一个 Run() 方法,该方法接受一个文本阅读器和一些参数,并写入一个 TextWriter。 然后,可以将该类包含在你的项目中,以便你可以将其用作代码生成模板,或者说如果你正在制作邮件发送器,你可以将其用作邮件合并 – 坦白说,它不在乎输出是什么,我通常只是用它来渲染 C# 代码,或者用其他语言编写的代码。

理解这段乱码

这东西比使用起来更难解释,但如果你曾经使用过 ASP(还记得吗?),你可以模板化 HTML 输出。 这几乎做了同样的事情,除了输出是你想要的任何东西,并且“服务器端”语言总是 C#。 你有一个 Response TextWriter 和一个 Arguments Dictionary<string, object>

如果仅仅是这样,它还是可行的,但还不够好,因为单独使用时,你实际上做不了多少事情,例如,它无法访问调用者的内部类型和方法。 此外,必须在类似 ASP 的页面中进行元编码,并且失去正确的 Intellisense 也非常麻烦。 你真的需要某种代码隐藏,并且你需要访问你正在运行的应用程序的对象。

为了解决这个问题,它输出了用于输出代码的代码。 然后,你可以将这些东西包含在你的项目中,并从你的代码内部调用它,在那里它可以访问所有内容。

如果你理解 ASP 上下文切换是如何工作的,这可能会有所帮助。 考虑以下内容

<%
var tokenizerDfa = (int[])Arguments["tokenizerDfa"];
var arrayCount = 1;
%>
static readonly int[] _TokenizerDfa = new int[] {
<%for(var i = 0;i<tokenizerDfa.Length;++i) {%><%=tokenizerDfa[i]%><%
if(i<tokenizerDfa.Length-1){%>, <%}%><%if(0==((arrayCount++)%50)){%>
<%}%><%}%>
};

请原谅格式。在这里,如果不影响最终输出,就不可能干净地换行。

这将解析为以下代码片段

var tokenizerDfa = (int[])Arguments["tokenizerDfa"];
var arrayCount = 1;

            Response.Write("\r\nstatic readonly int[] _TokenizerDfa = new int[] {\r\n");
for(var i = 0;i<tokenizerDfa.Length;++i) {
            Response.Write(tokenizerDfa[i]);
if(i<tokenizerDfa.Length-1){
            Response.Write(", ");
}
if(0==((arrayCount++)%50)){
            Response.Write("\r\n");
}
}
            Response.Write("\r\n};\r\n");

上面的代码将由 csppg 生成。当它被传递一个 DFA 状态机作为整数的打包数组时,它将在使用 int[] tokenizerDfa 参数执行后将其呈现为一些输出

var args = new Dictionary<string, object>();
args.Add("tokenizerDfa", _ToDfaTable(tokenizer));
TokenizerGenerator.Run(Console.Out, args); // invokes the csspgen created code

根据传入的分词器,输出将类似于这样

static readonly int[] _TokenizerDfa = new int[] {
-1, 9, 1146, 2, 9, 13, 32, 32, 1154, 1, 34, 34, 1210, 1, 39, 39, 1250, 
1, 46, 46, 1322, 1, 47, 47, 1330, 1, 48, 48, 1376, 1, 49, 57, 1790, 1, 
64, 64, 1814, 554, 65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 
216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 
886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 
931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1377, 1415, 
1488, 1514, 1520, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 
1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 
1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 
2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2208, 2228, 2308, 
2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 
2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 
2525, 2527, 2529, 2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 
2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 
2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 
2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 
2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 
2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 
2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 
3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 
3169, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 
3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 
3346, 3386, 3389, 3389, 3406, 3406, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 
3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 
3713, 3714, 3716, 3716, 3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 
3737, 3743, 3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760, 3762, 
3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 
3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 
4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 
4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 
4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 
4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 
4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 
5866, 5873, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 
5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312, 6314, 
6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 
6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 
7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409, 
7413, 7414, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 
8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 
8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 
8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 
8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 
8486, 8486, 8488, 8488, 8490 ... 
};

显然,输出格式化的数组甚至不是类似 ASP 的引擎特别擅长的事情,这就是我通常编写一个代码隐藏方法来为我完成它的原因。

static readonly int[] _TokenizerDfa = <%
var dt = ToLexer(rules);
WriteCSArray(dt,Response); 
%>

我开始对 Reggie 的最新版本使用这种方法,而不是传统的手动编写代码,因为这些例程越来越难以更新和维护。

使用这个烂摊子

使用该工具非常简单。

Usage: csppg.exe <inputfile> [/output <outputfile>] [/class <codeclass>]
   [/namespace <codenamespace>] [/internal] [/ifstale]

csppg 0.5.0.0 - Runs an ASP/T4 style template over some input, 
and generates code that can be used to run the template.

   <inputfile>     The input template
   <outputfile>    The preprocessor source file - defaults to STDOUT
   <codeclass>     The name of the main class to generate - default derived from <outputfile>
   <codenamespace> The namespace to generate the code under - defaults to none
   <internal>      Mark the generated class as internal - defaults to public
   <ifstale>       Only generate if the input is newer than the output

输出文件有点粗糙,但它可以包含在你的代码生成器项目中,以运行你提供给它的模板。

创建一个代码隐藏只是创建一个具有相同名称的 partial class 的问题。

这是一些输出的片段

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
namespace Reggie {
    internal partial class TableMatcherGenerator {
        public static void Run(TextWriter Response, IDictionary<string, object> Arguments) {

var rules = (IList<LexRule>)Arguments["rules"];
var ignoreCase = (bool)Arguments["ignorecase"];
var inputFile = (string)Arguments["inputfile"];
var blockEnds = BuildBlockEnds(rules,inputFile,ignoreCase);

            Response.Write("\r\n");
 for(var k = 0;k<2;++k) { 
bool reader = k==1;
string curtype = reader ? 
                 "System.IO.TextReader":"System.Collections.Generic.IEnumerator<char>";
string curname = reader ? "text":"cursor";
string texttype = reader ? 
                  "System.IO.TextReader":"System.Collections.Generic.IEnumerable<char>";

            Response.Write("\r\nstatic System.Collections.Generic.IEnumerable
            <System.Collections.Generic.KeyValuePair<long, string>> 
            _MatchTable(int[] entries,int[] blockEnd, ");
            Response.Write(texttype);
            Response.Write(" text, long position)\r\n
            {\r\n    var sb = new System.Text.StringBuilder();\r\n");
if(!reader) {
            Response.Write("    var cursor = text.GetEnumerator();");
}

请原谅格式。它是由一个工具生成的,实际上并不能很好地手动编辑。

现在你有了这样的东西,你可以这样使用它

var args = new Dictionary<string, object>();
args.Add("rules", rules);
args.Add("inputfile", inputFile);
args.Add("blockEnds", blockEnds);
args.Add("lines", false);
args.Add("ignorecase", ignoreCase);
// late bind so if we break the build it doesn't blow up all over the place
// when we have to delete the output
var tp = Type.GetType("Reggie.TableMatcherGenerator");
tp.GetMethod("Run", BindingFlags.Static | 
              BindingFlags.Public).Invoke(null, new object[] { writer, args });
//TableMatcherGenerator.Run(writer, args);

请注意,我正在进行后期绑定,而不是使用更简单的调用(上面注释掉的)。 这不是绝对必要的,但它允许的情况是,如果你在模板中编写了错误的代码,它可能会导致项目无法编译。 为了让项目重新编译,你可以删除生成文件中的代码。 由于它是后期绑定的,因此在未找到该方法时,它不会导致编译错误。 它只是让生活更轻松。

历史

  • 2021 年 10 月 28 日 - 初始提交
© . All rights reserved.