[OpenLayers-Commits] r12377 - in sandbox/jsdoc: lib/OpenLayers tools tools/closure-compiler tools/closure-compiler/bin tools/closure-library tools/closure-library/closure tools/closure-library/closure/bin tools/closure-library/closure/bin/build

commits-20090109 at openlayers.org commits-20090109 at openlayers.org
Sat Sep 17 13:30:37 EDT 2011


Author: tschaub
Date: 2011-09-17 10:30:36 -0700 (Sat, 17 Sep 2011)
New Revision: 12377

Added:
   sandbox/jsdoc/tools/NaturalDocs2JsDoc.py
   sandbox/jsdoc/tools/NaturalDocsPreserve.py
   sandbox/jsdoc/tools/closure-compiler/
   sandbox/jsdoc/tools/closure-compiler/bin/
   sandbox/jsdoc/tools/closure-compiler/bin/_where-is-the-compiler.txt
   sandbox/jsdoc/tools/closure-library/
   sandbox/jsdoc/tools/closure-library/closure/
   sandbox/jsdoc/tools/closure-library/closure/bin/
   sandbox/jsdoc/tools/closure-library/closure/bin/build/
   sandbox/jsdoc/tools/closure-library/closure/bin/build/jscompiler.py
Modified:
   sandbox/jsdoc/lib/OpenLayers/Events.js
   sandbox/jsdoc/tools/mergejs.py
Log:
Adding stuff from patch for #3008.

Modified: sandbox/jsdoc/lib/OpenLayers/Events.js
===================================================================
--- sandbox/jsdoc/lib/OpenLayers/Events.js	2011-09-17 16:11:32 UTC (rev 12376)
+++ sandbox/jsdoc/lib/OpenLayers/Events.js	2011-09-17 17:30:36 UTC (rev 12377)
@@ -381,6 +381,10 @@
 if (window.Event) {
     OpenLayers.Util.applyDefaults(window.Event, OpenLayers.Event);
 } else {
+    /**
+    * Silence Closure Compiler error: Variable Event first declared in externs.zip//w3c_event.js
+    * @suppress {duplicate}
+    */
     var Event = OpenLayers.Event;
 }
 

