[JEDI.NET-commits] main/run Jedi.System.CommandLine.pas,NONE,1.1
Status: Pre-Alpha
Brought to you by:
jedi_mbe
From: Marcel B. <jed...@us...> - 2005-03-01 14:26:17
|
Update of /cvsroot/jedidotnet/main/run In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv1971/main/run Added Files: Jedi.System.CommandLine.pas Log Message: Added: CommandLine parser --- NEW FILE: Jedi.System.CommandLine.pas --- {--------------------------------------------------------------------------------------------------- The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is: Jedi.System.CommandLine.pas, released on 2005-03-01. The Initial Developer of the Original Code is Marcel Bestebroer Portions created by Marcel Bestebroer are Copyright (C) 2004 Marcel Bestebroer All Rights Reserved. Contributor(s): You may retrieve the latest version of this file at the JEDI.NET home page, located at http://sf.net/projects/jedidotnet Known Issues: - Is only able to handle Boolean, Integer (unsigned Int16, Int32 and Int64) and String types. Other types require the user to write their own processing method and register the method as a command line argument. - If an argument starts with any of the starting symbols of the registered switches, an exception is thrown ('unknown argument'). Future version will allow you to specify an option if unknown switches should be ignored or result in an exception thrown. - Boolean type switches not having a + or - behind the option work as a toggle at all times. Future version will allow you to specify if the switch is a toggle or will result in the 'True' value if neither '+' or '-' follows the switch. ---------------------------------------------------------------------------------------------------} // $Id: Jedi.System.CommandLine.pas,v 1.1 2005/03/01 14:25:53 jedi_mbe Exp $ unit Jedi.System.CommandLine; interface {$REGION 'interface uses'} uses Jedi.System.SourceVersioning, Jedi.System.Strings, System.Collections, System.Globalization, System.IO, System.Reflection; {$ENDREGION} {$REGION 'Helper types'} type ObjectArray = array of &Object; StringArray = array of string; {$ENDREGION} {$REGION 'CommandLine exception type'} type [JediSourceInfo( '$RSCfile$', '$Revision: 1.1 $', '$Date: 2005/03/01 14:25:53 $')] CommandLineException = class (SystemException); {$ENDREGION} {$REGION 'Forward declaration of attribute'} type CommandLineArgumentAttribute = class; {$ENDREGION} {$REGION 'Parser'} [JediSourceInfo( '$RSCfile$', '$Revision: 1.1 $', '$Date: 2005/03/01 14:25:53 $')] CommandLine = class {$REGION 'Data'} strict private FArguments: ArrayList; FArgumentStarters: string; FLiterals: ArrayList; FResponseFilePrefix: string; {$ENDREGION} {$REGION 'Constructor'} strict protected constructor Create(arguments: array of &Object; responseFilePrefix: string); {$ENDREGION} {$REGION 'Nested type: Argument class'} protected type Argument = class (&Object, IComparable) {$REGION 'Constructor'} public constructor Create(matches: string; caseSensitive: Boolean; instance: &Object; memberInfo: MemberInfo); {$ENDREGION} {$REGION 'Data'} strict private FCaseSensitive: Boolean; FInstance: &Object; FMatches: string; FMemberInfo: MemberInfo; {$ENDREGION} {$REGION 'IComparable method'} strict protected function CompareTo(obj: &Object): Integer; {$ENDREGION} {$REGION 'Type specific processing methods'} strict protected procedure ProcessBoolean(match, commandLine: string; var index: Integer); procedure ProcessInt(match, commandLine: string; var index: Integer); procedure ProcessString(match, commandLine: string; var index: Integer); {$ENDREGION} {$REGION 'Public method'} public procedure Process(commandLine: string; var index: Integer); {$ENDREGION} {$REGION 'Properties'} public property CaseSensitive: Boolean read FCaseSensitive; property Instance: &Object read FInstance; property Matches: string read FMatches; property MemberInfo: MemberInfo read FMemberInfo; {$ENDREGION} end; {$ENDREGION} {$REGION 'Protected methods'} strict protected procedure AddLiteral(commandLine: string; var index: Integer); function CheckAndProcessArgument(commandLine: string; var index: Integer): Boolean; function CheckAndProcessResponseFile(commandLine: string; var index: Integer): Boolean; function GetLiterals: StringArray; procedure ParseImpl(commandLine: string); procedure RegisterArgument(instance: &Object; memberInfo: MemberInfo; attr: CommandLineArgumentAttribute); overload; procedure RegisterArgument(instance: &Object; memberInfo: MemberInfo; attributes: array of &Object); overload; procedure RegisterInstance(instance: &Object); procedure RegisterInstances(instances: array of &Object); procedure RegisterType(&type: &Type); overload; procedure RegisterType(&type: &Type; instance: &Object); overload; {$ENDREGION} {$REGION 'Public static methods'} public class function GetLiteral(commandLine: string; var index: Integer): string; static; class function Parse(arguments: array of &Object): StringArray; overload; static; class function Parse(arguments: array of &Object; responseFilePrefix: string): StringArray; overload; static; class function Parse(commandLine: string; arguments: array of &Object): StringArray; overload; static; class function Parse(commandLine: string; arguments: array of &Object; responseFilePrefix: string): StringArray; overload; static; {$ENDREGION} end; {$ENDREGION} {$REGION 'Attribute'} [JediSourceInfo( '$RSCfile$', '$Revision: 1.1 $', '$Date: 2005/03/01 14:25:53 $'), AttributeUsage(AttributeTargets.Property or AttributeTargets.Method, AllowMultiple = True, &Inherited = False)] CommandLineArgumentAttribute = class (Attribute) {$REGION 'Data'} strict private FCaseSensitive: Boolean; FNames: ArrayList; FPrefixes: ArrayList; FValueSeparators: ArrayList; {$ENDREGION} {$REGION 'Constructor'} public constructor Create; {$ENDREGION} {$REGION 'Public method'} public function GetMatches: StringArray; {$ENDREGION} {$REGION 'Property access methods'} public function get_Name: string; function get_NameCount: Integer; function get_Names(index: Integer): string; function get_Prefix: string; function get_PrefixCount: Integer; function get_Prefixes(index: Integer): string; function get_ValueSeparator: string; function get_ValueSeparatorCount: Integer; function get_ValueSeparators(index: Integer): string; procedure set_Name(value: string); procedure set_Prefix(value: string); procedure set_ValueSeparator(value: string); {$ENDREGION} {$REGION 'Properties'} public property CaseSensitive: Boolean read FCaseSensitive write FCaseSensitive; property Name: string read get_Name write set_Name; property NameCount: Integer read get_NameCount; property Names[&index: Integer]: string read get_Names; property Prefix: string read get_Prefix write set_Prefix; property PrefixCount: Integer read get_PrefixCount; property Prefixes[&index: Integer]: string read get_Prefixes; property ValueSeparator: string read get_ValueSeparator write set_ValueSeparator; property ValueSeparatorCount: Integer read get_ValueSeparatorCount; property ValueSeparators[&index: Integer]: string read get_ValueSeparators; {$ENDREGION} end; {$ENDREGION} implementation {$AUTOBOX ON} {$REGION 'CommandLine'} constructor CommandLine.Create(arguments: array of &Object; responseFilePrefix: string); begin inherited Create; if &Array(arguments) = nil then raise ArgumentNullException.Create('arguments'); FArguments := ArrayList.Create; if responseFilePrefix.Length > 1 then FArgumentStarters := responseFilePrefix.Chars[0] else FArgumentStarters := ''; FLiterals := ArrayList.Create; FResponseFilePrefix := responseFilePrefix; RegisterInstances(arguments); end; procedure CommandLine.AddLiteral(commandLine: string; var index: Integer); begin FLiterals.Add(GetLiteral(commandLine, index)); end; function CommandLine.CheckAndProcessArgument(commandLine: string; var index: Integer): Boolean; var partToCheck: string; argIdx: Integer; lastCompare: Integer; thisArgument: Argument; begin if FArgumentStarters.IndexOf(commandLine.Chars[index]) >= 0 then begin partToCheck := commandLine.Substring(index); argIdx := FArguments.Count; lastCompare := 1; thisArgument := nil; while (lastCompare > 0) and (argIdx > 0) do begin Dec(argIdx); thisArgument := Argument(FArguments[argIdx]); lastCompare := IComparable(thisArgument).CompareTo(partToCheck); end; Result := lastCompare = 0; if Result then thisArgument.Process(commandLine, index) else raise CommandLineException.Create(System.String.Format('Unknown argument: {0}', GetLiteral(commandLine, index))); end else Result := False; end; function CommandLine.CheckAndProcessResponseFile(commandLine: string; var index: Integer): Boolean; var orgPrefix: string; fileName: string; tr: TextReader; begin if (FResponseFilePrefix <> '') and (System.String.Compare(FResponseFilePrefix, 0, commandLine, index, FResponseFilePrefix.Length, True) = 0) then begin Result := True; orgPrefix := FResponseFilePrefix; try Inc(index, FResponseFilePrefix.Length); FResponseFilePrefix := ''; // disable nested response file processing fileName := GetLiteral(commandLine, index); if fileName = '' then raise CommandLineException.Create('Response file argument: No filename given.'); tr := StreamReader.Create(fileName); try while tr.Peek > -1 do ParseImpl('ignored ' + tr.ReadLine); finally tr.Close; end; finally FResponseFilePrefix := orgPrefix; end; end else Result := False; end; class function CommandLine.GetLiteral(commandLine: string; var index: Integer): string; var startIndex: Integer; lastQuote: Char; quoteIdx: Integer; begin startIndex := index; lastQuote := #0; while (index < commandLine.Length) and ((lastQuote <> #0) or (commandLine.Chars[index] <> ' ')) do begin if (commandLine.Chars[index] = '"') and (lastQuote = #0) then lastQuote := '"' else if (commandLine.Chars[index] = '''') and (lastQuote = #0) then lastQuote := '"' else if (commandLine.Chars[index] = lastQuote) then lastQuote := #0; Inc(index); end; if lastQuote <> #0 then raise CommandLineException.Create('Missing end quote (' + lastQuote + ') in value.'); if index > startIndex then begin Result := StringUtils.ExtractQuotedString(commandLine.Substring(startIndex, index - startIndex), quoteIdx); Inc(quoteIdx, startIndex + 1); if (quoteIdx >= startIndex) and (quoteIdx < index) then Result := Result + commandLine.Substring(quoteIdx, index - quoteIdx) end else Result := ''; while (index < commandLine.Length) and (commandLine.Chars[index] = ' ') do Inc(index); end; function CommandLine.GetLiterals: StringArray; begin Result := StringArray(FLiterals.ToArray(TypeOf(string))); end; class function CommandLine.Parse(arguments: array of &Object): StringArray; begin Result := Parse(Environment.CommandLine, arguments, ''); end; class function CommandLine.Parse(arguments: array of &Object; responseFilePrefix: string): StringArray; begin Result := Parse(Environment.CommandLine, arguments, responseFilePrefix); end; class function CommandLine.Parse(commandLine: string; arguments: array of &Object): StringArray; begin Result := Parse(commandLine, arguments, ''); end; class function CommandLine.Parse(commandLine: string; arguments: array of &Object; responseFilePrefix: string): StringArray; var parser: Jedi.System.CommandLine.CommandLine; begin parser := Jedi.System.CommandLine.CommandLine.Create(arguments, responseFilePrefix); parser.ParseImpl(commandLine); Result := parser.GetLiterals; end; procedure CommandLine.ParseImpl(commandLine: string); var idx: Integer; begin idx := 0; commandLine := commandLine + ' '; GetLiteral(commandLine, idx); // skips the path/file of the running process while idx < commandLine.Length do begin if not CheckAndProcessResponseFile(commandLine, idx) and not CheckAndProcessArgument(commandLine, idx) then AddLiteral(commandLine, idx); end; end; procedure CommandLine.RegisterArgument(instance: &Object; memberInfo: MemberInfo; attr: CommandLineArgumentAttribute); var thisMatch: string; argument: CommandLine.Argument; idx: Integer; begin for thisMatch in attr.GetMatches do begin argument := CommandLine.Argument.Create(thisMatch, attr.CaseSensitive, instance, memberInfo); idx := FArguments.BinarySearch(argument); if idx >= 0 then raise CommandLineException.Create(System.String.Format( 'Duplicate command line argument ({0}) specified by ''{1}.{2}''. The argument has already been assigned to ''{3}.{4}''', [thisMatch, memberInfo.ReflectedType.FullName, memberInfo.Name, CommandLine.Argument(FArguments[idx]).MemberInfo.ReflectedType.FullName, CommandLine.Argument(FArguments[idx]).MemberInfo.Name])); if FArgumentStarters.IndexOf(thisMatch.Chars[0]) = -1 then FArgumentStarters := FArgumentStarters + thisMatch.Chars[0]; FArguments.Insert(not idx, argument); end; end; procedure CommandLine.RegisterArgument(instance: &Object; memberInfo: MemberInfo; attributes: array of &Object); var obj: &Object; begin for obj in attributes do RegisterArgument(instance, memberInfo, CommandLineArgumentAttribute(obj)); end; procedure CommandLine.RegisterInstance(instance: &Object); begin if instance = nil then raise ArgumentNullException.Create('instance'); if TypeOf(&Type).IsInstanceOfType(instance) then RegisterType(&Type(instance)) else RegisterType(instance.GetType, instance); end; procedure CommandLine.RegisterInstances(instances: array of &Object); var instance: &Object; begin for instance in instances do RegisterInstance(instance); end; procedure CommandLine.RegisterType(&type: &Type); begin RegisterType(&type, nil); end; procedure CommandLine.RegisterType(&type: &Type; instance: &Object); var piArray: array of PropertyInfo; pi: PropertyInfo; attrs: array of &Object; miArray: array of MethodInfo; mi: MethodInfo; pmArray: array of ParameterInfo; begin if &type = nil then raise ArgumentNullException.Create('type'); // Retrieve properties... if instance = nil then piArray := &type.GetProperties(BindingFlags.Static or BindingFlags.Public or BindingFlags.NonPublic) else piArray := &type.GetProperties(BindingFlags.Instance or BindingFlags.Public or BindingFlags.NonPublic); for pi in piArray do begin attrs := pi.GetCustomAttributes(TypeOf(CommandLineArgumentAttribute), False); if &Array(attrs).Length > 0 then RegisterArgument(instance, pi, attrs); end; // Retrieve methods... if instance = nil then miArray := &type.GetMethods(BindingFlags.Static or BindingFlags.Public or BindingFlags.NonPublic) else miArray := &type.GetMethods(BindingFlags.Instance or BindingFlags.Public or BindingFlags.NonPublic); for mi in miArray do begin attrs := mi.GetCustomAttributes(TypeOf(CommandLineArgumentAttribute), False); if &Array(attrs).Length > 0 then begin // Check signature is valid pmArray := mi.GetParameters; if &Array(pmArray).Length <> 3 then raise CommandLineException.Create('Method signature incorrect: needs three parameters.'); if not pmArray[2].ParameterType.IsByRef then raise CommandLineException.Create('Method signature incorrect: the final int parameter should be by reference. '); if not TypeOf(string).IsAssignableFrom(pmArray[0].ParameterType) or not TypeOf(string).IsAssignableFrom(pmArray[1].ParameterType) or not TypeOf(Integer).IsAssignableFrom(pmArray[2].ParameterType.GetElementType) then raise CommandLineException.Create('Method signature incorrect: needs two strings and an int parameter.'); RegisterArgument(instance, mi, attrs); end; end; end; {$ENDREGION} {$REGION 'CommandLine.Argument'} constructor CommandLine.Argument.Create(matches: string; caseSensitive: Boolean; instance: &Object; memberInfo: MemberInfo); begin inherited Create; if memberInfo = nil then raise ArgumentNullException.Create('memberInfo'); if matches = '' then raise ArgumentException.Create('Cannot me an empty string', 'matches'); FCaseSensitive := caseSensitive; FInstance := instance; FMatches := matches; FMemberInfo := memberInfo; end; function CommandLine.Argument.CompareTo(obj: &Object): Integer; var arg2: CommandLine.Argument; strPart: string; begin if obj = nil then Result := 1 else if TypeOf(CommandLine.Argument).IsInstanceOfType(obj) then begin arg2 := CommandLine.Argument(obj); Result := System.String.Compare(FMatches, arg2.FMatches, not FCaseSensitive or not arg2.FCaseSensitive); end else if TypeOf(System.String).IsInstanceOfType(obj) then begin strPart := string(obj); if strPart.Length > FMatches.Length then strPart := strPart.Substring(0, FMatches.Length); Result := System.String.Compare(FMatches, strPart, not FCaseSensitive); end else raise ArgumentException.Create('objects cannot be compared.'); end; procedure CommandLine.Argument.Process(commandLine: string; var index: Integer); var match: string; pi: PropertyInfo; params: array of &Object; begin match := commandLine.Substring(index, FMatches.Length); Inc(index, FMatches.Length); if FMatches.EndsWith(' ') then while (index < commandLine.Length) and (commandLine.Chars[index] = ' ') do Inc(index); pi := PropertyInfo(FMemberInfo); if pi = nil then begin params := ObjectArray.Create(match, commandLine, index); MethodInfo(FMemberInfo).Invoke(FInstance, params); index := Integer(params[2]); end else if TypeOf(System.Boolean).IsAssignableFrom(pi.PropertyType) then ProcessBoolean(match, commandLine, index) else if TypeOf(System.Int16).IsAssignableFrom(pi.PropertyType) or TypeOf(System.Int32).IsAssignableFrom(pi.PropertyType) or TypeOf(System.Int64).IsAssignableFrom(pi.PropertyType) then ProcessInt(match, commandLine, index) else if TypeOf(System.String).IsAssignableFrom(pi.PropertyType) then ProcessString(match, commandLine, index) end; procedure CommandLine.Argument.ProcessBoolean(match, commandLine: string; var index: Integer); var value: string; begin value := Jedi.System.CommandLine.CommandLine.GetLiteral(commandLine, index); if value = '+' then PropertyInfo(FMemberInfo).SetValue(FInstance, True, []) else if value = '-' then PropertyInfo(FMemberInfo).SetValue(FInstance, False, []) else if value = '' then PropertyInfo(FMemberInfo).SetValue(FInstance, not Boolean(PropertyInfo(FMemberInfo).GetValue(FInstance, [])), []) else raise CommandLineException.Create(System.String.Format('Invalid boolean value. Argument: {0}. Value: {1}', Matches, value)); end; procedure CommandLine.Argument.ProcessInt(match, commandLine: string; var index: Integer); var value: string; pi: PropertyInfo; flags: BindingFlags; begin value := Jedi.System.CommandLine.CommandLine.GetLiteral(commandLine, index); pi := PropertyInfo(FMemberInfo); if FInstance = nil then flags := BindingFlags.Static else flags := BindingFlags.Instance; flags := flags or BindingFlags.Public or BindingFlags.NonPublic or BindingFlags.InvokeMethod; pi.SetValue( FInstance, pi.PropertyType.InvokeMember('Parse', flags, nil, FInstance, [value, NumberFormatInfo.InvariantInfo], [], CultureInfo.InvariantCulture, ['s', 'provider']), []); end; procedure CommandLine.Argument.ProcessString(match, commandLine: string; var index: Integer); begin PropertyInfo(FMemberInfo).SetValue(FInstance, Jedi.System.CommandLine.CommandLine.GetLiteral(commandLine, index), []); end; {$ENDREGION} {$REGION 'CommandLineArgumentAttribute'} constructor CommandLineArgumentAttribute.Create; begin inherited Create; FNames := ArrayList.Create; FPrefixes := ArrayList.Create; FValueSeparators := ArrayList.Create; end; function CommandLineArgumentAttribute.GetMatches: StringArray; var tmpPrefixes: ArrayList; tmpSeparators: ArrayList; matchList: ArrayList; thisPrefix: string; thisName: string; thisSeparator: string; begin tmpPrefixes := ArrayList.Create(FPrefixes); if tmpPrefixes.Count = 0 then tmpPrefixes.Add(''); tmpSeparators := ArrayList.Create(FValueSeparators); if tmpSeparators.Count = 0 then tmpSeparators.Add(''); matchList := ArrayList.Create(tmpPrefixes.Count * FNames.Count * tmpSeparators.Count); for thisPrefix in tmpPrefixes do for thisName in FNames do for thisSeparator in tmpSeparators do matchList.Add(System.String.Format('{0}{1}{2}', thisPrefix, thisName, thisSeparator)); Result := StringArray(matchList.ToArray(TypeOf(string))); end; function CommandLineArgumentAttribute.get_Name: string; begin if FNames.Count = 0 then Result := '' else Result := string(FNames[FNames.Count - 1]); end; function CommandLineArgumentAttribute.get_NameCount: Integer; begin Result := FNames.Count; end; function CommandLineArgumentAttribute.get_Names(index: Integer): string; begin Result := string(FNames[index]); end; function CommandLineArgumentAttribute.get_Prefix: string; begin if FPrefixes.Count = 0 then Result := '' else Result := string(FPrefixes[FPrefixes.Count - 1]); end; function CommandLineArgumentAttribute.get_PrefixCount: Integer; begin Result := FPrefixes.Count; end; function CommandLineArgumentAttribute.get_Prefixes(index: Integer): string; begin Result := string(FPrefixes[index]); end; function CommandLineArgumentAttribute.get_ValueSeparator: string; begin if FValueSeparators.Count = 0 then Result := '' else Result := string(FValueSeparators[FValueSeparators.Count - 1]); end; function CommandLineArgumentAttribute.get_ValueSeparatorCount: Integer; begin Result := FValueSeparators.Count; end; function CommandLineArgumentAttribute.get_ValueSeparators(index: Integer): string; begin Result := string(FValueSeparators[index]); end; procedure CommandLineArgumentAttribute.set_Name(value: string); begin FNames.Add(value); end; procedure CommandLineArgumentAttribute.set_Prefix(value: string); begin FPrefixes.Add(value); end; procedure CommandLineArgumentAttribute.set_ValueSeparator(value: string); begin FValueSeparators.Add(value); end; {$ENDREGION} end. |