September 2016

Band 31, Nummer 9

Essential .NET – Verarbeitung auf der Befehlszeile mit .NET Core 1.0

Von Mark Michaelis

Mark MichaelisIn der Essential .NET-Kolumne für diesen Monat setze ich meine Untersuchung der verschiedenen Features von .NET Core fort, diesmal mit einer veröffentlichten Version (kein Betastatus oder ein freundlich so bezeichneter „Release Candidate“). Mein Schwerpunkt liegt auf den Befehlszeilenhilfsprogrammen (die sich in der .NET Core Common-Bibliothek unter github.com/aspnet/Common befinden) und ihrer Nutzung zum Analysieren einer Befehlszeile. Ich muss gestehen, dass mich insbesondere die in .NET Core integrierte Unterstützung für die Analyse auf der Befehlszeile interessiert, denn das ist etwas, das ich mir schon seit .NET Framework 1.0 gewünscht habe. Ich hoffe, dass eine in .NET Core integrierte Bibliothek dabei helfen kann, die Befehlszeilenformate/-strukturen zwischen den Programmen wenigstens ein Stück weit zu vereinheitlichen. Dabei ist mit nicht unbedingt wichtig, wie dieser Standard aussieht, solange es eine Übereinkunft gibt, an die sich Menschen halten, statt immer neue und andere Lösungen zu entwickeln.

Eine Konvention für die Befehlszeile

Der größte Teil der Befehlszeilenfunktionalität befindet sich im NuGet-Paket „Microsoft.Extensions.CommandLineUtils“. Enthalten in der Assembly ist eine CommandLineApplication-Klasse, die Analyse auf der Befehlszeile bereitstellt, mit Unterstützung für kurze und lange Namen für Optionen, Werte (einen oder mehrere), die entweder mit Doppelpunkt oder Gleichheitszeichen zugewiesen werden, und Symbole wie -? für die Hilfe. Da wir bei der Hilfe sind: Die Klasse beinhaltet auch Unterstützung für die automatische Anzeige des Hilfetexts. Abbildung 1 zeigt einige Beispielbefehlszeilen, die unterstützt werden.

Abbildung 1 Beispielbefehlszeilen

Optionen Program.exe -f=Inigo, -l Montoya –hello –names Princess –names Buttercup

Option -f mit Wert „Inigo“

Option -l mit Wert „Montoya“

Option –hello mit Wert „on“

Option –names mit Werten „Princess“ und „Buttercup“

Befehle mit Argumenten Program.exe „hello“, „Inigo“, „Montoya“, „Ist“, „mir“, „ein“, „Vergnügen“, „Sie“, „zu“, „treffen“.

Befehl „hello“

Argument „Inigo“

Argument „Montoya“

Argument „Greetings“ mit den Werten „Ist“, „mir“. „ein“, „Vergnügen“, „Sie“, „zu“, „treffen“.

Symbole Program.exe -? Hilfe anzeigen

Wie im weiteren Verlauf beschrieben, gibt es mehrere Argumenttypen, von denen einer als „ARGUMENT“ bezeichnet wird. Die Überladung des Ausdrucks „Argument“, um auf die auf der Befehlszeile angegebenen Werte im Gegensatz zu den Konfigurationsdaten der Befehlszeile zu verweisen, kann zu erheblicher Mehrdeutigkeit führen. Im restlichen Artikel unterscheide ich daher zwischen generischen Argumenten beliebiger Art – die nach dem Namen der ausführbaren Datei angegeben werden – und dem Argumenttyp namens „ARGUMENT“ (in Versalien) durch die Großschreibung. In analoger Weise unterscheide ich die anderen Argumenttypen OPTION und BEFEHL durch Schreibung in Versalien gegenüber der normalen Schreibung, die für den allgemeinen Bezug auf das Argument verwendet wird. Bitte beachten Sie diesen Hinweis, da er für das Verständnis des restlichen Artikels wichtig ist.