Added: sandbox/jsdoc/tools/NaturalDocs2JsDoc.py
===================================================================
--- sandbox/jsdoc/tools/NaturalDocs2JsDoc.py	                        (rev 0)
+++ sandbox/jsdoc/tools/NaturalDocs2JsDoc.py	2011-09-17 17:30:36 UTC (rev 12377)
@@ -0,0 +1,383 @@
+#!/usr/bin/env python
+
+import re
+import os
+import sys
+
+# -----------------
+# Sintax: OpenLayers Natural Docs --> Closure Compiler jsDoc 
+# -----------------
+
+# Translate comment blocks
+syntax ={
+    "public": { # No effect: should be studied
+        "keywords": ["APIFunction:", "APIMethod:"]
+    },
+    "private": { # No effect: should be studied
+        "keywords": ["Function:", "Method:"]
+    },
+    "jsDoc": { # See Format/WKT.js:260-349
+        "keywords": ["@param ", "@return ", "@returns "],
+        "keywordLineCnv": "cnvKeywordJsDoc"
+    },
+    "extends": {
+        "keywords": ["Inherits from: *$"],
+        "lineConv": "lineSuperClass",
+        "maxLines": 1, # Closure Compiler does not understand the multiple-inheritance.
+        "prefixLine": "@extends"
+    },    
+    "constructor": {
+        "keywords": ["Constructor:"],
+        "prefixKeyword": "@constructor"
+    },
+    "const": {
+        "keywords": ["Constant:"],
+        "prefixKeyword": "@const",
+        "lineConv": "lineSingleDecl",
+        "prefixLine": "@type"
+    },    
+    "namespace": {
+        "keywords": ["Namespace:"],
+        "prefixKeyword": "@type {Object}"
+    },
+    "var": {
+        "keywords": ["Property:", "APIProperty:"],
+        "lineConv": "lineSingleDecl",
+        "prefixLine": "@type",
+        "maxLines": 1, # forced end of block
+        "maxKeywords": 1  ## TODO: 
+            ## A property can be a function with parameters 
+            ##      documented in other lines within the block, 
+            ##      this means stop working line by line to analyze 
+            ##      the whole block together. uff!
+            ##      See APIProperty:onStart on Control/DragFeature.js
+            # On the other hand it is very rare case and there is few
+            #   additional informations that can provide to the compiler.
+    },
+    "params": {
+        "keywords": ["Parameters: *$", "Parameter: *$"],
+        "lineConv": "lineParam",
+        "prefixLine": "@param"
+    },
+    "optionalParams": { # New block: To debate
+        "keywords": ["Optional Parameters: *$", "Optional Parameter: *$"],
+        "lineConv": "lineOptParam",
+        "prefixLine": "@param"
+    },
+    "optionsParam": { # Any "options" argument is forced to set optional.
+        "keywords": ["options - {Object}"],
+        "prefixKeyword": "@param {Object=} options",
+        "lineConv" : "lineOptParam", # Causes compiler warnings that allows 
+        "prefixLine": "@param"       #    better verification of documentation.
+                                     #    See below "optionsProperties"
+    },
+    "optionsProperties": { # The properties of the "options" are not translated into jsDoc
+                           #    It is necessary to complete the set of parameters 
+                           #    for the compiler and also for the documentation 
+                           #    generated by Natural Docs.
+        "keywords": [
+            "Options: *$", 
+            "Allowed Options: *$", 
+            "Valid Options: *$", 
+            "Valid options properties: *$"]
+    },
+    "scope": { # New block: To debate
+        "keywords": ["Scope: *$"],
+        "lineConv": "lineSingleDecl",
+        "prefixLine": "@this"
+    },
+    "return": {
+        "keywords": ["Returns: *$", "Return: *$"],
+        "lineConv": "lineSingleDecl",
+        "maxLines": 1, # forced end of block
+        "prefixLine": "@return"
+    }
+}
+
+# Translate types
+typesOL = [
+    # For DOM types see Closure Compiler source in the folder: closure-compiler/externs
+    ("XMLNode",      "Node"), # Closure said: "we put XMLNode properties on Node" (see: ie_dom.js)
+    ("DOMElement",   "Element"),
+    ("HTMLDOMElement",   "Element"), # Used in: Events.js
+    # js types
+    ("Number",       "number"),
+    ("Integer",      "number"),
+    ("int",          "number"), # Used in: Events.js
+    ("Float",        "number"),
+    ("String",       "string"),
+    ("Boolean",      "boolean"),
+    ("Function",     "function(...[*])"),
+    # Composed types
+    (r"Array\((.*)\)",   r"Array.<\1>"), # Array(...) to Array.<...>
+    (r"Array\[(.*)\]",   r"Array.<\1>"), # Used in: Popup/Framed.js
+    (r"Array\<(.*)\>",   r"Array.<\1>"), # Used in: Renderer/Elements.js:26
+    (r"\<(.*)>\((.*)\)", r"\1.<\2>")     # Used in Tween.js:24
+]
+
+# -----------------
+# Detect "Natural Docs" comments.
+# -----------------
+M_CODE = 0
+M_COM2_BLOC = 1
+reStarCom2Bloc =        re.compile(r"^ *\/\*\* *(?! )")
+reLineCom2 =            re.compile(r"^ *\* *(?! )")
+reEndComBloc =          re.compile(r"\*\/")
+reEndLine =             re.compile(r"\n")
+reProblematicEndLine =  re.compile(r"\\\n")
+
+def cnv4JsDoc (inputFilename, outputFilename):
+    print "Translating into jsDoc: ", outputFilename, " ",
+
+    if not os.path.isfile(inputFilename):
+        print "\nProcess aborted due to errors."
+        sys.exit('ERROR: Input file "%s" does not exist!' % inputFilename)
+
+    dirOut = os.path.dirname(outputFilename)
+    if dirOut == "":
+        print "\nProcess aborted due to errors."
+        sys.exit('ERROR: Output file "%s" without path!' % outputFilename)
+
+    if not os.path.exists(dirOut):
+        os.makedirs(dirOut)
+
+    fOut = open(outputFilename,"w")
+    fIn = open(inputFilename)
+    
+    mode = M_CODE
+    previousProblematicEndLine = False
+    nat = Com2()
+    lineNumber = 0
+    for line in fIn:
+        lineNumber += 1
+        startCom2 = -1
+        endCom2 = -1
+        if mode == M_CODE and not previousProblematicEndLine:
+            oo = reStarCom2Bloc.search(line)
+            if oo: 
+                startCom2 = oo.end()
+                mode = M_COM2_BLOC
+                nat.clearBlock()
+                
+        # Com2 line?
+        if mode == M_COM2_BLOC:
+            previousProblematicEndLine = False
+            oo = reEndComBloc.search(line)
+            if oo: 
+                endCom2 = oo.start()
+                mode = M_CODE
+
+            if startCom2 == -1:
+                oo = reLineCom2.search(line)
+                if oo: 
+                    startCom2 = oo.end()
+
+            if endCom2 == -1:
+                endCom2 = reEndLine.search(line).start()
+
+            if startCom2 >= 0 and startCom2 < endCom2:
+                # Com2 line? Yeah!
+                line = ( line[:startCom2] + 
+                         nat.toJsDoc(line[startCom2:endCom2]) + 
+                         line[endCom2:] )
+        fOut.write(line)
+        if mode == M_CODE: 
+            if reProblematicEndLine.search(line):
+                previousProblematicEndLine = True
+            else:
+                previousProblematicEndLine = False
+                
+    print "   Done!"
+    fIn.close()
+    fOut.close()
+    return outputFilename
+    
+# -----------------
+# Analyze comment blocks
+# -----------------    
+class Com2:
+    def __init__(self):
+        self.clearBlock()
+
+    def clearBlock(self):
+        self.maxKeywords = sys.maxint
+        self.processedKeywords = 0
+        self.blockActive = True
+        self.clearKeyword()
+
+    def clearKeyword(self):
+        self.maxLines = sys.maxint
+        self.processedLines = 0
+        self.currentModeBlock = None
+        self.blockName = None
+
+    def toJsDoc(self, line):
+        if self.blockActive == False:
+            return line
+        for k, v in syntax.iteritems():
+            for j in v["keywords"]:
+                if re.match(j, line, flags=re.IGNORECASE):
+                    # Close the previous keyword
+                    if self.maxKeywords <= self.processedKeywords:
+                        self.blockActive = False
+                        return line
+                    self.clearKeyword()
+                    
+                    # Start keyword
+                    self.blockName = k
+                    self.currentModeBlock = v
+                    
+                    if "keywordLineCnv" in self.currentModeBlock:
+                        conv = self.currentModeBlock["keywordLineCnv"]
+                        if conv == "cnvKeywordJsDoc":
+                            line = self.cnvKeywordJsDoc(line)
+                        # cnvKeywordType
+                    if v.get("maxLines"):
+                        self.maxLines = v.get("maxLines")
+                    if v.get("maxKeywords"):
+                        self.maxKeywords = v.get("maxKeywords")
+                    if "prefixKeyword" in self.currentModeBlock:
+                        line = self.currentModeBlock["prefixKeyword"] + " " + line
+                    self.processedKeywords += 1
+                    break
+            else:
+                continue
+            break
+        else:
+            if self.currentModeBlock and self.currentModeBlock.get("lineConv"):
+                conv = self.currentModeBlock.get("lineConv")
+                if conv == "lineParam":
+                    line = self.cnvLineParam(line, "", "")
+                elif conv == "lineOptParam":
+                    line = self.cnvLineParam(line, "", "|null|undefined=")
+                elif conv == "lineSingleDecl":
+                    line = self.cnvLineSingleDecl(line)
+                elif conv == "lineSuperClass":
+                    line = self.cnvLineSuperClass(line)
+
+                if self.maxLines <= self.processedLines:
+                    self.clearKeyword() # forced end of block
+        return line
+
+    # Keyword line converters
+    def cnvKeywordJsDoc(self, subLine):
+        words = subLine.split(None,1)
+        if len(words) < 2:
+            return subLine
+
+        decl = self.cnvChkDeclaration(words[1])
+        if decl == None:
+            return subLine
+
+        return ( words[0] + " {" + decl[0] + "} " + " " + decl[1])
+
+    # Line converters
+    def cnvLineSuperClass(self, subLine):
+        superClass = subLine.replace("- <", 
+            self.currentModeBlock.get("prefixLine") + " ")
+        if subLine == superClass:
+            return subLine
+
+        self.processedLines += 1
+        superClass = superClass.replace(">","")
+        return superClass
+
+    def cnvLineParam(self, subLine, start, end):
+        words = subLine.split(None,2)
+        if len(words) < 3:
+            return subLine
+
+        if words[1] != "-":
+            return subLine
+
+        decl = self.cnvChkDeclaration(words[2])
+        if decl == None:
+            return subLine
+
+        self.processedLines += 1
+        return (self.currentModeBlock.get("prefixLine") +
+            " {" + start + decl[0] + end + "} " +
+            words[0] + " "+ decl[1])
+
+    def cnvLineSingleDecl(self, subLine):
+        decl = self.cnvChkDeclaration(subLine)
+        if decl == None:
+            return subLine
+
+        self.processedLines += 1
+        return self.currentModeBlock.get("prefixLine") + " {" + decl[0] + "} " + decl[1]
+
+    # Type converter
+    def cnvChkDeclaration(self, subLine):
+        if subLine[0:1] != "{":
+            return None
+
+        declEnd = re.search(r"\}(\s|\n|$)", subLine)
+        if not declEnd:
+            return None
+
+        return [self.cnvTypeList(subLine[1:declEnd.start()]), 
+                subLine[declEnd.start()+1:]] 
+
+    def cnvTypeList(self, typeList):
+        repetitiveParameter = ""
+        if typeList[-4:] == " ...":
+            typeList = typeList[:-4]
+            repetitiveParameter = "..."
+        
+        return repetitiveParameter + self.cnvTypeName(typeList)
+
+    def cnvTypeName(self, typeName):
+        # print "ini:", typeName
+        if typeName == "":
+            return "*" # Any type from declaration as {}
+
+        for p, r in typesOL:
+            if r.find(r"\2") > 0:
+                spl = re.split(p,typeName)
+                if len(spl) >= 4 and spl[1] != "" and spl[2] != "":
+                    typeName = re.sub(p,r,typeName)
+                    typeName = typeName.replace(spl[1], self.cnvTypeName(spl[1]))
+                    typeName = typeName.replace(spl[2], self.cnvTypeName(spl[2]))
+                    # print "/2:",typeName
+                    return self.cnvTypeNameSplit(typeName)
+            elif r.find(r"\1") > 0:
+                spl = re.split(p,typeName)
+                if len(spl) >= 3 and spl[1] != "":
+                    typeName = re.sub(p,r,typeName)
+                    typeName = typeName.replace(spl[1], self.cnvTypeName(spl[1]))
+                    # print "/1:",typeName
+                    return self.cnvTypeNameSplit(typeName)
+            else:
+                if typeName.lower() == p.lower():
+                    return r
+
+        return self.cnvTypeNameSplit(typeName)
+
+    def cnvTypeNameSplit(self, typeName):
+        # print "or:",typeName
+        typeName = typeName.replace(" or ","|")
+        typeName = typeName.replace("||","|") # Used in Event.js
+        typeName = typeName.replace(" ","")
+        types = typeName.split("|")
+        if len(types) > 1:
+            for i in range(len(types)):
+                types[i] = self.cnvTypeName(self.cnvTypeNameClear(types[i]))
+            # print types
+            return "|".join(types)
+        else:
+            # print typeName
+            return self.cnvTypeNameClear(typeName)
+
+    def cnvTypeNameClear(self, typeAux):
+        if typeAux[0:1] == "{" and typeAux[len(typeAux)-1:] == "}": # See Protocol.js:105
+            typeAux = typeAux[1:-1]
+        if typeAux[0:1] == "<" and typeAux[len(typeAux)-1:] == ">":
+            typeAux = typeAux[1:-1]
+        return typeAux
+
+# -----------------
+# main
+# -----------------
+if __name__ == '__main__':
+    cnv4JsDoc(sys.argv[1],sys.argv[2])

