-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCliCenter.cs
1134 lines (1073 loc) · 48.5 KB
/
CliCenter.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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System.Reflection;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
namespace CJF.CommandLine;
#region Public Sealed Class : CliCenter
/// <summary>CLI 指定控制中心的集合類別。</summary>
[UnsupportedOSPlatform("browser")]
public sealed class CliCenter
{
#region Public Consts
/// <summary>一般字詞檢查式。</summary>
public const string WORD_REGEX = @"[^\d\s\.]\w+";
/// <summary>以單引號或雙引號標示的文字,或一般字詞。</summary>
public const string STRING_REGEX = @"(""[^""]*""|'[^']*'|[^\d\s\.]\w+)";
/// <summary>正整數數字檢查式。</summary>
public const string UINT_REGEX = @"\d+";
/// <summary>整數數字檢查式。</summary>
public const string INT_REGEX = @"-?\d+";
/// <summary>含小數點的數字檢查式。</summary>
public const string DECIMAL_REGEX = @"-?[0-9]+(\.[0-9]+)?";
/// <summary>UINT16 數字檢查式。</summary>
public const string UINT16_REGEX = @"(\d{1,4}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])";
/// <summary>INT16 數字檢查式。</summary>
public const string INT16_REGEX = @"(-?(\d{0,4}|[0-2]\d{4}|31\d{3}|3276[0-7])|-32768)";
/// <summary>UINT8 數字檢查式。</summary>
public const string SBYTE_REGEX = @"(-?(\d{0,2}|1[0-1]\d|12[0-7])|-128)";
/// <summary>BYTE 數字檢查式。</summary>
public const string BYTE_REGEX = @"(\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])";
/// <summary>16 進位字串檢查式。</summary>
public const string HEX_REGEX = @"[0-9a-fA-F]+";
/// <summary>1 位元組的 16 進位字串檢查式。</summary>
public const string HEX1BYTE_REGEX = @"[0-9a-fA-F]{2}";
/// <summary>2 位元組的 16 進位字串檢查式。</summary>
public const string HEX2BYTE_REGEX = @"[0-9a-fA-F]{4}";
/// <summary>IP 位址檢查式。</summary>
public const string IP_REGEX = @"((25[0-5]|2[0-4]\d|[01]?\d{1,2})(\.?|$)){4}";
/// <summary>通訊埠號檢查式。</summary>
public const string PORT_REGEX = UINT16_REGEX;
/// <summary>電話檢查式。</summary>
public const string PHONE_REGEX = @"(09\d{2}-*\d{3}-*\d{3}|\(*0\d\)*-*\d{3,4}-*\d{4})";
/// <summary>信箱檢查式。</summary>
public const string EMAIL_REGEX = @"\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z]+";
/// <summary>密碼驗證規則。</summary>
/// <remarks>
/// <para>1. 至少一個英文字母 (?=.*?[A-Za-z])。</para>
/// <para>2. 至少一個數字(?=.*?[0-9])。</para>
/// <para>3. 長度至少為 8 個字元.{8,}。</para>
/// </remarks>
public const string PWD_REGEX = @"(?=.*?[A-Za-z])(?=.*?[0-9]).{8,}";
/// <summary>密碼驗證規則。</summary>
/// <remarks>
/// <para>1. 至少一個大寫英文字母 (?=.*?[A-Z])。</para>
/// <para>2. 至少一個小寫的英文字母 (?=.*?[a-z])。</para>
/// <para>3. 至少一個數字 (?=.*?[0-9])。</para>
/// <para>4. 至少一個特殊字元 (?=.*?[#?!@$%^&*-])。</para>
/// <para>5. 長度至少為 8 個字元 .{8,}。</para>
/// </remarks>
public const string PWD_REGEX_COMPLEX = @"(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}";
/// <summary>ANSI Key codes</summary>
public const string ANSI_PATTERN = "\x1B\\[(\\d+)*;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?;?(\\d+)?([ABCDEFGHJKSTfmsu])";
/// <summary>預設的歷史指令清單分類名稱。</summary>
public const string DEFAULT_POOL = "default";
#endregion
#region Public Events
/// <summary>自 <see cref="Reader.Read"/> 接收到指令時所產生的事件回呼。</summary>
public event CommandEnterHandler? CommandEntered;
/// <summary>執行對應指令前所產生的事件回呼。</summary>
public event ExecuteCommandHandler? BeforeMethodExecute;
/// <summary>執行對應指令後所產生的事件回呼。</summary>
public event ExecuteCommandHandler? AfterMethodExecute;
#endregion
#region Public Properties
/// <summary>傳回列管的指令數量。</summary>
public int Count => _Commands.Count; // _items.Count;
/// <summary>傳回列管的指令清單。</summary>
public IEnumerable<CommandAttribute> Commands
{
get
{
//CommandAttribute[] cas = _items.Keys.ToArray();
CommandAttribute[] cas = _Commands.ToArray();
Array.Sort(cas, (a, b) =>
{
if (a.Tag is null && b.Tag is not null)
return -1;
else if (a.Tag is not null && b.Tag is null)
return 1;
else
{
if (a.Tag != b.Tag)
return a.Tag!.CompareTo(b.Tag);
else
return a.FullCommand.CompareTo(b.FullCommand);
}
});
foreach (CommandAttribute ca in cas)
yield return ca;
}
}
/// <summary>設定或取得 CLI 的指令提示字串。</summary>
public string Prompt { get; set; } = string.Empty;
/// <summary>CLI 的指令提示字串的顯示顏色。</summary>
public ConsoleColor PromptColor { get; set; } = Console.ForegroundColor;
/// <summary>設定或取得密碼輸入時的顯示字元。</summary>
public static char? PasswordChar { get; set; }
/// <summary>設定或取得是否啟用除錯模式。</summary>
public bool DebugMode { get; set; } = false;
/// <summary>設定或取得目前使用的分類標籤字串。</summary>
public string UseTag { get; set; } = string.Empty;
/// <summary>設定或取得歷史指令清單的分類名稱。</summary>
public string HistoryPool
{
get => Reader.PoolName;
set => Reader.SetPool(value);
}
/// <summary>取得目前當下正在執行的指令。</summary>
public CommandAttribute? ExecutingCommand { get; private set; }
/// <summary>取得或設定是否暫停輸入指令。</summary>
public bool Pause { get; set; } = false;
/// <summary>取得或設定是否忽略大小寫。</summary>
public bool IgnoreCase
{
get => _IgnoreCase;
set
{
if (value)
{
stringComparison = StringComparison.OrdinalIgnoreCase;
regexOptions = RegexOptions.IgnoreCase;
}
else
{
stringComparison = StringComparison.Ordinal;
regexOptions = RegexOptions.None;
}
_IgnoreCase = value;
}
}
#endregion
#region Private Variables
private static readonly Regex QuotesRegex = new("(\"[^\"]*\"|'[^']*')");
private static readonly Regex AnsiRegex = new(ANSI_PATTERN);
private readonly List<CommandAttribute> _Commands;
private readonly TimeSpan Delay = TimeSpan.FromSeconds(1);
private Func<string, int, string[]?>? _CommandSuggestions;
private Task? _executeTask;
private CancellationTokenSource? _cancellationTokenSource;
private bool _IgnoreCase = false;
private StringComparison stringComparison = StringComparison.Ordinal;
private RegexOptions regexOptions = RegexOptions.None;
#endregion
#region Public Construct Method : CliCenter()
/// <summary>建立新的 <see cref="CliCenter"/> 執行個體。</summary>
public CliCenter()
{
_Commands = new List<CommandAttribute>();
foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type t in a.GetTypes().Where(_t => _t.GetRuntimeMethods().Any(_m => _m.GetCustomAttributes<CommandAttribute>().Any())))
Join(t, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
}
RebindLinkRelationship();
HistoryPool = DEFAULT_POOL;
KeyHandler.PrintCommandsHandler = PrintCommands;
SetCommandSuggestions(null);
}
#endregion
#region Public Construct Method : CliCenter(string prompt, ConsoleColor? promptColor = null, string historyPool = DEFAULT_POOL, char? pwdChar = null, bool debug = false, int delay = 1000)
/// <summary>建立新的 <see cref="CliCenter"/> 執行個體。</summary>
/// <param name="prompt">指令提示字串。</param>
/// <param name="promptColor">指令提示字串顏色。</param>
/// <param name="historyPool">歷史指令清單的分類字串,預設值為 <see cref="DEFAULT_POOL"/>。</param>
/// <param name="pwdChar">密碼顯示字元。</param>
/// <param name="debug">是否啟用除錯模式。</param>
/// <param name="delay">延遲開始輸入的時間,單位豪秒。</param>
/// <param name="ignoreCase">是否忽略大小寫。</param>
public CliCenter(string prompt, ConsoleColor? promptColor = null, string historyPool = DEFAULT_POOL, char? pwdChar = null, bool debug = false, int delay = 1000, bool ignoreCase = false) : this()
{
Prompt = prompt;
HistoryPool = historyPool;
PromptColor = promptColor ?? Console.ForegroundColor;
PasswordChar = Reader.PasswordChar = pwdChar;
DebugMode = debug;
Delay = TimeSpan.FromMilliseconds(delay);
IgnoreCase = ignoreCase;
}
#endregion
#region Internal Construct Method : CliCenter(CliOptions opts)
/// <summary>建立新的 <see cref="CliCenter"/> 執行個體,本建立式僅供 <see cref="CliHostedService"/> 使用。</summary>
/// <param name="opts">供 <see cref="CliHostedService"/> 傳遞用的設定類別。</param>
internal CliCenter(CliOptions opts) : this(opts.Prompt, opts.PromptColor, opts.HistoryPool, opts.PasswordChar, opts.DebugMode, opts.Delay, opts.IgnoreCase) { }
#endregion
#region Public Static Method : string[] SplitCommand(string text)
/// <summary>從字串中切割出各個指令。</summary>
/// <param name="text">欲切割的原始字串。</param>
/// <param name="removeQuotes">傳回指令陣列時,是否移除內含的單引號(')和雙引號(")。</param>
/// <returns>切割完畢的指令。</returns>
public static string[] SplitCommand(string text, bool removeQuotes = false)
{
var reg = QuotesRegex;
string[] arr;
if (reg.IsMatch(text))
{
var res = new List<string>();
string tmp = text;
Match _m;
while ((_m = reg.Match(tmp)) is not null && _m.Success)
{
arr = tmp[.._m.Index].Split(' ');
if (res.Count == 0)
res.AddRange(arr);
else
{
if (string.IsNullOrWhiteSpace(arr[0]))
res[^1] += arr[0];
else
res.Add(arr[0]);
res.AddRange(arr);
res.RemoveAt(res.Count - arr.Length);
}
if (_m.Index != 0 && tmp[_m.Index - 1] == ' ')
res[^1] += removeQuotes ? _m.Value.Trim("'\"".ToCharArray()) : _m.Value;
else
res.Add(_m.Value);
tmp = tmp[(_m.Index + _m.Length)..].TrimStart();
}
if (!string.IsNullOrWhiteSpace(tmp))
res.AddRange(tmp.Split(' '));
return res.ToArray();
}
else
{
return text.Split(' ').Where(_t => !string.IsNullOrWhiteSpace(_t)).ToArray();
}
}
#endregion
#region Public Method : void Start()
/// <summary>以同步方式開始執行 <see cref="CliCenter"/> 執行個體。</summary>
public void Start()
{
_cancellationTokenSource = new CancellationTokenSource();
StartAsync(_cancellationTokenSource.Token).Wait();
}
#endregion
#region Public Method : Task StartAsync(CancellationToken cancellationToken)
/// <summary>以非同步方式開始執行 <see cref="CliCenter"/> 執行個體。</summary>
/// <param name="cancellationToken">自外部傳入中斷執行的通知。</param>
/// <returns></returns>
public Task StartAsync(CancellationToken cancellationToken)
{
_executeTask = WorkerProcess(cancellationToken);
if (_executeTask.IsCompleted)
return _executeTask;
return Task.CompletedTask;
}
#endregion
#region Public Method : void Stop()
/// <summary>停止執行 <see cref="CliCenter"/> 執行個體。</summary>
public void Stop()
{
if (_cancellationTokenSource is null) return;
_cancellationTokenSource!.Cancel();
StopAsync(_cancellationTokenSource!.Token).Wait();
}
#endregion
#region Public Method : async Task StopAsync(CancellationToken cancellationToken)
/// <summary>以非同步方式停止 <see cref="CliCenter"/> 執行個體。</summary>
/// <param name="cancellationToken">自外部傳入中斷執行的通知。</param>
/// <returns></returns>
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_executeTask == null) return;
await Task.WhenAny(_executeTask, Task.Delay(1000, cancellationToken)).ConfigureAwait(false);
}
#endregion
#region Public Method : void Append(CommandAttribute cmd, MethodInfo method)
/// <summary>新增指令與其定義的函示。</summary>
/// <param name="cmd">指令類別。</param>
/// <param name="method">指令綁定的函示。</param>
/// <exception cref="CommandRegularHelpEmptyException">正規表示式的指令說明文字未定義。</exception>
/// <exception cref="CommandDuplicateException">指令重複。</exception>
public void Append(CommandAttribute cmd, MethodInfo method)
{
if (_Commands.Find(_ca => _ca.Match(cmd)) is CommandAttribute ca)
{
// Exists
MethodInfo _m = ca.Method!;
StringBuilder sb = new();
sb.AppendLine($"Command is duplicate, please check setting again!!");
sb.AppendLine($" Command : {cmd.Command}");
sb.AppendLine($" Parent : {cmd.Parent}");
sb.AppendLine($" Existed : {_m.Name} @ {_m.DeclaringType!.FullName}");
sb.AppendLine($" Duplicate : {method.Name} @ {method.DeclaringType!.FullName}");
throw new CommandDuplicateException(cmd, method, sb.ToString());
}
else
{
// Not Exists
if (cmd.IsRegular && string.IsNullOrEmpty(cmd.RegularHelp))
{
if (method.DeclaringType is not null)
throw new CommandRegularHelpEmptyException($"\"{cmd.Command}\" in methodInfo \"{method.Name}\" from \"{method.DeclaringType.FullName}\" using regular expressions, but the \"{nameof(cmd.RegularHelp)}\" property is empty!");
else
throw new CommandRegularHelpEmptyException($"\"{cmd.Command}\" in methodInfo \"{method.Name}\" using regular expressions, but the \"{nameof(cmd.RegularHelp)}\" property is empty!");
}
else
{
cmd.Method = method;
_Commands.Add(cmd);
}
}
}
#endregion
#region Public Method : void RebindLinkRelationship()
/// <summary>重新綁定連結所有指令(<see cref="CommandAttribute"/>)的關係。</summary>
public void RebindLinkRelationship()
{
foreach (CommandAttribute ca in _Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent)))
SetChilds(ca);
if (DebugMode)
PrintCommandTree();
}
#endregion
#region Public Method : bool RemoveCommand(CommandAttribute cmd)
/// <summary>移除指令。</summary>
/// <param name="cmd">指令類別。</param>
/// <returns>如成功刪除指令,將回傳 <see langword="true"/>, 否則為 <see langword="false"/>。</returns>
public bool RemoveCommand(CommandAttribute cmd)
{
if (cmd is null)
return false;
else
return _Commands.Remove(cmd);
}
#endregion
#region Public Method : bool RemoveCommand(string command, string tag)
/// <summary>移除指令。</summary>
/// <param name="command">CLI 完整指令。</param>
/// <param name="tag">分類標籤。</param>
/// <returns>如成功刪除指令,將回傳 <see langword="true"/>, 否則為 <see langword="false"/>。</returns>
public bool RemoveCommand(string command, string? tag = null)
{
if (FindCommand(command, tag) is not CommandAttribute cmd)
return false;
else
return _Commands.Remove(cmd);
}
#endregion
#region Public Method : CommandAttribute? FindCommand(string command, string? tag = null)
/// <summary>尋找特定指令的 <see cref="CommandAttribute"/> 類別。</summary>
/// <param name="command">CLI 完整指令。</param>
/// <param name="tag">分類標籤。</param>
/// <returns>如找到指令,將回傳 <see cref="CommandAttribute"/> 類別,否則為 <see langword="null"/>。</returns>
public CommandAttribute? FindCommand(string command, string? tag = null)
{
var cmds = _Commands.Where(_ca => _ca.FullCommand.Equals(command, stringComparison) && (string.IsNullOrEmpty(tag) && string.IsNullOrEmpty(_ca.Tag) || !string.IsNullOrEmpty(tag) && tag.Equals(_ca.Tag)));
if (cmds.Count() == 1)
return cmds.First();
else if (cmds.Count() > 1)
{
if (string.IsNullOrEmpty(tag))
return cmds.FirstOrDefault(_c => string.IsNullOrEmpty(_c.Tag));
else
return cmds.FirstOrDefault(_c => tag.Equals(_c.Tag));
}
else
return null;
}
#endregion
#region Public Method : void Clear()
/// <summary>清除所有指令。</summary>
public void Clear() => _Commands.Clear();
#endregion
#region Public Method : bool TryGetMethod(string command, out MethodInfo? method)
/// <summary>以傳入的指令字串嘗試取得其綁定的函示。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <param name="method">傳回綁定的函示。</param>
/// <returns>如找到綁定的函示,將回傳 <see langword="true"/>, 否則為 <see langword="false"/>。</returns>
public bool TryGetMethod(string command, out MethodInfo? method)
{
if (TryGetCommand(command, out CommandAttribute? cca) && cca is not null)
{
method = cca.Method;
return method is not null;
}
else
{
method = null;
return false;
}
}
#endregion
#region Public Method : bool TryGetCommand(string command, out CommandAttribute? ca)
/// <summary>嘗試取得指令屬性資料。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <param name="cmd">[傳回]取得的指令屬性資料。</param>
/// <returns>如正確取得指令屬性資料,將回傳 <see langword="true"/>,否則為 <see langword="false"/>。</returns>
public bool TryGetCommand(string command, out CommandAttribute? cmd)
{
cmd = GetCommand(command);
return cmd is not null;
}
#endregion
#region Public Method : IEnumerable<string> GetCommandNames()
/// <summary>取得所有列管的指令名稱。</summary>
/// <returns>所有列管的指令名稱清單。</returns>
public IEnumerable<string> GetCommandNames()
{
foreach (CommandAttribute ca in _Commands)
yield return ca.FullCommand;
}
#endregion
#region Public Method : string? AnalyzeToFullCommand(string command)
/// <summary>分析輸入的字串,可傳入縮寫指令,會傳回完整指令字串。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <returns>完整指令字串。</returns>
public string? AnalyzeToFullCommand(string command)
{
string res = "";
CommandAttribute? cmd = null;
foreach (string s in SplitCommand(command))
{
if (string.IsNullOrWhiteSpace(s)) continue;
if (string.IsNullOrEmpty(res))
{
var _cas = _Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent) && _ca.Command.StartsWith(s, stringComparison) && (string.IsNullOrEmpty(UseTag) && string.IsNullOrEmpty(_ca.Tag) || !string.IsNullOrEmpty(UseTag) && UseTag.Equals(_ca.Tag)));
if (!_cas.Any()) return null;
if (_cas.Count() == 1)
{
cmd = _cas.First();
res = cmd.Command;
}
else
return null;
}
else
{
var _cc = cmd!.Childs.Where(_FindChild);
if (_cc.Count() == 1)
{
CommandAttribute _ca = _cc.First();
if (_ca.IsRegular)
res += " " + s;
else
res += " " + _ca.Command;
cmd = _ca;
}
else
break;
}
#region Predicate Method : bool _FindChild(CommandAttribute ca)
bool _FindChild(CommandAttribute ca)
{
if (string.IsNullOrEmpty(ca.Parent)) return false;
return ca.IsRegular && Regex.IsMatch(s, $"^{ca.Command}$", regexOptions) || !ca.IsRegular && ca.Command.StartsWith(s, stringComparison);
}
#endregion
}
return res;
}
#endregion
#region Public Method : CommandAttribute? GetParentCommand(CommandAttribute command)
/// <summary>取得上層父指令。</summary>
/// <param name="command">欲取得父指令的指令類別。</param>
/// <returns>取得的上層父指令,如無父指令則回傳 null。</returns>
public CommandAttribute? GetParentCommand(CommandAttribute command)
{
if (string.IsNullOrEmpty(command.Parent)) return null;
return _Commands.FirstOrDefault(_ca => _ca.FullCommand.Equals(command.Parent, stringComparison));
}
#endregion
#region Public Method : string[]? GetSuggestions(string command, int index)
/// <summary>取得指令建議的函示定義。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <param name="index">指令欄位索引值。</param>
/// <returns>建議的指令清單。</returns>
public string[]? GetSuggestions(string command, int index)
{
if (GetCommands(command, true) is IEnumerable<CommandAttribute> cmd)
return cmd.Select(_ca => _ca.Command).ToArray();
else
return null;
}
#endregion
#region Public Method : void ShowHelp(string command, string subCmd = "")
/// <summary>顯示指令說明。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <param name="subCmd">子指令。</param>
public void ShowHelp(string command, string subCmd = "")
{
CommandAttribute? cca = null;
IEnumerable<CommandAttribute>? _cas = null;
if (string.IsNullOrEmpty(command))
{
// 第一層指令
if (string.IsNullOrEmpty(UseTag))
_cas = _Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent) && string.IsNullOrEmpty(_ca.Tag));
else
_cas = _Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent) && _ca.Tag == UseTag);
}
else if (!TryGetCommand(command, out cca))
{
// 找不到特定指令
if (GetCommands(command).Any())
Console.WriteLine($"% Ambiguous command: \"{command}{(string.IsNullOrEmpty(subCmd) ? "" : " " + subCmd.TrimEnd('?'))}\"");
else
Console.WriteLine($"% Unknow command: \"{command}{(string.IsNullOrEmpty(subCmd) ? "" : " " + subCmd.TrimEnd('?'))}\"");
Console.WriteLine("");
return;
}
else if (string.IsNullOrEmpty(subCmd))
_cas = cca!.Childs;
else
_cas = GetChildCommands(cca!, subCmd, true);
int _m = 15;
if (_cas is not null && _cas.FirstOrDefault() is not null)
{
_cas = _cas.Where(_ca => !_ca.Hidden).OrderBy(_ca => _ca.IsRegular ? _ca.RegularHelp : _ca.Command);
_m = _cas.Max(_ca => _ca.IsRegular ? _ca.RegularHelp.Length : _ca.Command.Length);
foreach (CommandAttribute ca in _cas)
{
if (ca.IsRegular)
Console.WriteLine($" {ca.RegularHelp.PadRight(_m + 5)}{ca.HelpText}");
else
Console.WriteLine($" {ca.Command.PadRight(_m + 5)}{ca.HelpText}");
}
}
if (!string.IsNullOrEmpty(command) && !cca!.Childs.Any(_c => _c.Required))
Console.WriteLine($" {"<cr>".PadRight(_m + 5)}<cr>");
Console.WriteLine();
}
#endregion
#region Public Method : void SetCommandSuggestions(Func<string, int, string[]?>? func)
/// <summary>設定指令建議函示。</summary>
/// <param name="func">指令建議含式委派的繫結。</param>
public void SetCommandSuggestions(Func<string, int, string[]?>? func)
{
if (func is null)
_CommandSuggestions = GetSuggestions;
else
_CommandSuggestions = func;
Reader.AutoCompletionHandler = _CommandSuggestions;
}
#endregion
#region Public Method : string ReadLine(string prompt)
/// <summary>讀取一行文字。</summary>
/// <param name="prompt">提示文字。</param>
/// <returns>使用者輸入的文字字串。</returns>
public string ReadLine(string prompt)
{
var res = Reader.Read(prompt, PromptColor);
Reader.RemoveLastHistory();
return res;
}
#endregion
#region Public Method : string ReadPassword(string prompt)
/// <summary>讀取密碼。</summary>
/// <param name="prompt">提示文字。</param>
/// <returns>使用者輸入的密碼。</returns>
public string ReadPassword(string prompt) => Reader.ReadPassword(prompt, PasswordChar);
#endregion
#region Internal Method : Task WorkerProcess(CancellationToken cancellationToken)
/// <summary>指令等待背景執行緒。</summary>
internal Task WorkerProcess(CancellationToken cancellationToken)
{
if (Delay.TotalMilliseconds > 0)
Task.Delay(Delay, cancellationToken).Wait(cancellationToken);
while (!cancellationToken.IsCancellationRequested && Pause)
Task.Delay(100, cancellationToken).Wait(cancellationToken);
if (cancellationToken.IsCancellationRequested)
return Task.CompletedTask;
string? _full;
ConsoleColor _OrigColor = Console.ForegroundColor;
string cmd = Reader.Read(Prompt, PromptColor).Trim();
while (!cancellationToken.IsCancellationRequested)
{
if (string.IsNullOrWhiteSpace(cmd))
{
while (!cancellationToken.IsCancellationRequested && Pause)
Task.Delay(100, cancellationToken).Wait(cancellationToken);
if (cancellationToken.IsCancellationRequested)
break;
cmd = Reader.Read(Prompt, PromptColor).Trim();
continue;
}
OnCommandEntered(cmd);
_full = AnalyzeToFullCommand(cmd);
if (DebugMode)
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m Full Command : \x1B[92m{_full}\x1B[39m");
if (_full is null)
{
if (cmd.EndsWith("?"))
{
// ? / s?
HandleQuestionMark(cmd);
}
else
{
Console.WriteLine($"{"".PadLeft(AnsiRegex.Replace(Prompt, "").Length)}\x1B[91m^\x1B[39m");
Console.WriteLine("% Invalid input detected at '\x1B[91m^\x1B[39m' marker.");
}
}
else if (SplitCommand(cmd).Length != SplitCommand(_full).Length)
{
if (cmd.EndsWith('?'))
HandleQuestionMark(cmd);
else
{
Console.Write("".PadLeft(AnsiRegex.Replace(Prompt, "").Length));
Console.Write("".PadLeft(string.Join(" ", SplitCommand(cmd), 0, SplitCommand(_full).Length).Length + 1));
Console.WriteLine("\x1B[91m^\x1B[39m");
Console.WriteLine("% Invalid input detected at '\x1B[91m^\x1B[39m' marker.");
}
}
else
{
IEnumerable<CommandAttribute> cas = GetCommands(_full);
if (cas.Any())
{
CommandAttribute fca = cas.First();
if (cmd.EndsWith('?'))
HandleQuestionMark(cmd);
else if (cas.Count() == 1)
{
if (DebugMode)
{
Console.Write($"\x1B[90m[DEBUG]\x1B[39m Found Command: ");
if (!fca.IsRegular)
Console.Write($"\x1B[92m{fca.Command}\x1B[39m");
else
Console.Write($"\x1B[93m{fca.RegularHelp}\x1B[39m");
if (!string.IsNullOrEmpty(fca.Tag))
Console.WriteLine($"[{fca.Tag}]");
else
Console.WriteLine();
}
InvokeCommandMethod(_full, fca);
}
else
{
if (DebugMode)
{
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m Ambiguous command: \"\x1B[96m{cmd}\x1B[39m\"");
foreach (CommandAttribute _ca in cas)
{
StringBuilder sb = new StringBuilder();
CommandAttribute? _p = GetParentCommand(_ca);
while (_p is not null)
{
if (_p.IsRegular)
sb.Insert(0, $"{_p.RegularHelp} ");
else
sb.Insert(0, $"{_p.Command} ");
_p = GetParentCommand(_p);
}
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m \x1B[93m{sb}\x1B[39m >> \x1B[96m{_ca.Method!.Name}\x1B[39m");
}
}
Console.WriteLine($"% Ambiguous command: \"\x1B[93m{cmd}\x1B[39m\"");
}
}
else
{
Console.WriteLine($"{"".PadLeft(AnsiRegex.Replace(Prompt, "").Length)}\x1B[91m^\x1B[39m");
Console.WriteLine("% Invalid input detected at '\x1B[91m^\x1B[39m' marker.");
}
}
if (!cancellationToken.IsCancellationRequested)
{
if (Pause)
{
while (!cancellationToken.IsCancellationRequested && Pause)
Task.Delay(100, cancellationToken).Wait(cancellationToken);
if (cancellationToken.IsCancellationRequested)
break;
// 清除輸入緩衝區
while (Console.KeyAvailable)
Console.ReadKey(true);
}
if (cmd.EndsWith('?'))
cmd = Reader.Read(Prompt, PromptColor, cmd.Remove(cmd.Length - 1)).Trim();
else
cmd = Reader.Read(Prompt, PromptColor).Trim();
}
}
Console.ForegroundColor = _OrigColor;
return Task.CompletedTask;
}
#endregion
#region Private Method : bool InvokeCommandMethod(string fullCommand, CommandAttribute cmdAttr)
private bool InvokeCommandMethod(string fullCommand, CommandAttribute cmdAttr)
{
if (cmdAttr.Method is null) return false;
if (OnBeforeMethodExecute(cmdAttr, cmdAttr.Method))
{
object[] args;
var cmds = SplitCommand(fullCommand, true);
var _params = cmdAttr.Method.GetParameters();
if (_params.Length == 3 && _params[0].ParameterType == typeof(CliCenter) && _params[1].ParameterType == typeof(CommandAttribute) && _params[2].ParameterType == typeof(string[]))
args = new object[] { this, cmdAttr, cmds };
else if (_params.Length == 2 && _params[0].ParameterType == typeof(CliCenter) && _params[1].ParameterType == typeof(string[]))
args = new object[] { this, cmds };
else if (_params.Length == 2 && _params[0].ParameterType == typeof(CommandAttribute) && _params[1].ParameterType == typeof(string[]))
args = new object[] { cmdAttr, cmds };
else if (_params.Length == 1 && _params[0].ParameterType == typeof(CliCenter))
args = new object[] { this };
else if (_params.Length == 1 && _params[0].ParameterType == typeof(CommandAttribute))
args = new object[] { cmdAttr };
else if (_params.Length == 1 && _params[0].ParameterType == typeof(string[]))
args = new object[] { cmds };
else
args = Array.Empty<object>();
try
{
if (DebugMode)
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m Executing : \x1B[94m{cmdAttr.Method.Name}\x1B[39m");
if (cmdAttr.Method.IsStatic)
cmdAttr.Method.Invoke(null, args);
else
{
var instance = Activator.CreateInstance(cmdAttr.Method.DeclaringType!);
cmdAttr.Method.Invoke(instance, args);
}
}
catch (TargetException)
{
if (DebugMode)
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m Method \x1B[32m{cmdAttr.Method.Name}\x1B[39m declares error!");
return false;
}
catch (Exception ex)
{
if (DebugMode)
{
Console.WriteLine($"\x1B[90m[DEBUG]\x1B[39m Method \x1B[32m{cmdAttr.Method.Name}\x1B[39m happend error!");
Console.WriteLine(ex);
}
return false;
}
if (!OnAfterMethodExecute(cmdAttr, cmdAttr.Method)) return false;
return true;
}
else
return false;
}
#endregion
#region Private Method : void HandleQuestionMark(string text)
/// <summary>處理問號符號(?)。</summary>
/// <param name="text">當前輸入的完整指令字串,或字尾帶 "?" 的命令。</param>
private void HandleQuestionMark(string text)
{
// Show Help or commands
Reader.RemoveLastHistory();
int _idx = text.LastIndexOf(' ');
if (text.Length == 1)
ShowHelp("");
else if (_idx == -1 || !string.IsNullOrEmpty(text[(_idx + 1)..].TrimEnd('?')))
{
// First Command, exp: sh?
string[]? _cs = null;
if (_CommandSuggestions is not null)
{
_cs = _CommandSuggestions.Invoke(text[..^1], 0);
}
if (_cs is null || _cs.Length == 0)
Console.WriteLine("% Unrecognized command");
else
PrintCommands(_cs);
}
else
{
// Another command, exp: sh ?
ShowHelp(text[.._idx], text[(_idx + 1)..].TrimEnd('?'));
}
}
#endregion
#region Private Method : void Join(Type fromClass, BindingFlags binding)
/// <summary>連結特定類別的與其類型的指令函示。</summary>
/// <param name="fromClass">欲綁定的特定類別類型。</param>
/// <param name="binding">該特定類別函示的繫結旗標。</param>
private void Join(Type fromClass, BindingFlags binding)
{
foreach (MethodInfo mi in fromClass.GetMethods(binding).Where(_m => _m.IsStatic && _m.GetCustomAttributes<CommandAttribute>().Any()))
{
foreach (CommandAttribute cmd in mi.GetCustomAttributes<CommandAttribute>())
{
if (!cmd.IsValid())
throw new CommandArgumentException($"Command: \"{cmd.Command}\" 定義錯誤!");
Append(cmd, mi);
}
}
}
#endregion
#region Private Method : void SetChilds(CommandAttribute cmd)
/// <summary>尋找並設定指令的子指令。</summary>
/// <param name="cmd">欲歸屬的父指令。</param>
private void SetChilds(CommandAttribute cmd)
{
cmd.ClearChilds();
cmd.Level = (string.IsNullOrEmpty(cmd.Parent) ? 0 : SplitCommand(cmd.Parent).Length) + 1;
string _pc = string.IsNullOrEmpty(cmd.Parent) ? cmd.Command : $"{cmd.Parent} {cmd.Command}";
string[] fds = SplitCommand(cmd.FullCommand);
foreach (CommandAttribute ca in _Commands.Where(_Check))
{
if (!cmd.Childs.Contains(ca))
cmd.AddChild(ca);
SetChilds(ca);
}
#region Predicate Method : bool _Check(CommandAttribute _ca)
bool _Check(CommandAttribute _ca)
{
if (cmd.Tag != _ca.Tag || string.IsNullOrEmpty(_ca.Parent))
return false;
string[] _sc = SplitCommand(_ca.Parent);
if (fds.Length != _sc.Length) return false;
for (int i = 0; i < fds.Length; i++)
if (!Regex.IsMatch(fds[i], $"^{_sc[i]}$", regexOptions) && !Regex.IsMatch(_sc[i], $"^{fds[i]}$", regexOptions) && fds[i] != _sc[i]) return false;
return true;
}
#endregion
}
#endregion
#region Private Method : CommandAttribute? GetCommand(string command)
/// <summary>以指令字串取得指令類別。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <returns>找到的指令類別,否則為 null。</returns>
private CommandAttribute? GetCommand(string command)
{
CommandAttribute? cmd = null;
var lca = new List<CommandAttribute>();
if (!command.Contains(' '))
{
CommandAttribute? ca;
IEnumerable<CommandAttribute> _cas = GetCommands(command);
if (_cas.Count() == 1 && _cas.First() is not null)
cmd = _cas.First();
else
{
ca = _cas.FirstOrDefault(_ca => !_ca.IsRegular && _ca.Command.Equals(command, stringComparison));
if (ca is not null)
cmd = ca;
else
{
ca = _cas.FirstOrDefault(_ca => _ca.IsRegular && Regex.IsMatch(command, $"^{_ca.Command}$", regexOptions));
if (ca is not null)
cmd = ca;
}
}
}
else
{
string[] _cs = SplitCommand(command);
if (!TryGetCommand(_cs[0], out CommandAttribute? _fca) || _fca is null) return null;
int idx = 1;
CommandAttribute pca = _fca;
CommandAttribute[] cas;
while (idx < _cs.Length)
{
cas = GetChildCommands(pca, _cs[idx], false).ToArray();
if (cas.Length != 1)
{
CommandAttribute[] _cas = cas.Where(_ca => !_ca.IsRegular && _ca.Command.StartsWith(_cs[idx], stringComparison)).ToArray();
if (_cas.Length == 1)
{
pca = _cas[0];
idx++;
continue;
}
else if (_cas.Length == 0)
{
_cas = cas.Where(_ca => _ca.IsRegular && Regex.IsMatch(_cs[idx], $"^{_ca.Command}$", regexOptions)).ToArray();
if (_cas.Length == 1)
{
pca = _cas[0];
idx++;
continue;
}
else
break;
}
else
break;
}
pca = cas[0];
idx++;
}
cmd = pca;
}
return cmd;
}
#endregion
#region Private Method : IEnumerable<CommandAttribute> GetCommands(string command, bool ignoreRegular = false)
/// <summary>找尋相似的指定清單。</summary>
/// <param name="command">CLI 指令,可使用縮寫指令。</param>
/// <param name="ignoreRegular">是否忽略正規表示式類型的指令類別。</param>
/// <returns>找到的指令清單。</returns>
private IEnumerable<CommandAttribute> GetCommands(string command, bool ignoreRegular = false)
{
var lca = new List<CommandAttribute>();
if (!command.Contains(' '))
{
// 第一層指令
if (string.IsNullOrEmpty(UseTag))
lca.AddRange(_Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent) && _ca.Command.StartsWith(command, stringComparison) && string.IsNullOrEmpty(_ca.Tag)));
else
lca.AddRange(_Commands.Where(_ca => string.IsNullOrEmpty(_ca.Parent) && _ca.Command.StartsWith(command, stringComparison) && _ca.Tag == UseTag));
}
else
{
string[] _cs = SplitCommand(command);
if (!TryGetCommand(_cs[0], out CommandAttribute? _fca) || _fca is null) return Enumerable.Empty<CommandAttribute>();
int idx = 1;
CommandAttribute pca = _fca;
while (idx < _cs.Length)
{
if (!__Append(pca, out CommandAttribute[] _cas))
{
lca.AddRange(_cas);
break;
}
idx++;
foreach (CommandAttribute ca in _cas)
{
if (!__Append(ca, out CommandAttribute[] __cas))
lca.AddRange(__cas);
}
if (idx == _cs.Length - 1)
break;
else if (_cas.Length == 1)
pca = _cas[0];
else
break;
}
bool __Append(CommandAttribute ca, out CommandAttribute[] cas)
{
if (idx == _cs.Length - 1 && ignoreRegular)
cas = GetChildCommands(ca, _cs[idx], false).Where(_ca => !_ca.IsRegular).ToArray();
else
cas = GetChildCommands(ca, _cs[idx], false).ToArray();
return idx != _cs.Length - 1;
}
}
return lca;
}
#endregion
#region Private Static Method : IEnumerable<CommandAttribute> GetChildCommands(CommandAttribute parent, string word)
/// <summary>取得以特定的指令前置字串,找尋符合的子指令。</summary>