Jeder der Argumenttypen ist wie folgt beschrieben:

  • Optionen: Optionen werden durch einen Namen bezeichnet, und dem Namen steht entweder ein einzelner (-) oder ein doppelter Bindestrich (--) voran. Namen von Optionen werden programmtechnisch mithilfe von Vorlagen definiert, und eine Vorlage kann einen oder mehrere der folgenden Kennzeichner enthalten: „short name“, „long name“, „symbol“. Darüber hinaus kann einer OPTION ein Wert zugeordnet sein. Eine Vorlage kann beispielsweise „-n | --name | -# <Vollständiger Name>“ sein, wobei die Option für den vollständigen Namen mit jedem der drei Kennzeichner angegeben werden kann. (Die Vorlage benötigt aber nicht alle drei Kennzeichner.) Beachten Sie, dass die Verwendung des einzelnen oder doppelten Bindestrichs angibt, ob ein kurzer oder langer Name angegeben wird, unabhängig von der tatsächlichen Länge des Namens.
    Um einer Option einen Wert zuzuweisen, können Sie entweder ein Leerzeichen oder den Zuweisungsoperator (=) verwenden. „-f=Inigo“ und „-l Montoya“ sind beides gültige Beispiele für das Angeben eines Optionswerts.
    Wenn Zahlen in der Vorlage verwendet werden, sind sie als Teil von langen oder kurzen Namen anzusehen, nicht als Symbol.
  • Argumente: Argumente werden durch die Reihenfolge ihres Auftretens anstelle eines Namens identifiziert. Ein Wert auf der Befehlszeile, dem kein Optionsname voransteht, ist also ein Argument. Welchem Argument der Wert entspricht, wird auf der Grundlage der Reihenfolge ermittelt, in der er auftritt (Optionen und Befehle werden in der Zählung nicht berücksichtigt).
  • Befehle: Befehle stellen eine Gruppierung von Argumenten und Optionen bereit. Beispielsweise können Sie einen Befehlsnamen „hello“ verwenden, auf den eine Kombination von ARGUMENTEN und OPTIONEN folgt (oder sogar von Unter-BEFEHLEN). Befehle werden durch ein konfiguriertes Schlüsselwort identifiziert, den Befehlsnamen, der alle Werte, die auf den Befehlsnamen folgen, als Teil der Definition dieses BEFEHLS gruppieren.

Konfigurieren der Befehlszeile

Das Programmieren der Befehlszeile beginnt nach dem Verweis auf die .NET Core Microsoft.Extensions.CommandLineUtils mit der CommandLineApplication-Klasse. Mithilfe dieser Klasse können Sie jeden BEFEHL, jede OPTION und jedes ARGUMENT konfigurieren. Beim Instanziieren von CommandLineApplication hat der Konstruktor einen optionalen Booleschen Wert, mit dem die Befehlszeile auf das Auslösen einer Ausnahme (Standardeinstellung) beim Auftreten eines Arguments festgelegt werden kann, das nicht spezifisch konfiguriert wurde.

In einer vorhandenen Instanz von CommandLineApplication konfigurieren Sie Argumente mithilfe der Methoden „Option“, „Argument“ und „Command“. Angenommen, Sie möchten eine Befehlszeilensyntax wie die folgende unterstützen, bei der Elemente in eckigen Klammern optional und solche in spitzen Klammern vom Benutzer angegebene Werte oder Argumente sind:

Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>] 
     [-?|-h|--help] [-u|--uppercase]

Abbildung 2 konfiguriert die grundlegende Analysefunktionalität.

Abbildung 2 Konfigurieren der Befehlszeile

public static void Main(params string[] args)
{
    // Program.exe <-g|--greeting|-$ <greeting>> [name <fullname>]
    // [-?|-h|--help] [-u|--uppercase]
  CommandLineApplication commandLineApplication =
    new CommandLineApplication(throwOnUnexpectedArg: false);
  CommandArgument names = null;
  commandLineApplication.Command("name",
    (target) =>
      names = target.Argument(
        "fullname",
        "Enter the full name of the person to be greeted.",
        multipleValues: true));
  CommandOption greeting = commandLineApplication.Option(
    "-$|-g |--greeting <greeting>",
    "The greeting to display. The greeting supports"
    + " a format string where {fullname} will be "
    + "substituted with the full name.",
    CommandOptionType.SingleValue);
  CommandOption uppercase = commandLineApplication.Option(
    "-u | --uppercase", "Display the greeting in uppercase.",
    CommandOptionType.NoValue);
  commandLineApplication.HelpOption("-? | -h | --help");
  commandLineApplication.OnExecute(() =>
  {
    if (greeting.HasValue())
    {
      Greet(greeting.Value(), names.Values, uppercase.HasValue());
    }
    return 0;
  });
  commandLineApplication.Execute(args);
}
private static void Greet(
  string greeting, IEnumerable<string> values, bool useUppercase)
{
  Console.WriteLine(greeting);
}