Added: sandbox/jsdoc/tools/NaturalDocsPreserve.py
===================================================================
--- sandbox/jsdoc/tools/NaturalDocsPreserve.py	                        (rev 0)
+++ sandbox/jsdoc/tools/NaturalDocsPreserve.py	2011-09-17 17:30:36 UTC (rev 12377)
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+
+import re
+import os
+import sys
+
+# -----------------
+# Detect "Natural Docs" comments.
+# -----------------
+preserveText ="@preserve"
+endSeparatorText ="------------------------------"
+M_CODE = 0
+M_COM1_BLOC = 1
+M_COM2_BLOC = 2
+reStarTextComBloc =     re.compile(r"^ *\/\*+ *(?! )")
+reStarCom2Bloc =        re.compile(r"^ *\/\*\*")
+reStarCom1Bloc =        re.compile(r"^ *\/\*")
+reEndComBloc =          re.compile(r"\*\/")
+reEndSepComBloc =       re.compile(r"\-{30} *\*\/")
+reEndLine =             re.compile(r"\n")
+reProblematicEndLine =  re.compile(r"\\\n")
+
+def preserveCom (inputFilename, outputFilename):
+    print "Preserving comments to: ", outputFilename, " ",
+
+    if not os.path.isfile(inputFilename):
+        print "\nProcess aborted due to errors."
+        sys.exit('ERROR: Input file "%s" does not exist!' % inputFilename)
+
+    dirOut = os.path.dirname(outputFilename)
+    if dirOut == "":
+        print "\nProcess aborted due to errors."
+        sys.exit('ERROR: Output file "%s" without path!' % outputFilename)
+
+    if not os.path.exists(dirOut):
+        os.makedirs(dirOut)
+    fOut = open(outputFilename,"w")
+    fIn = open(inputFilename)
+
+    mode = M_CODE
+    previousProblematicEndLine = False
+    lineNumber = 0
+    for line in fIn:
+        lineNumber += 1
+        startCom = -1
+        endCom = -1
+        if mode == M_CODE and not previousProblematicEndLine:
+            oo = reStarCom2Bloc.search(line)
+            if oo: 
+                startCom = oo.end()
+                mode = M_COM2_BLOC
+            else:
+                oo = reStarCom1Bloc.search(line)
+                if oo: 
+                    startCom = oo.end()
+                    mode = M_COM1_BLOC
+
+        if mode != M_CODE:
+            modeOld = mode
+            previousProblematicEndLine = False
+            oo = reEndComBloc.search(line)
+            if oo: 
+                endCom = oo.start()
+                mode = M_CODE
+
+            if endCom == -1:
+                endCom = reEndLine.search(line).start()
+
+            if startCom >= 0 and startCom <= endCom: # first line 
+                aux = preserveText
+                if modeOld == M_COM1_BLOC: # Set simple comment block to comment-2 block
+                    aux = "*" + preserveText
+                startText = reStarTextComBloc.search(line).end()
+                if line[startText:startText+1] != "\n": # Add line if start block with text
+                    aux += "\n"
+                line = line[:startCom] + aux + line[startCom:]
+            if mode == M_CODE: # last line
+                if not reEndSepComBloc.search(line): # add separator at last line of block if not found
+                    endCom = reEndComBloc.search(line).start()
+                    line = ( line[:endCom] + 
+                                 endSeparatorText + 
+                                 line[endCom:] )
+
+        fOut.write(line)
+        if mode == M_CODE: 
+            if reProblematicEndLine.search(line):
+                previousProblematicEndLine = True
+            else:
+                previousProblematicEndLine = False
+
+    print "Done!"
+    fIn.close()
+    fOut.close()
+    return outputFilename
+    
+# -----------------
+# main
+# -----------------
+if __name__ == '__main__':
+    preserveCom(sys.argv[1],sys.argv[2])

