1 #Region "Microsoft.VisualBasic::be3eecb4606c697613eff4ec551101a2, Microsoft.VisualBasic.Core\CommandLine\CLITools.vb"
2
3     ' Author:
4     
5     '       asuka (amethyst.asuka@gcmodeller.org)
6     '       xie (genetics@smrucc.org)
7     '       xieguigang (xie.guigang@live.com)
8     
9     ' Copyright (c) 2018 GPL3 Licensed
10     
11     
12     ' GNU GENERAL PUBLIC LICENSE (GPL3)
13     
14     
15     ' This program is free software: you can redistribute it and/or modify
16     ' it under the terms of the GNU General Public License as published by
17     ' the Free Software Foundation, either version 3 of the License, or
18     ' (at your option) any later version.
19     
20     ' This program is distributed in the hope that it will be useful,
21     ' but WITHOUT ANY WARRANTY; without even the implied warranty of
22     ' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23     ' GNU General Public License for more details.
24     
25     ' You should have received a copy of the GNU General Public License
26     ' along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28
29
30     ' /********************************************************************************/
31
32     ' Summaries:
33
34     '     Module CLITools
35     
36     '         Function: __checkKeyDuplicated, Args, CreateObject, CreateParameterValues, Equals
37     '                   GetLogicalFlags, GetTokens, IsPossibleLogicFlag, Join, makesureQuot
38     '                   Print, ShellExec, SingleValueOrStdIn, TrimParamPrefix, (+3 OverloadsTryParse
39     
40     '         Sub: tupleParser
41     
42     
43     ' /********************************************************************************/
44
45 #End Region
46
47 Imports System.IO
48 Imports System.Runtime.CompilerServices
49 Imports System.Text
50 Imports System.Text.RegularExpressions
51 Imports Microsoft.VisualBasic.ApplicationServices.Terminal
52 Imports Microsoft.VisualBasic.CommandLine.Reflection
53 Imports Microsoft.VisualBasic.ComponentModel.DataSourceModel
54 Imports Microsoft.VisualBasic.Language
55 Imports Microsoft.VisualBasic.Linq
56 Imports Microsoft.VisualBasic.Scripting.MetaData
57 Imports Microsoft.VisualBasic.Text
58 Imports StringList = System.Collections.Generic.IEnumerable(Of String)
59 Imports ValueTuple = System.Collections.Generic.KeyValuePair(Of StringString)
60
61 Namespace CommandLine
62
63     ''' <summary>
64     ''' CLI parser and <see cref="CommandLine"/> object creates.
65     ''' </summary>
66     <Package("CommandLine",
67                         Url:="http://gcmodeller.org",
68                         Publisher:="xie.guigang@gcmodeller.org",
69                         Description:="",
70                         Revision:=52)>
71     Public Module CLITools
72
73         <Extension>
74         Public Function ShellExec(cli As IIORedirectAbstract) As String
75             Call cli.Run()
76             Return cli.StandardOutput
77         End Function
78
79         <Extension>
80         Public Function Print(args As CommandLine, Optional sep As Char = " "c, Optional leftMargin% = 0) As String
81             Dim sb As New StringBuilder("ArgumentsOf: `" & args.Name & "`")
82             Dim device As New StringWriter(sb)
83
84             Call device.WriteLine()
85             Call device.WriteLine(New String("-"c, args.Name.Length * 4))
86             Call device.WriteLine()
87
88             Call args _
89                 .ToArgumentVector _
90                 .Print(
91                     device,
92                     sep,
93                     trilinearTable:=True,
94                     leftMargin:=leftMargin)
95
96             Return sb.ToString
97         End Function
98
99         ''' <summary>
100         ''' Parsing parameters from a specific tokens.
101         ''' (从给定的词组之中解析出参数的结构)
102         ''' </summary>
103         ''' <param name="tokens">个数为偶数的,但是假若含有开关的时候,则可能为奇数了</param>
104         ''' <param name="IncludeLogicSW">返回来的列表之中是否包含有逻辑开关</param>
105         ''' <returns></returns>
106         ''' <remarks></remarks>
107         <Extension> Public Function CreateParameterValues(tokens$(), IncludeLogicSW As BooleanOptional note$ = NothingAs List(Of NamedValue(Of String))
108             Dim list As New List(Of NamedValue(Of String))
109
110             If tokens.IsNullOrEmpty Then
111                 Return list
112             ElseIf tokens.Length = 1 Then
113
114                 If IsPossibleLogicFlag(tokens(Scan0)) AndAlso
115                     IncludeLogicSW Then
116
117                     list += New NamedValue(Of StringWith {
118                         .Name = tokens(Scan0),
119                         .Value = CStr(True),
120                         .Description = note
121                     }
122                 Else
123                     Return list
124                 End If
125             End If
126
127             ' 下面都是多余或者等于两个元素的情况
128             ' 数目多于一个的
129             For i As Integer = 0 To tokens.Length - 1
130                 Dim [next] As Integer = i + 1
131
132                 If [next] = tokens.Length Then
133                     ' 这个元素是开关,已经到达最后则没有了,跳出循环
134                     If IsPossibleLogicFlag(tokens(i)) AndAlso IncludeLogicSW Then
135                         list += New NamedValue(Of String)(tokens(i), True, note)
136                     End If
137
138                     Exit For
139                 End If
140
141                 Dim s As String = tokens([next])
142
143                 ' 当前的这个元素是开关,下一个也是开关开头,则本元素肯定是一个开关
144                 If IsPossibleLogicFlag(s) Then
145                     If IncludeLogicSW Then
146                         list += New NamedValue(Of String)(tokens(i), True, note)
147                     End If
148
149                     Continue For
150                 Else
151                     ' 下一个元素不是开关,则当前元素为一个参数名,则跳过下一个元素
152                     Dim key As String = tokens(i).ToLower
153                     list += New NamedValue(Of String)(key, s, note)
154
155                     i += 1
156                 End If
157             Next
158
159             Return list
160         End Function
161
162         ''' <summary>
163         ''' Get all of the logical parameters from the input tokens.
164         ''' (这个函数所生成的逻辑参数的名称全部都是小写形式的)
165         ''' </summary>
166         ''' <param name="args">要求第一个对象不能够是命令的名称</param>
167         ''' <returns></returns>
168         <Extension> Public Function GetLogicalFlags(args As IEnumerable(Of String), ByRef SingleValue$) As String()
169             Dim tokens$() = args.SafeQuery.ToArray
170
171             If tokens.IsNullOrEmpty Then
172                 Return New String() {}
173             ElseIf tokens.Length = 1 Then
174                 ' 只有一个元素,则肯定为开关
175                 Return {tokens(0).ToLower}
176             End If
177
178             Dim tkList As New List(Of String)
179
180             For i As Integer = 0 To tokens.Length - 1 '数目多于一个的
181                 Dim next% = i + 1
182
183                 If [next] = tokens.Length Then
184                     If IsPossibleLogicFlag(obj:=tokens(i)) Then
185                         tkList += tokens(i)  '                     End If
186
187                     Exit For
188                 End If
189
190                 Dim s As String = tokens([next])
191
192                 If IsPossibleLogicFlag(obj:=s) Then  '当前的这个元素是开关,下一个也是开关开头,则本元素肯定是一个开关
193                     If IsPossibleLogicFlag(obj:=tokens(i)) Then
194                         tkList += tokens(i)
195                     Else
196
197                         If i = 0 Then
198                             SingleValue = tokens(i)
199                         End If
200
201                     End If
202                 Else  '下一个元素不是开关,则当前元素为一个参数名,则跳过下一个元素
203                     i += 1
204                 End If
205
206             Next
207
208             Return (From s As String In tkList Select s.ToLower).ToArray
209         End Function
210
211         ''' <summary>
212         ''' Try parsing the cli command string from the string value.(尝试着从文本行之中解析出命令行参数信息)
213         ''' </summary>
214         ''' <param name="args">The commandline arguments which is user inputs from the terminal.</param>
215         ''' <param name="duplicatedAllows">Allow the duplicated command parameter argument name in the input, 
216         ''' default is not allowed the duplication.(是否允许有重复名称的参数名出现,默认是不允许的)</param>
217         ''' <returns></returns>
218         ''' <remarks></remarks>
219         <ExportAPI("TryParse"Info:="Try parsing the cli command String from the String value.")>
220         <Extension>
221         Public Function TryParse(args As StringList,
222                                  Optional duplicatedAllows As Boolean = False,
223                                  Optional rawInput$ = NothingAs CommandLine
224
225             Dim tokens$() = args.SafeQuery.ToArray
226             Dim singleValue$ = ""
227
228             If tokens.Length = 0 Then
229                 Return New CommandLine
230             End If
231
232             Dim bools$() = tokens _
233                 .Skip(1) _
234                 .GetLogicalFlags(singleValue)
235             Dim cli As New CommandLine With {
236                 .Name = tokens(Scan0).ToLower,
237                 .Tokens = tokens,
238                 .BoolFlags = bools,
239                 .cliCommandArgvs = Join(tokens)
240             }
241
242             cli.SingleValue = singleValue
243             cli.cliCommandArgvs = rawInput
244
245             If cli.Parameters.Length = 1 AndAlso
246                 String.IsNullOrEmpty(cli.SingleValue) Then
247
248                 cli.SingleValue = cli.Parameters(0)
249             End If
250
251             If tokens.Length > 1 Then
252                 cli.__arguments = tokens.Skip(1).ToArray.CreateParameterValues(False)
253
254                 Dim Dk As String() = __checkKeyDuplicated(cli.__arguments)
255
256                 If Not duplicatedAllows AndAlso Not Dk.IsNullOrEmpty Then
257                     Dim Key$ = String.Join(", ", Dk)
258                     Dim msg$ = String.Format(KeyDuplicated, Key, String.Join(" ", tokens.Skip(1).ToArray))
259
260                     Throw New Exception(msg)
261                 End If
262             End If
263
264             Return cli
265         End Function
266
267         Const KeyDuplicated As String = "The command line switch key ""{0}"" Is already been added! Here Is your input data:  CMD {1}."
268
269         Private Function __checkKeyDuplicated(source As IEnumerable(Of NamedValue(Of String))) As String()
270             Dim LQuery = (From param As NamedValue(Of String)
271                           In source
272                           Select param.Name.ToLower
273                           Group By ToLower Into Group).ToArray
274
275             Return LinqAPI.Exec(Of String) _
276  _
277                 () <= From group
278                       In LQuery
279                       Where group.Group.Count > 1
280                       Select group.ToLower
281         End Function
282
283         ''' <summary>
284         ''' Gets the commandline object for the current program.
285         ''' </summary>
286         ''' <returns></returns>
287         <ExportAPI("args"Info:="Gets the commandline object for the current program.")>
288         <MethodImpl(MethodImplOptions.AggressiveInlining)>
289         Public Function Args() As CommandLine
290             Return App.CommandLine
291         End Function
292
293         ''' <summary>
294         ''' Try parsing the cli command string from the string value.
295         ''' (尝试着从文本行之中解析出命令行参数信息,假若value里面有空格,则必须要将value添加双引号)
296         ''' </summary>
297         ''' <param name="CLI">The commandline arguments which is user inputs from the terminal.</param>
298         ''' <param name="duplicateAllowed">Allow the duplicated command parameter argument name in the input, 
299         ''' default is not allowed the duplication.(是否允许有重复名称的参数名出现,默认是不允许的)</param>
300         ''' <returns></returns>
301         ''' <remarks></remarks>
302         ''' 
303         <ExportAPI("TryParse"Info:="Try parsing the cli command String from the String value.")>
304         Public Function TryParse(<Parameter("CLI", "The CLI arguments that inputs from the console by user.")> CLI$,
305                                  <Parameter("Duplicates.Allowed")> Optional duplicateAllowed As Boolean = FalseAs CommandLine
306
307             If String.IsNullOrEmpty(CLI) Then
308                 Return New CommandLine
309             Else
310 #Const DEBUG = False
311 #If DEBUG Then
312                 Call CLI.__DEBUG_ECHO
313 #End If
314             End If
315
316             Dim args As CommandLine = CLITools _
317                 .GetTokens(CLI) _
318                 .TryParse(duplicateAllowed, rawInput:=CLI)
319
320             Return args
321         End Function
322
323         ''' <summary>
324         ''' Is this string tokens is a possible <see cref="Boolean"/> value flag
325         ''' </summary>
326         ''' <param name="obj"></param>
327         ''' <returns></returns>
328         <ExportAPI("IsPossibleBoolFlag?")>
329         Public Function IsPossibleLogicFlag(obj As StringAs Boolean
330             If obj.Contains(" "Then
331                 Return False
332             End If
333             If IsNumeric(obj) Then
334                 Return False
335             End If
336
337             ' Linux上面全路径总是从/,即根目录开始的
338             If obj.Count("/"c) > 1 Then
339                 Return False
340             End If
341
342             Return obj.StartsWith("-"OrElse
343                 obj.StartsWith("/")
344         End Function
345
346         ''' <summary>
347         ''' ReGenerate the cli command line argument string text.(重新生成命令行字符串)
348         ''' </summary>
349         ''' <param name="tokens">If the token value have a space character, then this function will be wrap that token with quot character automatically.</param>
350         ''' <returns></returns>
351         ''' <remarks></remarks>
352         ''' 
353         <ExportAPI("Join",
354                    Info:="ReGenerate the cli command line argument string text. 
355                    NOTE: If the token have a space character, then this function will be wrap that token with quot character automatically.")>
356         Public Function Join(tokens As IEnumerable(Of String)) As String
357             If tokens Is Nothing Then
358                 Return ""
359             Else
360                 Return tokens _
361                     .Select(AddressOf makesureQuot) _
362                     .JoinBy(" ")
363             End If
364         End Function
365
366         Private Function makesureQuot(token As StringAs String
367             If InStr(token, " ") > 0 Then
368                 If token.First = """"AndAlso token.Last = """"Then
369                     Return token
370                 Else
371                     Return $"""{token}"""
372                 End If
373             Else
374                 Return token
375             End If
376         End Function
377
378         ''' <summary>
379         ''' A regex expression string that use for split the commandline text.
380         ''' (用于分析命令行字符串的正则表达式)
381         ''' </summary>
382         ''' <remarks></remarks>
383         Public Const SPLIT_REGX_EXPRESSION As String = "\s+(?=(?:[^""]|""[^""]*"")*$)"
384
385         ''' <summary>
386         ''' Try parse the argument tokens which comes from the user input commandline string. 
387         ''' (尝试从用户输入的命令行字符串之中解析出所有的参数)
388         ''' </summary>
389         ''' <param name="CLI"></param>
390         ''' <returns></returns>
391         ''' <remarks></remarks>
392         ''' 
393         <ExportAPI("Tokens")>
394         Public Function GetTokens(CLI As StringAs String()
395             If String.IsNullOrEmpty(CLI) Then
396                 Return New String() {""}
397             Else
398                 CLI = CLI.Trim
399             End If
400
401             ' 由于在前面App位置已经将应用程序的路径去除了,所以这里已经不需要了,只需要直接解析即可
402
403             'If Not Environment.OSVersion.Platform = PlatformID.Win32NT Then
404             '    'LINUX下面的命令行会将程序集的完整路径也传递进来
405             '    Dim l As Integer = Len(Application.ExecutablePath)
406             '    CLI = Mid(CLI, l + 2).Trim
407
408             '    If String.IsNullOrEmpty(CLI) Then  '在linux下面没有传递进来任何参数,则返回空集合
409             '        Return New String() {""}
410             '    End If
411             'End If
412
413             Dim tokens$() = Regex.Split(CLI, SPLIT_REGX_EXPRESSION)
414             Dim argv As New List(Of String)
415
416             tokens = tokens _
417                 .TakeWhile(Function(Token)
418                                Return Not String.IsNullOrEmpty(Token.Trim)
419                            End Function) _
420                 .ToArray
421
422             For i As Integer = 0 To tokens.Length - 1
423                 Dim s As String = tokens(i)
424
425                 ' 消除单词单元中的双引号
426                 If s.First = ASCII.Quot AndAlso s.Last = ASCII.Quot Then
427                     s = Mid(s, 2, Len(s) - 2)
428                 End If
429
430                 ' argv='dddddd'
431                 ' 键值对语法
432                 If s.Contains("="c) AndAlso Not s.IsURLPattern Then
433                     If i > 0 AndAlso tokens(i - 1).TextEquals("/@set"Then
434                         ' 在这里是环境变量,不需要进行解析
435                         argv += s
436                     Else
437                         Call s.tupleParser(argv)
438                     End If
439                 Else
440                     argv += s
441                 End If
442             Next
443
444             Return argv
445         End Function
446
447         ''' <summary>
448         ''' 只取第一个=符号出现的位置,结果会被添加进入<paramref name="argv"/>列表之中
449         ''' </summary>
450         ''' <param name="s$"></param>
451         ''' <param name="argv"></param>
452         <Extension>
453         Private Sub tupleParser(s$, ByRef argv As List(Of String))
454             Dim splitIndex% = -1
455
456             For j As Integer = 0 To s.Length - 1
457                 Dim c = s(j)
458
459                 If c = "="AndAlso splitIndex = -1 Then
460                     ' 如果前一个字符是\转义,则不是键值对
461                     If j > 1 AndAlso s(j - 1) <> "\"Then
462                         ' 这是第一个符号
463                         splitIndex = j
464                         Exit For
465                     Else
466                         splitIndex = -1
467                     End If
468                 End If
469             Next
470
471             If splitIndex > -1 Then
472                 Dim name$ = s.Substring(0, splitIndex)
473                 Dim value = s.Substring(splitIndex + 1)
474
475                 argv += name
476                 argv += value
477             Else
478                 argv += s
479             End If
480         End Sub
481
482         ''' <summary>
483         ''' 会对%进行替换的
484         ''' </summary>
485         Const TokenSplitRegex As String = "(?=(?:[^%]|%[^%]*%)*$)"
486
487         ''' <summary>
488         ''' 尝试从输入的语句之中解析出词法单元,注意,这个函数不是处理从操作系统所传递进入的命令行语句
489         ''' </summary>
490         ''' <param name="CommandLine"></param>
491         ''' <returns></returns>
492         ''' <remarks></remarks>
493         ''' 
494         <ExportAPI("TryParse")>
495         Public Function TryParse(CommandLine As StringTokenDelimited As StringInnerDelimited As CharAs String()
496             If String.IsNullOrEmpty(CommandLine) Then
497                 Return New String() {""}
498             End If
499
500             Dim regxPattern$ = TokenDelimited & TokenSplitRegex.Replace("%"c, InnerDelimited)
501             Dim tokens = Regex.Split(CommandLine, regxPattern)
502
503             For i As Integer = 0 To tokens.Length - 1
504                 Dim s As String = tokens(i)
505
506                 If s.First = InnerDelimited AndAlso s.Last = InnerDelimited Then    '消除单词单元中的双引号
507                     tokens(i) = Mid(s, 2, Len(s) - 2)
508                 End If
509             Next
510
511             Return tokens
512         End Function
513
514         ''' <summary>
515         ''' Creates command line object from a set obj <see cref="KeyValuePair(Of StringString)"/>
516         ''' </summary>
517         ''' <param name="name"></param>
518         ''' <param name="args"></param>
519         ''' <param name="bFlags"></param>
520         ''' <returns></returns>
521         <ExportAPI("CreateObject")>
522         Public Function CreateObject(name$, args As IEnumerable(Of ValueTuple), Optional bFlags As IEnumerable(Of String) = NothingAs CommandLine
523             Dim parameters As New List(Of NamedValue(Of String))
524             Dim tokens As New List(Of String) From {name}
525
526             For Each tuple As ValueTuple In args
527                 Dim key As String = tuple.Key.ToLower
528                 Dim param As New NamedValue(Of String)(key, tuple.Value)
529
530                 Call parameters.Add(param)
531                 Call tokens.AddRange(New String() {key, tuple.Value})
532             Next
533
534             Return New CommandLine With {
535                 .Name = name,
536                 .__arguments = parameters,
537                 .Tokens = tokens.Join(bFlags).ToArray,
538                 .BoolFlags = bFlags.SafeQuery.ToArray
539             }
540         End Function
541
542         ''' <summary>
543         ''' Trim the CLI argument name its prefix symbols.
544         ''' (修剪命令行参数名称的前置符号)
545         ''' </summary>
546         ''' <param name="argName"></param>
547         ''' <returns></returns>
548         <ExportAPI("Trim.Prefix.BoolFlag")>
549         <Extension>
550         Public Function TrimParamPrefix(argName$) As String
551             If argName.StartsWith("--"Then
552                 Return Mid(argName, 3)
553             ElseIf argName.StartsWith("-"OrElse argName.StartsWith("\"OrElse argName.StartsWith("/"Then
554                 Return Mid(argName, 2)
555             Else
556                 Return argName
557             End If
558         End Function
559
560         ''' <summary>
561         ''' 请注意,这个是有方向性的,由于是依照参数1来进行比较的,假若args2里面的参数要多于第一个参数,但是第一个参数里面的所有参数值都可以被参数2完全比对得上的话,就认为二者是相等的
562         ''' </summary>
563         ''' <param name="args1"></param>
564         ''' <param name="args2"></param>
565         ''' <returns></returns>
566         ''' 
567         <ExportAPI("CLI.Equals")>
568         Public Function Equals(args1 As CommandLine, args2 As CommandLine) As Boolean
569             If Not String.Equals(args1.Name, args2.Name, StringComparison.OrdinalIgnoreCase) Then
570                 Return False
571             End If
572
573             For Each bFlag As String In args1.BoolFlags
574                 If Not args2.GetBoolean(bFlag) Then
575                     Return False
576                 End If
577             Next
578
579             For Each arg As NamedValue(Of StringIn args1.__arguments
580                 Dim value2 As String = args2(arg.Name)
581
582                 If Not String.Equals(value2, arg.Value, StringComparison.OrdinalIgnoreCase) Then
583                     Return False
584                 End If
585             Next
586
587             Return True
588         End Function
589
590         <Extension>
591         Public Function SingleValueOrStdIn(args As CommandLine) As String
592             If Not String.IsNullOrEmpty(args.SingleValue) Then
593                 Return args.SingleValue
594             Else
595                 Dim reader As New StreamReader(Console.OpenStandardInput)
596                 Return reader.ReadToEnd
597             End If
598         End Function
599     End Module
600 End Namespace