From 91f7427ca7aaccae5c83f035e72607fda383fdcc Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Wed, 5 Jul 2017 07:14:17 +0200 Subject: [PATCH 01/14] refactoring: add class diagram and new Getopt --- docs/getopt.graphml | 425 +++++++++++++++++++++++++++++++++++++++ getopt3/Getopt.php | 198 ++++++++++++++++++ getopt3/OptionParser.php | 116 +++++++++++ 3 files changed, 739 insertions(+) create mode 100644 docs/getopt.graphml create mode 100644 getopt3/Getopt.php create mode 100644 getopt3/OptionParser.php diff --git a/docs/getopt.graphml b/docs/getopt.graphml new file mode 100644 index 0000000..704c081 --- /dev/null +++ b/docs/getopt.graphml @@ -0,0 +1,425 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"> + <!--Created by yEd 3.17--> + <key attr.name="Beschreibung" attr.type="string" for="graph" id="d0"/> + <key for="port" id="d1" yfiles.type="portgraphics"/> + <key for="port" id="d2" yfiles.type="portgeometry"/> + <key for="port" id="d3" yfiles.type="portuserdata"/> + <key attr.name="url" attr.type="string" for="node" id="d4"/> + <key attr.name="description" attr.type="string" for="node" id="d5"/> + <key for="node" id="d6" yfiles.type="nodegraphics"/> + <key for="graphml" id="d7" yfiles.type="resources"/> + <key attr.name="url" attr.type="string" for="edge" id="d8"/> + <key attr.name="description" attr.type="string" for="edge" id="d9"/> + <key for="edge" id="d10" yfiles.type="edgegraphics"/> + <graph edgedefault="directed" id="G"> + <data key="d0"/> + <node id="n0"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="536.0" y="601.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="c" textColor="#000000" verticalTextPosition="bottom" visible="true" width="54.15283203125" x="22.923583984375" y="3.0">Getopt</y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n1"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="725.0" y="282.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.6025390625" x="7.69873046875" y="3.0">Arguments<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n2"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="536.0" y="685.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="53.21337890625" x="23.393310546875" y="3.0">Option<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n3"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="406.00000000000006" y="685.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.896484375" x="11.5517578125" y="3.0">Command<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n4"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="857.0" y="601.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.45849609375" x="31.270751953125" y="3.0">Help<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n5"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="177.60000000000002" x="686.2" y="409.9000000000001"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="133.47314453125" x="22.06342773437501" y="3.0">ArgumentIterator<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n6"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="162.20000000000005" x="855.0" y="282.0"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="154.515625" x="3.8421875000000227" y="5.015625">Holds a list of arguments<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n7"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="268.6" x="237.39999999999998" y="601.0"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="262.779296875" x="2.9103515625000114" y="5.015625">Getopt is the container for all functionallity<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n8"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="536.0" y="743.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="76.86474609375" x="11.567626953125" y="3.0">Argument<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n9"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="100.0" x="987.0" y="601.0"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.328125" x="4.335937500000114" y="5.015625">Prints the help<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n10"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="420.3333333333328" x="893.8000000000001" y="409.9000000000001"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="l" textColor="#000000" verticalTextPosition="bottom" visible="true" width="412.73828125" x="4.0" y="5.015625">Iterates over arguments, fills options with values and sets operands</y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n11"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="525.4" x="796.0" y="685.0"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="520.908203125" x="2.245898437499932" y="5.015625">Command, Option and Operand are business objects that you get from command line<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <node id="n12"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="100.0" x="666.0" y="685.0"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="66.91796875" x="16.541015625" y="3.0">Operand<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n13"> + <data key="d4"/> + <data key="d6"> + <y:UMLClassNode> + <y:Geometry height="28.0" width="177.60000000000002" x="358.00000000000006" y="505.45000000000005"/> + <y:Fill color="#A6BCFF" transparent="false"/> + <y:BorderStyle color="#000000" type="line" width="1.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="100.890625" x="38.35468750000001" y="3.0">OptionParser<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> + <y:AttributeLabel/> + <y:MethodLabel/> + </y:UML> + </y:UMLClassNode> + </data> + </node> + <node id="n14"> + <data key="d6"> + <y:ShapeNode> + <y:Geometry height="28.0" width="255.20000000000005" x="72.79999999999995" y="505.45000000000005"/> + <y:Fill hasColor="false" transparent="false"/> + <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="245.154296875" x="5.022851562500023" y="5.015625">Creates Options from arrays and strings<y:LabelModel> + <y:SmartNodeLabelModel distance="4.0"/> + </y:LabelModel> + <y:ModelParameter> + <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> + </y:ModelParameter> + </y:NodeLabel> + <y:Shape type="rectangle"/> + </y:ShapeNode> + </data> + </node> + <edge id="e0" source="n0" target="n5"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="586.0" y="519.45"/> + <y:Point x="775.0" y="519.45"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e1" source="n5" target="n1"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e2" source="n0" target="n1"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="586.0" y="296.0"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e3" source="n0" target="n4"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e4" source="n0" target="n2"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e5" source="n0" target="n3"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="586.0" y="657.0"/> + <y:Point x="456.00000000000006" y="657.0"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e6" source="n0" target="n12"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="586.0" y="657.0"/> + <y:Point x="716.0" y="657.0"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e7" source="n2" target="n8"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="false"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e8" source="n12" target="n8"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="43.73333333333335" ty="1.9999999999998863"> + <y:Point x="716.0" y="758.9999999999999"/> + </y:Path> + <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:Arrows source="none" target="white_delta"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + <edge id="e9" source="n0" target="n13"> + <data key="d8"/> + <data key="d10"> + <y:PolyLineEdge> + <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> + <y:Point x="586.0" y="519.45"/> + </y:Path> + <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:Arrows source="none" target="plain"/> + <y:BendStyle smoothed="true"/> + </y:PolyLineEdge> + </data> + </edge> + </graph> + <data key="d7"> + <y:Resources/> + </data> +</graphml> diff --git a/getopt3/Getopt.php b/getopt3/Getopt.php new file mode 100644 index 0000000..077c44a --- /dev/null +++ b/getopt3/Getopt.php @@ -0,0 +1,198 @@ +<?php + +namespace GetOpt; + +class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate +{ + const NO_ARGUMENT = 0; + const REQUIRED_ARGUMENT = 1; + const OPTIONAL_ARGUMENT = 2; + const MULTIPLE_ARGUMENT = 3; + + const SETTING_DEFAULT_MODE = 'defaultMode'; + + /** @var OptionParser */ + protected $optionParser; + + /** @var array */ + protected $settings = [ + self::SETTING_DEFAULT_MODE => self::NO_ARGUMENT + ]; + + /**@var Option[] */ + protected $options = []; + + /** + * Creates a new Getopt object. + * + * The argument $options can be either a string in the format accepted by the PHP library + * function getopt() or an array. + * + * @param array $settings + * @link https://www.gnu.org/s/hello/manual/libc/Getopt.html GNU Getopt manual + */ + public function __construct(array $settings = []) + { + foreach ($settings as $setting => $value) { + $this->set($settings, $value); + } + } + + public function set($setting, $value) + { + switch ($setting) { + default: + $this->settings[$setting] = $value; + } + return $this; + } + + public function addOptions($options) + { + if (is_string($options)) { + $options = $this->getOptionParser()->parseString($options); + } + + foreach ($options as $option) { + $this->addOption($option); + } + + return $this; + } + + public function addOption($option) + { + if (!$option instanceof Option) { + if (is_string($option)) { + $options = $this->getOptionParser()->parseString($option); + if (count($options) === 0) { + throw new \InvalidArgumentException(sprintf( + 'Could not create options from string \'%s\'', + $option + )); + } + // this is addOption - so we use only the first one + $option = $options[0]; + } elseif (is_array($option)) { + $option = $this->getOptionParser()->parseArray($option); + } else { + throw new \InvalidArgumentException(sprintf( + '$option has to be a string, an array or an Option. %s given', + gettype($option) + )); + } + } + + $this->options[] = $option; + + return $this; + } + + /** + * Create or get the OptionParser + * + * @return OptionParser + */ + protected function getOptionParser() + { + if ($this->optionParser === null) { + $this->optionParser = new OptionParser($this->settings[self::SETTING_DEFAULT_MODE]); + } + + return $this->optionParser; + } + + /** + * Retrieve an external iterator + * + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing <b>Iterator</b> or + * <b>Traversable</b> + * @since 5.0.0 + */ + public function getIterator() + { + // TODO: Implement getIterator() method. + } + + /** + * Whether a offset exists + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset <p> + * An offset to check for. + * </p> + * @return boolean true on success or false on failure. + * </p> + * <p> + * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists($offset) + { + // TODO: Implement offsetExists() method. + } + + /** + * Offset to retrieve + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset <p> + * The offset to retrieve. + * </p> + * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + // TODO: Implement offsetGet() method. + } + + /** + * Offset to set + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset <p> + * The offset to assign the value to. + * </p> + * @param mixed $value <p> + * The value to set. + * </p> + * @return void + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + throw new \LogicException('Read only array access'); + } + + /** + * Offset to unset + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset <p> + * The offset to unset. + * </p> + * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + throw new \LogicException('Read only array access'); + } + + /** + * Count elements of an object + * + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + * </p> + * <p> + * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + // TODO: Implement count() method. + } +} diff --git a/getopt3/OptionParser.php b/getopt3/OptionParser.php new file mode 100644 index 0000000..556eb1e --- /dev/null +++ b/getopt3/OptionParser.php @@ -0,0 +1,116 @@ +<?php + +namespace GetOpt; + +/** + * Converts user-given option specifications into Option objects. + */ +class OptionParser +{ + private $defaultMode; + + /** + * Creates a new instance. + * + * @param int $defaultMode will be assigned to options when no mode is given for them. + */ + public function __construct($defaultMode) + { + $this->defaultMode = $defaultMode; + } + + /** + * Parse a GNU-style option string. + * + * @param string $string the option string + * @return Option[] + * @throws \InvalidArgumentException + */ + public function parseString($string) + { + if (!mb_strlen($string)) { + throw new \InvalidArgumentException('Option string must not be empty'); + } + $options = array(); + $eol = mb_strlen($string) - 1; + $nextCanBeColon = false; + for ($i = 0; $i <= $eol; ++$i) { + $ch = $string[$i]; + if (!preg_match('/^[A-Za-z0-9]$/', $ch)) { + $colon = $nextCanBeColon ? " or ':'" : ''; + throw new \InvalidArgumentException( + "Option string is not well formed: " + . "expected a letter$colon, found '$ch' at position " . ($i + 1) + ); + } + if ($i == $eol || $string[$i + 1] != ':') { + $options[] = new Option($ch, null, Getopt::NO_ARGUMENT); + $nextCanBeColon = true; + } elseif ($i < $eol - 1 && $string[$i + 2] == ':') { + $options[] = new Option($ch, null, Getopt::OPTIONAL_ARGUMENT); + $i += 2; + $nextCanBeColon = false; + } else { + $options[] = new Option($ch, null, Getopt::REQUIRED_ARGUMENT); + ++$i; + $nextCanBeColon = true; + } + } + return $options; + } + + /** + * Processes an option array. The array should be conform to the format + * (short, long, mode [, description [, default]]). See documentation for details. + * + * Developer note: Please don't add any further elements to the array. Future features should be configured only + * through the Option class's methods. + * + * @param array $array + * @return Option + */ + private function parseArray(array $array) + { + $rowSize = count($array); + if ($rowSize < 3) { + $array = $this->completeOptionArray($array); + } + $option = new Option($array[0], $array[1], $array[2]); + if ($rowSize >= 4) { + $option->setDescription($array[3]); + } + if ($rowSize >= 5 && $array[2] != Getopt::NO_ARGUMENT) { + $option->setArgument(new Argument($array[4])); + } + return $option; + } + + /** + * When using arrays, instead of a full option spec ([short, long, type]) users can leave out one or more of + * these parts and have Getopt fill them in intelligently: + * - If either the short or the long option string is left out, the first element of the given array is interpreted + * as either short (if it has length 1) or long, and the other one is set to null. + * - If the type is left out, it is set to NO_ARGUMENT. + * + * @param array $row + * @return array + */ + private function completeOptionArray(array $row) + { + $short = (strlen($row[0]) == 1) ? $row[0] : null; + + $long = null; + if (is_null($short)) { + $long = $row[0]; + } elseif (count($row) > 1 && !is_int($row[1])) { + $long = $row[1]; + } + + $mode = $this->defaultMode; + if (count($row) == 2 && is_int($row[1])) { + $mode = $row[1]; + } + + return array( $short, $long, $mode ); + } +} -- GitLab From 1170babc85ace016a06d2bfdc67d9162301030df Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Wed, 5 Jul 2017 12:34:40 +0200 Subject: [PATCH 02/14] refactoring: add argument processor (WIP) --- docs/getopt.graphml | 6 +- getopt3/Argument.php | 117 +++++++++++++++++++++ getopt3/ArgumentProcessor.php | 33 ++++++ getopt3/Arguments.php | 58 +++++++++++ getopt3/Getopt.php | 79 ++++++++++++++- getopt3/Option.php | 184 ++++++++++++++++++++++++++++++++++ getopt3/OptionParser.php | 2 +- 7 files changed, 471 insertions(+), 8 deletions(-) create mode 100644 getopt3/Argument.php create mode 100644 getopt3/ArgumentProcessor.php create mode 100644 getopt3/Arguments.php create mode 100644 getopt3/Option.php diff --git a/docs/getopt.graphml b/docs/getopt.graphml index 704c081..b562fa8 100644 --- a/docs/getopt.graphml +++ b/docs/getopt.graphml @@ -120,7 +120,7 @@ <y:Geometry height="28.0" width="177.60000000000002" x="686.2" y="409.9000000000001"/> <y:Fill color="#A6BCFF" transparent="false"/> <y:BorderStyle color="#000000" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="133.47314453125" x="22.06342773437501" y="3.0">ArgumentIterator<y:LabelModel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="149.07568359375" x="14.262158203125011" y="3.0">ArgumentProcessor<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -195,7 +195,7 @@ <y:Geometry height="28.0" width="100.0" x="987.0" y="601.0"/> <y:Fill hasColor="false" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.328125" x="4.335937500000114" y="5.015625">Prints the help<y:LabelModel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.328125" x="4.3359375" y="5.015625">Prints the help<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -223,7 +223,7 @@ <y:Geometry height="28.0" width="525.4" x="796.0" y="685.0"/> <y:Fill hasColor="false" transparent="false"/> <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="520.908203125" x="2.245898437499932" y="5.015625">Command, Option and Operand are business objects that you get from command line<y:LabelModel> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="520.908203125" x="2.2458984375000455" y="5.015625">Command, Option and Operand are business objects that you get from command line<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> diff --git a/getopt3/Argument.php b/getopt3/Argument.php new file mode 100644 index 0000000..ff6c46e --- /dev/null +++ b/getopt3/Argument.php @@ -0,0 +1,117 @@ +<?php + +namespace GetOpt; + +class Argument +{ + const CLASSNAME = __CLASS__; + + /** @var string */ + private $default; + /** @var callable */ + private $validation; + /** @var string */ + private $name; + + /** + * Creates a new argument. + * + * @param scalar|null $default Default value or NULL + * @param callable|null $validation a validation function (optional) + * @throws \InvalidArgumentException + */ + public function __construct($default = null, $validation = null, $name = "arg") + { + if (!is_null($default)) { + $this->setDefaultValue($default); + } + if (!is_null($validation)) { + $this->setValidation($validation); + } + $this->name = $name; + } + + /** + * Set the default value + * + * @param scalar $value + * @return Argument this object (for chaining calls) + * @throws \InvalidArgumentException + */ + public function setDefaultValue($value) + { + if (!is_scalar($value)) { + throw new \InvalidArgumentException("Default value must be scalar"); + } + $this->default = $value; + return $this; + } + + /** + * Set a validation function. + * The function must take a string and return true if it is valid, false otherwise. + * + * @param callable $callable + * @return Argument this object (for chaining calls) + * @throws \InvalidArgumentException + */ + public function setValidation($callable) + { + if (!is_callable($callable)) { + throw new \InvalidArgumentException("Validation must be a callable"); + } + $this->validation = $callable; + return $this; + } + + /** + * Check if an argument validates according to the specification. + * + * @param string $arg + * @return bool + */ + public function validates($arg) + { + return (bool)call_user_func($this->validation, $arg); + } + + /** + * Check if the argument has a validation function + * + * @return bool + */ + public function hasValidation() + { + return isset($this->validation); + } + + /** + * Check whether the argument has a default value + * + * @return boolean + */ + public function hasDefaultValue() + { + return !empty($this->default); + } + + /** + * Retrieve the default value + * + * @return scalar|null + */ + public function getDefaultValue() + { + return $this->default; + } + + /** + * Retrieve the argument name + * + * @return string + */ + public function getName() + { + return $this->name; + } +} diff --git a/getopt3/ArgumentProcessor.php b/getopt3/ArgumentProcessor.php new file mode 100644 index 0000000..ce77e80 --- /dev/null +++ b/getopt3/ArgumentProcessor.php @@ -0,0 +1,33 @@ +<?php + +namespace GetOpt; + +class ArgumentProcessor +{ + public static function process(Getopt $getopt, Arguments $arguments) + { + while ($arg = $arguments->next()) { + if ($arg === '--') { + $getopt->__addOperands($arguments->rest()); + } elseif (empty($arg) || $arg === '-' || $arg[0] !== '-') { + $getopt->__addOperands([$arg]); + } elseif ($arg[1] === '-') { + $p = strpos($arg, '='); + if ($p !== false) { + $name = substr($arg, 2, $p); + $arguments->unshift(substr($arg, $p + 1)); + } else { + $name = substr($arg, 2); + } + $option = $getopt->getOption($name); + if (!$option) { + throw new \UnexpectedValueException(sprintf( + 'Option \'%s\' is unknown', + $name + )); + } + $option->retrieveValue($arguments); + } + } + } +} diff --git a/getopt3/Arguments.php b/getopt3/Arguments.php new file mode 100644 index 0000000..c10b3c4 --- /dev/null +++ b/getopt3/Arguments.php @@ -0,0 +1,58 @@ +<?php + +namespace GetOpt; + +class Arguments +{ + /** @var string[] */ + protected $arguments; + + public function __construct(array $arguments) + { + $this->arguments = $arguments; + } + + public static function fromString($argsString) + { + $argv = array(); + $argsString = trim($argsString); + $argc = 0; + + $state = 'n'; // states: n (normal), d (double quoted), s(single quoted) + for ($i = 0; $i < strlen($argsString); $i++) { + $char = $argsString{$i}; + switch ($state) { + case 'n': + if ($char === '\'') { + $state = 's'; + } elseif ($char === '"') { + $state = 'd'; + } elseif (in_array($char, array("\n", "\t", ' '))) { + $argc++; + $argv[$argc] = ''; + } else { + $argv[$argc] .= $char; + } + break; + + case 's': + if ($char === '\'') { + $state = 'n'; + } else { + $argv[$argc] .= $char; + } + break; + + case 'd': + if ($char === '"') { + $state = 'n'; + } else { + $argv[$argc] .= $char; + } + break; + } + } + + return new self($argv); + } +} diff --git a/getopt3/Getopt.php b/getopt3/Getopt.php index 077c44a..a3e58a0 100644 --- a/getopt3/Getopt.php +++ b/getopt3/Getopt.php @@ -9,6 +9,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate const OPTIONAL_ARGUMENT = 2; const MULTIPLE_ARGUMENT = 3; + const SETTINGS_SCRIPT_NAME = 'scriptName'; const SETTING_DEFAULT_MODE = 'defaultMode'; /** @var OptionParser */ @@ -22,6 +23,9 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate /**@var Option[] */ protected $options = []; + /** @var Option[] */ + protected $optionMapping = []; + /** * Creates a new Getopt object. * @@ -33,6 +37,12 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate */ public function __construct(array $settings = []) { + $this->set( + self::SETTINGS_SCRIPT_NAME, + isset($_SERVER['argv'][0]) ? $_SERVER['argv'][0] : ( + isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : null + ) + ); foreach ($settings as $setting => $value) { $this->set($settings, $value); } @@ -40,13 +50,19 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate public function set($setting, $value) { - switch ($setting) { - default: - $this->settings[$setting] = $value; - } + $this->settings[$setting] = $value; +// switch ($setting) { +// default: +// +// } return $this; } + public function get($setting) + { + return isset($this->settings[$setting]) ? $this->settings[$setting] : null; + } + public function addOptions($options) { if (is_string($options)) { @@ -83,11 +99,57 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate } } + if ($this->getOption($option->short()) || $this->getOption($option->long())) { + throw new \InvalidArgumentException('$option`s short and long name have to be unique'); + } + $this->options[] = $option; return $this; } + /** + * Get a option by $name + * + * @param string $name Short or long name of the option + * @return Option + */ + public function getOption($name) + { + if (!isset($this->optionMapping[$name])) { + $this->optionMapping[$name] = null; + foreach ($this->options as $option) { + if ($option->matches($name)) { + $this->optionMapping[$name] = $option; + break; + } + } + } + + return $this->optionMapping[$name]; + } + + /** + * @param array|string|Arguments $arguments + */ + public function process($arguments = null) + { + if ($arguments === null) { + $arguments = isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : []; + $arguments = new Arguments($arguments); + } elseif (is_array($arguments)) { + $arguments = new Arguments($arguments); + } elseif (is_string($arguments)) { + $arguments = Arguments::fromString($arguments); + } elseif (!$arguments instanceof Arguments) { + throw new \InvalidArgumentException( + '$arguments has to be an instance of Arguments, an arguments string, an array of arguments or null' + ); + } + + + } + /** * Create or get the OptionParser * @@ -102,6 +164,15 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate return $this->optionParser; } + // backward compatibility + + public function setScriptName($scriptName) + { + return $this->set(self::SETTINGS_SCRIPT_NAME, $scriptName); + } + + // array functions + /** * Retrieve an external iterator * diff --git a/getopt3/Option.php b/getopt3/Option.php new file mode 100644 index 0000000..af886e9 --- /dev/null +++ b/getopt3/Option.php @@ -0,0 +1,184 @@ +<?php + +namespace GetOpt; + +/** + * Represents an option that Getopt accepts. + */ +class Option +{ + const CLASSNAME = __CLASS__; + + private $short; + private $long; + private $mode; + private $description = ''; + private $argument; + private $value = null; + + /** + * Creates a new option. + * + * @param string $short the option's short name (a single letter or digit) or null for long-only options + * @param string $long the option's long name (a string of 2+ letter/digit/_/- characters, starting with a letter + * or digit) or null for short-only options + * @param int $mode whether the option can/must have an argument (one of the constants defined in the Getopt + * class) + * (optional, defaults to no argument) + * @throws \InvalidArgumentException if both short and long name are null + */ + public function __construct($short, $long, $mode = Getopt::NO_ARGUMENT) + { + if (!$short && !$long) { + throw new \InvalidArgumentException("The short and long name may not both be empty"); + } + $this->setShort($short); + $this->setLong($long); + $this->setMode($mode); + $this->argument = new Argument(); + } + + /** + * Defines a description for the option. This is only used for generating usage information. + * + * @param string $description + * @return Option this object (for chaining calls) + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + + /** + * Defines a default value for the option. + * + * @param mixed $value + * @return Option this object (for chaining calls) + */ + public function setDefaultValue($value) + { + $this->argument->setDefaultValue($value); + return $this; + } + + /** + * Defines a validation function for the option. + * + * @param callable $function + * @return Option this object (for chaining calls) + */ + public function setValidation($function) + { + $this->argument->setValidation($function); + return $this; + } + + /** + * Sets the argument object directly. + * + * @param Argument $arg + * @return Option this object (for chaining calls) + */ + public function setArgument(Argument $arg) + { + if ($this->mode == Getopt::NO_ARGUMENT) { + throw new \InvalidArgumentException("Option should not have any argument"); + } + $this->argument = $arg; + return $this; + } + + /** + * Returns true if the given string is equal to either the short or the long name. + * + * @param string $string + * @return bool + */ + public function matches($string) + { + return ($string === $this->short) || ($string === $this->long); + } + + public function short() + { + return $this->short; + } + + public function long() + { + return $this->long; + } + + public function mode() + { + return $this->mode; + } + + public function getDescription() + { + return $this->description; + } + + /** + * Retrieve the argument object + * + * @return Argument + */ + public function getArgument() + { + return $this->argument; + } + + public function retrieveValue(Arguments $arguments) + { + if ($this->mode === Getopt::NO_ARGUMENT) { + if ($this->value === null) { + $this->value = 0; + } + $this->value++; + } + } + + /** + * Fluent interface for constructor so options can be added during construction + * + * @see Options::__construct() + */ + public static function create($short, $long, $mode = Getopt::NO_ARGUMENT) + { + return new self($short, $long, $mode); + } + + private function setShort($short) + { + if (!(is_null($short) || preg_match("/^[a-zA-Z0-9]$/", $short))) { + throw new \InvalidArgumentException("Short option must be null or a letter/digit, found '$short'"); + } + $this->short = $short; + } + + private function setLong($long) + { + if (!(is_null($long) || preg_match("/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,}$/", $long))) { + throw new \InvalidArgumentException("Long option must be null or an alphanumeric string, found '$long'"); + } + $this->long = $long; + } + + private function setMode($mode) + { + if (!in_array( + $mode, + array( Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT, Getopt::REQUIRED_ARGUMENT ), + true + ) + ) { + throw new \InvalidArgumentException( + "Option mode must be one of " + . "Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT and Getopt::REQUIRED_ARGUMENT" + ); + } + $this->mode = $mode; + } +} diff --git a/getopt3/OptionParser.php b/getopt3/OptionParser.php index 556eb1e..f3a9d1a 100644 --- a/getopt3/OptionParser.php +++ b/getopt3/OptionParser.php @@ -69,7 +69,7 @@ class OptionParser * @param array $array * @return Option */ - private function parseArray(array $array) + public function parseArray(array $array) { $rowSize = count($array); if ($rowSize < 3) { -- GitLab From 54fe7c64be340a8f4e666169e31bb9541b647460 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sat, 8 Jul 2017 22:50:03 +0200 Subject: [PATCH 03/14] refactoring: add help and test everything --- getopt3/Argument.php | 117 ------ getopt3/ArgumentProcessor.php | 33 -- getopt3/Arguments.php | 58 --- getopt3/Getopt.php | 269 ------------ getopt3/Option.php | 184 --------- getopt3/OptionParser.php | 116 ------ resources/option.php | 32 ++ resources/usage.php | 12 + src/Arguments.php | 181 ++++++++ src/CommandLineParser.php | 286 ------------- src/Getopt.php | 386 ++++++++++-------- src/Help.php | 31 ++ src/Option.php | 79 +++- src/OptionParser.php | 41 +- ...ndLineParserTest.php => ArgumentsTest.php} | 309 +++++++------- test/GetoptTest.php | 89 +++- test/OptionParserTest.php | 65 +-- test/OptionTest.php | 26 ++ 18 files changed, 851 insertions(+), 1463 deletions(-) delete mode 100644 getopt3/Argument.php delete mode 100644 getopt3/ArgumentProcessor.php delete mode 100644 getopt3/Arguments.php delete mode 100644 getopt3/Getopt.php delete mode 100644 getopt3/Option.php delete mode 100644 getopt3/OptionParser.php create mode 100644 resources/option.php create mode 100644 resources/usage.php create mode 100644 src/Arguments.php delete mode 100644 src/CommandLineParser.php create mode 100644 src/Help.php rename test/{CommandLineParserTest.php => ArgumentsTest.php} (57%) diff --git a/getopt3/Argument.php b/getopt3/Argument.php deleted file mode 100644 index ff6c46e..0000000 --- a/getopt3/Argument.php +++ /dev/null @@ -1,117 +0,0 @@ -<?php - -namespace GetOpt; - -class Argument -{ - const CLASSNAME = __CLASS__; - - /** @var string */ - private $default; - /** @var callable */ - private $validation; - /** @var string */ - private $name; - - /** - * Creates a new argument. - * - * @param scalar|null $default Default value or NULL - * @param callable|null $validation a validation function (optional) - * @throws \InvalidArgumentException - */ - public function __construct($default = null, $validation = null, $name = "arg") - { - if (!is_null($default)) { - $this->setDefaultValue($default); - } - if (!is_null($validation)) { - $this->setValidation($validation); - } - $this->name = $name; - } - - /** - * Set the default value - * - * @param scalar $value - * @return Argument this object (for chaining calls) - * @throws \InvalidArgumentException - */ - public function setDefaultValue($value) - { - if (!is_scalar($value)) { - throw new \InvalidArgumentException("Default value must be scalar"); - } - $this->default = $value; - return $this; - } - - /** - * Set a validation function. - * The function must take a string and return true if it is valid, false otherwise. - * - * @param callable $callable - * @return Argument this object (for chaining calls) - * @throws \InvalidArgumentException - */ - public function setValidation($callable) - { - if (!is_callable($callable)) { - throw new \InvalidArgumentException("Validation must be a callable"); - } - $this->validation = $callable; - return $this; - } - - /** - * Check if an argument validates according to the specification. - * - * @param string $arg - * @return bool - */ - public function validates($arg) - { - return (bool)call_user_func($this->validation, $arg); - } - - /** - * Check if the argument has a validation function - * - * @return bool - */ - public function hasValidation() - { - return isset($this->validation); - } - - /** - * Check whether the argument has a default value - * - * @return boolean - */ - public function hasDefaultValue() - { - return !empty($this->default); - } - - /** - * Retrieve the default value - * - * @return scalar|null - */ - public function getDefaultValue() - { - return $this->default; - } - - /** - * Retrieve the argument name - * - * @return string - */ - public function getName() - { - return $this->name; - } -} diff --git a/getopt3/ArgumentProcessor.php b/getopt3/ArgumentProcessor.php deleted file mode 100644 index ce77e80..0000000 --- a/getopt3/ArgumentProcessor.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -namespace GetOpt; - -class ArgumentProcessor -{ - public static function process(Getopt $getopt, Arguments $arguments) - { - while ($arg = $arguments->next()) { - if ($arg === '--') { - $getopt->__addOperands($arguments->rest()); - } elseif (empty($arg) || $arg === '-' || $arg[0] !== '-') { - $getopt->__addOperands([$arg]); - } elseif ($arg[1] === '-') { - $p = strpos($arg, '='); - if ($p !== false) { - $name = substr($arg, 2, $p); - $arguments->unshift(substr($arg, $p + 1)); - } else { - $name = substr($arg, 2); - } - $option = $getopt->getOption($name); - if (!$option) { - throw new \UnexpectedValueException(sprintf( - 'Option \'%s\' is unknown', - $name - )); - } - $option->retrieveValue($arguments); - } - } - } -} diff --git a/getopt3/Arguments.php b/getopt3/Arguments.php deleted file mode 100644 index c10b3c4..0000000 --- a/getopt3/Arguments.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -namespace GetOpt; - -class Arguments -{ - /** @var string[] */ - protected $arguments; - - public function __construct(array $arguments) - { - $this->arguments = $arguments; - } - - public static function fromString($argsString) - { - $argv = array(); - $argsString = trim($argsString); - $argc = 0; - - $state = 'n'; // states: n (normal), d (double quoted), s(single quoted) - for ($i = 0; $i < strlen($argsString); $i++) { - $char = $argsString{$i}; - switch ($state) { - case 'n': - if ($char === '\'') { - $state = 's'; - } elseif ($char === '"') { - $state = 'd'; - } elseif (in_array($char, array("\n", "\t", ' '))) { - $argc++; - $argv[$argc] = ''; - } else { - $argv[$argc] .= $char; - } - break; - - case 's': - if ($char === '\'') { - $state = 'n'; - } else { - $argv[$argc] .= $char; - } - break; - - case 'd': - if ($char === '"') { - $state = 'n'; - } else { - $argv[$argc] .= $char; - } - break; - } - } - - return new self($argv); - } -} diff --git a/getopt3/Getopt.php b/getopt3/Getopt.php deleted file mode 100644 index a3e58a0..0000000 --- a/getopt3/Getopt.php +++ /dev/null @@ -1,269 +0,0 @@ -<?php - -namespace GetOpt; - -class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate -{ - const NO_ARGUMENT = 0; - const REQUIRED_ARGUMENT = 1; - const OPTIONAL_ARGUMENT = 2; - const MULTIPLE_ARGUMENT = 3; - - const SETTINGS_SCRIPT_NAME = 'scriptName'; - const SETTING_DEFAULT_MODE = 'defaultMode'; - - /** @var OptionParser */ - protected $optionParser; - - /** @var array */ - protected $settings = [ - self::SETTING_DEFAULT_MODE => self::NO_ARGUMENT - ]; - - /**@var Option[] */ - protected $options = []; - - /** @var Option[] */ - protected $optionMapping = []; - - /** - * Creates a new Getopt object. - * - * The argument $options can be either a string in the format accepted by the PHP library - * function getopt() or an array. - * - * @param array $settings - * @link https://www.gnu.org/s/hello/manual/libc/Getopt.html GNU Getopt manual - */ - public function __construct(array $settings = []) - { - $this->set( - self::SETTINGS_SCRIPT_NAME, - isset($_SERVER['argv'][0]) ? $_SERVER['argv'][0] : ( - isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : null - ) - ); - foreach ($settings as $setting => $value) { - $this->set($settings, $value); - } - } - - public function set($setting, $value) - { - $this->settings[$setting] = $value; -// switch ($setting) { -// default: -// -// } - return $this; - } - - public function get($setting) - { - return isset($this->settings[$setting]) ? $this->settings[$setting] : null; - } - - public function addOptions($options) - { - if (is_string($options)) { - $options = $this->getOptionParser()->parseString($options); - } - - foreach ($options as $option) { - $this->addOption($option); - } - - return $this; - } - - public function addOption($option) - { - if (!$option instanceof Option) { - if (is_string($option)) { - $options = $this->getOptionParser()->parseString($option); - if (count($options) === 0) { - throw new \InvalidArgumentException(sprintf( - 'Could not create options from string \'%s\'', - $option - )); - } - // this is addOption - so we use only the first one - $option = $options[0]; - } elseif (is_array($option)) { - $option = $this->getOptionParser()->parseArray($option); - } else { - throw new \InvalidArgumentException(sprintf( - '$option has to be a string, an array or an Option. %s given', - gettype($option) - )); - } - } - - if ($this->getOption($option->short()) || $this->getOption($option->long())) { - throw new \InvalidArgumentException('$option`s short and long name have to be unique'); - } - - $this->options[] = $option; - - return $this; - } - - /** - * Get a option by $name - * - * @param string $name Short or long name of the option - * @return Option - */ - public function getOption($name) - { - if (!isset($this->optionMapping[$name])) { - $this->optionMapping[$name] = null; - foreach ($this->options as $option) { - if ($option->matches($name)) { - $this->optionMapping[$name] = $option; - break; - } - } - } - - return $this->optionMapping[$name]; - } - - /** - * @param array|string|Arguments $arguments - */ - public function process($arguments = null) - { - if ($arguments === null) { - $arguments = isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : []; - $arguments = new Arguments($arguments); - } elseif (is_array($arguments)) { - $arguments = new Arguments($arguments); - } elseif (is_string($arguments)) { - $arguments = Arguments::fromString($arguments); - } elseif (!$arguments instanceof Arguments) { - throw new \InvalidArgumentException( - '$arguments has to be an instance of Arguments, an arguments string, an array of arguments or null' - ); - } - - - } - - /** - * Create or get the OptionParser - * - * @return OptionParser - */ - protected function getOptionParser() - { - if ($this->optionParser === null) { - $this->optionParser = new OptionParser($this->settings[self::SETTING_DEFAULT_MODE]); - } - - return $this->optionParser; - } - - // backward compatibility - - public function setScriptName($scriptName) - { - return $this->set(self::SETTINGS_SCRIPT_NAME, $scriptName); - } - - // array functions - - /** - * Retrieve an external iterator - * - * @link http://php.net/manual/en/iteratoraggregate.getiterator.php - * @return Traversable An instance of an object implementing <b>Iterator</b> or - * <b>Traversable</b> - * @since 5.0.0 - */ - public function getIterator() - { - // TODO: Implement getIterator() method. - } - - /** - * Whether a offset exists - * - * @link http://php.net/manual/en/arrayaccess.offsetexists.php - * @param mixed $offset <p> - * An offset to check for. - * </p> - * @return boolean true on success or false on failure. - * </p> - * <p> - * The return value will be casted to boolean if non-boolean was returned. - * @since 5.0.0 - */ - public function offsetExists($offset) - { - // TODO: Implement offsetExists() method. - } - - /** - * Offset to retrieve - * - * @link http://php.net/manual/en/arrayaccess.offsetget.php - * @param mixed $offset <p> - * The offset to retrieve. - * </p> - * @return mixed Can return all value types. - * @since 5.0.0 - */ - public function offsetGet($offset) - { - // TODO: Implement offsetGet() method. - } - - /** - * Offset to set - * - * @link http://php.net/manual/en/arrayaccess.offsetset.php - * @param mixed $offset <p> - * The offset to assign the value to. - * </p> - * @param mixed $value <p> - * The value to set. - * </p> - * @return void - * @since 5.0.0 - */ - public function offsetSet($offset, $value) - { - throw new \LogicException('Read only array access'); - } - - /** - * Offset to unset - * - * @link http://php.net/manual/en/arrayaccess.offsetunset.php - * @param mixed $offset <p> - * The offset to unset. - * </p> - * @return void - * @since 5.0.0 - */ - public function offsetUnset($offset) - { - throw new \LogicException('Read only array access'); - } - - /** - * Count elements of an object - * - * @link http://php.net/manual/en/countable.count.php - * @return int The custom count as an integer. - * </p> - * <p> - * The return value is cast to an integer. - * @since 5.1.0 - */ - public function count() - { - // TODO: Implement count() method. - } -} diff --git a/getopt3/Option.php b/getopt3/Option.php deleted file mode 100644 index af886e9..0000000 --- a/getopt3/Option.php +++ /dev/null @@ -1,184 +0,0 @@ -<?php - -namespace GetOpt; - -/** - * Represents an option that Getopt accepts. - */ -class Option -{ - const CLASSNAME = __CLASS__; - - private $short; - private $long; - private $mode; - private $description = ''; - private $argument; - private $value = null; - - /** - * Creates a new option. - * - * @param string $short the option's short name (a single letter or digit) or null for long-only options - * @param string $long the option's long name (a string of 2+ letter/digit/_/- characters, starting with a letter - * or digit) or null for short-only options - * @param int $mode whether the option can/must have an argument (one of the constants defined in the Getopt - * class) - * (optional, defaults to no argument) - * @throws \InvalidArgumentException if both short and long name are null - */ - public function __construct($short, $long, $mode = Getopt::NO_ARGUMENT) - { - if (!$short && !$long) { - throw new \InvalidArgumentException("The short and long name may not both be empty"); - } - $this->setShort($short); - $this->setLong($long); - $this->setMode($mode); - $this->argument = new Argument(); - } - - /** - * Defines a description for the option. This is only used for generating usage information. - * - * @param string $description - * @return Option this object (for chaining calls) - */ - public function setDescription($description) - { - $this->description = $description; - return $this; - } - - /** - * Defines a default value for the option. - * - * @param mixed $value - * @return Option this object (for chaining calls) - */ - public function setDefaultValue($value) - { - $this->argument->setDefaultValue($value); - return $this; - } - - /** - * Defines a validation function for the option. - * - * @param callable $function - * @return Option this object (for chaining calls) - */ - public function setValidation($function) - { - $this->argument->setValidation($function); - return $this; - } - - /** - * Sets the argument object directly. - * - * @param Argument $arg - * @return Option this object (for chaining calls) - */ - public function setArgument(Argument $arg) - { - if ($this->mode == Getopt::NO_ARGUMENT) { - throw new \InvalidArgumentException("Option should not have any argument"); - } - $this->argument = $arg; - return $this; - } - - /** - * Returns true if the given string is equal to either the short or the long name. - * - * @param string $string - * @return bool - */ - public function matches($string) - { - return ($string === $this->short) || ($string === $this->long); - } - - public function short() - { - return $this->short; - } - - public function long() - { - return $this->long; - } - - public function mode() - { - return $this->mode; - } - - public function getDescription() - { - return $this->description; - } - - /** - * Retrieve the argument object - * - * @return Argument - */ - public function getArgument() - { - return $this->argument; - } - - public function retrieveValue(Arguments $arguments) - { - if ($this->mode === Getopt::NO_ARGUMENT) { - if ($this->value === null) { - $this->value = 0; - } - $this->value++; - } - } - - /** - * Fluent interface for constructor so options can be added during construction - * - * @see Options::__construct() - */ - public static function create($short, $long, $mode = Getopt::NO_ARGUMENT) - { - return new self($short, $long, $mode); - } - - private function setShort($short) - { - if (!(is_null($short) || preg_match("/^[a-zA-Z0-9]$/", $short))) { - throw new \InvalidArgumentException("Short option must be null or a letter/digit, found '$short'"); - } - $this->short = $short; - } - - private function setLong($long) - { - if (!(is_null($long) || preg_match("/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,}$/", $long))) { - throw new \InvalidArgumentException("Long option must be null or an alphanumeric string, found '$long'"); - } - $this->long = $long; - } - - private function setMode($mode) - { - if (!in_array( - $mode, - array( Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT, Getopt::REQUIRED_ARGUMENT ), - true - ) - ) { - throw new \InvalidArgumentException( - "Option mode must be one of " - . "Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT and Getopt::REQUIRED_ARGUMENT" - ); - } - $this->mode = $mode; - } -} diff --git a/getopt3/OptionParser.php b/getopt3/OptionParser.php deleted file mode 100644 index f3a9d1a..0000000 --- a/getopt3/OptionParser.php +++ /dev/null @@ -1,116 +0,0 @@ -<?php - -namespace GetOpt; - -/** - * Converts user-given option specifications into Option objects. - */ -class OptionParser -{ - private $defaultMode; - - /** - * Creates a new instance. - * - * @param int $defaultMode will be assigned to options when no mode is given for them. - */ - public function __construct($defaultMode) - { - $this->defaultMode = $defaultMode; - } - - /** - * Parse a GNU-style option string. - * - * @param string $string the option string - * @return Option[] - * @throws \InvalidArgumentException - */ - public function parseString($string) - { - if (!mb_strlen($string)) { - throw new \InvalidArgumentException('Option string must not be empty'); - } - $options = array(); - $eol = mb_strlen($string) - 1; - $nextCanBeColon = false; - for ($i = 0; $i <= $eol; ++$i) { - $ch = $string[$i]; - if (!preg_match('/^[A-Za-z0-9]$/', $ch)) { - $colon = $nextCanBeColon ? " or ':'" : ''; - throw new \InvalidArgumentException( - "Option string is not well formed: " - . "expected a letter$colon, found '$ch' at position " . ($i + 1) - ); - } - if ($i == $eol || $string[$i + 1] != ':') { - $options[] = new Option($ch, null, Getopt::NO_ARGUMENT); - $nextCanBeColon = true; - } elseif ($i < $eol - 1 && $string[$i + 2] == ':') { - $options[] = new Option($ch, null, Getopt::OPTIONAL_ARGUMENT); - $i += 2; - $nextCanBeColon = false; - } else { - $options[] = new Option($ch, null, Getopt::REQUIRED_ARGUMENT); - ++$i; - $nextCanBeColon = true; - } - } - return $options; - } - - /** - * Processes an option array. The array should be conform to the format - * (short, long, mode [, description [, default]]). See documentation for details. - * - * Developer note: Please don't add any further elements to the array. Future features should be configured only - * through the Option class's methods. - * - * @param array $array - * @return Option - */ - public function parseArray(array $array) - { - $rowSize = count($array); - if ($rowSize < 3) { - $array = $this->completeOptionArray($array); - } - $option = new Option($array[0], $array[1], $array[2]); - if ($rowSize >= 4) { - $option->setDescription($array[3]); - } - if ($rowSize >= 5 && $array[2] != Getopt::NO_ARGUMENT) { - $option->setArgument(new Argument($array[4])); - } - return $option; - } - - /** - * When using arrays, instead of a full option spec ([short, long, type]) users can leave out one or more of - * these parts and have Getopt fill them in intelligently: - * - If either the short or the long option string is left out, the first element of the given array is interpreted - * as either short (if it has length 1) or long, and the other one is set to null. - * - If the type is left out, it is set to NO_ARGUMENT. - * - * @param array $row - * @return array - */ - private function completeOptionArray(array $row) - { - $short = (strlen($row[0]) == 1) ? $row[0] : null; - - $long = null; - if (is_null($short)) { - $long = $row[0]; - } elseif (count($row) > 1 && !is_int($row[1])) { - $long = $row[1]; - } - - $mode = $this->defaultMode; - if (count($row) == 2 && is_int($row[1])) { - $mode = $row[1]; - } - - return array( $short, $long, $mode ); - } -} diff --git a/resources/option.php b/resources/option.php new file mode 100644 index 0000000..2763c2a --- /dev/null +++ b/resources/option.php @@ -0,0 +1,32 @@ +<?php + +use GetOpt\Getopt; + +/** @var \GetOpt\Option $option */ +/** @var int $padding */ + +switch ($option->mode()) { + case Getopt::OPTIONAL_ARGUMENT: + $argument = '[<' . $option->getArgument()->getName() . '>]'; + break; + + case GetOpt::REQUIRED_ARGUMENT: + case GetOpt::MULTIPLE_ARGUMENT: + $argument = '<' . $option->getArgument()->getName() . '>'; + break; + + case Getopt::NO_ARGUMENT: + default: + $argument = ''; +} + +$definition = str_pad(sprintf( + ' %s %s', + implode(', ', array_filter( array( + $option->short() ? '-' . $option->short() : null, + $option->long() ? '--' . $option->long() : null, + ))), + $argument +), $padding); + +return sprintf("%s %s", $definition, $option->getDescription()); diff --git a/resources/usage.php b/resources/usage.php new file mode 100644 index 0000000..ebc20e0 --- /dev/null +++ b/resources/usage.php @@ -0,0 +1,12 @@ +<?php + +use GetOpt\Getopt; + +/** @var string $banner */ +/** @var string $scriptName */ + +if ($banner) { + return sprintf($banner, $scriptName); +} + +return sprintf("Usage: %s [options] [operands]", $scriptName); diff --git a/src/Arguments.php b/src/Arguments.php new file mode 100644 index 0000000..413fe06 --- /dev/null +++ b/src/Arguments.php @@ -0,0 +1,181 @@ +<?php + +namespace GetOpt; + +class Arguments +{ + /** @var string[] */ + protected $arguments; + + public function __construct(array $arguments) + { + $this->arguments = $arguments; + } + + public function process(Getopt $getopt, callable $addOperand) + { + while (($arg = array_shift($this->arguments)) !== null) { + if ($this->isMeta($arg)) { + // everything from here are operands + foreach ($this->arguments as $argument) { + $addOperand($argument); + } + break; + } + + if ($this->isValue($arg)) { + $addOperand($arg); + } + + if ($this->isLongOption($arg)) { + $name = $this->longName($arg); + $option = $getopt->getOption($name, true); + + if (!$option) { + throw new \UnexpectedValueException(sprintf( + 'Option \'%s\' is unknown', + $name + )); + } + + $value = null; + if ($option->mode() !== Getopt::NO_ARGUMENT) { + $value = $this->value($arg); + } + + $option->setValue($value); + continue; + } + + // the only left is short options + foreach ($this->shortNames($arg) as $name) { + $option = $getopt->getOption($name, true); + + if (!$option) { + throw new \UnexpectedValueException(sprintf( + 'Option \'%s\' is unknown', + $name + )); + } + + $value = null; + if ($option->mode() !== Getopt::NO_ARGUMENT) { + $value = $this->value($arg, $name); + } + + $option->setValue($value); + if ($value) { + // when there is a value it was the last option + break; + } + } + } + return true; + } + + protected function isOption($arg) + { + return !$this->isValue($arg) && !$this->isMeta($arg); + } + + protected function isValue($arg) + { + return (empty($arg) || $arg === '-' || $arg[0] !== '-'); + } + + protected function isMeta($arg) + { + return $arg && $arg === '--'; + } + + protected function isLongOption($arg) + { + return $this->isOption($arg) && $arg[1] === '-'; + } + + protected function longName($arg) + { + $name = substr($arg, 2); + $p = strpos($name, '='); + return $p ? substr($name, 0, $p) : $name; + } + + protected function shortNames($arg) + { + if (!$this->isOption($arg) || $this->isLongOption($arg)) { + return array(); + } + + return array_map(function ($i) use ($arg) { + return mb_substr($arg, $i, 1); + }, range(1, mb_strlen($arg) -1)); + } + + protected function value($arg, $name = null) + { + $p = strpos($arg, $this->isLongOption($arg) ? '=' : $name); + if ($this->isLongOption($arg) && $p || !$this->isLongOption($arg) && $p < strlen($arg)-1) { + return substr($arg, $p+1); + } + + if (!empty($this->arguments) && $this->isValue($this->arguments[0])) { + return array_shift($this->arguments); + } + + return null; + } + + /** + * Parse arguments from argument string + * + * @param string $argsString + * @return Arguments + */ + public static function fromString($argsString) + { + $argv = array(''); + $argsString = trim($argsString); + $argc = 0; + + if (empty($argsString)) { + return new self([]); + } + + $state = 'n'; // states: n (normal), d (double quoted), s(single quoted) + for ($i = 0; $i < strlen($argsString); $i++) { + $char = $argsString{$i}; + switch ($state) { + case 'n': + if ($char === '\'') { + $state = 's'; + } elseif ($char === '"') { + $state = 'd'; + } elseif (in_array($char, array("\n", "\t", ' '))) { + $argc++; + $argv[$argc] = ''; + } else { + $argv[$argc] .= $char; + } + break; + + case 's': + if ($char === '\'') { + $state = 'n'; + } else { + $argv[$argc] .= $char; + } + break; + + case 'd': + if ($char === '"') { + $state = 'n'; + } else { + $argv[$argc] .= $char; + } + break; + } + } + + return new self($argv); + } +} diff --git a/src/CommandLineParser.php b/src/CommandLineParser.php deleted file mode 100644 index fd7f02f..0000000 --- a/src/CommandLineParser.php +++ /dev/null @@ -1,286 +0,0 @@ -<?php - -namespace GetOpt; - -/** - * Parses command line arguments according to a list of allowed options. - */ -class CommandLineParser -{ - /** @var Option[] */ - private $optionList; - - private $options = array(); - private $operands = array(); - - /** - * Creates a new instance. - * - * @param Option[] $optionList the list of allowed options - */ - public function __construct(array $optionList) - { - $this->optionList = $optionList; - } - - /** - * Parses the given arguments and converts them into options and operands. - * - * @param mixed $arguments a string or an array with one argument per element - */ - public function parse($arguments) - { - if (!is_array($arguments)) { - $arguments = $this->parseArgumentString($arguments); - } - $operands = &$this->operands; - $numArgs = count($arguments); - for ($i = 0; $i < $numArgs; ++$i) { - $arg = $arguments[$i]; - - if ($arg === '--') { - // the rest are operands - $operands = array_merge($operands, array_slice($arguments, $i + 1)); - break; - } - - if (empty($arg) || $arg === '-' || $arg[0] !== '-') { - // this is an operand - $operands[] = $arg; - continue; - } - - if (mb_substr($arg, 0, 2) == '--') { - $this->addLongOption($arguments, $i); - } else { - $this->addShortOption($arguments, $i); - } - } // endfor - - $this->addDefaultValues(); - } - - /** - * Returns the options created by a previous invocation of parse(). - * - * @return array - */ - public function getOptions() - { - return $this->options; - } - - - /** - * Returns the operands created by a previous invocation of parse(), - * - * @return array - */ - public function getOperands() - { - return $this->operands; - } - - private function addShortOption($arguments, &$i) - { - $numArgs = count($arguments); - $option = mb_substr($arguments[$i], 1); - if (mb_strlen($option) > 1) { - // multiple options strung together - $options = $this->splitString($option, 1); - foreach ($options as $j => $ch) { - // If a required argument is encountered, treat the rest of the - // string (or the next argument, if it's the last character) as - // its value - if ($this->optionHasArgument($ch)) { - if ($j < count($options) - 1) { - // Required argument in the middle of the string, treat - // the remainder as the argument; e.g. `ssh -vvvp222` - $value = implode('', array_slice($options, $j + 1)); - $this->addOption($ch, $value); - } else { - // Required argument was last, e.g. `ssh -vvvp 222` - $value = isset($arguments[$i + 1]) ? $arguments[$i + 1] : null; - $this->addOption($ch, $value); - } - break; - } else { - $this->addOption($ch, null); - } - } - } else { - if ($i < $numArgs - 1 - && ((mb_substr($arguments[$i + 1], 0, 1) !== '-') || ($arguments[$i + 1] === '-')) - && $this->optionHasArgument($option) - ) { - $value = $arguments[$i + 1]; - ++$i; - } else { - $value = null; - } - $this->addOption($option, $value); - } - } - - private function addLongOption($arguments, &$i) - { - $option = mb_substr($arguments[$i], 2); - if (strpos($option, '=') === false) { - if ($i < count($arguments) - 1 - && ((mb_substr($arguments[$i + 1], 0, 1) !== '-') || ($arguments[$i + 1] === '-')) - && $this->optionHasArgument($option) - ) { - $value = $arguments[$i + 1]; - ++$i; - } else { - $value = null; - } - } else { - list($option, $value) = explode('=', $option, 2); - } - $this->addOption($option, $value); - } - - /** - * Add an option to the list of known options. - * - * @param string $string the option's name - * @param string $value the option's value (or null) - * @throws \UnexpectedValueException - * @return void - */ - private function addOption($string, $value) - { - foreach ($this->optionList as $option) { - if ($option->matches($string)) { - if ($option->mode() == Getopt::REQUIRED_ARGUMENT && $value === null) { - throw new \UnexpectedValueException("Option '$string' must have a value"); - } - if ($option->getArgument()->hasValidation()) { - if ((mb_strlen($value) > 0) && !$option->getArgument()->validates($value)) { - throw new \UnexpectedValueException("Option '$string' has an invalid value"); - } - } - // for no-argument options, check if they are duplicate - if ($option->mode() == Getopt::NO_ARGUMENT) { - $oldValue = isset($this->options[$string]) ? $this->options[$string] : null; - $value = is_null($oldValue) ? 1 : $oldValue + 1; - } - // for optional-argument options, set value to 1 if none was given - $value = $value !== null ? $value : 1; - // add both long and short names (if they exist) to the option array to facilitate lookup - if ($option->short()) { - $this->options[$option->short()] = $value; - } - if ($option->long()) { - $this->options[$option->long()] = $value; - } - return; - } - } - throw new \UnexpectedValueException("Option '$string' is unknown"); - } - - /** - * If there are options with default values that were not overridden by the parsed option string, - * add them to the list of known options. - */ - private function addDefaultValues() - { - foreach ($this->optionList as $option) { - if ($option->getArgument()->hasDefaultValue() - && !isset($this->options[$option->short()]) - && !isset($this->options[$option->long()]) - ) { - if ($option->short()) { - $this->addOption($option->short(), $option->getArgument()->getDefaultValue()); - } - if ($option->long()) { - $this->addOption($option->long(), $option->getArgument()->getDefaultValue()); - } - } - } - } - - /** - * Return true if the given option can take an argument, false if it can't or is unknown. - * - * @param string $name the option's name - * @return boolean - */ - private function optionHasArgument($name) - { - foreach ($this->optionList as $option) { - if ($option->matches($name)) { - return $option->mode() != Getopt::NO_ARGUMENT; - } - } - return false; - } - - /** - * Split the string into individual characters, - * - * @param string $string string to split - * @return array - */ - private function splitString($string) - { - $result = array(); - for ($i = 0; $i < mb_strlen($string, "UTF-8"); ++$i) { - $result[] = mb_substr($string, $i, 1, "UTF-8"); - } - return $result; - } - - - /** - * Prase the command line string and returns an array. - * - * @param string $argsString - * @return array - */ - protected function parseArgumentString($argsString) - { - $argv = array(''); - $argsString = trim($argsString); - $argc = 0; - - $state = 'n'; // states: n (normal), d (double quoted), s(single quoted) - for ($i = 0; $i < strlen($argsString); $i++) { - $char = $argsString{$i}; - switch ($state) { - case 'n': - if ($char === '\'') { - $state = 's'; - } elseif ($char === '"') { - $state = 'd'; - } elseif (in_array($char, array("\n", "\t", ' '))) { - $argc++; - $argv[$argc] = ''; - } else { - $argv[$argc] .= $char; - } - break; - - case 's': - if ($char === '\'') { - $state = 'n'; - } else { - $argv[$argc] .= $char; - } - break; - - case 'd': - if ($char === '"') { - $state = 'n'; - } else { - $argv[$argc] .= $char; - } - break; - } - } - - return $argv; - } -} diff --git a/src/Getopt.php b/src/Getopt.php index 5ea188b..4820a98 100644 --- a/src/Getopt.php +++ b/src/Getopt.php @@ -2,32 +2,36 @@ namespace GetOpt; -/** - * Getopt.PHP allows for easy processing of command-line arguments. - * It is a more powerful, object-oriented alternative to PHP's built-in getopt() function. - * - * @version 2.1.0 - * @license MIT - * @link http://ulrichsg.github.io/getopt-php - */ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate { const NO_ARGUMENT = 0; const REQUIRED_ARGUMENT = 1; const OPTIONAL_ARGUMENT = 2; + const MULTIPLE_ARGUMENT = 3; + + const SETTING_SCRIPT_NAME = 'scriptName'; + const SETTING_DEFAULT_MODE = 'defaultMode'; + const SETTING_BANNER = 'banner'; /** @var OptionParser */ protected $optionParser; - /** @var string */ - protected $scriptName; - /** @var Option[] */ - protected $optionList = array(); + + /** @var Help */ + protected $help; + /** @var array */ + protected $settings = [ + self::SETTING_DEFAULT_MODE => self::NO_ARGUMENT + ]; + + /**@var Option[] */ protected $options = array(); - /** @var array */ + + /** @var Option[] */ + protected $optionMapping = array(); + + /** @var string[] */ protected $operands = array(); - /** @var string */ - protected $banner = "Usage: %s [options] [operands]\n"; /** * Creates a new Getopt object. @@ -35,137 +39,154 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate * The argument $options can be either a string in the format accepted by the PHP library * function getopt() or an array. * - * @param mixed $options Array of options, a String, or null (see documentation for details) - * @param int $defaultType The default option type to use when omitted (optional) - * @throws \InvalidArgumentException - * + * @param array $settings * @link https://www.gnu.org/s/hello/manual/libc/Getopt.html GNU Getopt manual */ - public function __construct($options = null, $defaultType = Getopt::NO_ARGUMENT) + public function __construct($options = null, array $settings = array()) { - $this->optionParser = new OptionParser($defaultType); if ($options !== null) { $this->addOptions($options); } + + $this->set( + self::SETTING_SCRIPT_NAME, + isset($_SERVER['argv'][0]) ? $_SERVER['argv'][0] : ( + isset($_SERVER['SCRIPT_NAME']) ? $_SERVER['SCRIPT_NAME'] : null + ) + ); + foreach ($settings as $setting => $value) { + $this->set($setting, $value); + } } - /** - * Set the scriptName manually - * - * @param $scriptName - */ - public function setScriptName($scriptName) + public function set($setting, $value) { - $this->scriptName = $scriptName; + $this->settings[$setting] = $value; +// switch ($setting) { +// default: +// +// } + return $this; + } + + public function get($setting) + { + return isset($this->settings[$setting]) ? $this->settings[$setting] : null; } - /** - * Extends the list of known options. Takes the same argument types as the constructor. - * - * @param mixed $options - * @throws \InvalidArgumentException - */ public function addOptions($options) { if (is_string($options)) { - $this->mergeOptions($this->optionParser->parseString($options)); - } elseif (is_array($options)) { - $this->mergeOptions($this->optionParser->parseArray($options)); - } else { - throw new \InvalidArgumentException("Getopt(): argument must be string or array"); + $options = $this->getOptionParser()->parseString($options); + } + + if (!is_array($options)) { + throw new \InvalidArgumentException('Getopt(): argument must be string or array'); } + + foreach ($options as $option) { + $this->addOption($option); + } + + return $this; } - /** - * Merges new options with the ones already in the Getopt optionList, making sure the resulting list is free of - * conflicts. - * - * @param Option[] $options The list of new options - * @throws \InvalidArgumentException - */ - protected function mergeOptions(array $options) + public function addOption($option) { - /** @var Option[] $mergedList */ - $mergedList = array_merge($this->optionList, $options); - $duplicates = array(); - foreach ($mergedList as $option) { - foreach ($mergedList as $otherOption) { - if (($option === $otherOption) || in_array($otherOption, $duplicates)) { - continue; - } - if ($this->optionsConflict($option, $otherOption)) { - throw new \InvalidArgumentException('Failed to add options due to conflict'); - } - if (($option->short() === $otherOption->short()) && ($option->long() === $otherOption->long())) { - $duplicates[] = $option; - } + if (!$option instanceof Option) { + if (is_string($option)) { + $options = $this->getOptionParser()->parseString($option); + // this is addOption - so we use only the first one + $option = $options[0]; + } elseif (is_array($option)) { + $option = $this->getOptionParser()->parseArray($option); + } else { + throw new \InvalidArgumentException(sprintf( + '$option has to be a string, an array or an Option. %s given', + gettype($option) + )); } } - foreach ($mergedList as $index => $option) { - if (in_array($option, $duplicates)) { - unset($mergedList[$index]); - } + + if ($this->getOption($option->short(), true) || $this->getOption($option->long(), true)) { + throw new \InvalidArgumentException('$option`s short and long name have to be unique'); } - $this->optionList = array_values($mergedList); + + $this->options[] = $option; + + return $this; } - protected function optionsConflict(Option $option1, Option $option2) + /** + * @param array|string|Arguments $arguments + */ + public function process($arguments = null) { - if ((is_null($option1->short()) && is_null($option2->short())) - || (is_null($option1->long()) && is_null($option2->long()))) { - return false; + if ($arguments === null) { + $arguments = isset($_SERVER['argv']) ? array_slice($_SERVER['argv'], 1) : array(); + $arguments = new Arguments($arguments); + } elseif (is_array($arguments)) { + $arguments = new Arguments($arguments); + } elseif (is_string($arguments)) { + $arguments = Arguments::fromString($arguments); + } elseif (!$arguments instanceof Arguments) { + throw new \InvalidArgumentException( + '$arguments has to be an instance of Arguments, an arguments string, an array of arguments or null' + ); } - return ((($option1->short() === $option2->short()) && ($option1->long() !== $option2->long())) - || (($option1->short() !== $option2->short()) && ($option1->long() === $option2->long()))); + + $arguments->process($this, function ($operand) { + $this->operands[] = $operand; + }); } /** - * Evaluate the given arguments. These can be passed either as a string or as an array. - * If nothing is passed, the running script's command line arguments are used. - * - * An {@link \UnexpectedValueException} or {@link \InvalidArgumentException} is thrown - * when the arguments are not well-formed or do not conform to the options passed by the user. + * Returns an usage information text generated from the given options. + * @param int $padding Number of characters to pad output of options to + * @return string + */ + public function getHelpText($padding = 25) + { + return $this->getHelp()->getText( + $this->get(self::SETTING_SCRIPT_NAME), + $this->options, + $this->get(self::SETTING_BANNER), + $padding + ); + } + + /** + * Get a option by $name * - * @param mixed $arguments optional ARGV array or space separated string + * @param string $name Short or long name of the option + * @return Option|mixed */ - public function parse($arguments = null) + public function getOption($name, $object = false) { - if (!$this->scriptName && isset($_SERVER['argv'][0])) { - $this->scriptName = $_SERVER['argv'][0]; + if (!isset($this->optionMapping[$name])) { + $this->optionMapping[$name] = null; + foreach ($this->options as $option) { + if ($option->matches($name)) { + $this->optionMapping[$name] = $option; + break; + } + } } - $this->options = array(); - if (!isset($arguments)) { - $arguments = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); - $this->scriptName = array_shift($arguments); // $argv[0] is the script's name - } elseif (is_string($arguments) && empty($this->scriptName)) { - $this->scriptName = $_SERVER['PHP_SELF']; + if ($object) { + return $this->optionMapping[$name]; } - $parser = new CommandLineParser($this->optionList); - $parser->parse($arguments); - $this->options = $parser->getOptions(); - $this->operands = $parser->getOperands(); + return $this->optionMapping[$name] !== null ? $this->optionMapping[$name]->getValue() : null; } - /** - * Returns the value of the given option. Must be invoked after parse(). - * - * The return value can be any of the following: - * <ul> - * <li><b>null</b> if the option is not given and does not have a default value</li> - * <li><b>the default value</b> if it has been defined and the option is not given</li> - * <li><b>an integer</b> if the option is given without argument. The - * returned value is the number of occurrences of the option.</li> - * <li><b>a string</b> if the option is given with an argument. The returned value is that argument.</li> - * </ul> - * - * @param string $name The (short or long) option name. - * @return mixed - */ - public function getOption($name) + public function getHelp() { - return isset($this->options[$name]) ? $this->options[$name] : null; + if (!$this->help) { + $this->help = new Help(); + } + + return $this->help; } /** @@ -175,7 +196,21 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate */ public function getOptions() { - return $this->options; + $result = []; + + foreach ($this->options as $option) { + $value = $option->getValue(); + if ($value !== null) { + if ($short = $option->short()) { + $result[$short] = $value; + } + if ($long = $option->long()) { + $result[$long] = $value; + } + } + } + + return $result; } /** @@ -200,111 +235,112 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate } /** - * Returns the banner string + * Create or get the OptionParser * - * @return string + * @return OptionParser */ - public function getBanner() + protected function getOptionParser() { - return $this->banner; + if ($this->optionParser === null) { + $this->optionParser = new OptionParser($this->settings[self::SETTING_DEFAULT_MODE]); + } + + return $this->optionParser; } + // backward compatibility + /** - * Set the banner string + * Set script name manually * - * @param string $banner The banner string; will be passed to sprintf(), can include %s for current scripts name. - * Be sure to include a trailing line feed. + * @param string $scriptName * @return Getopt + * @deprecated Use `Getopt::set(Getopt::SETTING_SCRIPT_NAME, $scriptName)` instead + * @codeCoverageIgnore */ - public function setBanner($banner) + public function setScriptName($scriptName) { - $this->banner = $banner; - return $this; + return $this->set(self::SETTING_SCRIPT_NAME, $scriptName); } /** - * Returns an usage information text generated from the given options. - * @param int $padding Number of characters to pad output of options to - * @return string + * Process $arguments or $_SERVER['argv'] + * + * These function is an alias for process now. Parse was not the correct verb for what + * the function is currently doing. + * + * @deprecated Use `Getopt::process($arguments)` instead + * @param mixed $arguments optional ARGV array or argument string + * @codeCoverageIgnore */ - public function getHelpText($padding = 25) + public function parse($arguments = null) { - $helpText = sprintf($this->getBanner(), $this->scriptName); - if (!empty($this->optionList)) { - $helpText .= "Options:\n"; - foreach ($this->optionList as $option) { - $mode = ''; - switch ($option->mode()) { - case self::NO_ARGUMENT: - $mode = ''; - break; - case self::REQUIRED_ARGUMENT: - $mode = "<".$option->getArgument()->getName().">"; - break; - case self::OPTIONAL_ARGUMENT: - $mode = "[<".$option->getArgument()->getName().">]"; - break; - } - $short = ($option->short()) ? '-'.$option->short() : ''; - $long = ($option->long()) ? '--'.$option->long() : ''; - if ($short && $long) { - $options = $short.', '.$long; - } else { - $options = $short ? : $long; - } - $padded = str_pad(sprintf(" %s %s", $options, $mode), $padding); - $helpText .= sprintf("%s %s\n", $padded, $option->getDescription()); - } - } - return $helpText; + $this->process($arguments); } + /** + * Get the current banner if defined + * + * @return string + * @deprecated Use `Help` for formatting the help message + */ + public function getBanner() + { + return $this->get(self::SETTING_BANNER); + } - /* - * Interface support functions + /** + * Set the banner + * + * @param string $banner + * @return Getopt + * @deprecated Use `Help` for formatting the help message */ + public function setBanner($banner) + { + return $this->set(self::SETTING_BANNER, $banner); + } - public function count() + // array functions + + public function getIterator() { - return count($this->options); + $result = []; + + foreach ($this->options as $option) { + if ($value = $option->getValue()) { + $name = $option->short() ?: $option->long(); + $result[$name] = $value; + } + } + + return new \ArrayIterator($result); } public function offsetExists($offset) { - return isset($this->options[$offset]); + $option = $this->getOption($offset, true); + return $option && $option->getValue() !== null; } public function offsetGet($offset) { - return $this->getOption($offset); + $option = $this->getOption($offset, true); + return $option ? $option->getValue() : null; } public function offsetSet($offset, $value) { - throw new \LogicException('Getopt is read-only'); + throw new \LogicException('Read only array access'); } public function offsetUnset($offset) { - throw new \LogicException('Getopt is read-only'); + throw new \LogicException('Read only array access'); } - public function getIterator() + public function count() { - // For options that have both short and long names, $this->options has two entries. - // We don't want this when iterating, so we have to filter the duplicates out. - $filteredOptions = array(); - foreach ($this->options as $name => $value) { - $keep = true; - foreach ($this->optionList as $option) { - if ($option->long() == $name && !is_null($option->short())) { - $keep = false; - } - } - if ($keep) { - $filteredOptions[$name] = $value; - } - } - return new \ArrayIterator($filteredOptions); + return $this->getIterator()->count(); } } diff --git a/src/Help.php b/src/Help.php new file mode 100644 index 0000000..caaf60d --- /dev/null +++ b/src/Help.php @@ -0,0 +1,31 @@ +<?php + +namespace GetOpt; + +class Help +{ + /** @var string */ + protected $usageTemplate = __DIR__ . '/../resources/usage.php'; + + /** @var string */ + protected $optionTemplate = __DIR__ . '/../resources/option.php'; + + /** @var int */ + protected $padding = 25; + + public function getText($scriptName, $options, $banner = null, $padding = null) + { + $padding = $padding ?: $this->padding; + $usage = trim(include($this->usageTemplate)); + $rows = array($usage); + + if (!empty($options)) { + $rows[] = 'Options:'; + foreach ($options as $option) { + $rows[] = include($this->optionTemplate); + } + } + + return implode(PHP_EOL, $rows) . PHP_EOL; + } +} diff --git a/src/Option.php b/src/Option.php index dcb12e0..627b2de 100644 --- a/src/Option.php +++ b/src/Option.php @@ -14,6 +14,7 @@ class Option private $mode; private $description = ''; private $argument; + private $value = null; /** * Creates a new option. @@ -26,7 +27,7 @@ class Option * (optional, defaults to no argument) * @throws \InvalidArgumentException if both short and long name are null */ - public function __construct($short, $long, $mode = Getopt::NO_ARGUMENT) + public function __construct($short, $long = null, $mode = Getopt::NO_ARGUMENT) { if (!$short && !$long) { throw new \InvalidArgumentException("The short and long name may not both be empty"); @@ -96,6 +97,10 @@ class Option */ public function matches($string) { + if ($string === null) { + return false; + } + return ($string === $this->short) || ($string === $this->long); } @@ -129,6 +134,55 @@ class Option return $this->argument; } + public function setValue($value = null) + { + if ($value === null && in_array($this->mode, [Getopt::REQUIRED_ARGUMENT, Getopt::MULTIPLE_ARGUMENT])) { + throw new \UnexpectedValueException(sprintf( + 'Option \'%s\' must have a value', + $this->long() ?: $this->short() + )); + } + + if ($value !== null && $this->mode !== Getopt::NO_ARGUMENT) { + if ($this->getArgument()->hasValidation() && !$this->getArgument()->validates($value)) { + throw new \UnexpectedValueException(sprintf( + 'Option \'%s\' has an invalid value', + $this->long() ?: $this->short() + )); + } + + if ($this->mode === Getopt::MULTIPLE_ARGUMENT) { + $this->value = $this->value === null ? array($value) : array_merge($this->value, array($value)); + } else { + $this->value = $value; + } + } elseif ($this->mode() !== Getopt::OPTIONAL_ARGUMENT || !is_string($this->value)) { + $this->value = $this->value === null ? 1 : $this->value + 1; + } + } + + public function getValue() + { + switch ($this->mode) { + case Getopt::OPTIONAL_ARGUMENT: + case Getopt::REQUIRED_ARGUMENT: + return $this->value === null ? $this->argument->getDefaultValue() : $this->value; + + case Getopt::MULTIPLE_ARGUMENT: + return $this->value === null ? array($this->argument->getDefaultValue()) : $this->value; + + case Getopt::NO_ARGUMENT: + default: + return $this->value; + } + } + + public function __toString() + { + $value = $this->getValue(); + return !is_array($value) ? $value . '' : implode(',', $value); + } + /** * Fluent interface for constructor so options can be added during construction * @@ -157,16 +211,19 @@ class Option private function setMode($mode) { - if (!in_array( - $mode, - array( Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT, Getopt::REQUIRED_ARGUMENT ), - true - ) - ) { - throw new \InvalidArgumentException( - "Option mode must be one of " - . "Getopt::NO_ARGUMENT, Getopt::OPTIONAL_ARGUMENT and Getopt::REQUIRED_ARGUMENT" - ); + if (!in_array($mode, array( + Getopt::NO_ARGUMENT, + Getopt::OPTIONAL_ARGUMENT, + Getopt::REQUIRED_ARGUMENT, + Getopt::MULTIPLE_ARGUMENT, + ), true)) { + throw new \InvalidArgumentException(sprintf( + 'Option mode must be one of %s, %s, %s and %s', + 'Getopt::NO_ARGUMENT', + 'Getopt::OPTIONAL_ARGUMENT', + 'Getopt::REQUIRED_ARGUMENT', + 'Getopt::MULTIPLE_ARGUMENT' + )); } $this->mode = $mode; } diff --git a/src/OptionParser.php b/src/OptionParser.php index d7bf186..7425e4a 100644 --- a/src/OptionParser.php +++ b/src/OptionParser.php @@ -60,51 +60,36 @@ class OptionParser } /** - * Processes an option array. The array elements can either be Option objects or arrays conforming to the format + * Processes an option array. The array should be conform to the format * (short, long, mode [, description [, default]]). See documentation for details. * * Developer note: Please don't add any further elements to the array. Future features should be configured only * through the Option class's methods. * * @param array $array - * @return Option[] - * @throws \InvalidArgumentException + * @return Option */ public function parseArray(array $array) { if (empty($array)) { - throw new \InvalidArgumentException('No options given'); + throw new \InvalidArgumentException('Invalid option array (at least a name has to be given)'); } - $options = array(); - foreach ($array as $row) { - if ($row instanceof Option) { - $options[] = $row; - } elseif (is_array($row)) { - $options[] = $this->createOption($row); - } else { - throw new \InvalidArgumentException("Invalid option type, must be Option or array"); - } - } - return $options; - } - /** - * @param array $row - * @return Option - */ - private function createOption(array $row) - { - $rowSize = count($row); + $rowSize = count($array); if ($rowSize < 3) { - $row = $this->completeOptionArray($row); + $array = $this->completeOptionArray($array); } - $option = new Option($row[0], $row[1], $row[2]); + + $option = new Option($array[0], $array[1], $array[2]); + if ($rowSize >= 4) { - $option->setDescription($row[3]); + $option->setDescription($array[3]); } - if ($rowSize >= 5 && $row[2] != Getopt::NO_ARGUMENT) { - $option->setArgument(new Argument($row[4])); + + if ($rowSize >= 5 && $array[2] != Getopt::NO_ARGUMENT) { + $option->setArgument(new Argument($array[4])); } + return $option; } diff --git a/test/CommandLineParserTest.php b/test/ArgumentsTest.php similarity index 57% rename from test/CommandLineParserTest.php rename to test/ArgumentsTest.php index 79794c2..7f49122 100644 --- a/test/CommandLineParserTest.php +++ b/test/ArgumentsTest.php @@ -2,16 +2,24 @@ namespace GetOpt; -class CommandLineParserTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class ArgumentsTest extends TestCase { + /** @var Getopt */ + protected $getopt; + + protected function setUp() + { + $this->getopt = new Getopt(); + } + public function testParseNoOptions() { - $parser = new CommandLineParser(array( - new Option('a', null) - )); - $parser->parse('something'); - $this->assertCount(0, $parser->getOptions()); - $operands = $parser->getOperands(); + $this->getopt->process(Arguments::fromString('something')); + + $this->assertCount(0, $this->getopt->getOptions()); + $operands = $this->getopt->getOperands(); $this->assertCount(1, $operands); $this->assertEquals('something', $operands[0]); } @@ -19,91 +27,103 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase public function testParseUnknownOption() { $this->setExpectedException('UnexpectedValueException'); - $parser = new CommandLineParser(array( - new Option('a', null) - )); - $parser->parse('-b'); + $this->getopt->addOption(new Option('a', null)); + + $this->getopt->process('-b'); + } + + public function testUnknownLongOption() + { + $this->setExpectedException('UnexpectedValueException'); + $this->getopt->addOption(new Option('a', 'alpha')); + + $this->getopt->process('--beta'); } public function testParseRequiredArgumentMissing() { $this->setExpectedException('UnexpectedValueException'); - $parser = new CommandLineParser(array( - new Option('a', null, Getopt::REQUIRED_ARGUMENT) - )); - $parser->parse('-a'); + $this->getopt->addOption(new Option('a', null, Getopt::REQUIRED_ARGUMENT)); + + $this->getopt->process('-a'); } public function testParseMultipleOptionsWithOneHyphen() { - $parser = new CommandLineParser(array( - new Option('a', null), - new Option('b', null) + $this->getopt->addOptions(array( + new Option('a'), + new Option('b'), )); - $parser->parse('-ab'); - $options = $parser->getOptions(); + $this->getopt->process('-ab'); + + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['a']); $this->assertEquals(1, $options['b']); } public function testParseCumulativeOption() { - $parser = new CommandLineParser(array( - new Option('a', null), - new Option('b', null) + $this->getopt->addOptions(array( + new Option('a'), + new Option('b'), )); - $parser->parse('-a -b -a -a'); - $options = $parser->getOptions(); + $this->getopt->process('-a -b -a -a'); + + $options = $this->getopt->getOptions(); $this->assertEquals(3, $options['a']); $this->assertEquals(1, $options['b']); } public function testParseCumulativeOptionShort() { - $parser = new CommandLineParser(array( - new Option('a', null), - new Option('b', null) + $this->getopt->addOptions(array( + new Option('a'), + new Option('b'), )); - $parser->parse('-abaa'); - $options = $parser->getOptions(); + $this->getopt->process('-abaa'); + + $options = $this->getopt->getOptions(); $this->assertEquals(3, $options['a']); $this->assertEquals(1, $options['b']); } public function testParseShortOptionWithArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a value'); - $options = $parser->getOptions(); + $this->getopt->process('-a value'); + + $options = $this->getopt->getOptions(); $this->assertEquals('value', $options['a']); } public function testParseZeroArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a 0'); - $options = $parser->getOptions(); + $this->getopt->process('-a 0'); + + $options = $this->getopt->getOptions(); $this->assertEquals('0', $options['a']); } public function testParseNumericOption() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT), new Option('2', null) )); - $parser->parse('-a 2 -2'); - $options = $parser->getOptions(); + $this->getopt->process('-a 2 -2'); + + $options = $this->getopt->getOptions(); $this->assertEquals('2', $options['a']); $this->assertEquals(1, $options['2']); } @@ -111,71 +131,71 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase public function testParseCollapsedShortOptionsRequiredArgumentMissing() { $this->setExpectedException('UnexpectedValueException'); - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null), new Option('b', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-ab'); + $this->getopt->process('-ab'); } public function testParseCollapsedShortOptionsWithArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null), new Option('b', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-ab value'); + $this->getopt->process('-ab value'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['a']); $this->assertEquals('value', $options['b']); } public function testParseNoArgumentOptionAndOperand() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null), )); - $parser->parse('-a b'); + $this->getopt->process('-a b'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['a']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(1, $operands); $this->assertEquals('b', $operands[0]); } - public function testParsedRequiredArumentWithNoSpace() + public function testParsedRequiredArgumentWithNoSpace() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('p', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-ppassword'); - $options = $parser->getOptions(); + $this->getopt->process('-ppassword'); + $options = $this->getopt->getOptions(); $this->assertEquals('password', $options['p']); } public function testParseCollapsedRequiredArgumentWithNoSpace() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('v', null), new Option('p', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-vvvppassword'); - $options = $parser->getOptions(); + $this->getopt->process('-vvvppassword'); + $options = $this->getopt->getOptions(); $this->assertEquals('password', $options['p']); $this->assertEquals(3, $options['v']); } public function testParseOperandsOnly() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT), new Option('b', null) )); - $parser->parse('-- -a -b'); + $this->getopt->process('-- -a -b'); - $this->assertCount(0, $parser->getOptions()); - $operands = $parser->getOperands(); + $this->assertCount(0, $this->getopt->getOptions()); + $operands = $this->getopt->getOperands(); $this->assertCount(2, $operands); $this->assertEquals('-a', $operands[0]); $this->assertEquals('-b', $operands[1]); @@ -183,85 +203,85 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase public function testParseLongOptionWithoutArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('o', 'option', Getopt::OPTIONAL_ARGUMENT) )); - $parser->parse('--option'); + $this->getopt->process('--option'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['option']); } public function testParseLongOptionWithoutArgumentAndOperand() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('o', 'option', Getopt::NO_ARGUMENT) )); - $parser->parse('--option something'); + $this->getopt->process('--option something'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['option']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(1, $operands); $this->assertEquals('something', $operands[0]); } public function testParseLongOptionWithArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('o', 'option', Getopt::OPTIONAL_ARGUMENT) )); - $parser->parse('--option value'); + $this->getopt->process('--option value'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('value', $options['option']); $this->assertEquals('value', $options['o']); } public function testParseLongOptionWithEqualsSignAndArgument() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('o', 'option', Getopt::OPTIONAL_ARGUMENT) )); - $parser->parse('--option=value something'); + $this->getopt->process('--option=value something'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('value', $options['option']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(1, $operands); $this->assertEquals('something', $operands[0]); } public function testParseLongOptionWithValueStartingWithHyphen() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('o', 'option', Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('--option=-value'); + $this->getopt->process('--option=-value'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('-value', $options['option']); } public function testParseNoValueStartingWithHyphenRequired() { $this->setExpectedException('UnexpectedValueException'); - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT), new Option('b', null) )); - $parser->parse('-a -b'); + $this->getopt->process('-a -b'); } public function testParseNoValueStartingWithHyphenOptional() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::OPTIONAL_ARGUMENT), new Option('b', null) )); - $parser->parse('-a -b'); + $this->getopt->process('-a -b'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(1, $options['a']); $this->assertEquals(1, $options['b']); } @@ -272,25 +292,34 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase $optionA->setArgument(new Argument(10)); $optionB = new Option('b', 'beta', Getopt::REQUIRED_ARGUMENT); $optionB->setArgument(new Argument(20)); - $parser = new CommandLineParser(array($optionA, $optionB)); - $parser->parse('-a 12'); + $this->getopt->addOptions(array($optionA, $optionB)); + $this->getopt->process('-a 12'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals(12, $options['a']); $this->assertEquals(20, $options['b']); $this->assertEquals(20, $options['beta']); } + public function testMultipleArgumentOptions() + { + $this->getopt->addOption(new Option('a', null, Getopt::MULTIPLE_ARGUMENT)); + + $this->getopt->process('-a value1 -a value2'); + + $this->assertEquals(['value1', 'value2'], $this->getopt->getOption('a')); + } + public function testDoubleHyphenNotInOperands() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a 0 foo -- bar baz'); + $this->getopt->process('-a 0 foo -- bar baz'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('0', $options['a']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(3, $operands); $this->assertEquals('foo', $operands[0]); $this->assertEquals('bar', $operands[1]); @@ -299,91 +328,91 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase public function testSingleHyphenValue() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'alpha', Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a -'); + $this->getopt->process('-a -'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('-', $options['a']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(0, $operands); - $parser->parse('--alpha -'); + $this->getopt->process('--alpha -'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('-', $options['a']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(0, $operands); } public function testSingleHyphenOperand() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a 0 -'); + $this->getopt->process('-a 0 -'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertEquals('0', $options['a']); - $operands = $parser->getOperands(); + $operands = $this->getopt->getOperands(); $this->assertCount(1, $operands); $this->assertEquals('-', $operands[0]); } public function testOptionsAfterOperands() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT), new Option('b', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a 42 operand -b "don\'t panic"'); + $this->getopt->process('-a 42 operand -b "don\'t panic"'); $this->assertEquals(array( 'a' => 42, 'b' => 'don\'t panic' - ), $parser->getOptions()); - $this->assertEquals(array('operand'), $parser->getOperands()); + ), $this->getopt->getOptions()); + $this->assertEquals(array('operand'), $this->getopt->getOperands()); } public function testEmptyOperandsAndOptionsWithString() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); - $parser->parse('-a "" ""'); + $this->getopt->process('-a "" ""'); - $this->assertSame(array('a' => ''), $parser->getOptions()); - $this->assertSame(array(''), $parser->getOperands()); + $this->assertSame(array('a' => ''), $this->getopt->getOptions()); + $this->assertSame(array(''), $this->getopt->getOperands()); } public function testEmptyOperandsAndOptionsWithArray() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', null, Getopt::REQUIRED_ARGUMENT) )); // this is how we get it in $_SERVER['argv'] - $parser->parse(array( + $this->getopt->process(array( '-a', '', '' )); - $this->assertSame(array('a' => ''), $parser->getOptions()); - $this->assertSame(array(''), $parser->getOperands()); + $this->assertSame(array('a' => ''), $this->getopt->getOptions()); + $this->assertSame(array(''), $this->getopt->getOperands()); } public function testSpaceOperand() { - $parser = new CommandLineParser(array()); + $this->getopt->addOptions(array()); - $parser->parse('" "'); + $this->getopt->process('" "'); - $this->assertSame(array(' '), $parser->getOperands()); + $this->assertSame(array(' '), $this->getopt->getOperands()); } public function testParseWithArgumentValidation() @@ -395,10 +424,10 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase $optionB->setArgument(new Argument(null, $validation)); $optionC = new Option('c', null, Getopt::OPTIONAL_ARGUMENT); $optionC->setArgument(new Argument(null, $validation)); - $parser = new CommandLineParser(array($optionA, $optionB, $optionC)); - $parser->parse('-a 1 -b 2 -c'); + $this->getopt->addOptions(array($optionA, $optionB, $optionC)); + $this->getopt->process('-a 1 -b 2 -c'); - $options = $parser->getOptions(); + $options = $this->getopt->getOptions(); $this->assertSame('1', $options['a']); $this->assertSame('2', $options['b']); $this->assertSame(1, $options['c']); @@ -410,67 +439,67 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase $validation = 'is_numeric'; $option = new Option('a', null, Getopt::OPTIONAL_ARGUMENT); $option->setArgument(new Argument(null, $validation)); - $parser = new CommandLineParser(array($option)); - $parser->parse('-a nonnumeric'); + $this->getopt->addOptions(array($option)); + $this->getopt->process('-a nonnumeric'); } public function testStringWithSingleQuotes() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse('-a \'the value\''); - $options = $parser->getOptions(); + $this->getopt->process('-a \'the value\''); + $options = $this->getopt->getOptions(); self::assertSame('the value', $options['a']); } public function testStringWithDoubleQuotes() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse('-a "the value"'); - $options = $parser->getOptions(); + $this->getopt->process('-a "the value"'); + $options = $this->getopt->getOptions(); self::assertSame('the value', $options['a']); } public function testSingleQuotesInString() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse('-a "the \'"'); - $options = $parser->getOptions(); + $this->getopt->process('-a "the \'"'); + $options = $this->getopt->getOptions(); self::assertSame('the \'', $options['a']); } public function testDoubleQuotesInString() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse('-a \'the "\''); - $options = $parser->getOptions(); + $this->getopt->process('-a \'the "\''); + $options = $this->getopt->getOptions(); self::assertSame('the "', $options['a']); } public function testQuoteConcatenation() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), new Option('b', 'optB', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse('-a \'this uses \'"\'"\' inside single quote\' -b "this uses "\'"\'" inside double quote"'); - $options = $parser->getOptions(); + $this->getopt->process('-a \'this uses \'"\'"\' inside single quote\' -b "this uses "\'"\'" inside double quote"'); + $options = $this->getopt->getOptions(); self::assertSame('this uses \' inside single quote', $options['a']); self::assertSame('this uses " inside double quote', $options['b']); @@ -478,24 +507,24 @@ class CommandLineParserTest extends \PHPUnit_Framework_TestCase public function testLinefeedAsSeparator() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse("-a\nvalue"); - $options = $parser->getOptions(); + $this->getopt->process("-a\nvalue"); + $options = $this->getopt->getOptions(); self::assertSame('value', $options['a']); } public function testTabAsSeparator() { - $parser = new CommandLineParser(array( + $this->getopt->addOptions(array( new Option('a', 'optA', Getopt::REQUIRED_ARGUMENT), )); - $parser->parse("-a\tvalue"); - $options = $parser->getOptions(); + $this->getopt->process("-a\tvalue"); + $options = $this->getopt->getOptions(); self::assertSame('value', $options['a']); } diff --git a/test/GetoptTest.php b/test/GetoptTest.php index b91199a..6923fce 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -17,6 +17,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase ); $getopt->parse('-a aparam -s sparam --long longparam'); + $this->assertEquals('aparam', $getopt->getOption('a')); $this->assertEquals('longparam', $getopt->getOption('long')); $this->assertEquals('sparam', $getopt->getOption('s')); @@ -39,7 +40,9 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testAddOptionsUseDefaultArgumentType() { - $getopt = new Getopt(null, Getopt::REQUIRED_ARGUMENT); + $getopt = new Getopt(null, [ + Getopt::SETTING_DEFAULT_MODE => Getopt::REQUIRED_ARGUMENT + ]); $getopt->addOptions( array( array('l', 'long') @@ -63,11 +66,12 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testAddOptionsOverwritesExistingOptions() { $getopt = new Getopt(array( - array('a', null, Getopt::REQUIRED_ARGUMENT) - )); - $getopt->addOptions(array( array('a', null, Getopt::NO_ARGUMENT) )); +// $getopt->addOptions(array( +// array('a', null, Getopt::NO_ARGUMENT) +// )); + $getopt->parse('-a foo'); $this->assertEquals(1, $getopt->getOption('a')); @@ -112,7 +116,11 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testCountable() { - $getopt = new Getopt('abc'); + $getopt = new Getopt(array( + new Option('a', 'alpha'), + new Option('b', 'beta'), + new Option('c', 'gamma'), + )); $getopt->parse('-abc'); $this->assertEquals(3, count($getopt)); } @@ -177,13 +185,6 @@ class GetoptTest extends \PHPUnit_Framework_TestCase $this->assertEquals($expected, $getopt->getHelpText()); } - public function testHelpTextNoParse() - { - $getopt = new Getopt(); - $expected = "Usage: [options] [operands]\n"; - $this->assertSame($expected, $getopt->getHelpText()); - } - public function testHelpTextWithCustomScriptName() { $getopt = new Getopt(); @@ -198,9 +199,67 @@ class GetoptTest extends \PHPUnit_Framework_TestCase $getopt = new Getopt(); $getopt->setBanner("My custom Banner %s\n"); - $this->assertSame("My custom Banner \n", $getopt->getHelpText()); - - $getopt->parse(''); $this->assertSame("My custom Banner $script\n", $getopt->getHelpText()); } + + public function testThrowsWithInvalidParameter() + { + $this->setExpectedException(\InvalidArgumentException::class); + $getopt = new Getopt(); + + $getopt->process(42); + } + + public function testAddOptionByString() + { + $getopt = new Getopt(); + $getopt->addOption('c'); + + $this->assertEquals(new Option('c', null), $getopt->getOption('c', true)); + } + + public function testThrowsForUnparsableString() + { + $this->setExpectedException(\InvalidArgumentException::class); + $getopt = new Getopt(); + + $getopt->addOption(''); + } + + public function testThrowsForInvalidParameter() + { + $this->setExpectedException(\InvalidArgumentException::class); + $getopt = new Getopt(); + + $getopt->addOption(42); + } + + public function testIssetArrayAccess() + { + $getopt = new Getopt(); + $getopt->addOption('a'); + $getopt->process('-a'); + + $result = isset($getopt['a']); + + self::assertTrue($result); + } + + public function testRestirctsArraySet() + { + $this->setExpectedException(\LogicException::class); + $getopt = new Getopt(); + + $getopt['a'] = 'test'; + } + + public function testRestirctsArrayUnset() + { + $this->setExpectedException(\LogicException::class); + $getopt = new Getopt(); + $getopt->addOption('a'); + $getopt->process('-a'); + + unset($getopt['a']); + } } diff --git a/test/OptionParserTest.php b/test/OptionParserTest.php index 2de93f9..737cb46 100644 --- a/test/OptionParserTest.php +++ b/test/OptionParserTest.php @@ -61,40 +61,43 @@ class OptionParserTest extends \PHPUnit_Framework_TestCase $this->parser->parseString('ab:c:::d'); } - public function testParseArray() + public function provideOptionArrays() { - $options = $this->parser->parseArray( - array( - array('a', 'alpha', Getopt::OPTIONAL_ARGUMENT, 'Description', 42), - new Option('b', 'beta'), - array('c') - ) + return array( + array(array('a', 'alpha', Getopt::OPTIONAL_ARGUMENT, 'Description', 42)), + array(array('b', 'beta')), + array(array('c')), ); + } - $this->assertCount(3, $options); - foreach ($options as $option) { - $this->assertInstanceOf(Option::CLASSNAME, $option); - switch ($option->short()) { - case 'a': - $this->assertEquals('alpha', $option->long()); - $this->assertEquals(Getopt::OPTIONAL_ARGUMENT, $option->mode()); - $this->assertEquals('Description', $option->getDescription()); - $this->assertEquals(42, $option->getArgument()->getDefaultValue()); - break; - case 'b': - $this->assertEquals('beta', $option->long()); - $this->assertEquals(Getopt::NO_ARGUMENT, $option->mode()); - $this->assertEquals('', $option->getDescription()); - break; - case 'c': - $this->assertNull($option->long()); - $this->assertEquals(Getopt::REQUIRED_ARGUMENT, $option->mode()); - $this->assertEquals('', $option->getDescription()); - $this->assertFalse($option->getArgument()->hasDefaultValue()); - break; - default: - $this->fail('Unexpected option: '.$option->short()); - } + /** + * @dataProvider provideOptionArrays + */ + public function testParseArray($array) + { + $option = $this->parser->parseArray($array); + + $this->assertInstanceOf(Option::CLASSNAME, $option); + switch ($option->short()) { + case 'a': + $this->assertEquals('alpha', $option->long()); + $this->assertEquals(Getopt::OPTIONAL_ARGUMENT, $option->mode()); + $this->assertEquals('Description', $option->getDescription()); + $this->assertEquals(42, $option->getArgument()->getDefaultValue()); + break; + case 'b': + $this->assertEquals('beta', $option->long()); + $this->assertEquals(Getopt::REQUIRED_ARGUMENT, $option->mode()); + $this->assertEquals('', $option->getDescription()); + break; + case 'c': + $this->assertNull($option->long()); + $this->assertEquals(Getopt::REQUIRED_ARGUMENT, $option->mode()); + $this->assertEquals('', $option->getDescription()); + $this->assertFalse($option->getArgument()->hasDefaultValue()); + break; + default: + $this->fail('Unexpected option: '.$option->short()); } } diff --git a/test/OptionTest.php b/test/OptionTest.php index dac4915..4292f6c 100644 --- a/test/OptionTest.php +++ b/test/OptionTest.php @@ -77,4 +77,30 @@ class OptionTest extends \PHPUnit_Framework_TestCase $this->assertEquals($option, $option->setValidation('is_numeric')); $this->assertTrue($option->getArgument()->hasValidation()); } + + public function testToStringWithoutArgument() + { + $option = new Option('a', null); + $option->setValue(null); + $option->setValue(null); + + $this->assertSame('2', (string)$option); + } + + public function testToStringWithArgument() + { + $option = new Option('a', null, Getopt::REQUIRED_ARGUMENT); + $option->setValue('valueA'); + + $this->assertSame('valueA', (string)$option); + } + + public function testToStringWithMultipleArguments() + { + $option = new Option('a', null, Getopt::MULTIPLE_ARGUMENT); + $option->setValue('valueA'); + $option->setValue('valueB'); + + $this->assertSame('valueA,valueB', (string)$option); + } } -- GitLab From 4e02b8666fe11d3e3bd62952441b5e131b274218 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sun, 9 Jul 2017 12:37:41 +0200 Subject: [PATCH 04/14] refactoring: fix php 5.3 --- src/Arguments.php | 71 ++++++++++++++++++++++++++++++++++++++++-- src/Help.php | 24 ++++++++++++-- test/ArgumentsTest.php | 15 ++++++--- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/Arguments.php b/src/Arguments.php index 413fe06..7ee5408 100644 --- a/src/Arguments.php +++ b/src/Arguments.php @@ -7,11 +7,25 @@ class Arguments /** @var string[] */ protected $arguments; + /** + * Create an Arguments object + * + * @param array $arguments + */ public function __construct(array $arguments) { $this->arguments = $arguments; } + /** + * Process the arguments for $getopt + * + * Stores operands using $addOperand callback. + * + * @param Getopt $getopt + * @param callable $addOperand + * @return bool + */ public function process(Getopt $getopt, callable $addOperand) { while (($arg = array_shift($this->arguments)) !== null) { @@ -73,26 +87,56 @@ class Arguments return true; } + /** + * Check if $arg is an option + * + * @param string $arg + * @return bool + */ protected function isOption($arg) { return !$this->isValue($arg) && !$this->isMeta($arg); } + /** + * Check if $arg is a value + * + * @param string $arg + * @return bool + */ protected function isValue($arg) { return (empty($arg) || $arg === '-' || $arg[0] !== '-'); } + /** + * Check if $arg is meta '--' + * + * @param string $arg + * @return bool + */ protected function isMeta($arg) { return $arg && $arg === '--'; } + /** + * Check if $arg is a long option + * + * @param $arg + * @return bool + */ protected function isLongOption($arg) { return $this->isOption($arg) && $arg[1] === '-'; } + /** + * Get the long option name from $arg + * + * @param string $arg + * @return string + */ protected function longName($arg) { $name = substr($arg, 2); @@ -100,6 +144,12 @@ class Arguments return $p ? substr($name, 0, $p) : $name; } + /** + * Get all short option names from $arg + * + * @param string $arg + * @return string[] (single character string multi byte safe) + */ protected function shortNames($arg) { if (!$this->isOption($arg) || $this->isLongOption($arg)) { @@ -111,6 +161,17 @@ class Arguments }, range(1, mb_strlen($arg) -1)); } + /** + * Get the value for an option + * + * $name might be the short name to separate the value from the argument in `-abcppassword`. + * + * Returns the value inside $arg or the next argument when it is a value. + * + * @param string $arg + * @param string $name + * @return string + */ protected function value($arg, $name = null) { $p = strpos($arg, $this->isLongOption($arg) ? '=' : $name); @@ -138,10 +199,10 @@ class Arguments $argc = 0; if (empty($argsString)) { - return new self([]); + return new self(array()); } - $state = 'n'; // states: n (normal), d (double quoted), s(single quoted) + $state = 'n'; // states: n (normal), d (double quoted), s (single quoted) for ($i = 0; $i < strlen($argsString); $i++) { $char = $argsString{$i}; switch ($state) { @@ -161,6 +222,9 @@ class Arguments case 's': if ($char === '\'') { $state = 'n'; + } elseif ($char === '\\') { + $i++; + $argv[$argc] .= $argsString{$i}; } else { $argv[$argc] .= $char; } @@ -169,6 +233,9 @@ class Arguments case 'd': if ($char === '"') { $state = 'n'; + } elseif ($char === '\\') { + $i++; + $argv[$argc] .= $argsString{$i}; } else { $argv[$argc] .= $char; } diff --git a/src/Help.php b/src/Help.php index caaf60d..38567ec 100644 --- a/src/Help.php +++ b/src/Help.php @@ -5,14 +5,34 @@ namespace GetOpt; class Help { /** @var string */ - protected $usageTemplate = __DIR__ . '/../resources/usage.php'; + protected $usageTemplate; /** @var string */ - protected $optionTemplate = __DIR__ . '/../resources/option.php'; + protected $optionTemplate; /** @var int */ protected $padding = 25; + /** + * Create a Help object + * + * @param array $settings + */ + public function __construct(array $settings = array()) + { + $this->usageTemplate = __DIR__ . '/../resources/usage.php'; + $this->optionTemplate = __DIR__ . '/../resources/option.php'; + } + + /** + * Get the help text for $options + * + * @param string $scriptName + * @param string $options + * @param string $banner + * @param string $padding + * @return string + */ public function getText($scriptName, $options, $banner = null, $padding = null) { $padding = $padding ?: $this->padding; diff --git a/test/ArgumentsTest.php b/test/ArgumentsTest.php index 7f49122..1ff25f8 100644 --- a/test/ArgumentsTest.php +++ b/test/ArgumentsTest.php @@ -307,7 +307,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a value1 -a value2'); - $this->assertEquals(['value1', 'value2'], $this->getopt->getOption('a')); + $this->assertEquals(array('value1', 'value2'), $this->getopt->getOption('a')); } public function testDoubleHyphenNotInOperands() @@ -498,11 +498,18 @@ class ArgumentsTest extends TestCase new Option('b', 'optB', Getopt::REQUIRED_ARGUMENT), )); - $this->getopt->process('-a \'this uses \'"\'"\' inside single quote\' -b "this uses "\'"\'" inside double quote"'); + $this->getopt->process('-a \'\'"\'"\' inside single quote\' -b ""\'"\'" inside double quote"'); $options = $this->getopt->getOptions(); - self::assertSame('this uses \' inside single quote', $options['a']); - self::assertSame('this uses " inside double quote', $options['b']); + self::assertSame('\' inside single quote', $options['a']); + self::assertSame('" inside double quote', $options['b']); + } + + public function testQuoteEscaping() + { + $this->getopt->process('-- "this \\" is a double quote"'); + + self::assertSame('this " is a double quote', $this->getopt->getOperand(0)); } public function testLinefeedAsSeparator() -- GitLab From 323a4094e17051fea06458c1ad64dae4da028a50 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sun, 9 Jul 2017 12:49:20 +0200 Subject: [PATCH 05/14] refactoring: fix php 5.3 --- test/GetoptTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/GetoptTest.php b/test/GetoptTest.php index 6923fce..141d2eb 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -40,9 +40,9 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testAddOptionsUseDefaultArgumentType() { - $getopt = new Getopt(null, [ + $getopt = new Getopt(null, array( Getopt::SETTING_DEFAULT_MODE => Getopt::REQUIRED_ARGUMENT - ]); + )); $getopt->addOptions( array( array('l', 'long') @@ -204,7 +204,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testThrowsWithInvalidParameter() { - $this->setExpectedException(\InvalidArgumentException::class); + $this->setExpectedException('InvalidArgumentException'); $getopt = new Getopt(); $getopt->process(42); @@ -220,7 +220,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testThrowsForUnparsableString() { - $this->setExpectedException(\InvalidArgumentException::class); + $this->setExpectedException('InvalidArgumentException'); $getopt = new Getopt(); $getopt->addOption(''); @@ -228,7 +228,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testThrowsForInvalidParameter() { - $this->setExpectedException(\InvalidArgumentException::class); + $this->setExpectedException('InvalidArgumentException'); $getopt = new Getopt(); $getopt->addOption(42); @@ -247,7 +247,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testRestirctsArraySet() { - $this->setExpectedException(\LogicException::class); + $this->setExpectedException('LogicException'); $getopt = new Getopt(); $getopt['a'] = 'test'; @@ -255,7 +255,7 @@ class GetoptTest extends \PHPUnit_Framework_TestCase public function testRestirctsArrayUnset() { - $this->setExpectedException(\LogicException::class); + $this->setExpectedException('LogicException'); $getopt = new Getopt(); $getopt->addOption('a'); $getopt->process('-a'); -- GitLab From df81d0d2997577a6f754adfa39255fda4c52ecd3 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sun, 9 Jul 2017 16:07:11 +0200 Subject: [PATCH 06/14] refactoring: use external class for formatting a table --- composer.json | 3 ++- resources/option.php | 32 -------------------------------- resources/options.php | 43 +++++++++++++++++++++++++++++++++++++++++++ resources/usage.php | 5 +++-- src/Arguments.php | 10 +++++----- src/Getopt.php | 20 ++++++++++---------- src/Help.php | 23 +++++++++-------------- test/GetoptTest.php | 12 ++++++------ 8 files changed, 78 insertions(+), 70 deletions(-) delete mode 100644 resources/option.php create mode 100644 resources/options.php diff --git a/composer.json b/composer.json index 25f5e82..c758124 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ } ], "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "phplucidframe/console-table": "^1.1.0" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/resources/option.php b/resources/option.php deleted file mode 100644 index 2763c2a..0000000 --- a/resources/option.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -use GetOpt\Getopt; - -/** @var \GetOpt\Option $option */ -/** @var int $padding */ - -switch ($option->mode()) { - case Getopt::OPTIONAL_ARGUMENT: - $argument = '[<' . $option->getArgument()->getName() . '>]'; - break; - - case GetOpt::REQUIRED_ARGUMENT: - case GetOpt::MULTIPLE_ARGUMENT: - $argument = '<' . $option->getArgument()->getName() . '>'; - break; - - case Getopt::NO_ARGUMENT: - default: - $argument = ''; -} - -$definition = str_pad(sprintf( - ' %s %s', - implode(', ', array_filter( array( - $option->short() ? '-' . $option->short() : null, - $option->long() ? '--' . $option->long() : null, - ))), - $argument -), $padding); - -return sprintf("%s %s", $definition, $option->getDescription()); diff --git a/resources/options.php b/resources/options.php new file mode 100644 index 0000000..4c96bd5 --- /dev/null +++ b/resources/options.php @@ -0,0 +1,43 @@ +<?php + +use GetOpt\Getopt; + + +/** @var \GetOpt\Option[] $options */ +/** @var int $padding */ + +$table = new \LucidFrame\Console\ConsoleTable(); +$table->setIndent(1)->hideBorder(); +foreach ($options as $option) { + switch ($option->mode()) { + case Getopt::OPTIONAL_ARGUMENT: + $argument = '[<' . $option->getArgument()->getName() . '>]'; + break; + + case GetOpt::REQUIRED_ARGUMENT: + case GetOpt::MULTIPLE_ARGUMENT: + $argument = '<' . $option->getArgument()->getName() . '>'; + break; + + case Getopt::NO_ARGUMENT: + default: + $argument = ''; + } + + + $table->addRow(array( + // col1: + sprintf( + '%s %s', + implode(', ', array_filter( array( + $option->short() ? '-' . $option->short() : null, + $option->long() ? '--' . $option->long() : null, + ))), + $argument + ), + // col2: + $option->getDescription() + )); +} + +return array_merge(array('Options:'), array_filter(explode(PHP_EOL, $table->getTable()))); diff --git a/resources/usage.php b/resources/usage.php index ebc20e0..f7de94b 100644 --- a/resources/usage.php +++ b/resources/usage.php @@ -5,8 +5,9 @@ use GetOpt\Getopt; /** @var string $banner */ /** @var string $scriptName */ +// backward compatibility if ($banner) { - return sprintf($banner, $scriptName); + return array(trim(sprintf($banner, $scriptName))); } -return sprintf("Usage: %s [options] [operands]", $scriptName); +return array(sprintf("Usage: %s [options] [operands]", $scriptName)); diff --git a/src/Arguments.php b/src/Arguments.php index 7ee5408..196e0bc 100644 --- a/src/Arguments.php +++ b/src/Arguments.php @@ -22,23 +22,23 @@ class Arguments * * Stores operands using $addOperand callback. * - * @param Getopt $getopt - * @param callable $addOperand + * @param Getopt $getopt + * @param string $operands * @return bool */ - public function process(Getopt $getopt, callable $addOperand) + public function process(Getopt $getopt, &$operands) { while (($arg = array_shift($this->arguments)) !== null) { if ($this->isMeta($arg)) { // everything from here are operands foreach ($this->arguments as $argument) { - $addOperand($argument); + $operands[] = $argument; } break; } if ($this->isValue($arg)) { - $addOperand($arg); + $operands[] = $arg; } if ($this->isLongOption($arg)) { diff --git a/src/Getopt.php b/src/Getopt.php index 4820a98..75a0f9f 100644 --- a/src/Getopt.php +++ b/src/Getopt.php @@ -135,23 +135,23 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate ); } - $arguments->process($this, function ($operand) { - $this->operands[] = $operand; - }); + $arguments->process($this, $this->operands); } /** * Returns an usage information text generated from the given options. - * @param int $padding Number of characters to pad output of options to + * + * The $padding got removed due to refactoring. Help is an own class now. You can change the layout by using a + * custom template. + * * @return string */ - public function getHelpText($padding = 25) + public function getHelpText() { - return $this->getHelp()->getText( + return $this->getHelp()->render( $this->get(self::SETTING_SCRIPT_NAME), $this->options, - $this->get(self::SETTING_BANNER), - $padding + $this->get(self::SETTING_BANNER) ); } @@ -196,7 +196,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate */ public function getOptions() { - $result = []; + $result = array(); foreach ($this->options as $option) { $value = $option->getValue(); @@ -305,7 +305,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate public function getIterator() { - $result = []; + $result = array(); foreach ($this->options as $option) { if ($value = $option->getValue()) { diff --git a/src/Help.php b/src/Help.php index 38567ec..39330d4 100644 --- a/src/Help.php +++ b/src/Help.php @@ -8,10 +8,7 @@ class Help protected $usageTemplate; /** @var string */ - protected $optionTemplate; - - /** @var int */ - protected $padding = 25; + protected $optionsTemplate; /** * Create a Help object @@ -21,7 +18,7 @@ class Help public function __construct(array $settings = array()) { $this->usageTemplate = __DIR__ . '/../resources/usage.php'; - $this->optionTemplate = __DIR__ . '/../resources/option.php'; + $this->optionsTemplate = __DIR__ . '/../resources/options.php'; } /** @@ -30,20 +27,18 @@ class Help * @param string $scriptName * @param string $options * @param string $banner - * @param string $padding * @return string */ - public function getText($scriptName, $options, $banner = null, $padding = null) + public function render($scriptName, $options, $banner = null) { - $padding = $padding ?: $this->padding; - $usage = trim(include($this->usageTemplate)); - $rows = array($usage); + $rows = array(); + + // we always append the usage + $rows = array_merge($rows, include($this->usageTemplate)); + // when we have options we add them too if (!empty($options)) { - $rows[] = 'Options:'; - foreach ($options as $option) { - $rows[] = include($this->optionTemplate); - } + $rows = array_merge($rows, include($this->optionsTemplate)); } return implode(PHP_EOL, $rows) . PHP_EOL; diff --git a/test/GetoptTest.php b/test/GetoptTest.php index 141d2eb..d9542fc 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -158,9 +158,9 @@ class GetoptTest extends \PHPUnit_Framework_TestCase $expected = "Usage: $script [options] [operands]\n"; $expected .= "Options:\n"; - $expected .= " -a, --alpha Short and long options with no argument\n"; - $expected .= " --beta [<arg>] Long option only with an optional argument\n"; - $expected .= " -c <arg> Short option only with a mandatory argument\n"; + $expected .= " -a, --alpha Short and long options with no argument \n"; + $expected .= " --beta [<arg>] Long option only with an optional argument \n"; + $expected .= " -c <arg> Short option only with a mandatory argument \n"; $this->assertEquals($expected, $getopt->getHelpText()); } @@ -178,9 +178,9 @@ class GetoptTest extends \PHPUnit_Framework_TestCase $expected = "Usage: $script [options] [operands]\n"; $expected .= "Options:\n"; - $expected .= " -a, --alpha \n"; - $expected .= " --beta [<arg>] \n"; - $expected .= " -c <arg> \n"; + $expected .= " -a, --alpha \n"; + $expected .= " --beta [<arg>] \n"; + $expected .= " -c <arg> \n"; $this->assertEquals($expected, $getopt->getHelpText()); } -- GitLab From 3676dac0964f9c1bed3af66f4dd5fbc27feec45c Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sun, 9 Jul 2017 16:07:37 +0200 Subject: [PATCH 07/14] refactoring: fix php 5.3 --- src/Getopt.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Getopt.php b/src/Getopt.php index 75a0f9f..df472cc 100644 --- a/src/Getopt.php +++ b/src/Getopt.php @@ -20,9 +20,9 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate protected $help; /** @var array */ - protected $settings = [ + protected $settings = array( self::SETTING_DEFAULT_MODE => self::NO_ARGUMENT - ]; + ); /**@var Option[] */ protected $options = array(); -- GitLab From 393e09d67a2f5796abd1eddc1e871444fb2ebf6c Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Sun, 9 Jul 2017 16:15:44 +0200 Subject: [PATCH 08/14] refactoring: fix php 5.3 --- src/Option.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Option.php b/src/Option.php index 627b2de..eebe75e 100644 --- a/src/Option.php +++ b/src/Option.php @@ -136,7 +136,7 @@ class Option public function setValue($value = null) { - if ($value === null && in_array($this->mode, [Getopt::REQUIRED_ARGUMENT, Getopt::MULTIPLE_ARGUMENT])) { + if ($value === null && in_array($this->mode, array(Getopt::REQUIRED_ARGUMENT, Getopt::MULTIPLE_ARGUMENT))) { throw new \UnexpectedValueException(sprintf( 'Option \'%s\' must have a value', $this->long() ?: $this->short() -- GitLab From d84e035ca45bc90ccd3163212f88b02718aeb949 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Mon, 10 Jul 2017 08:02:36 +0200 Subject: [PATCH 09/14] refactoring: add method descriptions and argument parameter --- src/Argument.php | 8 +- src/Option.php | 179 ++++++++++++++++++++++++++------------ test/ArgumentTest.php | 4 +- test/ArgumentsTest.php | 135 ++++++++++++++-------------- test/GetoptTest.php | 12 +-- test/OptionParserTest.php | 4 +- test/OptionTest.php | 52 +++++++---- 7 files changed, 241 insertions(+), 153 deletions(-) diff --git a/src/Argument.php b/src/Argument.php index ff6c46e..fe76e74 100644 --- a/src/Argument.php +++ b/src/Argument.php @@ -6,7 +6,7 @@ class Argument { const CLASSNAME = __CLASS__; - /** @var string */ + /** @var int|float|string|bool */ private $default; /** @var callable */ private $validation; @@ -16,7 +16,7 @@ class Argument /** * Creates a new argument. * - * @param scalar|null $default Default value or NULL + * @param int|float|string|bool|null $default Default value or NULL * @param callable|null $validation a validation function (optional) * @throws \InvalidArgumentException */ @@ -34,7 +34,7 @@ class Argument /** * Set the default value * - * @param scalar $value + * @param int|float|string|bool $value * @return Argument this object (for chaining calls) * @throws \InvalidArgumentException */ @@ -98,7 +98,7 @@ class Argument /** * Retrieve the default value * - * @return scalar|null + * @return string|int|null */ public function getDefaultValue() { diff --git a/src/Option.php b/src/Option.php index eebe75e..f691eb5 100644 --- a/src/Option.php +++ b/src/Option.php @@ -19,15 +19,13 @@ class Option /** * Creates a new option. * - * @param string $short the option's short name (a single letter or digit) or null for long-only options - * @param string $long the option's long name (a string of 2+ letter/digit/_/- characters, starting with a letter - * or digit) or null for short-only options - * @param int $mode whether the option can/must have an argument (one of the constants defined in the Getopt - * class) - * (optional, defaults to no argument) - * @throws \InvalidArgumentException if both short and long name are null + * @param string $short The option's short name (one of [a-zA-Z0-9?!§$%#]) or null for long-only options + * @param string $long The option's long name (a string of 2+ letter/digit/_/- characters, starting with a letter + * or digit) or null for short-only options + * @param int $mode Whether the option can/must have an argument (optional, defaults to no argument) + * @param Argument $argument The argument definition */ - public function __construct($short, $long = null, $mode = Getopt::NO_ARGUMENT) + public function __construct($short, $long = null, $mode = Getopt::NO_ARGUMENT, Argument $argument = null) { if (!$short && !$long) { throw new \InvalidArgumentException("The short and long name may not both be empty"); @@ -35,7 +33,27 @@ class Option $this->setShort($short); $this->setLong($long); $this->setMode($mode); - $this->argument = new Argument(); + + if ($argument !== null) { + $this->setArgument($argument); + } else { + $this->argument = new Argument(); + } + } + + /** + * Fluent interface for constructor so options can be added during construction + * + * @see Options::__construct() + * @param string $short + * @param string $long + * @param int $mode + * @param Argument $argument + * @return Option + */ + public static function create($short, $long = null, $mode = Getopt::NO_ARGUMENT, Argument $argument = null) + { + return new self($short, $long, $mode, $argument); } /** @@ -50,6 +68,14 @@ class Option return $this; } + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + /** * Defines a default value for the option. * @@ -104,24 +130,90 @@ class Option return ($string === $this->short) || ($string === $this->long); } + /** + * Change the short name + * + * @param string $short + * @return Option this object (for chaining calls) + */ + public function setShort($short) + { + if (!(is_null($short) || preg_match("/^[a-zA-Z0-9?!§$%#]$/", $short))) { + throw new \InvalidArgumentException(sprintf( + 'Short option must be null or one of [a-zA-Z0-9?!§$%%#], found \'%s\'', + $short + )); + } + $this->short = $short; + return $this; + } + + /** + * @return string + */ public function short() { return $this->short; } + /** + * Change the long name + * + * @param $long + * @return Option this object (for chaining calls) + */ + public function setLong($long) + { + if (!(is_null($long) || preg_match("/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,}$/", $long))) { + throw new \InvalidArgumentException(sprintf( + 'Long option must be null or an alphanumeric string, found \'%s\'', + $long + )); + } + $this->long = $long; + return $this; + } + + /** + * @return string + */ public function long() { return $this->long; } - public function mode() + /** + * Change the mode + * + * @param $mode + * @return Option this object (for chaining calls) + */ + public function setMode($mode) { - return $this->mode; + if (!in_array($mode, array( + Getopt::NO_ARGUMENT, + Getopt::OPTIONAL_ARGUMENT, + Getopt::REQUIRED_ARGUMENT, + Getopt::MULTIPLE_ARGUMENT, + ), true)) { + throw new \InvalidArgumentException(sprintf( + 'Option mode must be one of %s, %s, %s and %s', + 'Getopt::NO_ARGUMENT', + 'Getopt::OPTIONAL_ARGUMENT', + 'Getopt::REQUIRED_ARGUMENT', + 'Getopt::MULTIPLE_ARGUMENT' + )); + } + $this->mode = $mode; + return $this; } - public function getDescription() + /** + * @return mixed + */ + public function mode() { - return $this->description; + return $this->mode; } /** @@ -134,6 +226,12 @@ class Option return $this->argument; } + /** + * Internal method to set the current value + * + * @param mixed $value + * @internal + */ public function setValue($value = null) { if ($value === null && in_array($this->mode, array(Getopt::REQUIRED_ARGUMENT, Getopt::MULTIPLE_ARGUMENT))) { @@ -161,6 +259,11 @@ class Option } } + /** + * Get the current value + * + * @return mixed + */ public function getValue() { switch ($this->mode) { @@ -177,54 +280,14 @@ class Option } } - public function __toString() - { - $value = $this->getValue(); - return !is_array($value) ? $value . '' : implode(',', $value); - } - /** - * Fluent interface for constructor so options can be added during construction + * Get a string from value * - * @see Options::__construct() + * @return string */ - public static function create($short, $long, $mode = Getopt::NO_ARGUMENT) - { - return new self($short, $long, $mode); - } - - private function setShort($short) - { - if (!(is_null($short) || preg_match("/^[a-zA-Z0-9]$/", $short))) { - throw new \InvalidArgumentException("Short option must be null or a letter/digit, found '$short'"); - } - $this->short = $short; - } - - private function setLong($long) - { - if (!(is_null($long) || preg_match("/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,}$/", $long))) { - throw new \InvalidArgumentException("Long option must be null or an alphanumeric string, found '$long'"); - } - $this->long = $long; - } - - private function setMode($mode) + public function __toString() { - if (!in_array($mode, array( - Getopt::NO_ARGUMENT, - Getopt::OPTIONAL_ARGUMENT, - Getopt::REQUIRED_ARGUMENT, - Getopt::MULTIPLE_ARGUMENT, - ), true)) { - throw new \InvalidArgumentException(sprintf( - 'Option mode must be one of %s, %s, %s and %s', - 'Getopt::NO_ARGUMENT', - 'Getopt::OPTIONAL_ARGUMENT', - 'Getopt::REQUIRED_ARGUMENT', - 'Getopt::MULTIPLE_ARGUMENT' - )); - } - $this->mode = $mode; + $value = $this->getValue(); + return !is_array($value) ? (string)$value : implode(',', $value); } } diff --git a/test/ArgumentTest.php b/test/ArgumentTest.php index 20605b1..4f07a37 100644 --- a/test/ArgumentTest.php +++ b/test/ArgumentTest.php @@ -2,7 +2,9 @@ namespace GetOpt; -class ArgumentTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class ArgumentTest extends TestCase { public function testConstructor() { diff --git a/test/ArgumentsTest.php b/test/ArgumentsTest.php index 1ff25f8..302560f 100644 --- a/test/ArgumentsTest.php +++ b/test/ArgumentsTest.php @@ -18,10 +18,10 @@ class ArgumentsTest extends TestCase { $this->getopt->process(Arguments::fromString('something')); - $this->assertCount(0, $this->getopt->getOptions()); + self::assertCount(0, $this->getopt->getOptions()); $operands = $this->getopt->getOperands(); - $this->assertCount(1, $operands); - $this->assertEquals('something', $operands[0]); + self::assertCount(1, $operands); + self::assertEquals('something', $operands[0]); } public function testParseUnknownOption() @@ -58,8 +58,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-ab'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['a']); - $this->assertEquals(1, $options['b']); + self::assertEquals(1, $options['a']); + self::assertEquals(1, $options['b']); } public function testParseCumulativeOption() @@ -72,8 +72,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a -b -a -a'); $options = $this->getopt->getOptions(); - $this->assertEquals(3, $options['a']); - $this->assertEquals(1, $options['b']); + self::assertEquals(3, $options['a']); + self::assertEquals(1, $options['b']); } public function testParseCumulativeOptionShort() @@ -86,8 +86,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-abaa'); $options = $this->getopt->getOptions(); - $this->assertEquals(3, $options['a']); - $this->assertEquals(1, $options['b']); + self::assertEquals(3, $options['a']); + self::assertEquals(1, $options['b']); } public function testParseShortOptionWithArgument() @@ -99,7 +99,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a value'); $options = $this->getopt->getOptions(); - $this->assertEquals('value', $options['a']); + self::assertEquals('value', $options['a']); } public function testParseZeroArgument() @@ -111,7 +111,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 0'); $options = $this->getopt->getOptions(); - $this->assertEquals('0', $options['a']); + self::assertEquals('0', $options['a']); } public function testParseNumericOption() @@ -124,8 +124,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 2 -2'); $options = $this->getopt->getOptions(); - $this->assertEquals('2', $options['a']); - $this->assertEquals(1, $options['2']); + self::assertEquals('2', $options['a']); + self::assertEquals(1, $options['2']); } public function testParseCollapsedShortOptionsRequiredArgumentMissing() @@ -147,8 +147,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-ab value'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['a']); - $this->assertEquals('value', $options['b']); + self::assertEquals(1, $options['a']); + self::assertEquals('value', $options['b']); } public function testParseNoArgumentOptionAndOperand() @@ -159,10 +159,10 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a b'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['a']); + self::assertEquals(1, $options['a']); $operands = $this->getopt->getOperands(); - $this->assertCount(1, $operands); - $this->assertEquals('b', $operands[0]); + self::assertCount(1, $operands); + self::assertEquals('b', $operands[0]); } public function testParsedRequiredArgumentWithNoSpace() @@ -172,7 +172,7 @@ class ArgumentsTest extends TestCase )); $this->getopt->process('-ppassword'); $options = $this->getopt->getOptions(); - $this->assertEquals('password', $options['p']); + self::assertEquals('password', $options['p']); } public function testParseCollapsedRequiredArgumentWithNoSpace() { @@ -182,8 +182,8 @@ class ArgumentsTest extends TestCase )); $this->getopt->process('-vvvppassword'); $options = $this->getopt->getOptions(); - $this->assertEquals('password', $options['p']); - $this->assertEquals(3, $options['v']); + self::assertEquals('password', $options['p']); + self::assertEquals(3, $options['v']); } public function testParseOperandsOnly() @@ -194,11 +194,11 @@ class ArgumentsTest extends TestCase )); $this->getopt->process('-- -a -b'); - $this->assertCount(0, $this->getopt->getOptions()); + self::assertCount(0, $this->getopt->getOptions()); $operands = $this->getopt->getOperands(); - $this->assertCount(2, $operands); - $this->assertEquals('-a', $operands[0]); - $this->assertEquals('-b', $operands[1]); + self::assertCount(2, $operands); + self::assertEquals('-a', $operands[0]); + self::assertEquals('-b', $operands[1]); } public function testParseLongOptionWithoutArgument() @@ -209,7 +209,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('--option'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['option']); + self::assertEquals(1, $options['option']); } public function testParseLongOptionWithoutArgumentAndOperand() @@ -220,10 +220,10 @@ class ArgumentsTest extends TestCase $this->getopt->process('--option something'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['option']); + self::assertEquals(1, $options['option']); $operands = $this->getopt->getOperands(); - $this->assertCount(1, $operands); - $this->assertEquals('something', $operands[0]); + self::assertCount(1, $operands); + self::assertEquals('something', $operands[0]); } public function testParseLongOptionWithArgument() @@ -234,8 +234,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('--option value'); $options = $this->getopt->getOptions(); - $this->assertEquals('value', $options['option']); - $this->assertEquals('value', $options['o']); + self::assertEquals('value', $options['option']); + self::assertEquals('value', $options['o']); } public function testParseLongOptionWithEqualsSignAndArgument() @@ -246,10 +246,10 @@ class ArgumentsTest extends TestCase $this->getopt->process('--option=value something'); $options = $this->getopt->getOptions(); - $this->assertEquals('value', $options['option']); + self::assertEquals('value', $options['option']); $operands = $this->getopt->getOperands(); - $this->assertCount(1, $operands); - $this->assertEquals('something', $operands[0]); + self::assertCount(1, $operands); + self::assertEquals('something', $operands[0]); } public function testParseLongOptionWithValueStartingWithHyphen() @@ -260,7 +260,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('--option=-value'); $options = $this->getopt->getOptions(); - $this->assertEquals('-value', $options['option']); + self::assertEquals('-value', $options['option']); } public function testParseNoValueStartingWithHyphenRequired() @@ -282,8 +282,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a -b'); $options = $this->getopt->getOptions(); - $this->assertEquals(1, $options['a']); - $this->assertEquals(1, $options['b']); + self::assertEquals(1, $options['a']); + self::assertEquals(1, $options['b']); } public function testParseOptionWithDefaultValue() @@ -296,9 +296,9 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 12'); $options = $this->getopt->getOptions(); - $this->assertEquals(12, $options['a']); - $this->assertEquals(20, $options['b']); - $this->assertEquals(20, $options['beta']); + self::assertEquals(12, $options['a']); + self::assertEquals(20, $options['b']); + self::assertEquals(20, $options['beta']); } public function testMultipleArgumentOptions() @@ -307,7 +307,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a value1 -a value2'); - $this->assertEquals(array('value1', 'value2'), $this->getopt->getOption('a')); + self::assertEquals(array('value1', 'value2'), $this->getopt->getOption('a')); } public function testDoubleHyphenNotInOperands() @@ -318,12 +318,12 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 0 foo -- bar baz'); $options = $this->getopt->getOptions(); - $this->assertEquals('0', $options['a']); + self::assertEquals('0', $options['a']); $operands = $this->getopt->getOperands(); - $this->assertCount(3, $operands); - $this->assertEquals('foo', $operands[0]); - $this->assertEquals('bar', $operands[1]); - $this->assertEquals('baz', $operands[2]); + self::assertCount(3, $operands); + self::assertEquals('foo', $operands[0]); + self::assertEquals('bar', $operands[1]); + self::assertEquals('baz', $operands[2]); } public function testSingleHyphenValue() @@ -335,16 +335,16 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a -'); $options = $this->getopt->getOptions(); - $this->assertEquals('-', $options['a']); + self::assertEquals('-', $options['a']); $operands = $this->getopt->getOperands(); - $this->assertCount(0, $operands); + self::assertCount(0, $operands); $this->getopt->process('--alpha -'); $options = $this->getopt->getOptions(); - $this->assertEquals('-', $options['a']); + self::assertEquals('-', $options['a']); $operands = $this->getopt->getOperands(); - $this->assertCount(0, $operands); + self::assertCount(0, $operands); } public function testSingleHyphenOperand() @@ -355,10 +355,10 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 0 -'); $options = $this->getopt->getOptions(); - $this->assertEquals('0', $options['a']); + self::assertEquals('0', $options['a']); $operands = $this->getopt->getOperands(); - $this->assertCount(1, $operands); - $this->assertEquals('-', $operands[0]); + self::assertCount(1, $operands); + self::assertEquals('-', $operands[0]); } public function testOptionsAfterOperands() @@ -370,11 +370,11 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 42 operand -b "don\'t panic"'); - $this->assertEquals(array( + self::assertEquals(array( 'a' => 42, 'b' => 'don\'t panic' ), $this->getopt->getOptions()); - $this->assertEquals(array('operand'), $this->getopt->getOperands()); + self::assertEquals(array('operand'), $this->getopt->getOperands()); } public function testEmptyOperandsAndOptionsWithString() @@ -385,8 +385,8 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a "" ""'); - $this->assertSame(array('a' => ''), $this->getopt->getOptions()); - $this->assertSame(array(''), $this->getopt->getOperands()); + self::assertSame(array('a' => ''), $this->getopt->getOptions()); + self::assertSame(array(''), $this->getopt->getOperands()); } public function testEmptyOperandsAndOptionsWithArray() @@ -402,8 +402,8 @@ class ArgumentsTest extends TestCase '' )); - $this->assertSame(array('a' => ''), $this->getopt->getOptions()); - $this->assertSame(array(''), $this->getopt->getOperands()); + self::assertSame(array('a' => ''), $this->getopt->getOptions()); + self::assertSame(array(''), $this->getopt->getOperands()); } public function testSpaceOperand() @@ -412,7 +412,7 @@ class ArgumentsTest extends TestCase $this->getopt->process('" "'); - $this->assertSame(array(' '), $this->getopt->getOperands()); + self::assertSame(array(' '), $this->getopt->getOperands()); } public function testParseWithArgumentValidation() @@ -428,9 +428,9 @@ class ArgumentsTest extends TestCase $this->getopt->process('-a 1 -b 2 -c'); $options = $this->getopt->getOptions(); - $this->assertSame('1', $options['a']); - $this->assertSame('2', $options['b']); - $this->assertSame(1, $options['c']); + self::assertSame('1', $options['a']); + self::assertSame('2', $options['b']); + self::assertSame(1, $options['c']); } public function testParseInvalidArgument() @@ -505,13 +505,20 @@ class ArgumentsTest extends TestCase self::assertSame('" inside double quote', $options['b']); } - public function testQuoteEscaping() + public function testQuoteEscapingDoubleQuote() { $this->getopt->process('-- "this \\" is a double quote"'); self::assertSame('this " is a double quote', $this->getopt->getOperand(0)); } + public function testQuoteEscapingSingleQuote() + { + $this->getopt->process("-- 'this \\' is a single quote'"); + + self::assertSame("this ' is a single quote", $this->getopt->getOperand(0)); + } + public function testLinefeedAsSeparator() { $this->getopt->addOptions(array( diff --git a/test/GetoptTest.php b/test/GetoptTest.php index d9542fc..41bd121 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -2,7 +2,9 @@ namespace GetOpt; -class GetoptTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class GetoptTest extends TestCase { public function testAddOptions() { @@ -63,15 +65,13 @@ class GetoptTest extends \PHPUnit_Framework_TestCase $getopt->addOptions(new Option('a', 'alpha')); } - public function testAddOptionsOverwritesExistingOptions() + public function testChangeModeAfterwards() { $getopt = new Getopt(array( - array('a', null, Getopt::NO_ARGUMENT) + array('a', null, Getopt::REQUIRED_ARGUMENT) )); -// $getopt->addOptions(array( -// array('a', null, Getopt::NO_ARGUMENT) -// )); + $getopt->getOption('a', true)->setMode(Getopt::NO_ARGUMENT); $getopt->parse('-a foo'); $this->assertEquals(1, $getopt->getOption('a')); diff --git a/test/OptionParserTest.php b/test/OptionParserTest.php index 737cb46..59c3191 100644 --- a/test/OptionParserTest.php +++ b/test/OptionParserTest.php @@ -2,7 +2,9 @@ namespace GetOpt; -class OptionParserTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class OptionParserTest extends TestCase { /** @var OptionParser */ private $parser; diff --git a/test/OptionTest.php b/test/OptionTest.php index 4292f6c..03cb32d 100644 --- a/test/OptionTest.php +++ b/test/OptionTest.php @@ -2,7 +2,9 @@ namespace GetOpt; -class OptionTest extends \PHPUnit_Framework_TestCase +use PHPUnit\Framework\TestCase; + +class OptionTest extends TestCase { public function testConstruct() { @@ -20,34 +22,37 @@ class OptionTest extends \PHPUnit_Framework_TestCase $this->assertEquals(Getopt::OPTIONAL_ARGUMENT, $option->mode()); } - public function testConstructEmptyOption() + /** @dataProvider dataConstructFails */ + public function testConstructFails($short, $long, $mode) { $this->setExpectedException('InvalidArgumentException'); - new Option(null, null, Getopt::NO_ARGUMENT); + new Option($short, $long, $mode); } - public function testConstructNoLetter() + public function dataConstructFails() { - $this->setExpectedException('InvalidArgumentException'); - new Option('?', null, Getopt::NO_ARGUMENT); + return array( + array(null, null, Getopt::NO_ARGUMENT), // long and short are both empty + array('&', null, Getopt::NO_ARGUMENT), // short name must be one of [a-zA-Z0-9?!§$%%#] + array(null, 'öption', Getopt::NO_ARGUMENT), // long name may contain only alphanumeric chars, _ and - + array('a', null, 'no_argument'), // invalid mode + array(null, 'a', Getopt::NO_ARGUMENT) // long name must be at least 2 characters long + ); } - public function testConstructInvalidCharacter() + /** @dataProvider dataMatches */ + public function testMatches(Option $option, $string, $matches) { - $this->setExpectedException('InvalidArgumentException'); - new Option(null, 'öption', Getopt::NO_ARGUMENT); - } - - public function testConstructInvalidArgumentType() - { - $this->setExpectedException('InvalidArgumentException'); - new Option('a', null, 'no_argument'); + $this->assertEquals($matches, $option->matches($string)); } - - public function testConstructLongOptionTooShort() + public function dataMatches() { - $this->setExpectedException('InvalidArgumentException'); - new Option(null, 'a', Getopt::REQUIRED_ARGUMENT); + return array( + array(new Option('v', null), 'v', true), + array(new Option(null, 'verbose'), 'verbose', true), + array(new Option(null, 'verbose'), 'v', false), + array(new Option('v', 'verbose'), 'v', true) + ); } public function testSetArgument() @@ -64,6 +69,15 @@ class OptionTest extends \PHPUnit_Framework_TestCase $option->setArgument(new Argument()); } + public function testSetArgumentFromConstructor() + { + $argument = new Argument(); + + $option = new Option('a', null, Getopt::OPTIONAL_ARGUMENT, $argument); + + self::assertSame($argument, $option->getArgument()); + } + public function testSetDefaultValue() { $option = new Option('a', null, Getopt::OPTIONAL_ARGUMENT); -- GitLab From 5e4dc6b9d06ce5ca7ec0909610622fce1174e1b9 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Mon, 10 Jul 2017 19:51:29 +0200 Subject: [PATCH 10/14] refactoring: implement own table rendering and HelpInterface The default options template respects the width of the console up to 120 characters. Longer is not good for readability. When the console width can not be determined it's fixed to 90 characters. --- composer.json | 3 +- resources/options.php | 68 ++++++++++++++++++++++++------------------- resources/usage.php | 14 +-------- src/Getopt.php | 32 +++++++++++++++----- src/Help.php | 60 +++++++++++++++++++++++++++++++------- src/HelpInterface.php | 14 +++++++++ test/GetoptTest.php | 43 +++++++++++++++++---------- 7 files changed, 157 insertions(+), 77 deletions(-) create mode 100644 src/HelpInterface.php diff --git a/composer.json b/composer.json index c758124..25f5e82 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ } ], "require": { - "php": ">=5.3.0", - "phplucidframe/console-table": "^1.1.0" + "php": ">=5.3.0" }, "require-dev": { "phpunit/phpunit": "^4.8", diff --git a/resources/options.php b/resources/options.php index 4c96bd5..c3ba84d 100644 --- a/resources/options.php +++ b/resources/options.php @@ -1,43 +1,51 @@ +Options: <?php use GetOpt\Getopt; - /** @var \GetOpt\Option[] $options */ -/** @var int $padding */ -$table = new \LucidFrame\Console\ConsoleTable(); -$table->setIndent(1)->hideBorder(); +$data = array(); +$definitionWidth = 0; foreach ($options as $option) { - switch ($option->mode()) { - case Getopt::OPTIONAL_ARGUMENT: - $argument = '[<' . $option->getArgument()->getName() . '>]'; - break; - - case GetOpt::REQUIRED_ARGUMENT: - case GetOpt::MULTIPLE_ARGUMENT: - $argument = '<' . $option->getArgument()->getName() . '>'; - break; - - case Getopt::NO_ARGUMENT: - default: - $argument = ''; + $argument = ''; + if ($option->mode() !== Getopt::NO_ARGUMENT) { + $argument = '<' . $option->getArgument()->getName() . '>'; + if ($option->mode() === Getopt::OPTIONAL_ARGUMENT) { + $argument = '[' . $argument . ']'; + } } + $definition = sprintf( + '%s %s', + implode(', ', array_filter( array( + $option->short() ? '-' . $option->short() : null, + $option->long() ? '--' . $option->long() : null, + ))), + $argument + ); + + if (strlen($definition) > $definitionWidth) { + $definitionWidth = strlen($definition); + } - $table->addRow(array( - // col1: - sprintf( - '%s %s', - implode(', ', array_filter( array( - $option->short() ? '-' . $option->short() : null, - $option->long() ? '--' . $option->long() : null, - ))), - $argument - ), - // col2: + $data[] = array( + $definition, $option->getDescription() - )); + ); } -return array_merge(array('Options:'), array_filter(explode(PHP_EOL, $table->getTable()))); +$screenWidth = @getenv('COLUMNS') ?: @exec('tput cols 2>/dev/null'); +$screenWidth = $screenWidth ?: 90; +$screenWidth = min(array(120, $screenWidth)); +foreach ($data as $dataRow) { + $row = sprintf(' % -' . $definitionWidth . 's %s', $dataRow[0], $dataRow[1]); + + while (mb_strlen($row) > $screenWidth) { + $p = strrpos(substr($row, 0, $screenWidth), ' '); + echo substr($row, 0, $p) . PHP_EOL; + $row = sprintf(' %s %s', str_repeat(' ', $definitionWidth), substr($row, $p+1)); + } + + echo $row . PHP_EOL; +} diff --git a/resources/usage.php b/resources/usage.php index f7de94b..4d0ef6c 100644 --- a/resources/usage.php +++ b/resources/usage.php @@ -1,13 +1 @@ -<?php - -use GetOpt\Getopt; - -/** @var string $banner */ -/** @var string $scriptName */ - -// backward compatibility -if ($banner) { - return array(trim(sprintf($banner, $scriptName))); -} - -return array(sprintf("Usage: %s [options] [operands]", $scriptName)); +Usage: <?= $getopt->get(GetOpt\Getopt::SETTING_SCRIPT_NAME) ?> [options] [operands] diff --git a/src/Getopt.php b/src/Getopt.php index df472cc..69afd1a 100644 --- a/src/Getopt.php +++ b/src/Getopt.php @@ -16,7 +16,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate /** @var OptionParser */ protected $optionParser; - /** @var Help */ + /** @var HelpInterface */ protected $help; /** @var array */ @@ -148,11 +148,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate */ public function getHelpText() { - return $this->getHelp()->render( - $this->get(self::SETTING_SCRIPT_NAME), - $this->options, - $this->get(self::SETTING_BANNER) - ); + return $this->getHelp()->render($this); } /** @@ -180,6 +176,23 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate return $this->optionMapping[$name] !== null ? $this->optionMapping[$name]->getValue() : null; } + /** + * Define a custom Help object + * + * @param HelpInterface $help + * @return $this + */ + public function setHelp(HelpInterface $help) + { + $this->help = $help; + return $this; + } + + /** + * Get the current HelpObject + * + * @return HelpInterface + */ public function getHelp() { if (!$this->help) { @@ -192,10 +205,15 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate /** * Returns the list of options. Must be invoked after parse() (otherwise it returns an empty array). * + * @param bool $objects Wether to return the Option objects * @return array */ - public function getOptions() + public function getOptions($objects = false) { + if ($objects) { + return $this->options; + } + $result = array(); foreach ($this->options as $option) { diff --git a/src/Help.php b/src/Help.php index 39330d4..0a9f3a2 100644 --- a/src/Help.php +++ b/src/Help.php @@ -2,7 +2,7 @@ namespace GetOpt; -class Help +class Help implements HelpInterface { /** @var string */ protected $usageTemplate; @@ -21,26 +21,66 @@ class Help $this->optionsTemplate = __DIR__ . '/../resources/options.php'; } + /** + * @return string + */ + public function getUsageTemplate() + { + return $this->usageTemplate; + } + + /** + * @param string $usageTemplate + */ + public function setUsageTemplate($usageTemplate) + { + $this->usageTemplate = $usageTemplate; + } + + /** + * @return string + */ + public function getOptionsTemplate() + { + return $this->optionsTemplate; + } + + /** + * @param string $optionsTemplate + */ + public function setOptionsTemplate($optionsTemplate) + { + $this->optionsTemplate = $optionsTemplate; + } + /** * Get the help text for $options * - * @param string $scriptName - * @param string $options - * @param string $banner + * @param Getopt $getopt * @return string */ - public function render($scriptName, $options, $banner = null) + public function render(Getopt $getopt) { - $rows = array(); - // we always append the usage - $rows = array_merge($rows, include($this->usageTemplate)); + $helpText = $this->renderTemplate($this->usageTemplate, array('getopt' => $getopt)); // when we have options we add them too + $options = $getopt->getOptions(true); if (!empty($options)) { - $rows = array_merge($rows, include($this->optionsTemplate)); + $helpText .= $this->renderTemplate($this->optionsTemplate, array( + 'getopt' => $getopt, + 'options' => $options + )); } - return implode(PHP_EOL, $rows) . PHP_EOL; + return $helpText; + } + + protected function renderTemplate($template, $data) + { + extract($data, EXTR_SKIP); + ob_start(); + include($template); + return ob_get_clean(); } } diff --git a/src/HelpInterface.php b/src/HelpInterface.php new file mode 100644 index 0000000..e51b363 --- /dev/null +++ b/src/HelpInterface.php @@ -0,0 +1,14 @@ +<?php + +namespace GetOpt; + +interface HelpInterface +{ + /** + * Render the help text for $getopt + * + * @param Getopt $getopt + * @return string + */ + public function render(Getopt $getopt); +} diff --git a/test/GetoptTest.php b/test/GetoptTest.php index 41bd121..d5de0f4 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -158,9 +158,9 @@ class GetoptTest extends TestCase $expected = "Usage: $script [options] [operands]\n"; $expected .= "Options:\n"; - $expected .= " -a, --alpha Short and long options with no argument \n"; - $expected .= " --beta [<arg>] Long option only with an optional argument \n"; - $expected .= " -c <arg> Short option only with a mandatory argument \n"; + $expected .= " -a, --alpha Short and long options with no argument\n"; + $expected .= " --beta [<arg>] Long option only with an optional argument\n"; + $expected .= " -c <arg> Short option only with a mandatory argument\n"; $this->assertEquals($expected, $getopt->getHelpText()); } @@ -178,9 +178,31 @@ class GetoptTest extends TestCase $expected = "Usage: $script [options] [operands]\n"; $expected .= "Options:\n"; - $expected .= " -a, --alpha \n"; - $expected .= " --beta [<arg>] \n"; - $expected .= " -c <arg> \n"; + $expected .= " -a, --alpha \n"; + $expected .= " --beta [<arg>] \n"; + $expected .= " -c <arg> \n"; + + $this->assertEquals($expected, $getopt->getHelpText()); + } + + public function testHelpTextWithLongDescriptions() + { + $getopt = new Getopt(array( + array('a', 'alpha', Getopt::NO_ARGUMENT, 'Short and long options with no argument and a very long text ' . + 'that exceeds the length of the row'), + array(null, 'beta', Getopt::OPTIONAL_ARGUMENT, 'Long option only with an optional argument'), + array('c', null, Getopt::REQUIRED_ARGUMENT, 'Short option only with a mandatory argument') + )); + $getopt->parse(''); + + $script = $_SERVER['PHP_SELF']; + + $expected = "Usage: $script [options] [operands]\n" . + "Options:\n" . + " -a, --alpha Short and long options with no argument and a very long text that\n" . + " exceeds the length of the row\n" . + " --beta [<arg>] Long option only with an optional argument\n" . + " -c <arg> Short option only with a mandatory argument\n"; $this->assertEquals($expected, $getopt->getHelpText()); } @@ -193,15 +215,6 @@ class GetoptTest extends TestCase $this->assertSame($expected, $getopt->getHelpText()); } - public function testHelpTextWithCustomBanner() - { - $script = $_SERVER['PHP_SELF']; - - $getopt = new Getopt(); - $getopt->setBanner("My custom Banner %s\n"); - $this->assertSame("My custom Banner $script\n", $getopt->getHelpText()); - } - public function testThrowsWithInvalidParameter() { $this->setExpectedException('InvalidArgumentException'); -- GitLab From ae8c3b930c9effeee900ad12edde32b8f5de618e Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Mon, 10 Jul 2017 22:15:24 +0200 Subject: [PATCH 11/14] fix tests for screen width --- resources/options.php | 5 ++--- test/GetoptTest.php | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/options.php b/resources/options.php index c3ba84d..1b267b6 100644 --- a/resources/options.php +++ b/resources/options.php @@ -35,9 +35,8 @@ foreach ($options as $option) { ); } -$screenWidth = @getenv('COLUMNS') ?: @exec('tput cols 2>/dev/null'); -$screenWidth = $screenWidth ?: 90; -$screenWidth = min(array(120, $screenWidth)); +$screenWidth = defined('COLUMNS') ? COLUMNS : @getenv('COLUMNS') ?: @exec('tput cols 2>/dev/null') ?: 90; +$screenWidth = min(array(120, $screenWidth)); // max 120 foreach ($data as $dataRow) { $row = sprintf(' % -' . $definitionWidth . 's %s', $dataRow[0], $dataRow[1]); diff --git a/test/GetoptTest.php b/test/GetoptTest.php index d5de0f4..86af9ca 100644 --- a/test/GetoptTest.php +++ b/test/GetoptTest.php @@ -187,6 +187,7 @@ class GetoptTest extends TestCase public function testHelpTextWithLongDescriptions() { + defined('COLUMNS') || define('COLUMNS', 90); $getopt = new Getopt(array( array('a', 'alpha', Getopt::NO_ARGUMENT, 'Short and long options with no argument and a very long text ' . 'that exceeds the length of the row'), -- GitLab From f469f19d2c557367f8919e10b0b3ea8cb9909d3e Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Mon, 10 Jul 2017 22:22:23 +0200 Subject: [PATCH 12/14] fix tests in php 5.3 Sooner or later I will drop the support of this shitty php version. --- resources/usage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/usage.php b/resources/usage.php index 4d0ef6c..b2aca76 100644 --- a/resources/usage.php +++ b/resources/usage.php @@ -1 +1 @@ -Usage: <?= $getopt->get(GetOpt\Getopt::SETTING_SCRIPT_NAME) ?> [options] [operands] +Usage: <?php echo $getopt->get(GetOpt\Getopt::SETTING_SCRIPT_NAME) ?> [options] [operands] -- GitLab From eb278d98da22beee56e98c9422eef611cec0e0e4 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Tue, 11 Jul 2017 07:19:39 +0200 Subject: [PATCH 13/14] improve coverage by ignoring trivials and remove old code --- src/Argument.php | 8 ++-- src/Getopt.php | 115 +++++++++++++++++++++++++++++------------------ src/Help.php | 4 ++ 3 files changed, 79 insertions(+), 48 deletions(-) diff --git a/src/Argument.php b/src/Argument.php index fe76e74..dbc9c94 100644 --- a/src/Argument.php +++ b/src/Argument.php @@ -6,7 +6,7 @@ class Argument { const CLASSNAME = __CLASS__; - /** @var int|float|string|bool */ + /** @var mixed */ private $default; /** @var callable */ private $validation; @@ -16,7 +16,7 @@ class Argument /** * Creates a new argument. * - * @param int|float|string|bool|null $default Default value or NULL + * @param mixed $default Default value or NULL * @param callable|null $validation a validation function (optional) * @throws \InvalidArgumentException */ @@ -34,7 +34,7 @@ class Argument /** * Set the default value * - * @param int|float|string|bool $value + * @param mixed $value The value has to be a scalar value * @return Argument this object (for chaining calls) * @throws \InvalidArgumentException */ @@ -98,7 +98,7 @@ class Argument /** * Retrieve the default value * - * @return string|int|null + * @return mixed */ public function getDefaultValue() { diff --git a/src/Getopt.php b/src/Getopt.php index 69afd1a..b245f4e 100644 --- a/src/Getopt.php +++ b/src/Getopt.php @@ -11,7 +11,6 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate const SETTING_SCRIPT_NAME = 'scriptName'; const SETTING_DEFAULT_MODE = 'defaultMode'; - const SETTING_BANNER = 'banner'; /** @var OptionParser */ protected $optionParser; @@ -39,6 +38,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate * The argument $options can be either a string in the format accepted by the PHP library * function getopt() or an array. * + * @param array $options * @param array $settings * @link https://www.gnu.org/s/hello/manual/libc/Getopt.html GNU Getopt manual */ @@ -59,21 +59,53 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate } } + /** + * Set $setting to $value + * + * @param string $setting + * @param mixed $value + * @return Getopt + */ public function set($setting, $value) { $this->settings[$setting] = $value; -// switch ($setting) { -// default: -// -// } return $this; } + /** + * Get the current value of $setting + * + * @param string $setting + * @return mixed + */ public function get($setting) { return isset($this->settings[$setting]) ? $this->settings[$setting] : null; } + /** + * Add $options to the list of options + * + * $options can be a string as for phps `getopt()` function, an array of Option instances or an array of arrays. + * + * You can also mix Option instances and arrays. Eg.: + * $getopt->addOptions([ + * ['?', 'help', Getopt::NO_ARGUMENT, 'Show this help'], + * new Option('v', 'verbose'), + * (new Option(null, 'version'))->setDescription('Print version and exit'), + * Option::create('q', 'quiet')->setDescription('Don\'t write any output') + * new Option( + * 'c', + * 'config', + * Getopt::REQUIRED_ARGUMENT, + * new Argument(getenv('HOME') . '/.myapp.inc', 'file_exists', 'file') + * ) + * ]); + * + * @see OptionParser::parseArray() fo see how to use arrays + * @param string|array|Option[] $options + * @return Getopt + */ public function addOptions($options) { if (is_string($options)) { @@ -91,6 +123,17 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate return $this; } + /** + * Add $option to the list of options + * + * $option can also be a string in format of phps `getopt()` function. But only the first option will be added. + * + * Otherwise it has to be an array or an Option instance. + * + * @see Getopt::addOptions() for more details + * @param string|array|Option $option + * @return Getopt + */ public function addOption($option) { if (!$option instanceof Option) { @@ -139,20 +182,9 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate } /** - * Returns an usage information text generated from the given options. - * - * The $padding got removed due to refactoring. Help is an own class now. You can change the layout by using a - * custom template. + * Get an option by $name * - * @return string - */ - public function getHelpText() - { - return $this->getHelp()->render($this); - } - - /** - * Get a option by $name + * If $object is set to true it returns the Option instead of the value. * * @param string $name Short or long name of the option * @return Option|mixed @@ -180,7 +212,8 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate * Define a custom Help object * * @param HelpInterface $help - * @return $this + * @return Getopt + * @codeCoverageIgnore trivial */ public function setHelp(HelpInterface $help) { @@ -189,7 +222,7 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate } /** - * Get the current HelpObject + * Get the current Help instance * * @return HelpInterface */ @@ -202,10 +235,27 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate return $this->help; } + /** + * Returns an usage information text generated from the given options. + * + * The $padding got removed due to refactoring. Help is an own class now. You can change the layout by using a + * custom template or using a custom help formatter (has to implement HelpInterface) + * + * @see Help for setting a custom template + * @see HelpInterface for creating an custom help formatter + * @return string + */ + public function getHelpText() + { + return $this->getHelp()->render($this); + } + /** * Returns the list of options. Must be invoked after parse() (otherwise it returns an empty array). * - * @param bool $objects Wether to return the Option objects + * If $object is set to true it returns an array of Option instances. + * + * @param bool $objects * @return array */ public function getOptions($objects = false) @@ -296,29 +346,6 @@ class Getopt implements \Countable, \ArrayAccess, \IteratorAggregate $this->process($arguments); } - /** - * Get the current banner if defined - * - * @return string - * @deprecated Use `Help` for formatting the help message - */ - public function getBanner() - { - return $this->get(self::SETTING_BANNER); - } - - /** - * Set the banner - * - * @param string $banner - * @return Getopt - * @deprecated Use `Help` for formatting the help message - */ - public function setBanner($banner) - { - return $this->set(self::SETTING_BANNER, $banner); - } - // array functions public function getIterator() diff --git a/src/Help.php b/src/Help.php index 0a9f3a2..d3075be 100644 --- a/src/Help.php +++ b/src/Help.php @@ -23,6 +23,7 @@ class Help implements HelpInterface /** * @return string + * @codeCoverageIgnore trivial */ public function getUsageTemplate() { @@ -31,6 +32,7 @@ class Help implements HelpInterface /** * @param string $usageTemplate + * @codeCoverageIgnore trivial */ public function setUsageTemplate($usageTemplate) { @@ -39,6 +41,7 @@ class Help implements HelpInterface /** * @return string + * @codeCoverageIgnore trivial */ public function getOptionsTemplate() { @@ -47,6 +50,7 @@ class Help implements HelpInterface /** * @param string $optionsTemplate + * @codeCoverageIgnore trivial */ public function setOptionsTemplate($optionsTemplate) { -- GitLab From d997dc42011729be008afb54f6a659e65b2c0a57 Mon Sep 17 00:00:00 2001 From: Thomas Flori <thflori@gmail.com> Date: Tue, 11 Jul 2017 07:48:21 +0200 Subject: [PATCH 14/14] update class diagram --- docs/getopt.graphml | 130 +++++++++++++------------------------------- test/OptionTest.php | 2 +- 2 files changed, 38 insertions(+), 94 deletions(-) diff --git a/docs/getopt.graphml b/docs/getopt.graphml index b562fa8..45781ed 100644 --- a/docs/getopt.graphml +++ b/docs/getopt.graphml @@ -33,7 +33,7 @@ <data key="d4"/> <data key="d6"> <y:UMLClassNode> - <y:Geometry height="28.0" width="100.0" x="725.0" y="282.0"/> + <y:Geometry height="28.0" width="100.0" x="684.6800000000001" y="505.4500000000001"/> <y:Fill color="#A6BCFF" transparent="false"/> <y:BorderStyle color="#000000" type="line" width="1.0"/> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="84.6025390625" x="7.69873046875" y="3.0">Arguments<y:LabelModel> @@ -114,33 +114,12 @@ </data> </node> <node id="n5"> - <data key="d4"/> - <data key="d6"> - <y:UMLClassNode> - <y:Geometry height="28.0" width="177.60000000000002" x="686.2" y="409.9000000000001"/> - <y:Fill color="#A6BCFF" transparent="false"/> - <y:BorderStyle color="#000000" type="line" width="1.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="13" fontStyle="bold" hasBackgroundColor="false" hasLineColor="false" height="19.1328125" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="149.07568359375" x="14.262158203125011" y="3.0">ArgumentProcessor<y:LabelModel> - <y:SmartNodeLabelModel distance="4.0"/> - </y:LabelModel> - <y:ModelParameter> - <y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="-0.03703090122767855" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/> - </y:ModelParameter> - </y:NodeLabel> - <y:UML clipContent="true" constraint="" omitDetails="false" stereotype="" use3DEffect="false"> - <y:AttributeLabel/> - <y:MethodLabel/> - </y:UML> - </y:UMLClassNode> - </data> - </node> - <node id="n6"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="28.0" width="162.20000000000005" x="855.0" y="282.0"/> + <y:Geometry height="28.0" width="162.20000000000005" x="814.6800000000001" y="505.4500000000001"/> <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="154.515625" x="3.8421875000000227" y="5.015625">Holds a list of arguments<y:LabelModel> + <y:BorderStyle color="#FFFFFF" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="154.515625" x="3.8421875000000227" y="5.015625">Holds a list of arguments<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -151,13 +130,13 @@ </y:ShapeNode> </data> </node> - <node id="n7"> + <node id="n6"> <data key="d6"> <y:ShapeNode> <y:Geometry height="28.0" width="268.6" x="237.39999999999998" y="601.0"/> <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="262.779296875" x="2.9103515625000114" y="5.015625">Getopt is the container for all functionallity<y:LabelModel> + <y:BorderStyle color="#FFFFFF" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="262.779296875" x="2.9103515625000114" y="5.015625">Getopt is the container for all functionallity<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -168,7 +147,7 @@ </y:ShapeNode> </data> </node> - <node id="n8"> + <node id="n7"> <data key="d4"/> <data key="d6"> <y:UMLClassNode> @@ -189,13 +168,13 @@ </y:UMLClassNode> </data> </node> - <node id="n9"> + <node id="n8"> <data key="d6"> <y:ShapeNode> <y:Geometry height="28.0" width="100.0" x="987.0" y="601.0"/> <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="91.328125" x="4.3359375" y="5.015625">Prints the help<y:LabelModel> + <y:BorderStyle color="#FFFFFF" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="91.328125" x="4.3359375" y="5.015625">Prints the help<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -206,24 +185,14 @@ </y:ShapeNode> </data> </node> - <node id="n10"> - <data key="d6"> - <y:ShapeNode> - <y:Geometry height="28.0" width="420.3333333333328" x="893.8000000000001" y="409.9000000000001"/> - <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="l" textColor="#000000" verticalTextPosition="bottom" visible="true" width="412.73828125" x="4.0" y="5.015625">Iterates over arguments, fills options with values and sets operands</y:NodeLabel> - <y:Shape type="rectangle"/> - </y:ShapeNode> - </data> - </node> - <node id="n11"> + <node id="n9"> <data key="d6"> <y:ShapeNode> - <y:Geometry height="28.0" width="525.4" x="796.0" y="685.0"/> + <y:Geometry height="37.60000000000002" width="282.20000000000016" x="796.0" y="685.0"/> <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="520.908203125" x="2.2458984375000455" y="5.015625">Command, Option and Operand are business objects that you get from command line<y:LabelModel> + <y:BorderStyle color="#FFFFFF" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="left" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="31.9375" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="275.11328125" x="3.5433593750001364" y="2.8312499999999545">Command, Option and Operand are business +objects that you get from command line<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -234,7 +203,7 @@ </y:ShapeNode> </data> </node> - <node id="n12"> + <node id="n10"> <data key="d4"/> <data key="d6"> <y:UMLClassNode> @@ -255,7 +224,7 @@ </y:UMLClassNode> </data> </node> - <node id="n13"> + <node id="n11"> <data key="d4"/> <data key="d6"> <y:UMLClassNode> @@ -276,13 +245,13 @@ </y:UMLClassNode> </data> </node> - <node id="n14"> + <node id="n12"> <data key="d6"> <y:ShapeNode> <y:Geometry height="28.0" width="255.20000000000005" x="72.79999999999995" y="505.45000000000005"/> <y:Fill hasColor="false" transparent="false"/> - <y:BorderStyle color="#000000" raised="false" type="dotted" width="2.0"/> - <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="245.154296875" x="5.022851562500023" y="5.015625">Creates Options from arrays and strings<y:LabelModel> + <y:BorderStyle color="#FFFFFF" raised="false" type="dotted" width="2.0"/> + <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#FFFFFF" verticalTextPosition="bottom" visible="true" width="245.154296875" x="5.022851562500023" y="5.015625">Creates Options from arrays and strings<y:LabelModel> <y:SmartNodeLabelModel distance="4.0"/> </y:LabelModel> <y:ModelParameter> @@ -293,67 +262,42 @@ </y:ShapeNode> </data> </node> - <edge id="e0" source="n0" target="n5"> + <edge id="e0" source="n0" target="n1"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> <y:Point x="586.0" y="519.45"/> - <y:Point x="775.0" y="519.45"/> - </y:Path> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> - <y:Arrows source="none" target="plain"/> - <y:BendStyle smoothed="true"/> - </y:PolyLineEdge> - </data> - </edge> - <edge id="e1" source="n5" target="n1"> - <data key="d8"/> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> - <y:Arrows source="none" target="plain"/> - <y:BendStyle smoothed="false"/> - </y:PolyLineEdge> - </data> - </edge> - <edge id="e2" source="n0" target="n1"> - <data key="d8"/> - <data key="d10"> - <y:PolyLineEdge> - <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> - <y:Point x="586.0" y="296.0"/> </y:Path> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="true"/> </y:PolyLineEdge> </data> </edge> - <edge id="e3" source="n0" target="n4"> + <edge id="e1" source="n0" target="n4"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="false"/> </y:PolyLineEdge> </data> </edge> - <edge id="e4" source="n0" target="n2"> + <edge id="e2" source="n0" target="n2"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="false"/> </y:PolyLineEdge> </data> </edge> - <edge id="e5" source="n0" target="n3"> + <edge id="e3" source="n0" target="n3"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> @@ -361,13 +305,13 @@ <y:Point x="586.0" y="657.0"/> <y:Point x="456.00000000000006" y="657.0"/> </y:Path> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="true"/> </y:PolyLineEdge> </data> </edge> - <edge id="e6" source="n0" target="n12"> + <edge id="e4" source="n0" target="n10"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> @@ -375,44 +319,44 @@ <y:Point x="586.0" y="657.0"/> <y:Point x="716.0" y="657.0"/> </y:Path> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="true"/> </y:PolyLineEdge> </data> </edge> - <edge id="e7" source="n2" target="n8"> + <edge id="e5" source="n2" target="n7"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="false"/> </y:PolyLineEdge> </data> </edge> - <edge id="e8" source="n12" target="n8"> + <edge id="e6" source="n10" target="n7"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="43.73333333333335" ty="1.9999999999998863"> <y:Point x="716.0" y="758.9999999999999"/> </y:Path> - <y:LineStyle color="#000000" type="line" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="line" width="1.0"/> <y:Arrows source="none" target="white_delta"/> <y:BendStyle smoothed="true"/> </y:PolyLineEdge> </data> </edge> - <edge id="e9" source="n0" target="n13"> + <edge id="e7" source="n0" target="n11"> <data key="d8"/> <data key="d10"> <y:PolyLineEdge> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"> <y:Point x="586.0" y="519.45"/> </y:Path> - <y:LineStyle color="#000000" type="dashed" width="1.0"/> + <y:LineStyle color="#FFFFFF" type="dashed" width="1.0"/> <y:Arrows source="none" target="plain"/> <y:BendStyle smoothed="true"/> </y:PolyLineEdge> diff --git a/test/OptionTest.php b/test/OptionTest.php index 03cb32d..144766e 100644 --- a/test/OptionTest.php +++ b/test/OptionTest.php @@ -33,7 +33,7 @@ class OptionTest extends TestCase { return array( array(null, null, Getopt::NO_ARGUMENT), // long and short are both empty - array('&', null, Getopt::NO_ARGUMENT), // short name must be one of [a-zA-Z0-9?!§$%%#] + array('&', null, Getopt::NO_ARGUMENT), // short name must be one of [a-zA-Z0-9?!§$%#] array(null, 'öption', Getopt::NO_ARGUMENT), // long name may contain only alphanumeric chars, _ and - array('a', null, 'no_argument'), // invalid mode array(null, 'a', Getopt::NO_ARGUMENT) // long name must be at least 2 characters long -- GitLab