Added: sandbox/jsdoc/tools/closure-compiler/bin/_where-is-the-compiler.txt
===================================================================
--- sandbox/jsdoc/tools/closure-compiler/bin/_where-is-the-compiler.txt	                        (rev 0)
+++ sandbox/jsdoc/tools/closure-compiler/bin/_where-is-the-compiler.txt	2011-09-17 17:30:36 UTC (rev 12377)
@@ -0,0 +1,2 @@
+Download the "compiler.jar" in this directory from:
+    http://code.google.com/p/closure-compiler/downloads/list

Added: sandbox/jsdoc/tools/closure-library/closure/bin/build/jscompiler.py
===================================================================
--- sandbox/jsdoc/tools/closure-library/closure/bin/build/jscompiler.py	                        (rev 0)
+++ sandbox/jsdoc/tools/closure-library/closure/bin/build/jscompiler.py	2011-09-17 17:30:36 UTC (rev 12377)
@@ -0,0 +1,57 @@
+# Copyright 2010 The Closure Library Authors. All Rights Reserved.
+
+"""Utility to use the Closure Compiler CLI from Python."""
+
+import distutils.version
+import logging
+import re
+import subprocess
+
+
+# Pulls a version number from the first line of 'java -version'
+_VERSION_REGEX = re.compile('[\.0-9]+')
+
+
+def _GetJavaVersion():
+  """Returns the string for the current version of Java installed."""
+  proc = subprocess.Popen(['java', '-version'], stderr=subprocess.PIPE)
+  unused_stdoutdata, stderrdata = proc.communicate()
+  version_line = stderrdata.splitlines()[0]
+  return _VERSION_REGEX.search(version_line).group()
+
+
+def Compile(compiler_jar_path, source_paths, flags=None):
+  """Prepares command-line call to Closure Compiler.
+
+  Args:
+    compiler_jar_path: Path to the Closure compiler .jar file.
+    source_paths: Source paths to build, in order.
+    flags: A list of additional flags to pass on to Closure Compiler.
+
+  Returns:
+    The compiled source, as a string, or None if compilation failed.
+  """
+
+  # User friendly version check.
+  if not (distutils.version.LooseVersion(_GetJavaVersion()) >
+          distutils.version.LooseVersion('1.6')):
+    logging.error('Closure Compiler requires Java 1.6 or higher. '
+                  'Please visit http://www.java.com/getjava')
+    return
+
+  args = ['java', '-jar', compiler_jar_path]
+  for path in source_paths:
+    args += ['--js', path]
+
+  if flags:
+    args += flags
+
+  logging.info('Compiling with the following command: %s', ' '.join(args))
+
+  proc = subprocess.Popen(args, stdout=subprocess.PIPE)
+  stdoutdata, unused_stderrdata = proc.communicate()
+
+  if proc.returncode != 0:
+    return
+
+  return stdoutdata