Die Grundlage: CommandLineApplication

Zunächst instanziiere ich die Klasse CommandLineApplication und gebe an, ob die Analyse der Befehlszeile streng – dann ist throwOnUnexpectedArg WAHR – oder tolerant erfolgen soll. Wenn ich angebe, dass beim Auftreten eines unerwarteten Arguments eine Ausnahme ausgelöst werden soll, müssen alle Argumente explizit konfiguriert werden. Wenn throwOnUnexpectedArg hingegen FALSCH ist, werden alle Argumente, die von der Konfiguration nicht erkannt werden, im Feld CommandLineApplication.Remaining­Arguments gespeichert.

Konfigurieren eines Befehls und seiner Argumente

Der nächste Schritt in Abbildung 2 besteht im Konfigurieren des Befehls „name“. Das Schlüsselwort, mit dem dieser Befehl innerhalb einer Liste mit Argumenten identifiziert wird, ist der erste Parameter der Funktion „Command“ – name. Der zweite Parameter ist ein Action<CommandLineApplication>-Stellvertreter mit dem Namen „configuration“, in dem alle Unterargumente des BEFEHLS „name“ konfiguriert werden. In diesen Fall gibt es davon nur einen, ein Argument vom Typ CommandArgument mit dem Variablennamen „greeting“. Es ist jedoch ohne weiteres möglich, dem configuration-Stellvertreter weitere ARGUMENTE, OPTIONEN und sogar Unter-BEFEHLE hinzuzufügen. Außerdem weist der Zielparameter des Stellvertreters, eine CommandLineApplication-Instanz, eine Eigenschaft „Parent“ auf, die auf commandLineArgument zurück verweist – die übergeordnete CommandLineArgument-Instanz des Ziels, unter der der BEFEHL „name“ konfiguriert ist.

Beachten Sie, dass ich beim Konfigurieren des ARGUMENTS „names“ spezifisch angebe, dass es mehrere Werte (multipleValues) unterstützt. Dadurch erlaube ich die Angabe von mehr als einem Wert – in diesem Fall mehrerer Namen. Jeder dieser Werte wird nach dem Argumentbezeichner „name“ aufgelistet, bis ein anderer Argument- oder Optionsbezeichner auftritt. Die ersten zwei Parameter der Argument-Funktion sind „name“, die auf den Namen des ARGUMENTS verweisen, damit Sie es in einer Liste mit ARGUMENTEN identifizieren können, und „description“.

Ein letzter Aspekt, auf den bei der Konfiguration des BEFEHLS „name“ hinzuweisen ist, ist, dass Sie den Rückgabewert der Argument-Funktion (und der Option-Funktion, falls vorhanden) speichern müssen. Dies ist erforderlich, damit Sie später die dem ARGUMENT „names“ zugewiesenen Argumente abrufen können. Ohne den Verweis zu speichern, müssten Sie sonst die commandLineApplication.Commands[0].Arguments-Sammlung durchsuchen, um die Daten für das ARGUMENT abzurufen.

Eine elegante Möglichkeit, die Befehlszeilendaten zu speichern, besteht darin, sie in einer separaten Klasse zu platzieren, die mit den Attributen aus dem ASP.NET Gerüstbaurepository (github.com/aspnet/Scaffolding) ausgestattet ist, insbesondere aus dem Ordner „src/Microsoft.VisualStudio.Web.CodeGeneration.Core/CommandLine“. Weitere Informationen finden Sie unter „Implementing a Command-Line Class with .NET Core” (Implementieren einer Befehlszeilenklasse mit .NET Core, bit.ly/296SluA).

Konfigurieren einer Option

