Commit cb61cc74 authored by Tiago Peixoto's avatar Tiago Peixoto
Browse files

Complete overhaul of command line parsing, and support for loading graph-tool...

Complete overhaul of command line parsing, and support for loading graph-tool as whole as a python module
    
The command line parsing was completely rewritten. It now supports
better parsing of sub-options, with type checking and grouping
support. Error reporting was also significantly improved, and it now
warns of invalid options and option values, before the option is
executed. Some syntax has changed, such as range filtering:
--[vertex|edge]-range-filter was replaced by
--exclude-[vertex|edge]-range and --keep-[vertex|edge]-range, which
should have a clearer meaning. Ranges can also be specified now by
comparison operators (>,<,>=,<=,=), such as ">=10", to indicate a
range of (10, inf). In addition, ranges can now be easily open or
closed at either end, by suffixing the specific end with '*', to
indicate it is closed, ex: "10 700*" means (10,700].
    
The graph-tool script can now be loaded as a python module (it must be
renamed first to 'something.py'). All the command line options (except
'for' and 'history' which become irrelevant) are available as
functions, with full description and optional parameter support. In
addition, pure function objects can be given as parameters where
expressions are asked, instead of strings and files, which enables
convenient extension of graph-tool.
parent 55028b02
......@@ -174,9 +174,9 @@ AC_DEFINE_UNQUOTED([COPYRIGHT],
"Copyright (C) 2007 Tiago de Paula Peixoto\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.",
[copyright info])
AC_DEFINE([GIT_COMMIT], "esyscmd(git show | head -n 1 | sed 's/commit //' | tr -d '\n')", [git commit hash])
AC_DEFINE([GIT_COMMIT], "esyscmd(git show | head -n 1 | sed 's/commit //' | grep -o -e '.\{8\}' | head -n 1 |tr -d '\n')", [git HEAD commit hash])
AC_DEFINE([GIT_COMMIT_DATE], "esyscmd(git log -1 | head -n 3 | grep 'Date:' | sed s/'Date: '// | tr -d '\n')", [git HEAD commit date])
dnl fi
AC_OUTPUT([
Makefile
src/Makefile
......
......@@ -6,4 +6,11 @@ SUBDIRS = graph .
bin_SCRIPTS = graph-tool
EXTRA_DIST = $(bin_SCRIPTS)
\ No newline at end of file
EXTRA_DIST = $(bin_SCRIPTS)
# Also install graph-tool as a python module (graph_tool.py)
install-exec-hook:
ln -s $(bindir)/graph-tool $(pythondir)/graph_tool.py
uninstall-hook:
rm -f $(pythondir)/graph_tool.py
......@@ -3,57 +3,34 @@
#
# Copyright (C) 2007 Tiago de Paula Peixoto <tiago@forked.de>
#
# This program is free software; you can redistribute it and/or modify
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Library General Public License for more details.
#
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
# along with this program. If not, see <http://www.gnu.org/licenses/>.
__author__="Tiago de Paula Peixoto <tiago@forked.de>"
__copyright__="Copyright 2007 Tiago de Paula Peixoto"
__license__="GPL version 3 or above"
__URL__="http://graph-tool.forked.de"
import sys
sys.path.append(".")
from libgraph_tool import *
__version__= mod_info().version
from optparse import *
import os
import os.path
import re
import struct
import fcntl
import termios
import gzip
import bz2
import string
import time
import signal
import traceback
from time import *
from math import *
#import gc # garbage collector
#gc.enable()
#gc.set_debug(gc.DEBUG_LEAK|gc.DEBUG_STATS)
prog_info = mod_info()
version_string = "%s %s\nWritten by %s\n\n%s" % (prog_info.name, prog_info.version, \
prog_info.author,prog_info.copyright)
__version__ = mod_info().version
import sys, os, os.path, re, struct, fcntl, termios, gzip, bz2, string,\
textwrap, time, signal, traceback, shutil, optparse, time, math,\
inspect
# General utilities
def getheightwidth():
""" This will get the terminal width and height"""
height, width = 20, 80
try:
height, width = struct.unpack("hhhh", fcntl.ioctl(0, termios.TIOCGWINSZ ,"\000"*8))[0:2]
......@@ -61,127 +38,558 @@ def getheightwidth():
pass
return height, width
option_list = list()
class Opt:
def __init__(self, name, value):
self.name = name
self.value = value
# Some exceptions
class ParserError:
"""Error raised from option parser"""
def __init__(self, error):
self.error = error
def __str__(self):
return self.error
class OptionError:
"""Parse error in a command line option"""
def __init__(self, option, error):
self.option = option
self.error = error
def __str__(self):
msg = "error parsing option --%s: %s\n\n" % (self.option.name, self.error)
help = "--%s=%s\n" % (self.option.name, self.option.metavars)
wrapper = textwrap.TextWrapper(initial_indent = " "*3, subsequent_indent = " "*3, width = getheightwidth()[1])
msg += help + wrapper.fill( self.option.help)
return msg
def parse_options(arguments = sys.argv[1:]):
"""This function will parse all the command-line options, and
return a list of parsed options, ordered according to how it was
given in the command line. The list will contain tuples (option,
value), where 'option' is an instance of the Opt class, defined
below. The option will be checked for validity, except when
contextual information is necessary (such as deciding whether a
specific property exists in a graph)."""
# The values below will be used to initialize the Opt classes. See the class
# definition for the meaning of each field.
option_groups = []
basic = ["Basic options",
["load", "[FILE[|FORMAT]]", "load graph from file. The format is guessed from the FILE, or can be specified by FORMAT.",
{"FILE":"-", "FORMAT":"auto"},
["if $FORMAT == 'auto':\n graph.ReadFromFile($FILE)\nelse:\n graph.ReadFromFile($FILE, $FORMAT)","None"]],
["save", "[FILE[|FORMAT]]", "save graph to file. The format is guessed from the FILE, or can be specified by FORMAT.",
{"FILE":"-", "FORMAT":"auto"},
["if $FORMAT == 'auto':\n graph.WriteToFile($FILE)\nelse:\n graph.WriteToFile($FILE, $FORMAT)","None"]]]
option_groups.append(basic)
generation = [ "Graph generation",
["correlated-configurational-model", "OPTIONS",
"generate graph using the configurational model with arbitrary degree correlations. Options are: $OPTIONS_LIST. See documentation for details.",
{"OPTIONS":[ ("N",("int",10000)), ("pjk",("exp","lambda j,k: 1.0")), ("pjk_ceil",("exp", "lambda j,k: pjk(j,k)")),
("pjk_m",("float",1.0)), ("inv_pjk_ceil",("exp","lambda p,r:(inv_poisson(p,2), inv_poisson(p,2))")),
("corr",("exp","lambda jl,kl,j,k: 1.0")), ("corr_ceil",("exp","lambda jl,kl,j,k: corr(jl,kl,j,k)")),
("corr_m",("float",1.0)),("inv_corr_ceil",("exp","lambda p,r,a,b: inv_pjk_ceil(p,r)")),("undirected",("bool[1]",False)),
("directed",("bool[1]",True)), ("progress",("bool",False)), ("seed",("int",int(time.time())))]},
"graph.GenerateCorrelatedConfigurationalModel($N, $pjk, $pjk_ceil, $inv_pjk_ceil, $pjk_m, $corr, $corr_ceil, $inv_corr_ceil, $corr_m, $undirected, $seed, $progress)"] ]
option_groups.append(generation)
filtering = ["Filtering",
["directed", None,"treat graph as directed (default).", {}, "graph.SetDirected(True)"],
["undirected", None,"treat graph as undirected.", {}, "graph.SetDirected(False)"],
["reverse", None, "reverse edges.", {}, "graph.SetReversed(not graph.GetReversed())"],
["exclude-vertex-range", "PROPERTY|RANGE", "choose vertex property and range to exclude.", {},
["graph.SetVertexFilterProperty($PROPERTY)",
"graph.SetVertexFilterRange($RANGE[0],$RANGE[1], True)"] ],
["keep-vertex-range", "PROPERTY|RANGE", "choose vertex property and range to keep (and exclude the rest).", {},
["graph.SetVertexFilterProperty($PROPERTY)",
"graph.SetVertexFilterRange($RANGE[0],$RANGE[1], False)"] ],
["reset-vertex-filter", None, "remove edge filter.", {}, "graph.SetVertexFilterProperty('')"],
["exclude-edge-range", "PROPERTY|RANGE", "choose edge property and range to exclude.", {},
["graph.SetEdgeFilterProperty($PROPERTY)",
"graph.SetEdgeFilterRange($RANGE[0],$RANGE[1], True)"] ],
["keep-edge-range", "PROPERTY|RANGE", "choose edge property and range to keep (and exclude the rest).", {},
["graph.SetEdgeFilterProperty($PROPERTY)",
"graph.SetEdgeFilterRange($RANGE[0],$RANGE[1], False)"] ],
["reset-edge-filter", None, "remove edge filter.", {}, "graph.SetEdgeFilterProperty('')"] ]
option_groups.append(filtering)
modification = ["Graph Modification",
["edit-vertex-property", "PROPERTY[|TYPE]|EXPRESSION", "edit the selected vertex property", {"TYPE":"double"},
"graph.EditVertexProperty($PROPERTY, $TYPE, edit_function_wrap('vertex',$EXPRESSION))"],
["edit-edge-property", "PROPERTY[|TYPE]|EXPRESSION", "edit the selected edge property", {"TYPE":"double"},
"graph.EditEdgeProperty($PROPERTY, $TYPE, $EXPRESSION)"],
["edit-graph-property", "PROPERTY[|TYPE]|EXPRESSION", "edit the selected graph property", {"TYPE":"double"},
"graph.EditGraphProperty($PROPERTY, $TYPE, $EXPRESSION)"],
["remove-vertex-property", "PROPERTY", "remove the selected vertex property", {}, "graph.RemoveVertexProperty($PROPERTY)"],
["remove-edge-property", "PROPERTY", "remove the selected edge property", {}, "graph.RemoveEdgeProperty($PROPERTY)"],
["remove-graph-property", "PROPERTY", "remove the selected graph property", {}, "graph.RemoveGraphProperty($PROPERTY)"],
["insert-vertex-index-property", "PROPERTY", "insert vertex index as property", {}, "graph.InsertVertexIndexProperty($PROPERTY)"],
["insert-edge-index-property", "PROPERTY", "insert edge index as property", {}, "graph.InsertEdgeIndexProperty($PROPERTY"],
["list-properties", None, "list all properties", {}, "graph.ListProperties()"]]
option_groups.append(modification)
statistics = ["Basic Statistics",
["number-of-vertices", "[FILE]", "get the number of vertices.", {"FILE":"-"}, "graph.GetNumberOfVertices()" ],
["number-of-edges", "[FILE]", "get the number of edges", {"FILE":"-"}, "graph.GetNumberOfEdges()"],
["vertex-histogram", "DEGREE[|FILE]", "get the vertex degree/property histogram", {"FILE":"-"}, "graph.GetVertexHistogram($DEGREE)"],
["edge-histogram", "PROPERTY[|FILE]", "get the edge property histogram", {"FILE":"-"}, "graph.GetEdgeHistogram($PROPERTY)"],
["average-vertex-property", "DEGREE[|FILE]", "get the average of the vertex property", {"FILE":"-"}, "get_mean(graph.GetVertexHistogram($DEGREE))"],
["average-edge-property", "PROPERTY[|FILE]", "get the average of the edge property", {"FILE":"-"}, "get_mean(graph.GetEdgeHistogram($PROPERTY))"],
["combined-vertex-histogram", "DEGREE1|DEGREE2[|FILE]", "get the combined (DEGREE1,DEGREE2) histogram. Scalar properties are also accepted as DEGREE1 or DEGREE2",
{"FILE":"-"}, "graph.GetCombinedVertexHistogram($DEGREE1,$DEGREE2)"],
["distance-histogram", "[[WEIGHT|]FILE]", "get the distance histogram", {"FILE":"-", "WEIGHT":""}, "graph.GetDistanceHistogram($WEIGHT)"],
["average-distance", "[[WEIGHT|]FILE]", "get the averarge distance", {"FILE":"-", "WEIGHT":""}, "get_mean(graph.GetDistanceHistogram($WEIGHT))"],
["average-harmonic-distance", "[[WEIGHT|]FILE]", "get the averarge harmonic distance", {"FILE":"-", "WEIGHT":""},
["hist = graph.GetDistanceHistogram($WEIGHT)",
"avg, err = get_mean(dict((1.0/k,v) for k,v in hist.iteritems()))",
"(1.0/avg, err/(avg**2))"]],
["sampled-distance-histogram", "[OPTIONS[|FILE]]", "get the sampled distance histogram. Options are $OPTIONS_LIST",
{"FILE":"-", "OPTIONS":[ ("samples",("int",1000)), ("weight",("str","")), ("seed",("int",int(time.time())))] },
"graph.GetSampledDistanceHistogram($weight,$samples,$seed)"],
["average-sampled-distance", "[OPTIONS[|FILE]]", "get the average sampled distance. Options are $OPTIONS_LIST",
{"FILE":"-", "OPTIONS":[ ("samples",("int",1000)), ("weight",("str","")), ("seed",("int",int(time.time())))] },
"get_mean(graph.GetSampledDistanceHistogram($weight,$samples,$seed))"],
["average-sampled-harmonic-distance", "[OPTIONS[|FILE]]", "get the average sampled harmonic distance. Options are $OPTIONS_LIST",
{"FILE":"-", "OPTIONS":[ ("samples",("int",1000)), ("weight",("str","")), ("seed",("int",int(time.time())))] },
["hist = graph.GetSampledDistanceHistogram($weight,$samples,$seed)",
"avg, err = get_mean(dict((1.0/k,v) for k,v in hist.iteritems()))",
"(1.0/avg, err/(avg**2))"]],
["label-components", "PROPERTY", "label components to PROPERTY", {}, "graph.LabelComponents($PROPERTY)"],
["label-parallel-edges", "PROPERTY", "label parallel edges to PROPERTY", {}, "graph.LabelParallelEdges($PROPERTY)"],
["reciprocity", "[FILE]", "get the edge reciprocity", {"FILE":"-"}, "graph.GetReciprocity()"],
["minimum-spanning-tree", "[WEIGHT|]PROPERTY", "mark the minimum spanning tree edges in PROPERTY", {"WEIGHT":""}, "graph.GetMinimumSpanningTree($WEIGHT, $PROPERTY)"],
["line-graph", "FILE[|FORMAT]", "save the corresponding line graph to FILE", {"FORMAT":"xml"}, "graph.GetLineGraph($FILE,$FORMAT)"],
["betweenness-centrality", "VERTEX_BETWEENESS[|EDGE_BETWEENESS[|WEIGHT]]", "calculate and store the vertex and/or edge betweenness centrality",
{"EDGE_BETWEENESS":"", "WEIGHT":""}, "graph.GetBetweenness($WEIGHT, $EDGE_BETWEENESS, $VERTEX_BETWEENESS)" ],
["central-point-dominance", "VERTEX_BETWEENESS[|FILE]", "calculate central point dominance, given the VERTEX_BETWEENESS vertex property",
{"FILE":"-"}, "graph.GetCentralPointDominance($VERTEX_BETWEENESS)"]]
option_groups.append(statistics)
correlations = ["Correlations",
["average-combined-vertex-correlation", "DEGREE1|DEGREE2[|FILE]",
"get the average of DEGREE2 in function of DEGREE1. Scalar properties are also accepted as DEGREE1 or DEGREE2",
{"FILE":"-"}, "graph.GetAverageCombinedVertexCorrelation($DEGREE1, $DEGREE2)"],
["vertex-correlation-histogram", "ORIGIN_DEGREE|NEIGHBOUR_DEGREE[[|WEIGHT]|FILE]",
"get the degree correlation histogram. Scalar properties are also accepted in place of *_DEGREE. An optional edge weight property can be passed by WEIGHT",
{"FILE":"-", "WEIGHT":""}, "graph.GetVertexCorrelationHistogram($ORIGIN_DEGREE, $NEIGHBOUR_DEGREE, $WEIGHT)"],
["average-nearest-neighbours-correlation", "ORIGIN_DEGREE|NEIGHBOUR_DEGREE[[|WEIGHT]|FILE]",
"get the average nearest neighbours correlation. Scalar properties are also accepted in place of *_DEGREE. An optional edge weight property can be passed by WEIGHT",
{"FILE":"-", "WEIGHT":""}, "graph.GetAverageNearestNeighboursCorrelation($ORIGIN_DEGREE, $NEIGHBOUR_DEGREE, $WEIGHT)"],
["edge-vertex-correlation-histogram", "DEGREE_SOURCE|EDGE_PROP|DEGREE_TARGET[|FILE]",
"get the source degree vs. edge scalar vs. target degree correlation histogram. Scalar properties are also accepted in place of DEGREE_*",
{"FILE":"-"}, "graph.GetEdgeVertexCorrelationHistogram($DEGREE_SOURCE, $EDGE_PROP, $DEGREE_TARGET)"],
["assortativity-coefficient", "DEGREE[|FILE]", "get the assortativity coefficient. Scalar properties are also accepted in place of DEGREE",
{"FILE":"-"}, "graph.GetAssortativityCoefficient($DEGREE)"],
["scalar-assortativity-coefficient", "DEGREE[|FILE]", "get the scalar assortativity coefficient. Scalar properties are also accepted in place of DEGREE",
{"FILE":"-"}, "graph.GetScalarAssortativityCoefficient($DEGREE)"]]
option_groups.append(correlations)
clustering = ["Clustering",
["local-clustering-coefficient", "PROPERTY", "set the local clustering coefficient to vertex property", {},
"graph.SetLocalClusteringToProperty($PROPERTY)"],
["global-clustering-coefficient", "[FILE]", "get the global clustering coefficient", {"FILE":"-"}, "graph.GetGlobalClustering()"],
["extended-clustering-coefficient", "PREFIX|MAX", "set the extended clustering coefficients c1 to cMAX to vertex properties PREFIX1 to PREFIXMAX", {},
"graph.SetExtendedClusteringToProperty($PREFIX, $MAX)"]]
option_groups.append(clustering)
layout = ["Layout",
["spring-block-layout", "PROPERTY|OPTIONS", "compute the spring block layout. The positions will be stored in PROPERTY. Options are $OPTIONS_LIST",
{"OPTIONS": [("kw",("str[1]",True)), ("fg-grid",("str[1]",False)), ("fg-all-pairs",("str[1]",False)), ("iter",("int",1000)),
("seed",("int",int(time.time()))), ("weight",("str",""))] },
"graph.ComputeGraphLayoutSpringBlock($PROPERTY, $weight, $kw, $iter, $seed)"],
["gursoy-atun-layout", "PROPERTY|OPTIONS", "compute the Gursoy-Atun layout. Options are $OPTIONS_LIST",
{"OPTIONS": [("square",("str[1]",True)), ("circle",("str[1]",False)), ("heart",("str[1]",False)), ("iter",("int",1000)),
("seed",("int",int(time.time()))), ("weight",("str",""))] },
"graph.ComputeGraphLayoutGursoy($PROPERTY, $weight, $circle, $iter, $seed)" ]]
option_groups.append(layout)
community = [ "Community",
["community-structure", "PROPERTY|OPTIONS", "calculate the community structure and assign it to PROPERTY. Options are: $OPTIONS_LIST",
{ "OPTIONS": [("g",("float",1.0)), ("N",("int",1000)), ("Tmin",("float",0.01)), ("Tmax",("float",1.0)), ("spins",("int",0)),
("correlated",("str[1]",False)), ("uncorrelated",("str[1]",True)), ("random",("str[1]",False)), ("weight",("str","")),
("seed",("int",int(time.time()))), ("verbose",("bool",False)), ("history",("str",""))]},
["strat_map = {'correlated':CommCorr.Correlated, 'uncorrelated':CommCorr.Uncorrelated, 'random':CommCorr.ErdosReyni}",
"graph.GetCommunityStructure($g, strat_map[$correlated], $N, $Tmin, $Tmax, $spins, $seed, $verbose, $history, $weight, $PROPERTY)"]],
["modularity", "PROPERTY[[|WEIGHT]|FILE]", "calculate the modularity, given a community partition specified by PROPERTY",
{"FILE":"-", "WEIGHT":""},
"graph.GetModularity($WEIGHT, $PROPERTY)" ],
["community-graph", "PROPERTY|SIZE_PROPERTY|FILE[|FORMAT]",
"obtain the graph of communities, given a community partition specified by PROPERTY. The resulting graph will have a vertex property named SIZE-PROPERTY, which will contain the size of the corresponding communities.",
{"FORMAT":"auto"},
["if $FORMAT == 'auto':\n format = ''\nelse:\n format = $FORMAT",
"graph.GetCommunityNetwork($PROPERTY, $SIZE_PROPERTY, $FILE, format)"]]]
option_groups.append(community)
history = ["History",
["for", "INIT_EXPRESSION|CONDITION_EXPRESSION|STEP_EXPRESSION[|PREFIX_EXPRESSION]", "simplified scripting", {"PREFIX_EXPRESSION":"''"}, "None"],
["history", "INIT_EXPRESSION|CONDITION_EXPRESSION|STEP_EXPRESSION[|PREFIX_EXPRESSION]", "simplified scripting (does not overwrite previous results)", {"PREFIX_EXPRESSION":"''"}, "None"],
["flush-rate", "TIME", "for/history files flush rate (default 1h)", {}, "globals()['loop_io_flush_rate']=$TIME"]]
option_groups.append(history)
# Below are the defintion of the option classes (OptionError, SubOpt and
# Opt)
class SubOpt:
"""A suboption in a command line option. This will handle mostly groups."""
def __init__(self, init):
"this will initialize the suboption with a specific list"
type_re = re.compile(r"([a-z]+)\[(\d+)\]")
match = type_re.match(init[1][0])
if match != None:
self.type = match.group(1)
self.group_name = int(match.group(2))
else:
self.type = init[1][0]
self.group_name = None
self.group = {}
self.name = init[0]
self.default = init[1][1]
def __str__(self):
if self.default == None:
return self.name
elif self.group_name == None:
return "%s (default: %s)" % (self.name, str(self.default))
else:
if self.default == True:
return self.name + " (default)"
else:
return self.name
class Opt:
"""This class represents a command line option. It understands
multiple arguments, separated by '|', as specified by the
meta-variables (self.metavars), as well as optional arguments
(enclosed by '[]'), which can be recursevly optional. The
parse() method will parse a given argument string into a
dictionary, containing the parsed value of each metavar. This
later can be used to interpolate the self.action field, with
the run() method, which will activate the proper action for
the option. It also understands suboptions, which is a series
of 'subopt=value' strings, separated by commas, with option
grouping and type-checking."""
def __init__(self, init):
"""This will initialize the option with a specific list"""
self.name = init[0]
self.metavars = init[1]
self.help = init[2]
self.params = init[3]
self.action = init[4]
self.groups = {}
# Build sub-option list, if necessary
if self.params.has_key("OPTIONS"):
self.suboptions = [SubOpt(x) for x in self.params["OPTIONS"]]
self.help = self.help.replace("$OPTIONS_LIST", ", ".join([str(x) for x in self.suboptions]))
self.suboptions = dict([(x.name,x) for x in self.suboptions])
# Handle groups
for opt in self.suboptions.values():
if opt.group_name != None:
if not self.groups.has_key(opt.group_name):
self.groups[opt.group_name] = [[opt.name], opt.name]
else:
self.groups[opt.group_name][0].append(opt.name)
if opt.default == True:
self.groups[opt.group_name][1] = opt.name
opt.group = self.groups[opt.group_name][0]
def unquote(self, string):
""" This will unquote a string, if quoted"""
quote_re = re.compile(r"('(.*)')|(\"(.*)\")")
m = quote_re.match(string)
if m != None:
if m.group(2) != "":
return m.group(2)
else:
return m.group(4)
else:
return string
def parse(self, args):
"""This method will parse a given argument string into a
dictionary, containing the parsed value of each
metavar. The dictionary will also contain the values of
any eventual suboption."""
if self.metavars == None:
return
# Parameter regexp
p = re.compile(r"((('[^']*')|(\"[^\"']*\"))|([^\|]+)|(^(?=[^$]))(?=\||$))")
values = [x[0].strip() for x in p.findall(args)]
# Match each value to its metavar, by finding the best match in the
# 'optional-tree'
optional_re = re.compile(r"\[([^][]*)\]")
bracket_re = re.compile(r"[][]")
metavars = self.metavars
mvar_list = [x[0].strip() for x in p.findall(bracket_re.sub("", metavars))]
while len(mvar_list) != len(values) and "[" in metavars:
metavars = optional_re.sub(r"", metavars)
mvar_list = [x[0].strip() for x in p.findall(bracket_re.sub("", metavars))]
if len(mvar_list) != len(values):
opt_re = re.compile(r"\[.*\]")
min_args = len([x[0].strip() for x in p.findall(opt_re.sub(r"", self.metavars))])
max_args = len([x[0].strip() for x in p.findall(bracket_re.sub(r"", self.metavars))])
if min_args != max_args:
raise OptionError(self, "invalid number of arguments, %d (expected between %d and %d)" % (len(values), min_args, max_args))
else:
raise OptionError(self, "invalid number of arguments, %d (expected %d)" % (len(values), min_args))
# Put in the default values
parsed_mvars = self.params.copy()
if parsed_mvars.has_key("OPTIONS"):
parsed_mvars["OPTIONS"] = dict([ (k, v.default) for k,v in self.suboptions.iteritems() if v.default != None and v.group_name == None])
# Do the parsing
for i in xrange(0, len(values)):
mvar = mvar_list[i]
value = values[i]
if "DEGREE" in mvar:
parsed_mvars[mvar] = degree(value)
elif mvar == "MAX":
try:
parsed_mvars[mvar] = int(value)
except ValueError:
raise OptionError(self, "invalid value for '%s': %s (expected an int)" % (mvar, value))
elif mvar == "OPTIONS":
subopts = {}
groups = {}
# Default suboption values
subopt_list = parsed_mvars[mvar]
# Parse suboptions
p = re.compile(r"([^,'\"]+|[^,]+'[^']*'|[^,]+\"[^\"]*\")(?:,|$)")
subopt_list.update(dict([ (x.split("=",1)[0].strip(),x.split("=",1)[1].strip()) for x in p.findall(value) if "=" in x]))
subopt_list.update(dict([ (x.strip(),None) for x in p.findall(value) if "=" not in x]))
subopt_context = {}
for name,value in subopt_list.iteritems():
if not self.suboptions.has_key(name):
raise OptionError(self, "invalid option: " + name)
opt = self.suboptions[name]
# Handle suboption grouping
if opt.group_name != None:
if value != False:
if groups.has_key(opt.group_name):
raise OptionError(self, "cannot specify '%s' and '%s' simultaneously " % (name, groups[opt.group_name]) )
groups[opt.group_name] = name
self.groups[opt.group_name][1] = name
continue
if value == None and opt.type != "bool":
raise OptionError(self, "you must specify a value for '%s'" % name)
if value.__class__ == str and opt.type != "str":
value = self.unquote(value)
try:
if value == None:
if opt.type == "bool":
value = True
else:
value = eval("%s('%s')" % (opt.type, value))
elif opt.type == "exp":
try:
value = eval_expr(value, subopt_context)
subopt_context[opt.name] = value
except:
(file_name, line, function, text) = traceback.extract_tb(sys.exc_info()[2])[-1]
raise OptionError(self, "suboption %s: error evaluating expression \"%s\": %s \n%s" % \
(opt.name, value, sys.exc_info()[1], "[traceback: File %s, line %d, in %s (text: %s)]" % \
(file_name, line, function, str(text))))
elif value.__class__ == str:
if opt.type == "bool":
value = eval(value)
else:
value = eval("%s('%s')" % (opt.type, value))
except ValueError:
raise OptionError(self, "invalid value for '%s': %s (expected %s)" % (opt.name, value, opt.type))
subopts[opt.name] = value
# Handle value for groups
for g in self.groups.values():
for opt_name in g[0]:
opt = self.suboptions[opt_name]
if opt.type == "bool":
if g[1] == opt.name:
subopts[opt.name] = True
else:
subopts[opt.name] = False
else:
subopts[opt.name] = g[1]
parsed_mvars[mvar] = subopts
elif "EXPRESSION" in mvar:
try:
context = {}
value = (eval_expr(value, context), context)
except:
(file_name, line, function, text) = traceback.extract_tb(sys.exc_info()[2])[-1]
raise OptionError(self, "error evaluating expression \"%s\": %s \n%s" % \
(value, sys.exc_info()[1], "[traceback: File %s, line %d, in %s (text: %s)]" % \
(file_name, line, function, str(text))))
parsed_mvars[mvar] = value
elif mvar == "TIME":
r = re.compile(r"(.*)(s|m|h|d|y)")
m = r.match(value)
if m != None:
try:
time = float(m.group(1))
except ValueError:
raise OptionError(self, "invalid value for TIME: %s" % value)
if m.group(2) == "m":
time *= 60
if m.group(2) == "h":
time *= 60*60
if m.group(2) == "d":
time *= 60*60*24
if m.group(2) == "d":
time *= 60*60*24*365
parsed_mvars[mvar] = time
elif "RANGE" in mvar:
try:
rcomp = re.compile(r"(>|<|>=|<=|==|=)\s*([^=]+)")
rrange = re.compile(r"([^\*]+)(\*?)\s+([^\*]+)(\*?)")
if rcomp.match(value):
comp = rcomp.match(value).group(1)
thres = rcomp.match(value).group(2)
thres = float(thres)
if "<" in comp:
ran = (float('-Inf'), thres)
if "=" in comp:
inc = (False, True)
else:
inc = (False, False)
elif ">" in comp:
ran = (thres, float('Inf'))
if "=" in comp:
inc = (True, False)
else:
inc = (False, False)
elif comp == "==" or comp == "=" :
ran = (thres, thres)
inc = (True, True)
elif rrange.match(value):
m = rrange.match(value)
r1 = float(m.group(1))
i1 = "*" in m.group(2)
r2 = float(m.group(3))
i2 = "*" in m.group(4)
ran = (r1,r2)
inc = (i1,i2)
else:
raise ValueError
except ValueError:
raise OptionError(self, "invalid value for RANGE: " + value)
parsed_mvars[mvar] = (ran, inc)
else:
parsed_mvars[mvar] = value
return parsed_mvars
def run(self, args):
"""This will interpolate the self.action field with the parsed
values, and run the proper action for the option"""
parsed_mvars = self.parse(args)
globals()['parsed_mvars'] = parsed_mvars
subst_list = {}
if parsed_mvars != None:
for mvar in parsed_mvars.keys():
subst_list[mvar] = "parsed_mvars['%s']" % mvar
if mvar == "OPTIONS":
for k,v in parsed_mvars[mvar].iteritems():
subst_list[k] = "parsed_mvars['OPTIONS']['%s']" % k
if self.action.__class__ == list:
for a in self.action[:-1]:
action = string.Template(a).substitute(subst_list)
exec action in globals()
action = self.action[-1]
else:
action = self.action
action = string.Template(action).substitute(subst_list)
retval = eval(action, globals())
del globals()['parsed_mvars']
return retval
# Setup 'optparse' and load it with all the options
prog_info = mod_info()
version_string = "%s %s\nWritten by %s\n\n%s" % (prog_info.name, prog_info.version, \
prog_info.author,prog_info.copyright)
# The option parser