Modified: sandbox/jsdoc/tools/mergejs.py
===================================================================
--- sandbox/jsdoc/tools/mergejs.py	2011-09-17 16:11:32 UTC (rev 12376)
+++ sandbox/jsdoc/tools/mergejs.py	2011-09-17 17:30:36 UTC (rev 12377)
@@ -142,6 +142,12 @@
             
 
 def run (sourceDirectory, outputFilename = None, configFile = None):
+    return getSortedFiles(sourceDirectory, outputFilename, configFile, False)
+
+def getNames (sourceDirectory, configFile = None):
+    return getSortedFiles(sourceDirectory, None, configFile, True)
+
+def getSortedFiles (sourceDirectory, outputFilename = None, configFile = None, onlyNames = False):
     cfg = None
     if configFile:
         cfg = Config(configFile)
@@ -210,6 +216,8 @@
     ## Move forced first and last files to the required position
     if cfg:
         print "Re-ordering files..."
+        cfg.forceFirst = chkListFiles("first", sourceDirectory, cfg.forceFirst, files)
+        cfg.forceLast = chkListFiles("last", sourceDirectory, cfg.forceLast, files)
         order = cfg.forceFirst + [item
                      for item in order
                      if ((item not in cfg.forceFirst) and
@@ -219,6 +227,14 @@
     ## Output the files in the determined order
     result = []
 
+    if onlyNames:
+        for fp in order:
+            fExp = os.path.normpath(os.path.join(sourceDirectory, fp)).replace("\\","/")
+            print "To compile: ", fExp
+            result.append(fExp)
+        print "\nTotal files to compile: %d " % len(result)
+        return result
+        
     for fp in order:
         f = files[fp]
         print "Exporting: ", f.filepath
@@ -235,6 +251,18 @@
         open(outputFilename, "w").write("".join(result))
     return "".join(result)
 
+def chkListFiles (listName, sourceDirectory, listFiles, files):
+    finalList = []
+    for item in listFiles:
+        if item not in files:
+            fFull = os.path.join(sourceDirectory, item).strip()
+            if not os.path.isfile(fFull):
+                print "File does not exist: [{0}] {1}".format(listName, item)
+                continue
+            files[item] = SourceFile(item, open(fFull, "U").read())
+        finalList.append(item)
+    return finalList
+    
 if __name__ == "__main__":
     import getopt
 



More information about the Commits mailing list