Das nächste in Abbildung 2 konfigurierte Argument ist die OPTION „greeting“, die den Typ CommandOption aufweist. Die Konfiguration einer Option erfolgt über die Funktion „Option“, bei der der erste Parameter ein Zeichenfolgenparameter mit den Namen „template“ ist. Beachten Sie, dass Sie drei verschiedene Namen (z. B. „-$“, „-g“ und „-greeting“) für die Option angegeben können, und sie alle für die Identifikation der Option in der Liste der Argumente verwendet werden können. Ferner kann optional über eine Vorlage ein Wert in Form eines Namens in spitzen Klammern angegeben werden, der auf die Optionsbezeichner folgt. Nach dem Beschreibungsparameter enthält die Funktion OPTION einen erforderlichen CommandOptionType-Parameter. Diese Option gibt an:

  1. Ob nach dem Optionsbezeichner ein Wert angegeben werden kann. Wenn der CommandOptionType NoValue angegeben wird, wird die Funktion CommandOption.Value auf „on“ festgelegt, wenn die Option innerhalb der Argumentliste vorkommt. Der Wert „on“ wird auch dann zurückgegeben, wenn nach dem Optionsbezeichner ein anderer Wert angegeben wird, sogar, wenn kein Wert angegeben wird. Ein Beispiel dafür finden Sie in der Option „uppercase“ in Abbildung 2.
  2. Wenn andererseits der Wert von CommandOptionType SingleValue ist und der Optionsbezeichner angegeben wird, aber kein Wert vorkommt, wird eine CommandParsingException ausgelöst, die angibt, dass die Option nicht identifiziert wurde – da sie nicht der Vorlage entsprach. SingleValue bietet also die Möglichkeit, zu prüfen, ob der Wert angegeben wird, sofern der Optionsbezeichner überhaupt vorkommt.
  3. Schließlich kann für CommandOptionType auch Multiple­Value festgelegt werden. Anders als bei den mehreren Werten, die einem Befehl zugeordnet sind, ermöglichen mehrere Werte im Fall einer Option die mehrfache Angabe der gleichen Option. Beispielsweise „program.exe -name Inigo -name Montoya“.

Beachten Sie, dass keine der Konfigurationsoptionen konfiguriert wird, daher ist die Option erforderlich. Und das gleiche gilt auch für ein Argument. Um einen Fehler auszugeben, wenn kein Wert angegeben wird, müssen Sie überprüfen, ob die Funktion HasValue einen Fehler meldet, wenn sie FALSCH zurückgibt. Im Fall eines Befehlsarguments (CommandArgument) gibt die Value-Eigenschaft null zurück, wenn kein Wert angegeben wird. Um den Fehler zu melden, erwägen Sie die Anzeige einer Fehlermeldung, gefolgt von dem entsprechenden Hilfetext, damit die Benutzer mehr Informationen dazu erhalten, was sie zur Behebung des Problems unternehmen müssen.

Ein weiterer wichtiger Aspekt im Verhalten des Analysemechanismus von CommandLineApplication besteht in seiner Unterscheidung von Groß- und Kleinschreibung. Und zurzeit gibt es auch keine leicht umzusetzende Konfigurationsoption, die es ermöglicht, die Berücksichtigung der Groß-/Kleinschreibung aufzuheben. Daher müssen Sie die Großschreibung der an CommandLineApplication übergebenen Argumente (mithilfe der Methode „Execute“, wie ich gleich beschreibe) vorher ändern, um das Ignorieren der Groß-/Kleinschreibung zu erreichen. (Alternativ könnten Sie versuchen, einen Pull Request an github.com/aspnet/Common zu senden, um diese Option zu aktivieren.)

Anzeigen von Hilfe und Version

In die Klasse CommandLineApplication integriert ist eine Funktion ShowHelp, die den der Befehlszeilenkonfiguration zugeordneten Hilfetext automatisch anzeigt. Beispielsweise zeigt Abbildung 3 die ShowHelp-Ausgabe für Abbildung 2.

Abbildung 3 ShowHelp-Ausgabe in der Anzeige

Usage:  [options] [command]
Options:
  -$|-g |--greeting <greeting>  The greeting to display. 
                                The greeting supports a format string 
                                where {fullname} will be substituted 
                                with the full name.
  -u | --uppercase              Display the greeting in uppercase.
  -? | -h | --help              Show help information
Commands:
  name 
Use " [command] --help" for more information about a command.

Unglücklicherweise gibt die angezeigte Hilfe nicht an, ob eine Option oder ein Befehl tatsächlich optional ist. Im Hilfetext werden alle Optionen und Befehle (durch Angabe eckiger Klammern) als optional dargestellt.

