-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathParsers.cs
279 lines (236 loc) · 11.3 KB
/
Parsers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using Sprache;
namespace DotNetEnv
{
class Parsers
{
public static KeyValuePair<string, string> SetEnvVar (KeyValuePair<string, string> kvp)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
return kvp;
}
public static KeyValuePair<string, string> DoNotSetEnvVar (KeyValuePair<string, string> kvp)
{
return kvp;
}
public static KeyValuePair<string, string> NoClobberSetEnvVar (KeyValuePair<string, string> kvp)
{
if (Environment.GetEnvironmentVariable(kvp.Key) == null)
{
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
}
// not sure if maybe should return something different if avoided clobber... (current value?)
// probably not since the point is to return what the dotenv file reported, but it's arguable
return kvp;
}
// helpful blog I discovered only after digging through all the Sprache source myself:
// https://justinpealing.me.uk/post/2020-03-11-sprache1-chars/
private static readonly Parser<char> DollarSign = Parse.Char('$');
private static readonly Parser<char> Backslash = Parse.Char('\\');
private static readonly Parser<char> Underscore = Parse.Char('_');
private static readonly Parser<char> InlineWhitespaceChars = Parse.Chars(" \t");
private const string EscapeChars = "abfnrtv\\'\"?$`";
private static string ToEscapeChar (char escapedChar)
{
switch (escapedChar)
{
case 'a': return "\a";
case 'b': return "\b";
case 'f': return "\f";
case 'n': return "\n";
case 'r': return "\r";
case 't': return "\t";
case 'v': return "\v";
case '\\': return "\\";
case '\'': return "'";
case '"': return "\"";
case '?': return "?";
case '$': return "$";
case '`': return "`";
default: return $"\\{escapedChar}";
}
}
// https://thomaslevesque.com/2017/02/23/easy-text-parsing-in-c-with-sprache/
internal static readonly Parser<string> EscapedChar =
from _ in Backslash
from c in Parse.AnyChar
select ToEscapeChar(c);
private static byte ToOctalByte (string value)
{
return Convert.ToByte(value, 8);
}
private static byte ToHexByte (string value)
{
return Convert.ToByte(value, 16);
}
private static string ToUtf8Char (IEnumerable<byte> value)
{
return Encoding.UTF8.GetString(value.ToArray());
}
// https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa/311179#311179
private static byte[] StringToByteArray (String hex, int len)
{
hex = hex.PadLeft(len, '0');
byte[] bytes = new byte[len / 2];
for (int i = 0; i < len; i += 2)
// note the (len - i - 2) for little endian
bytes[(len - i - 2) / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
private static string ToUtf16Char (string hex)
{
return Encoding.Unicode.GetString(StringToByteArray(hex, 4));
}
private static string ToUtf32Char (string hex)
{
return Encoding.UTF32.GetString(StringToByteArray(hex, 8));
}
private static readonly Parser<char> Hex = Parse.Chars("0123456789abcdefABCDEF");
internal static readonly Parser<byte> HexByte =
from start in Parse.String("\\x")
from value in Parse.Repeat(Hex, 1, 2).Text()
select ToHexByte(value);
internal static readonly Parser<byte> OctalByte =
from _ in Backslash
from value in Parse.Repeat(Parse.Chars("01234567"), 1, 3).Text()
select ToOctalByte(value);
internal static readonly Parser<string> OctalChar =
from value in Parse.Repeat(OctalByte, 1, 8)
select ToUtf8Char(value);
internal static readonly Parser<string> Utf8Char =
from value in Parse.Repeat(HexByte, 1, 4)
select ToUtf8Char(value);
internal static readonly Parser<string> Utf16Char =
from start in Parse.String("\\u")
from value in Parse.Repeat(Hex, 2, 4).Text()
select ToUtf16Char(value);
internal static readonly Parser<string> Utf32Char =
from start in Parse.String("\\U")
from value in Parse.Repeat(Hex, 2, 8).Text()
select ToUtf32Char(value);
internal static Parser<string> NotControlNorWhitespace (string exceptChars) =>
Parse.Char(
c => !char.IsControl(c) && !char.IsWhiteSpace(c) && !exceptChars.Contains(c),
$"not control nor whitespace nor {exceptChars}"
).AtLeastOnce().Text();
// officially *nix env vars can only be /[a-zA-Z_][a-zA-Z_0-9]*/
// but because technically you can set env vars that are basically anything except equals signs, allow some flexibility
private static readonly Parser<char> IdentifierSpecialChars = Parse.Chars(".-");
internal static readonly Parser<string> Identifier =
from head in Parse.Letter.Or(Underscore)
from tail in Parse.LetterOrDigit.Or(Underscore).Or(IdentifierSpecialChars).Many().Text()
select head + tail;
internal static readonly Parser<IValue> InterpolatedEnvVar =
from _d in DollarSign
from id in Identifier
select new ValueInterpolated(id);
internal static readonly Parser<IValue> InterpolatedBracesEnvVar =
from _d in DollarSign
from _o in Parse.Char('{')
from id in Identifier
from _c in Parse.Char('}')
select new ValueInterpolated(id);
internal static readonly Parser<IValue> JustDollarValue =
from d in DollarSign
select new ValueActual(d.ToString());
internal static readonly Parser<IValue> InterpolatedValue =
InterpolatedEnvVar.Or(InterpolatedBracesEnvVar).Or(JustDollarValue);
internal static readonly Parser<string> SpecialChar =
Utf32Char
.Or(Utf16Char)
.Or(Utf8Char)
.Or(OctalChar)
.Or(EscapedChar);
private static readonly Parser<string> InlineWhitespace =
InlineWhitespaceChars.Many().Text();
// unquoted values can have interpolated variables,
// but only inline whitespace -- until a comment,
// and no escaped chars, nor byte code chars
private static readonly Parser<ValueCalculator> UnquotedValueContents =
InterpolatedValue
.Or(from inlineWhitespaces in InlineWhitespace
from _ in Parse.Char('#').Not() // "#" after a whitespace is the beginning of a comment --> not allowed
from partOfValue in NotControlNorWhitespace("$\"'") // quotes are not allowed in values, because in a shell they mean something different
select new ValueActual(string.Concat(inlineWhitespaces, partOfValue)))
.Many()
.Select(vs => new ValueCalculator(vs));
internal static readonly Parser<ValueCalculator> UnquotedValue =
from _ in Parse.Chars(" \t\"'").Not()
from value in UnquotedValueContents
select value;
// double quoted values can have everything: interpolated variables,
// plus whitespace, escaped chars, and byte code chars
internal static readonly Parser<ValueCalculator> DoubleQuotedValueContents =
InterpolatedValue.Or(
SpecialChar
.Or(NotControlNorWhitespace("\"\\$"))
.Or(Parse.WhiteSpace.AtLeastOnce().Text())
.AtLeastOnce()
.Select(strs => new ValueActual(strs))
).Many().Select(vs => new ValueCalculator(vs));
// single quoted values can have whitespace,
// but no interpolation, no escaped chars, no byte code chars
// notably no single quotes inside either -- no escaping!
// single quotes are for when you want truly raw values
internal static readonly Parser<ValueCalculator> SingleQuotedValueContents =
NotControlNorWhitespace("'")
.Or(Parse.WhiteSpace.AtLeastOnce().Text())
.AtLeastOnce()
.Select(strs => new ValueActual(strs))
.Many()
.Select(vs => new ValueCalculator(vs));
// compare against bash quoting rules:
// https://stackoverflow.com/questions/6697753/difference-between-single-and-double-quotes-in-bash/42082956#42082956
internal static readonly Parser<ValueCalculator> SingleQuotedValue =
from _o in Parse.Char('\'')
from value in SingleQuotedValueContents
from _c in Parse.Char('\'')
select value;
internal static readonly Parser<ValueCalculator> DoubleQuotedValue =
from _o in Parse.Char('"')
from value in DoubleQuotedValueContents
from _c in Parse.Char('"')
select value;
internal static readonly Parser<ValueCalculator> Value =
SingleQuotedValue.Or(DoubleQuotedValue).Or(UnquotedValue);
internal static readonly Parser<string> Comment =
from _h in Parse.Char('#')
from comment in Parse.CharExcept("\r\n").Many().Text()
select comment;
private static readonly Parser<string> ExportExpression =
from export in Parse.String("export")
.Or(Parse.String("set -x"))
.Or(Parse.String("set"))
.Or(Parse.String("SET"))
.Text()
from _ws in InlineWhitespaceChars.AtLeastOnce()
select export;
internal static readonly Parser<KeyValuePair<string, string>> Assignment =
from _ws_head in InlineWhitespace
from export in ExportExpression.Optional()
from name in Identifier
from _ws_pre in InlineWhitespace
from _eq in Parse.Char('=')
from _ws_post in InlineWhitespace
from value in Value
from _ws_tail in InlineWhitespace
from _c in Comment.Optional()
from _lt in Parse.LineTerminator
select new KeyValuePair<string, string>(name, value.Value);
internal static readonly Parser<KeyValuePair<string, string>> Empty =
from _ws in InlineWhitespace
from _c in Comment.Optional()
from _lt in Parse.LineTerminator
select new KeyValuePair<string, string>(null, null);
public static IEnumerable<KeyValuePair<string, string>> ParseDotenvFile (
string contents,
Func<KeyValuePair<string, string>, KeyValuePair<string, string>> tranform
) {
return Assignment.Select(tranform).Or(Empty).AtLeastOnce().End()
.Parse(contents).Where(kvp => kvp.Key != null);
}
}
}