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