Zwar können Sie ShowHelp explizit aufrufen, beispielsweise beim Verarbeiten eines benutzerdefinierten Befehlszeilenfehlers, der Aufruf erfolgt jedoch automatisch bei jeder Angabe eines Arguments, das mit der HelpOption-Vorlage übereinstimmt. Die HelpOption-Vorlage wird ihrerseits in Form eines Arguments für die Methode CommandLineApplication.HelpOption angegeben.

Analog dazu gibt es eine ShowVersion-Methode zum Anzeigen der Version Ihrer Anwendung. Wie ShowHelp wird sie mithilfe einer von zwei Methoden konfiguriert:

public CommandOption VersionOption(
  string template, string shortFormVersion, string longFormVersion = null).
public CommandOption VersionOption(
  string template, Func<string> shortFormVersionGetter,
  Func<string> longFormVersionGetter = null)

Beachten Sie, dass für beide Methoden die Angabe der anzuzeigenden Versionsinformationen im Aufruf von VersionOption erforderlich ist.

Analysieren und Lesen der Befehlszeilendaten

Bisher habe ich ausführlich die Konfiguration von CommandLineApplication behandelt, ich habe aber weder den kritischen Prozess des Auslösens der Analyse der Befehlszeile, noch das besprochen, was unmittelbar im Anschluss an den Aufruf der Analyse geschieht.

Um die Analyse der Befehlszeile auszulösen, müssen Sie die Funktion CommandLineApplication.Execute aufrufen und die Liste der auf der Befehlszeile angegebenen Argumente übergeben. In Abbildung 1 sind die Argumente im args-Parameter von Main angegeben, sodass sie direkt an die Execute-Funktion übergeben werden (denken Sie daran, zuerst die Behandlung der Groß-/Kleinschreibung vorzunehmen, wenn nicht nach Groß- und Kleinschreibung unterschieden werden soll). Es ist die Execute-Methode, die die Befehlszeilendaten festlegt, die jedem konfigurierten ARGUMENT und jeder konfigurierten OPTION zugeordnet sind.

Beachten Sie, dass CommandLineAppliction eine Funktion „OnExecute(Func<int> invoke)“ enthält, in die Sie einen Stellvertreter „Func<int>“ übergeben können, der nach dem Abschluss der Analyse automatisch ausgeführt wird. In Abbildung 2 nimmt die OnExecute-Methode einen einfachen Stellvertreter an, der überprüft, ob der greet-Befehl angegeben wurde, bevor eine Funktion „Greet“ aufgerufen wurde.

Beachten Sie ferner, dass der vom Aufrufstellvertreter zurückgegebene int als Mittel vorgesehen ist, einen Rückgabewert von Main anzugeben. Und welcher Wert auch vom Aufruf zurückgegeben wird, er entspricht dem Rückgabewert von Execute. Da die Analyse als relativ langsamer Vorgang angesehen wird (das ist in der Tat relativ), unterstützt Execute darüber hinaus eine Überladung, die ein „Func<Task<int>>“ annimmt und auf diese Weise einen asynchronen Aufruf der Befehlszeilenanalyse ermöglicht.

Richtlinien: Befehle, Argumente und Optionen

Angesichts der drei verfügbaren Befehlstypen sollten wir kurz rekapitulieren, wann welcher verwendet werden soll.

Sie sollten BEFEHLE zur semantischen Bezeichnung einer Aktion verwenden, etwa Compilieren, Importieren oder Sichern.

Sie sollten OPTIONEN zum Aktivieren von Konfigurationsinformationen für entweder das gesamte Programm oder einen bestimmten Befehl verwenden.

Sie sollten einem Verb für den Namen eines Befehls den Vorzug geben und einem Adjektiv oder Substantiv für den Namen einer Option (wie etwa -farbe, -parallel, -projektname).

Unabhängig vom konfigurierten Argumenttyp sollten Sie die folgenden Richtlinien berücksichtigen:

Sie sollten die Groß-/Kleinschreibung der Namen von Argumentbezeichnern überprüfen. Es kann für einen Benutzer, der „-VollständigerName“ oder „-vollständigername“ eingibt, sehr verwirrend sein, wenn die Befehlszeile eine andere Groß-/Kleinschreibung erwartet.

