rc.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401
  1. #! /usr/bin/env python
  2. # rc.py
  3. # ------------------------------------------------
  4. # help
  5. # ------------------------------------------------
  6. """
  7. Deal with model settings in `rc` format.
  8. RCFILES
  9. A rcfile is a text file with key/value pairs seperated by a ':', e.g.
  10. my.flag : T
  11. my.answer : 42
  12. The following functionality is supported:
  13. * Empty lines are ignored.
  14. * Comment lines are introduced by a '!' as first character.
  15. * Long values could be continued at the next line after a '\' as last character.
  16. * Include the key/value pairs from another file:
  17. #include an/other.rc
  18. * Substitute environment variables when available:
  19. tm.dir : ${HOME}/TM5/cy3
  20. * Substitute values of other keys in a line:
  21. build.dir : ${tm.dir}/build
  22. grid : glb300x200
  23. input.${grid}.path : /data/input/${grid}
  24. Substitions are allowed in both key names as well as values.
  25. The substitutions are performed in a loop until nothing
  26. has to be substituted anymore, or some substitutions could
  27. not be applied at al; for the later an error is raised.
  28. Values to be substituted could therefore be set before and
  29. after they are used.
  30. Note that if a key has the same name as an environment variable,
  31. the new value will be assigned to the key instead of the value
  32. retrieved from the environment:
  33. HOME : /some/other/dir/
  34. * Substitude some specials:
  35. ${pid} # evaluates to the current process id;
  36. # useful for names of log files etc
  37. ${script} # evaluates to the base name of the calling script,
  38. # thus without .py etc
  39. * Instead of variables of the form '${..}' other patterns could be
  40. specified with the optional 'marks' tupple (see below).
  41. * Old-style '#eval' lines are still supported:
  42. #eval RUNDIR = /path/to/mydir
  43. tmdir : ${RUNDIR}/TM5/cy3
  44. In this example, the value of RUNDIR will be evaluated and substituted
  45. in all {key,value} pairs. This feature is obsolete and a warning will
  46. be issued. The proper way to use this is with {key,value} pairs too:
  47. run.dir : /path/to/mydir
  48. tmdir : ${run.dir}/TM5/cy3
  49. * Comment starting with '!' is stripped from the values.
  50. To have a value including exclamation marks, use '\!' but do
  51. not expect that the rest of the value is scanned for comment too:
  52. my.value : -999 ! just an integer value
  53. my.message : This value has 64 characters \! Count if you don't believe it ...
  54. * If you trust yourself you might try to use conditional expressions:
  55. #if ${my.number} == 1
  56. message : Welcome
  57. #else
  58. message : Whatever ...
  59. #endif
  60. The conditions should be valid python expressions that evaluate to a boolean;
  61. value substitutions are performed before evaluation. Examples:
  62. ${my.runmode} == 4
  63. "${my.tracer}" == "CH4"
  64. Keep it simple! Very complicated and nested if-statements might not be
  65. resolved correctly, and are in any case not easy to understand for other users!
  66. In the example above, an exception could be raised by the special error expression;
  67. everything behind the '#error' mark is displayed as an error message:
  68. #error No settings provided for value : ${my.value}
  69. USAGE AS SCRIPT
  70. Called in script form, the following syntaxis is supported:
  71. rc.py [options] <rcfile> <key>
  72. rc.py -h|--help
  73. The <rcfile> is read and the value defined by <key> is printed
  74. to the standard output.
  75. Use the --help option for more documentation.
  76. USAGE AS PYTHON MODULE
  77. Import the module with:
  78. import rc
  79. Initialiase by reading all settings in a rcfile,
  80. supporting the functionality described in the 'RCFILES' section.
  81. rcf = RcFile( 'settings.rc' )
  82. The initialisation accepts some optional arguments.
  83. Set the silent flag to True to ignore warnings.
  84. rcf = RcFile( 'settings.rc', silent=False )
  85. Use the optional 'marks' tupple to define that variables to be expanded
  86. are marked other than '${..}' but rather '<mark1>..<mark2>' :
  87. rcf = RcFile( 'settings.rc', marks=('${','}') )
  88. Test to see if a key is defined:
  89. if rcf.has_key('my.flag') :
  90. print 'value of my flag is : ', rcf['my.flag']
  91. Extract a list with all keys:
  92. rcf.keys()
  93. A 'get' function is provided to extract values:
  94. * by default, the 'get' function returns the value as a str type:
  95. s = rcf.get('my.value')
  96. * a second argument is the name of the python type to which
  97. the value is converted to:
  98. i = rcf.get('my.flag','int')
  99. * if the return value should be a 'bool', the result is
  100. True for values : 'True' , 'T', 'yes', or '1' ,
  101. and False for value : 'False', 'F', 'no' , or '0' ;
  102. for other values an error is raised;
  103. * return a default value if the key is not found:
  104. rcf.get( 'my.flag', default=False )
  105. * print a debug message to the logging system for each extracted key:
  106. rcf.get( 'my.flag', verbose=True )
  107. Add a new value, comment is optional:
  108. rcf.add( 'my.iter', 2, comment='iteration number for restart' )
  109. Assign a new value to an existing key:
  110. rcf.replace( 'my.flag', True )
  111. Scan a character line for all occurances of ${<key>} and subsitute for
  112. the rc value assigned to <key> :
  113. line = rcf.substitute( line )
  114. Write the dictionary (with all variables expanded and included files included)
  115. to new file:
  116. rcf.WriteFile('newfile.rc')
  117. USAGE AS PYTHON MODULE - BACKWARDS COMPATIBILITY
  118. For backwards compatibility with older implementations of the rc.py module,
  119. two extra routines are available.
  120. To read rc-file by making an instance of the RcFile class,
  121. and to returns a dictionary of values only, use:
  122. rcdict = read( 'test.rc' [,silent=False] )
  123. Create a new rcfile and fill with key/values from a dictionary:
  124. write( 'test.rc', rcdict )
  125. RAW MODE
  126. In raw mode, the rcfile read into the RcFile object is read 'as it is'.
  127. Thus, no substitution of keys, no evaluation of '#if' statements and
  128. '#include' lines, etc.
  129. This might be useful to create new rcfiles from a template.
  130. Consider the rcfile 'xyz.rc' with content:
  131. my.id : xyz
  132. data.dir : /data/${my.id}
  133. Now try to create a copy 'qqq.rc' of this file with "xyz" replaced by "qqq";
  134. the optional 'raw' argument to the RcFile() routine is 'False' by default :
  135. rcf = RcFile( 'template.rc', raw=False )
  136. rcf.replace( 'my.id', 'qqq' )
  137. rcf.WriteFile( 'qqq.rc' )
  138. In this case, the content of 'qqq.rc' would be:
  139. my.id : qqq
  140. data.dir : /data/xyz
  141. With 'raw=True' passed to the RcFile() routine however, the content will be:
  142. my.id : qqq
  143. data.dir : /data/${my.id}
  144. Note that in raw mode it will often happen that more than one definition of
  145. a key is found, which are normally hidden within '#if' constructions.
  146. The multiple values are collected in a list; the 'replace' value will act on
  147. all definitions of a key.
  148. HISTORY
  149. 2008? Andy Jacobson, NOAA
  150. Translation to python of original shell script 'go_readrc' .
  151. 2009-06 Wouter Peters, WUR
  152. Support substitution of previously defined variables.
  153. 2009-06 Arjo Segers, TNO
  154. Support include files.
  155. 2009-09 Arjo Segers, TNO
  156. Re-coded into class.
  157. Implemented substitution loop.
  158. 2009-11 Arjo Segers, JRC
  159. Added main program to run this file as a shell script.
  160. Added replace and substitute routines.
  161. 2010-03 Arjo Segers, JRC
  162. Support simple if-statements.
  163. Support comment in values.
  164. 2010-07 Wouter Peters, WUR
  165. Downgraded to work for python 2.4.3 too.
  166. Added read/write routines for backwards compatibility.
  167. 2010-07-27 Arjo Segers, JRC
  168. Maintain list with rcfile names and line numbers to be displayed
  169. with error messages to identify where problematic lines are found.
  170. 2010-07-28 Andy Jacobson, NOAA
  171. Add second dictionary of key,linetrace values to help track the
  172. provenance of #included keys (to debug multiple key instances).
  173. Identify duplicate keys by checking on different source lines
  174. instead of checking if the values are different.
  175. 2010-08026 Arjo Segers, JRC
  176. Added raw option to RcFile class.
  177. """
  178. # ------------------------------------------------
  179. # tools
  180. # ------------------------------------------------
  181. def MyLogger( name ) :
  182. # external:
  183. import logging
  184. import sys
  185. # setup logger:
  186. logger = logging.getLogger( name )
  187. # no handlers yet ? then print to standard output:
  188. if len(logger.handlers) == 0 : logger.addHandler(logging.StreamHandler(sys.stdout))
  189. # no level set yet ? then set for info and higher:
  190. if logger.level == 0 : logger.setLevel(logging.INFO)
  191. # ok:
  192. return logger
  193. #enddef
  194. # ------------------------------------------------
  195. # classes
  196. # ------------------------------------------------
  197. class RcFile( object ) :
  198. """
  199. Class to store settings read from a rcfile.
  200. """
  201. def __init__( self, filename, silent=False, marks=('${','}'), raw=False ) :
  202. """
  203. Usage:
  204. rcf = RcFile( 'settings.rc' [,silent=False] [marks=('${','}')] )
  205. Read an rc-file and expand all the keys and values into a dictionary.
  206. Do not shout messages if silent is set to True.
  207. The 2-item tupple (mark1,mark2) could be used to re-define the default
  208. key pattern '${..}' into something else:
  209. <mark1>...<mark2>
  210. """
  211. # external:
  212. import re
  213. import os
  214. import sys
  215. # get logger:
  216. logger = MyLogger('rc')
  217. # info ...
  218. logger.debug( 'reading rcfile %s ...' % filename )
  219. # check ...
  220. if not os.path.exists(filename) :
  221. msg = 'rcfile not found : %s' % filename ; logger.error(msg)
  222. raise IOError, msg
  223. #endif
  224. # store file name:
  225. self.filename = filename
  226. # store rc-file root directory:
  227. self.rootdir = os.path.split(filename)[0]
  228. # store flag:
  229. self.raw = raw
  230. # storage for processed rcfile:
  231. self.outfile = []
  232. # storage for key/value pairs:
  233. self.values = {}
  234. # storage for key/source file pairs:
  235. self.sources = {}
  236. # open the specified rc-file:
  237. f = open(filename,'r')
  238. # store all lines in a list:
  239. inpfile = f.readlines()
  240. # close:
  241. f.close()
  242. # create traceback info:
  243. inptrace = []
  244. for jline in range(len(inpfile)) :
  245. inptrace.append( '"%s", line %-10s' % (filename,str(jline+1)) )
  246. #endfor
  247. # flags:
  248. warned_for_eval = False
  249. # pass counter:
  250. ipass = 1
  251. # loop until all substitutions and inclusions are done:
  252. while True :
  253. # start again with empty output file:
  254. self.outfile = []
  255. # also empty traceback info:
  256. self.trace = []
  257. # init current line:
  258. line = ''
  259. # assume nothing has to be done after this loop:
  260. something_done = False
  261. something_to_be_done = False
  262. # maintain list with unresolved lines (with keys that could not be evaluated yet):
  263. unresolved_lines = []
  264. # maintain list with keys from which the value could not be resolved yet:
  265. keys_with_unresolved_value = []
  266. # maintain list with undefined keys;
  267. # some might be part of the keys_with_unresolved_value list:
  268. undefined_keys = []
  269. # stack for conditional evaluation;
  270. # each element is a tuple with elements:
  271. # resolved (boolean) true if the if-statement is evaluated
  272. # flag (boolean) true if the lines below the statement
  273. # are to be included
  274. # anyflag (boolean) used to check if any of the 'if' or 'elif' conditions
  275. # in this sequence evaluated to True
  276. # line (char) description of the line for messages
  277. ifstack = []
  278. #print ''
  279. #print '---[pass %i]-------------------------------------' % ipass
  280. #for line in inpfile : print line.strip()
  281. # loop over lines in input file:
  282. iline = -1
  283. for inpline in inpfile :
  284. # line counter:
  285. iline = iline + 1
  286. # cut current traceback info from list:
  287. linetrace_curr = inptrace.pop(0)
  288. # set full traceback info:
  289. if line.endswith('\\') :
  290. # current input line is a continuation; combine:
  291. qfile,qlinenrs = linetrace.split(',')
  292. qnrs = qlinenrs.replace('lines','').replace('line','')
  293. if '-' in qnrs :
  294. qnr1,qnr2 = qnrs.split('-')
  295. else :
  296. qnr1,qnr2 = qnrs,qnrs
  297. #endif
  298. linetrace = '%s, lines %-9s' % ( qfile, '%i-%i' % (int(qnr1),int(qnr2)+1) )
  299. else :
  300. # just copy:
  301. linetrace = linetrace_curr
  302. #endif
  303. # remove end-of-line character:
  304. inpline = inpline.strip()
  305. ## DEBUG: display current line ...
  306. #print '%4i | %s' % (iline,inpline)
  307. #print '%4i | %s %s' % (iline,inpline,linetrace)
  308. #
  309. # empty lines
  310. #
  311. # skip empty lines:
  312. if len(inpline) == 0 :
  313. # add empty line to output:
  314. self.outfile.append('\n')
  315. # add traceback info:
  316. self.trace.append( linetrace )
  317. # next will be a new line:
  318. line = ''
  319. # next input line:
  320. continue
  321. #endif
  322. #
  323. # comment lines
  324. #
  325. # skip comment:
  326. if inpline.startswith('!') :
  327. # add copy to output file:
  328. self.outfile.append( '%s\n' % inpline )
  329. # add traceback info:
  330. self.trace.append( linetrace )
  331. # next will be a new line:
  332. line = ''
  333. # next input line:
  334. continue
  335. #endif
  336. #
  337. # continuation lines
  338. #
  339. # current line has continuation mark '\' at the end ?
  340. # then add this input line:
  341. if line.endswith('\\') :
  342. # remove continuation character, add input line:
  343. line = line[:-1]+inpline
  344. else :
  345. # bright new line:
  346. line = inpline
  347. #endif
  348. # continuation mark ? then next line of input file:
  349. if line.endswith('\\') : continue
  350. #
  351. # evaluate specaials : #if, #include, etc
  352. #
  353. # raw mode ? then do not evaluate ...
  354. if raw :
  355. # treat '#xxx' lines the same as comment:
  356. if inpline.startswith('#') :
  357. # add copy to output file:
  358. self.outfile.append( '%s\n' % inpline )
  359. # add traceback info:
  360. self.trace.append( linetrace )
  361. # next will be a new line:
  362. line = ''
  363. # next input line:
  364. continue
  365. #endif
  366. else :
  367. #
  368. # conditional settings (1)
  369. #
  370. ## debug ...
  371. #print 'xxx0 ', ifstack
  372. # is this the begin of a new condition ?
  373. mark = '#if'
  374. if line.startswith(mark) :
  375. # push temporary flag to stack, will be replaced after evaluation of condition:
  376. ifstack.append( ( False, True, False, linetrace ) )
  377. # debug ...
  378. #print 'xxx1 ', ifstack
  379. #endif
  380. mark = '#elif'
  381. if line.startswith(mark) :
  382. # check ...
  383. if len(ifstack) == 0 :
  384. logger.error( 'found orphin "%s" in %s' % (mark,linetrace) )
  385. raise Exception
  386. #endif
  387. # remove current top from stack:
  388. resolved,flag,anyflag,msg = ifstack.pop()
  389. # did one of the previous #if/#elif evaluate to True already ?
  390. if resolved and anyflag :
  391. # push to stack that this line resolved to False :
  392. ifstack.append( ( True, False, anyflag, linetrace ) )
  393. # copy to output:
  394. self.outfile.append( '%s\n' % line )
  395. # add traceback info:
  396. self.trace.append( linetrace )
  397. # next input line:
  398. continue
  399. else :
  400. # push temporary flag to stack, will be replaced after evaluation of condition:
  401. ifstack.append( ( False, True, anyflag, linetrace ) )
  402. #endif
  403. ## debug ...
  404. #print 'xxx2 ', ifstack
  405. #endif
  406. mark = '#else'
  407. if line.startswith(mark) :
  408. # check ...
  409. if len(ifstack) == 0 :
  410. logger.error( 'found orphin "%s" in %s' % (mark,linetrace) )
  411. raise Exception
  412. #endif
  413. # remove current top from stack:
  414. resolved,flag,anyflag,msg = ifstack.pop()
  415. # get higher level settings:
  416. if len(ifstack) > 0 :
  417. resolved_prev,flag_prev,anyflag_prev,msg_prev = ifstack[-1]
  418. else :
  419. flag_prev = True
  420. #endif
  421. # should next lines be included ?
  422. # reverse flag, take into acount higher level:
  423. new_flag = (not flag) and (not anyflag) and flag_prev
  424. # push to stack:
  425. ifstack.append( ( resolved, new_flag, False, linetrace ) )
  426. # debug ...
  427. #print 'xxx3 ', ifstack
  428. # copy to output:
  429. self.outfile.append( '%s\n' % line )
  430. # add traceback info:
  431. self.trace.append( linetrace )
  432. # next input line:
  433. continue
  434. #endif
  435. # is this the end of a condition ?
  436. mark = '#endif'
  437. if line.startswith(mark) :
  438. # check ...
  439. if len(ifstack) == 0 :
  440. logger.error( 'found orphin "%s" in %s' % (mark,linetrace) )
  441. raise Exception
  442. #endif
  443. # remove top from stack:
  444. top = ifstack.pop()
  445. # copy to output:
  446. self.outfile.append( '%s\n' % line )
  447. # add traceback info:
  448. self.trace.append( linetrace )
  449. # next input line:
  450. continue
  451. #endif
  452. # within if-statements ?
  453. if len(ifstack) > 0 :
  454. # extract current top:
  455. resolved,flag,anyflag,msg = ifstack[-1]
  456. # already resolved ? then check if this line should be skipped:
  457. if resolved and (not flag) :
  458. # skip this line, but include commented version in output:
  459. self.outfile.append( '!%s\n' % line )
  460. # add traceback info:
  461. self.trace.append( linetrace )
  462. # read the next input line:
  463. continue
  464. #endif
  465. #endif
  466. #
  467. # handle '#eval' lines
  468. #
  469. mark = '#eval'
  470. if line.startswith(mark):
  471. # info ..
  472. if not warned_for_eval :
  473. if not silent: logger.warning( 'the #eval statements in rc-files are deprecated, use {key:value} pairs instead' )
  474. warned_for_eval = True
  475. #endif
  476. # add commented copy to output:
  477. self.outfile.append( '!evaluated>>> '+line )
  478. # add traceback info:
  479. self.trace.append( linetrace )
  480. # remove leading mark:
  481. line = line.lstrip(mark)
  482. # multiple settings are seperated by ';' :
  483. evals = line.split(';')
  484. # insert in output file:
  485. for k in range(len(evals)) :
  486. # split in key and value:
  487. key,value = evals[k].split('=')
  488. # insert:
  489. self.outfile.append( '%s : %s' % (key,value) )
  490. # add traceback info:
  491. self.trace.append( linetrace )
  492. #endfor
  493. # next input line:
  494. continue
  495. #endif
  496. #
  497. # replace ${..} values
  498. #
  499. # ensure that common marks are evaluated correctly:
  500. start_mark = marks[0].replace('{','\{').replace('<','\<').replace('$','\$')
  501. close_mark = marks[1].replace('}','\}').replace('>','\>')
  502. # set syntax of keywords to be matched, e.g. '${...}' :
  503. pattern = start_mark+'[A-Za-z0-9_.]+'+close_mark
  504. # make a regular expression that matches all variables:
  505. rc_varpat = re.compile( pattern )
  506. # search all matching paterns:
  507. pats = re.findall(rc_varpat,line)
  508. # counter for unexpanded substitutions:
  509. ntobedone = 0
  510. # loop over matches:
  511. for pat in pats :
  512. # remove enclosing characters:
  513. key = pat.lstrip(start_mark).rstrip(close_mark)
  514. # test some dictionaries for matching key:
  515. if self.values.has_key(key) :
  516. # get previously defined value:
  517. val = self.values[key]
  518. # substitute value:
  519. line = line.replace(pat,val)
  520. # set flag:
  521. something_done = True
  522. elif os.environ.has_key(key) :
  523. # get value from environment:
  524. val = os.environ[key]
  525. # substitute value:
  526. line = line.replace(pat,val)
  527. # set flag:
  528. something_done = True
  529. elif key == 'pid' :
  530. # special value: process id; convert to character:
  531. val = '%i' % os.getpid()
  532. # substitute value:
  533. line = line.replace(pat,val)
  534. # set flag:
  535. something_done = True
  536. elif key == 'script' :
  537. # special value: base name of the calling script, without extension:
  538. script,ext = os.path.splitext(os.path.basename(sys.argv[0]))
  539. # substitute value:
  540. line = line.replace(pat,script)
  541. # set flag:
  542. something_done = True
  543. else :
  544. # could not substitute yet; set flag:
  545. ntobedone = ntobedone + 1
  546. # add to list with unresolved keys:
  547. if key not in undefined_keys : undefined_keys.append(key)
  548. # continue with next substitution:
  549. continue
  550. #endif
  551. #endfor # matched patterns
  552. # not all substituted ?
  553. if ntobedone > 0 :
  554. # add line to output:
  555. self.outfile.append(line)
  556. # add traceback info:
  557. self.trace.append( linetrace )
  558. # a new pass is needed:
  559. something_to_be_done = True
  560. # store for info messages:
  561. unresolved_lines.append( '%s | %s' % (linetrace,line) )
  562. # could this be a 'key : value' line ?
  563. if ':' in line :
  564. # split, remove leading and end space:
  565. qkey,qvalue = line.split(':',1)
  566. qkey = qkey.strip()
  567. # assume it is indeed a key if it does not contain whitespace,
  568. # no start mark, and does not start wiht '#' :
  569. if (' ' not in qkey) and (start_mark not in qkey) and (not qkey.startswith('#')) :
  570. # add to list:
  571. if qkey not in keys_with_unresolved_value : keys_with_unresolved_value.append(qkey)
  572. #endif
  573. # next input line:
  574. continue
  575. #endif
  576. #
  577. # handle '#include' lines
  578. #
  579. mark = '#include'
  580. if line.startswith(mark) :
  581. # remove leading mark, what remains is file to be included:
  582. inc_file = line.lstrip(mark).strip()
  583. # check ...
  584. if not os.path.exists(inc_file) :
  585. inc_file = os.path.join(self.rootdir,inc_file)
  586. logger.debug( 'Added rootdir to requested include: %s' % inc_file )
  587. #endif
  588. if not os.path.exists(inc_file) :
  589. logger.error( 'include file not found : %s' % inc_file )
  590. logger.error( linetrace )
  591. raise IOError, 'include file not found : %s' % inc_file
  592. #endif
  593. # read content:
  594. inc_f = open( inc_file, 'r' )
  595. inc_rc = inc_f.readlines()
  596. inc_f.close()
  597. # add extra comment for output file:
  598. self.outfile.append( '! >>> %s >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n' % inc_file )
  599. self.outfile.extend( inc_rc )
  600. self.outfile.append( '! <<< %s <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n' % inc_file )
  601. # add traceback info:
  602. self.trace.append( linetrace )
  603. for jline in range(len(inc_rc)) :
  604. self.trace.append( '"%s", line %-10s' % (inc_file,str(jline+1)) )
  605. #endfor
  606. self.trace.append( linetrace )
  607. # set flag:
  608. something_done = True
  609. # a new pass is needed:
  610. something_to_be_done = True
  611. # next input line:
  612. continue
  613. #endif
  614. #
  615. # conditional settings (2)
  616. #
  617. # evaluate conditional expressions:
  618. mark1 = '#if'
  619. mark2 = '#elif'
  620. if line.startswith(mark1) or line.startswith(mark2) :
  621. # remove leading mark, what remains is logical expression:
  622. expression = line.lstrip(mark1).strip()
  623. expression = line.lstrip(mark2).strip()
  624. # common mistake is to add a ':' as in python; remove this:
  625. if expression.endswith(':') : expression = expression.rstrip(':').strip()
  626. # evaluate:
  627. try :
  628. flag = eval( expression )
  629. except :
  630. logger.error( 'could not evaluate expression:' )
  631. logger.error( ' %s' % expression )
  632. logger.error( 'in %s' % linetrace )
  633. raise Exception
  634. #endtry
  635. # remove temporary top added before during this pass:
  636. tmp_statement,tmp_flag,tmp_anyflag,tmp_msg = ifstack.pop()
  637. # extract current top if necessary:
  638. if len(ifstack) > 0 :
  639. dummy_statement,prev_flag,dummy_anyflag,dummy_msg = ifstack[-1]
  640. else :
  641. prev_flag = True
  642. #endif
  643. # should next lines be included ?
  644. new_flag = prev_flag and tmp_flag and flag
  645. # any if/elif evaluated to true in this sequence ?
  646. new_anyflag = tmp_anyflag or new_flag
  647. # add to stack, now resolved, take into accout current flag:
  648. ifstack.append( ( True, new_flag, new_anyflag, linetrace ) )
  649. # debug ...
  650. #print 'xxx2 ', ifstack
  651. # copy to output:
  652. self.outfile.append( '%s\n' % line )
  653. # add traceback info:
  654. self.trace.append( linetrace )
  655. # next input line:
  656. continue
  657. #endif
  658. #
  659. # error message
  660. #
  661. # special command to rais an exception:
  662. mark = '#error'
  663. if line.startswith(mark) :
  664. # remove leading mark, what remains is error message:
  665. msg = line.lstrip(mark).strip()
  666. # display:
  667. logger.error( msg )
  668. # add info:
  669. logger.error( 'error message in %s' % linetrace )
  670. # stop:
  671. raise Exception
  672. #endif
  673. #
  674. # checks
  675. #
  676. # common mistake ...
  677. if line.startswith('#') :
  678. logger.error( 'line in rcfile starts with "#" but is not an "#include" or other special line;' )
  679. logger.error( 'if it is supposed to be comment, please start with "!" ...' )
  680. logger.error( ' %s' % line )
  681. logger.error( '%s' % linetrace )
  682. raise IOError
  683. #endif
  684. # check ...
  685. if ':' not in line :
  686. logger.error( 'key/value line should contain a ":"' )
  687. logger.error( '%s' % linetrace )
  688. raise IOError
  689. #endif
  690. #endif # not in raw mode
  691. #
  692. # add to output
  693. #
  694. # add line to output:
  695. self.outfile.append( '%s\n' % line )
  696. # add traceback info:
  697. self.trace.append( linetrace )
  698. #
  699. # add key/value pair
  700. #
  701. # not if inside an unresolved if-statement ...
  702. if len(ifstack) > 0 :
  703. # get top values:
  704. resolved,flag,anyflag,msg = ifstack[-1]
  705. # not resolved yet ? then continue:
  706. if not resolved : continue
  707. #endif
  708. # split in key and value;
  709. # value might contain ':' too, so at maximum 1 split:
  710. key,val = line.split(':',1)
  711. # remove comment from value:
  712. if '!' in val :
  713. # not if '\!' is in the value ...
  714. if not '\!' in val : val,comment = val.split('!')
  715. # replace all slash-comments:
  716. val = val.replace('\!','!')
  717. #endif
  718. # remove spaces:
  719. key = key.strip()
  720. val = val.strip()
  721. # already defined ?
  722. if self.values.has_key(key) :
  723. # in raw mode this happends often due to #if statements ...
  724. if self.raw :
  725. # reset to a lists if not yet:
  726. if not isinstance(self.values[key],list) :
  727. self.values [key] = [self.values [key]]
  728. self.sources[key] = [self.sources[key]]
  729. #endif
  730. # add:
  731. self.values [key].append(val)
  732. self.sources[key].append(linetrace)
  733. ## info ...
  734. #logger.warning( 'found double key (ignored in raw mode) : %s' % key )
  735. else :
  736. # this will occure often after the first pass since
  737. # the keys are resolved again and again ;
  738. # therefore, only complain if this definition is read
  739. # from a different line :
  740. if linetrace != self.sources[key] :
  741. logger.error( 'duplicated key \"%s\" found:' % key)
  742. logger.error( 'first definition in %s is:' % self.sources[key])
  743. logger.error( ' %s : %s' % (key,str(self.values[key])) )
  744. logger.error( 'second definition in %s is:' % linetrace.strip() )
  745. logger.error( ' %s : %s' % (key,str(val)) )
  746. raise Exception
  747. #endif
  748. #endif
  749. else :
  750. # store new value:
  751. self.values[key] = val
  752. self.sources[key] = linetrace
  753. # set flag:
  754. something_done = True
  755. #endif
  756. # display key and value ...
  757. #print ' --> %s : %s, from %s' % (key,val, linetrace)
  758. #endfor # loop over lines in text
  759. ## info ...
  760. #print '~~~ outfile ~~~~~~~~~~~~~~~~~~~~~~~'
  761. #for line in self.outfile : print line.strip()
  762. #print '~~~ key/values ~~~~~~~~~~~~~~~~~~~~'
  763. #for k,v in self.iteritems() :
  764. # print '%s : %s' % (k,v)
  765. ##endfor
  766. #print '-------------------------------------------------'
  767. #print ''
  768. # check ...
  769. if len(ifstack) > 0 :
  770. logger.error( 'unterminated if-statement ; current stack:' )
  771. for resolved,flag,anyflag,msg in ifstack : logger.error( msg )
  772. logger.error( 'please fix the rcfile or debug this script ...' )
  773. raise Exception
  774. #endif
  775. # check ...
  776. if something_to_be_done :
  777. # check for unterminated loop ...
  778. if not something_done :
  779. # list all unresolved lines:
  780. logger.error( 'Could not resolve the following lines in rcfile(s):' )
  781. logger.error( '' )
  782. for uline in unresolved_lines :
  783. logger.error( ' %s' % uline )
  784. #endfor
  785. logger.error( '' )
  786. # list all undefined keys:
  787. logger.error( ' Undefined key(s):' )
  788. logger.error( '' )
  789. for ukey in undefined_keys :
  790. # do not list them if they are undefined because the value
  791. # depends on other undefined keys:
  792. if ukey not in keys_with_unresolved_value :
  793. # display:
  794. logger.error( ' %s' % ukey )
  795. # loop over unresolved lines to see in which the key is used:
  796. for uline in unresolved_lines :
  797. # search for '${key}' pattern:
  798. if marks[0]+ukey+marks[1] in uline :
  799. logger.error( ' %s' % uline )
  800. #endif
  801. #endfor
  802. logger.error( '' )
  803. #endif
  804. #endfor
  805. logger.error( 'please fix the rcfile(s) or debug this script ...' )
  806. raise Exception
  807. #endif
  808. else :
  809. # finished ...
  810. break
  811. #endif
  812. # for safety ...
  813. if ipass == 100 :
  814. logger.error( 'resolving rc file has reached pass %i ; something wrong ?' % ipass )
  815. raise Exception
  816. #endif
  817. # new pass:
  818. ipass = ipass + 1
  819. # renew input:
  820. inpfile = self.outfile
  821. # renew traceback:
  822. inptrace = self.trace
  823. #endwhile # something to be done
  824. #enddef # __init__
  825. # ***
  826. def has_key( self, key ) :
  827. # from dictionairy:
  828. return self.values.has_key(key)
  829. #enddef
  830. # ***
  831. def keys( self ) :
  832. # from dictionairy:
  833. return self.values.keys()
  834. #enddef
  835. # ***
  836. def get( self, key, totype='', default=None, verbose=False ) :
  837. """
  838. rcf.get( 'my.value' [,default=None] )
  839. Return element 'key' from the dictionairy.
  840. If the element is not present but a default is specified, than return
  841. the default value.
  842. If 'verbose' is set to True, then print debug messages to the logging
  843. about which values is returned for the given key.
  844. The option argument 'totype' defines the conversion to a Python type.
  845. If 'totype' is set to 'bool', the return value is the
  846. boolean True for values 'T', 'True', 'yes', and '1',
  847. and False for 'F', 'False', 'no', or '0' ;
  848. for other values, an exception will be raised.
  849. """
  850. # get logger:
  851. logger = MyLogger('rc')
  852. # element found ?
  853. if self.values.has_key(key) :
  854. # copy value:
  855. value = self.values[key]
  856. # convert ?
  857. if totype == 'bool' :
  858. # convert to boolean:
  859. if value in ['T','True','yes','1'] :
  860. value = True
  861. elif value in ['F','False','no','0'] :
  862. value = False
  863. else :
  864. logger.error( "value of key '%s' is not a boolean : %s" % (key,str(value)) )
  865. raise Exception
  866. #endif
  867. elif len(totype) > 0 :
  868. # convert to other type ...
  869. value = eval( '%s(%s)' % (totype,value) )
  870. #endif
  871. # for debugging ...
  872. if verbose : logger.debug( 'rc setting "%s" : "%s"' % (key,str(value)) )
  873. else :
  874. # default value specified ?
  875. if default != None :
  876. # copy default:
  877. value = default
  878. # for debugging ...
  879. if verbose : logger.debug( 'rc setting "%s" : "%s" (deault)' % (key,str(value)) )
  880. else :
  881. # something wrong ...
  882. logger.error( "key '%s' not found in '%s' and no default specified" % (key,self.filename) )
  883. raise KeyError
  884. # ok
  885. return value
  886. #enddef
  887. # ***
  888. def replace( self, key, val ) :
  889. """
  890. Replace a key by a new value.
  891. """
  892. # get logger:
  893. logger = MyLogger('rc')
  894. # search for a line '<key> : <val>'
  895. # loop over lines in output file:
  896. found = False
  897. for iline in range(len(self.outfile)) :
  898. # extract:
  899. line = self.outfile[iline]
  900. # skip lines that are no key:value pair for sure ...
  901. if ':' not in line : continue
  902. # split once at first ':'
  903. k,v = line.split(':',1)
  904. # match ?
  905. if k.strip() == key :
  906. # replace line in original file:
  907. self.outfile[iline] = '%s : %s\n' % (k,str(val))
  908. # replace value:
  909. self.values[key] = val
  910. # set flag:
  911. found = True
  912. # found, thus no need to continue;
  913. # except in raw mode, since there multiple keys might exist:
  914. if not self.raw : break
  915. #endif
  916. #endfor # lines
  917. # not found ?
  918. if not found :
  919. logger.error( 'could not replace key : %s' % key )
  920. raise Exception
  921. #endif
  922. # ok
  923. return
  924. #enddef
  925. # ***
  926. def add( self, key, val, comment='' ) :
  927. """Add a new key/value pair."""
  928. # add lines:
  929. self.outfile.append( '\n' )
  930. if len(comment) > 0 : self.outfile.append( '! %s\n' % comment )
  931. self.outfile.append( '%s : %s\n' % (key,str(val)) )
  932. # add to dictionairy:
  933. self.values[key] = val
  934. # ok
  935. return
  936. #enddef
  937. # ***
  938. def substitute( self, line, marks=('${','}') ) :
  939. """
  940. Return a line with all '${..}' parts replaced by the corresponding rcfile values.
  941. The 2-item tupple (mark1,mark2) could be used to re-define the default
  942. key pattern '${..}' into something else:
  943. <mark1>...<mark2>
  944. """
  945. # external:
  946. import re
  947. # ensure that common marks are evaluated correctly:
  948. start_mark = marks[0].replace('{','\{').replace('<','\<').replace('$','\$')
  949. close_mark = marks[1].replace('}','\}').replace('>','\>')
  950. # set syntax of keywords to be matched, e.g. '${...}' :
  951. pattern = start_mark+'[A-Za-z0-9_.]+'+close_mark
  952. # make a regular expression that matches all variables:
  953. rc_varpat = re.compile( pattern )
  954. # search all matching paterns:
  955. pats = re.findall(rc_varpat,line)
  956. # loop over matches:
  957. for pat in pats :
  958. # remove enclosing characters:
  959. key = pat.lstrip(start_mark).rstrip(close_mark)
  960. # test dictionary for matching key:
  961. if self.values.has_key(key) :
  962. # get previously defined value:
  963. val = self.values[key]
  964. # substitute value:
  965. line = line.replace(pat,val)
  966. #endif
  967. #endfor # matched patterns
  968. # ok
  969. return line
  970. #enddef
  971. # ***
  972. def WriteFile( self, filename ) :
  973. """ write the dictionary to file"""
  974. # open file for writing:
  975. f = open(filename,'w')
  976. ## loop over key/value pairs:
  977. #for k,v in self.iteritems():
  978. # # add line; at least the specified number of characters
  979. # # is used for the key:
  980. # f.write( '%-20s:%s\n' % (k,v) )
  981. ##endfor
  982. # write processed input:
  983. f.writelines( self.outfile )
  984. # close file:
  985. f.close()
  986. #endif
  987. #endclass # RcFile
  988. # ***
  989. def read( rcfilename, silent=False ) :
  990. """
  991. This method reads an rc-file by making an instance of the RcFile class,
  992. and then returns the dictionary of values only.
  993. This makes it backwards compatible with older implementations of the rc.py module
  994. """
  995. rcdict = RcFile( rcfilename, silent=silent )
  996. return rcdict.values
  997. #enddef
  998. # ***
  999. def write( filename, rcdict ) :
  1000. """
  1001. This method writes an rc-file dictionary.
  1002. This makes it backwards compatible with older implementations of the rc.py module
  1003. """
  1004. # open file for writing:
  1005. f = open(filename,'w')
  1006. # loop over key/value pairs:
  1007. for k,v in rcdict.items():
  1008. # add line; at least the specified number of characters
  1009. # is used for the key:
  1010. f.write( '%-20s:%s\n' % (k,v) )
  1011. #endfor
  1012. # close file:
  1013. f.close()
  1014. #enddef
  1015. # ------------------------------------------------
  1016. # script
  1017. # ------------------------------------------------
  1018. if __name__ == '__main__':
  1019. # external ...
  1020. import sys
  1021. import optparse
  1022. import traceback
  1023. # get logger:
  1024. logger = MyLogger('rc')
  1025. # extract arguments from sys.argv array:
  1026. # 0 = name of calling script, 1: = actual arguments
  1027. args = sys.argv[1:]
  1028. # set text for 'usage' help line:
  1029. usage = "\n %prog <rcfile> <key> [-b|--bool] [--default<=value>]\n %prog <rcfile> -w|--write\n %prog -h|--help\n %prog -d|--doc"
  1030. # initialise the option parser:
  1031. parser = optparse.OptionParser(usage=usage)
  1032. # define options:
  1033. parser.add_option( "-d", "--doc",
  1034. help="print documentation",
  1035. dest="doc", action="store_true", default=False )
  1036. parser.add_option( "-v", "--verbose",
  1037. help="print information messages",
  1038. dest="verbose", action="store_true", default=False )
  1039. parser.add_option( "-b", "--bool",
  1040. help="return 'True' for values 'T', 'True', 'yes', or '1', and 'False' for 'F', 'False', 'no', or '0'",
  1041. dest="boolean", action="store_true", default=False )
  1042. parser.add_option( "--default",
  1043. help="default value returned if key is not found",
  1044. dest="default", action="store" )
  1045. parser.add_option( "-w", "--write",
  1046. help="write pre-processed rcfile",
  1047. dest="write", action="store_true", default=False )
  1048. # now parse the actual arguments:
  1049. opts,args = parser.parse_args( args=args )
  1050. # print documentation ?
  1051. if opts.doc :
  1052. print __doc__
  1053. sys.exit(0)
  1054. #endif
  1055. # recfile argument should be provided:
  1056. if len(args) < 1 :
  1057. parser.error("no name of rcfile provided\n")
  1058. #endif
  1059. # extract:
  1060. rcfile = args[0]
  1061. # read rcfile in dictionary:
  1062. try :
  1063. rcf = RcFile( rcfile, silent=(not opts.verbose) )
  1064. except :
  1065. if opts.verbose : logger.error( traceback.format_exc() )
  1066. sys.exit(1)
  1067. #endtry
  1068. # print pre-processed file ?
  1069. if opts.write :
  1070. for line in rcf.outfile : print line.strip()
  1071. sys.exit(0)
  1072. #endif
  1073. # key argument should be provided:
  1074. if len(args) < 2 :
  1075. parser.error("no name of rckey provided\n")
  1076. #endif
  1077. # extract:
  1078. rckey = args[1]
  1079. # key present ?
  1080. if rcf.has_key(rckey) :
  1081. # print requested value:
  1082. if opts.boolean :
  1083. # extract value:
  1084. flag = rcf.get(rckey,'bool')
  1085. # print result:
  1086. if flag :
  1087. print 'True'
  1088. else :
  1089. print 'False'
  1090. #endif
  1091. else :
  1092. # extract value:
  1093. value = rcf.get(rckey)
  1094. # display:
  1095. print value
  1096. #endif
  1097. else :
  1098. # default value provided ?
  1099. if opts.default != None :
  1100. # display:
  1101. print opts.default
  1102. else :
  1103. print 'ERROR - key "%s" not found in rcfile "%s" and no default specified' % (rckey,rcfile)
  1104. sys.exit(1)
  1105. #endif
  1106. #endif
  1107. #endif
  1108. # ------------------------------------------------
  1109. # end
  1110. # ------------------------------------------------