Sie sollten Tests für die Analyse der Befehlszeile erstellen. Dank Methoden wie „Execute“ und „OnExecute“ ist das relativ einfach.

Sie sollten ARGUMENTE verwenden, wenn die Identifikation einzelner Argumente anhand des Namens mühselig ist oder mehrere Werte zulässig sind, aber das Voranstellen eines Optionsbezeichners für jeden einzelnen unhandlich ist.

Erwägen Sie die Nutzung von IntelliTect.AssertConsole (itl.tc/Command­LineUtils) für die Umleitung der Konsolenein- und ausgabe, um Zeichenfolgen in die Konsole einzufügen und ihre Ausgabe zu erfassen, damit die Konsole getestet werden kann.

Einen möglicher Fallstrick bei der Verwendung der .NET Core Command­LineUtils liegt in der Tatsache, dass sie in englischer Sprache vorliegen und nicht lokalisiert erhältlich sind. Anzeigetexte, wie der in ShowHelp (zusammen mit Ausnahmemeldungen, die generell nicht lokalisiert sind), sind sämtlich in englischer Sprache. Normalerweise ist das vielleicht kein Problem, da aber die Befehlszeile Teil der Schnittstelle einer Anwendung mit dem Benutzer ist, können durchaus Szenarien eintreten, in denen die Beschränkung auf Englisch nicht vertretbar ist. Daher:

Erwägen Sie das Erstellen benutzerdefinierter Funktionen für ShowHelp und ShowHint, wenn Lokalisierung wichtig ist.

Sie sollten CommandLineApplication.RemainingArguments beim Konfigurieren der Anwendung überprüfen, um keine Ausnahmen auszulösen (throwOnUnexpectedArg = false).

Zusammenfassung

Innerhalb der letzten drei Jahre hat das .NET Framework eine Reihe größerer Veränderungen durchlaufen:

  • Es bietet nun plattformübergreifende Unterstützung, einschließlich Unterstützung für iOS, Android und Linux – tolle Sache!
  • Es hat sich aus einem geheimen, proprietären Ansatz hin zu einem vollständig offenen – im Sinne von Open Source – Modul entwickelt.
  • Es hat in erheblichem Umfang Refactoring der BCL-APIs für die .NET-Standardbibliothek zu einer hochgradig modularen (übergreifenden) Plattform stattgefunden, die für einen riesigen Bereich von Anwendungstypen genutzt werden kann, sei es in Software-as-a-Service, mobil, lokal, für das Internet der Dinge, den Desktop und mehr.
  • .NET hat eine Wiedergeburt erfahren, die auf die Windows 8-Ära folgte, in der es weitgehend ignoriert wurde und sich kaum eine Perspektive abzeichnete.

Und das soll vor allem besagen: Wenn Sie noch nicht in das neue .NET Core 1.0 eingestiegen sind, ist jetzt der richtige Zeitpunkt, denn so kann sich Ihre Lernkurve langfristig amortisieren. Oder anders gesagt: Wenn Sie erwägen, ein Upgrade von früheren Versionen vorzunehmen, tun Sie es jetzt. Die Wahrscheinlichkeit ist hoch, dass Sie früher oder später sowieso ein Upgrade durchführen. Je eher dies erfolgt, desto schneller kommen Sie in den Genuss der neuen Features.


Mark Michaelis ist der Gründer von IntelliTect und arbeitet als leitender technischer Architekt und Trainer. Seit fast zwei Jahrzehnten ist er ein Microsoft MVP und seit 2007 Microsoft-Regionalleiter. Michaelis arbeitet in verschiedenen Microsoft-Softwareentwicklungs-Reviewteams mit, einschließlich C#, Microsoft Azure, SharePoint und Visual Studio ALM. Er hält häufig Vorträge bei Entwicklerkonferenzen und hat viele Bücher geschrieben, einschließlich seines letzten „Essential C# 6.0 (5th Edition)“ (itl.tc/­EssentialCSharp). Sie können ihn auf Facebook unter facebook.com/Mark.Michaelis, über seinen Blog unter IntelliTect.com/Mark, auf Twitter: @markmichaelis oder per E-Mail unter mark@IntelliTect.com erreichen.

Unser Dank gilt den folgenden technischen Experten von IntelliTect für die Durchsicht dieses Artikels: Phil Spokas und Michael Stokesbary