python-fhs
Python module for using the FHS and XDG basedir paths.
fhs.py
Go to the documentation of this file.
1 # This module implements fhs directory support in Python.
2 # vim: set fileencoding=utf-8 foldmethod=marker :
3 
4 # {{{ Copyright 2013-2019 Bas Wijnen <wijnen@debian.org>
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or(at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 # }}}
18 
19 # File documentation. {{{
20 
28 
29 '''@file
30 This module makes it easy to find files in the locations that are defined for
31 them by the FHS. Some locations are not defined there. This module chooses a
32 location for those.
33 
34 It also defines a configuration file format which is used automatically when
35 initializing this module.
36 '''
37 
38 '''@package fhs Module for using paths as described in the FHS.
39 This module makes it easy to find files in the locations that are defined for
40 them by the FHS. Some locations are not defined there. This module chooses a
41 location for those.
42 
43 It also defines a configuration file format which is used automatically when
44 initializing this module.
45 '''
46 # }}}
47 
48 # Paths and how they are handled by this module: {{{
49 # /etc configfile
50 # /run runtimefile
51 # /tmp tempfile
52 # /usr/lib/package datafile
53 # /usr/local datafile
54 # /usr/share/package datafile
55 # /var/cache cachefile
56 # /var/games datafile
57 # /var/lib/package datafile
58 # /var/lock lockfile
59 # /var/log logfile
60 # /var/spool spoolfile
61 # /var/tmp tempfile?
62 
63 # /home (xdgbasedir)
64 # /root (xdgbasedir)
65 # /bin -
66 # /boot -
67 # /dev -
68 # /lib -
69 # /lib<qual> -
70 # /media -
71 # /mnt -
72 # /opt -
73 # /sbin -
74 # /srv -
75 # /usr/bin -
76 # /usr/include -
77 # /usr/libexec -
78 # /usr/lib<qual> -
79 # /usr/sbin -
80 # /usr/src -
81 # /var/lib -
82 # /var/opt -
83 # /var/run -
84 
85 # FHS: http://www.linuxbase.org/betaspecs/fhs/fhs.html
86 # XDG basedir: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
87 
88 # So: configfile, runtimefile, tempfile, datafile, cachefile, lockfile, logfile, spoolfile
89 # }}}
90 
91 # Imports. {{{
92 import os
93 import sys
94 import shutil
95 import argparse
96 import tempfile
97 import atexit
98 # }}}
99 
100 # Globals. {{{
101 
102 initialized = False
103 
104 is_system = False
105 
106 is_game = False
107 
108 pname = os.getenv('PACKAGE_NAME', os.path.basename(sys.argv[0]))
109 
110 HOME = os.path.expanduser('~')
111 # Internal variables.
112 
115 _tempfiles = []
116 
119 _options = {}
120 
123 _option_order = []
124 
127 _module_info = {}
128 
131 _module_config = {}
132 
135 _module_values = {}
136 
139 _module_present = {}
140 
143 _base = os.path.abspath(os.path.dirname(sys.argv[0]))
144 # }}}
145 
146 # Configuration files. {{{
147 
148 XDG_CONFIG_HOME = os.getenv('XDG_CONFIG_HOME', os.path.join(HOME, '.config'))
149 
150 XDG_CONFIG_DIRS = tuple([XDG_CONFIG_HOME] + os.getenv('XDG_CONFIG_DIRS', '/etc/xdg').split(':'))
151 
152 
160 def write_config(name = None, text = True, dir = False, opened = True, packagename = None):
161  assert initialized
162  if name is None:
163  if dir:
164  filename = packagename or pname
165  else:
166  filename = (packagename or pname) + os.extsep + 'cfg'
167  else:
168  filename = name if is_system else os.path.join(packagename or pname, name)
169  if is_system:
170  if packagename and packagename != pname:
171  d = os.path.join('/etc/xdg', pname, packagename)
172  else:
173  d = os.path.join('/etc/xdg', pname)
174  else:
175  d = XDG_CONFIG_HOME
176  target = os.path.join(d, filename)
177  if dir:
178  if opened and not os.path.exists(target):
179  os.makedirs(target)
180  return target
181  else:
182  d = os.path.dirname(target)
183  if opened and not os.path.exists(d):
184  os.makedirs(d)
185  return open(target, 'w+' if text else 'w+b') if opened else target
186 # }}}
187 
188 
196 def read_config(name = None, text = True, dir = False, multiple = False, opened = True, packagename = None):
197  assert initialized
198  if name is None:
199  if dir:
200  filename = packagename or pname
201  else:
202  filename = (packagename or pname) + os.extsep + 'cfg'
203  else:
204  filename = name
205  seen = set()
206  target = []
207  if not is_system:
208  t = os.path.join(XDG_CONFIG_HOME, filename if name is None else os.path.join(packagename or pname, name))
209  if os.path.realpath(t) not in seen and os.path.exists(t) and (dir if os.path.isdir(t) else not dir):
210  r = t if dir or not opened else open(t, 'r' if text else 'rb')
211  if not multiple:
212  return r
213  seen.add(os.path.realpath(t))
214  target.append(r)
215  dirs = ['/etc/xdg', '/usr/local/etc/xdg']
216  if not is_system:
217  for d in XDG_CONFIG_DIRS:
218  dirs.insert(0, d)
219  if packagename and packagename != pname:
220  dirs = [os.path.join(x, pname, packagename) for x in dirs] + [os.path.join(x, packagename) for x in dirs]
221  else:
222  dirs = [os.path.join(x, pname) for x in dirs]
223  if not is_system:
224  dirs.insert(0, packagename or pname)
225  dirs.insert(0, os.path.curdir)
226  dirs.insert(0, _base)
227  for d in dirs:
228  t = os.path.join(d, filename)
229  if os.path.realpath(t) not in seen and os.path.exists(t) and (dir if os.path.isdir(t) else not dir):
230  r = t if dir or not opened else open(t, 'r' if text else 'rb')
231  if not multiple:
232  return r
233  seen.add(os.path.realpath(t))
234  target.append(r)
235  if multiple:
236  return target
237  else:
238  return None
239 # }}}
240 
241 
247 def remove_config(name = None, dir = False, packagename = None):
248  assert initialized
249  if dir:
250  shutil.rmtree(read_config(name, False, True, False, False, packagename), ignore_errors = False)
251  else:
252  os.unlink(read_config(name, False, False, False, False, packagename))
253 # }}}
254 
255 # Config file helper functions. {{{
256 def _protect(data, extra = ''):
257  ret = ''
258  extra += '\\'
259  for x in str(data):
260  o = ord(x)
261  if o < 32 or o >= 127 or x in extra:
262  ret += '\\%x;' % o
263  else:
264  ret += x
265  return ret
266 # }}}
267 
268 def _unprotect(data):
269  ret = ''
270  while len(data) > 0:
271  if data[0] == '%':
272  l = data.index(';')
273  ret += chr(int(data[1:l], 16))
274  data = data[l + 1:]
275  else:
276  if 32 <= ord(data[0]) < 127:
277  # newlines can happen; only this range is valid.
278  ret += data[0]
279  data = data[1:]
280  return ret
281 # }}}
282 
283 
288 def decode_value(value, argtype):
289  if value == 'None':
290  return None
291  if argtype is str:
292  if len(value) < 2 or not value.startswith("'") or not value.endswith("'"):
293  raise ValueError('str value without quotes')
294  return value[1:-1].replace(r"'\''", "'")
295  if argtype is bool:
296  if value not in ('False', 'True'):
297  raise ValueError('incorrect bool value %s' % value)
298  return value == 'True'
299  return argtype(value)
300 # }}}
301 
302 
305 def encode_value(value):
306  if value is None:
307  return 'None'
308  if isinstance(value, str):
309  return "'" + value.replace("'", r"'\''") + "'"
310  return str(value)
311 # }}}
312 
313 def help_text(main, options, option_order):
314  if _info['help']:
315  print(_info['help'], file = sys.stderr)
316  else:
317  if _info['version']:
318  print('this is %s version %s\n' % (pname, _info['version']), file = sys.stderr)
319  else:
320  print('this is %s\n' % pname, file = sys.stderr)
321 
322  print('\nSupported option arguments:', file = sys.stderr)
323  for module in (False, True):
324  for opt in option_order:
325  option = options[opt]
326  if (option['module'] is not None) != module:
327  continue
328  m = ' (This option can be passed multiple times)' if option['multiple'] else ''
329  if option['argtype'] is bool:
330  optname = '--' + opt
331  if option['short'] is not None:
332  optname += ', -' + option['short']
333  print('\t%s\n\t\t%s%s' % (optname, option['help'], m), file = sys.stderr)
334  elif option['optional']:
335  optname = '--' + opt + '[=<value>]'
336  if option['short'] is not None:
337  optname += ', -' + option['short'] + '[<value>]'
338  default = ' Default: %s' % str(option['default']) if option['default'] is not None else ''
339  print('\t%s\n\t\t%s%s%s' % (optname, option['help'], default, m), file = sys.stderr)
340  else:
341  optname = '--' + opt + '=<value>'
342  if option['short'] is not None:
343  optname += ', -' + option['short'] + '<value>'
344  default = ' Default: %s' % str(option['default']) if option['default'] is not None else ''
345  print('\t%s\n\t\t%s%s%s' % (optname, option['help'], default, m), file = sys.stderr)
346 
347  if _info['contact'] is not None:
348  print('\nPlease send feedback and bug reports to %s' % _info['contact'], file = sys.stderr)
349 # }}}
350 
352  if _info['version']:
353  print('%s version %s' % (pname, _info['version']), file = sys.stderr)
354  else:
355  print('%s' % pname, file = sys.stderr)
356  if _info['contact']:
357  print('\tPlease send feedback and bug reports to %s' % _info['contact'], file = sys.stderr)
358 
359  if len(_module_info) > 0:
360  print('\nUsing modules:', file = sys.stderr)
361  for mod in _module_info:
362  print('\t%s version %s\n\t\t%s' % (mod, _module_info[mod]['version'], _module_info[mod]['desc']), file = sys.stderr)
363  if _module_info[mod]['contact'] is not None:
364  print('\t\tPlease send feedback and bug reports for %s to %s' % (mod, _module_info[mod]['contact']), file = sys.stderr)
365 # }}}
366 
367 def load_config(filename, values = None, present = None, options = None):
368  if present is None:
369  present = {}
370  if values is None:
371  new_values = True
372  values = {}
373  else:
374  new_values = False
375  config = read_config(filename + os.extsep + 'ini')
376  if config is None:
377  return {}
378  for cfg in config:
379  if cfg.strip() == '' or cfg.strip().startswith('#'):
380  continue
381  if '=' not in cfg:
382  print('invalid line in config file %s: %s' % (filename, cfg), file = sys.stderr)
383  key, value = cfg.split('=', 1)
384  key = _unprotect(key)
385  if not new_values and key not in values:
386  print('invalid key %s in config file' % key, file = sys.stderr)
387  continue
388  if key in present and present[key]:
389  continue
390  try:
391  if options is not None and options[key]['multiple']:
392  values[key] = [decode_value(_unprotect(v), options[key]['argtype']) for v in value.split(',')]
393  else:
394  values[key] = _unprotect(value) if options is None else decode_value(_unprotect(value), options[key]['argtype'])
395  except ValueError:
396  print('Warning: error loading value for %s; ignoring' % key, file = sys.stderr)
397  continue
398  if present is not None:
399  present[key] = True
400  return values
401 # }}}
402 
403 
412 def save_config(config, name = None, packagename = None):
413  assert initialized
414  if name is None:
415  filename = 'commandline' + os.extsep + 'ini'
416  else:
417  filename = name + os.extsep + 'ini'
418  keys = list(config.keys())
419  keys.sort()
420  with write_config(filename) as f:
421  for key in keys:
422  if isinstance(config[key], list):
423  value = ','.join(_protect(encode_value(x), ',') for x in config[key])
424  else:
425  value = _protect(encode_value(config[key]))
426  f.write('%s=%s\n' % (_protect(key, '='), value))
427 # }}}
428 # }}}
429 
430 # Commandline argument handling. {{{
431 
448 def option(name, help, short = None, multiple = False, optional = False, default = None, noarg = None, argtype = None, module = None, options = None, option_order = None):
449  if options is None:
450  assert not initialized
451  options = _options
452  if option_order is None:
453  option_order = _option_order
454  if name in options:
455  raise ValueError('duplicate registration of argument name %s' % name)
456  if not isinstance(name, str) or len(name) == 0 or name.startswith('-'):
457  raise ValueError('argument must not start with "-": %s' % name)
458  if short is not None:
459  if any(options[x]['short'] == short for x in options):
460  raise ValueError('duplicate short option %s defined' % short)
461  if len(short) != 1:
462  raise ValueError('length of short option %s for %s must be 1' % (short, name))
463  if short == '-':
464  raise ValueError('short option for %s cannot be "-"' % name)
465  if argtype is None:
466  if default is not None:
467  argtype = type(default)
468  else:
469  argtype = str
470  if argtype is bool:
471  if default is None and noarg is None:
472  default, noarg = False, True
473  if optional:
474  if argtype is bool:
475  if not isinstance(noarg, bool):
476  raise ValueError('noarg value for %s must be of type bool if argtype is bool' % name)
477  else:
478  try:
479  # Testing suggests that this works for floats, but can rounding errors cause a false positive here?
480  if decode_value(encode_value(noarg), argtype) != noarg:
481  raise ValueError('noarg value %s for %s changes when saving to config file' % (str(noarg), name))
482  except:
483  raise ValueError('noarg value %s for %s cannot be restored from config file' % (str(noarg), name))
484  options[name] = {'help': help, 'short': short, 'multiple': multiple, 'optional': optional, 'default': default, 'noarg': noarg, 'argtype': argtype, 'module': module}
485  option_order.append(name)
486  return options[name]
487 # }}}
488 
489 def parse_args(argv = None, options = None, extra = False):
490  if argv is None:
491  argv = sys.argv
492  if options is None:
493  options = _options
494  shorts = {options[name]['short']: name for name in options}
495  values = {name: [] if options[name]['multiple'] else options[name]['default'] for name in options}
496  present = {name: False for name in options}
497  pos = 1
498  while pos < len(argv):
499  current = argv[pos]
500  nextarg = argv[pos + 1] if pos + 1 < len(argv) else None
501  if current == '--':
502  argv.pop(pos)
503  break
504  if len(current) < 2 or not current.startswith('-'):
505  pos += 1
506  continue
507  if current.startswith('--'):
508  # This is a long option.
509  if '=' in current:
510  optname, arg = current.split('=', 1)
511  else:
512  optname, arg = current, None
513  optname = optname[2:]
514  if optname not in options:
515  print('Warning: ignoring unrecognized option %s' % optname)
516  argv.pop(pos)
517  continue
518  opt = options[optname]
519  argtype = opt['argtype']
520  if argtype is bool:
521  # This option takes no argument.
522  value = opt['noarg']
523  elif opt['optional']:
524  # This option takes an optional argument.
525  if arg is not None:
526  value = opt['argtype'](arg)
527  else:
528  value = opt['noarg']
529  else:
530  # This option requires an argument.
531  if arg is not None:
532  value = opt['argtype'](arg)
533  else:
534  argv.pop(pos)
535  if pos >= len(argv):
536  print('Warning: option %s requires an argument' % optname, file = sys.stderr)
537  continue
538  value = opt['argtype'](argv[pos])
539  if opt['multiple']:
540  values[optname].append(value)
541  else:
542  if present[optname]:
543  print('Warning: option %s must only be passed once' % optname, file = sys.stderr)
544  values[optname] = value
545  present[optname] = True
546  else:
547  # This is a short options argument.
548  optpos = 1
549  while optpos < len(current):
550  o = current[optpos]
551  optpos += 1
552  if o not in shorts:
553  print('Warning: short option %s is not recognized' % o, file = sys.stderr)
554  continue
555  optname = shorts[o]
556  opt = options[optname]
557  argtype = opt['argtype']
558  if argtype is bool:
559  # This option takes no argument.
560  value = opt['noarg']
561  elif opt['optional']:
562  # This option takes an optional argument.
563  if optpos < len(current):
564  value = opt['argtype'](current[optpos:])
565  else:
566  value = opt['noarg']
567  optpos = len(current)
568  else:
569  # This option requires an argument.
570  if optpos < len(current):
571  value = opt['argtype'](current[optpos:])
572  else:
573  argv.pop(pos)
574  if pos >= len(argv):
575  print('Warning: option %s (%s) requires an argument' % (o, optname), file = sys.stderr)
576  continue
577  value = opt['argtype'](argv[pos])
578  optpos = len(current)
579  if opt['multiple']:
580  values[optname].append(value)
581  else:
582  if present[optname]:
583  print('Warning: option %s (%s) must only be passed once' % (o, optname), file = sys.stderr)
584  values[optname] = value
585  present[optname] = True
586  argv.pop(pos)
587  if extra:
588  return values, present
589  else:
590  return values
591 # }}}
592 
593 
615 def init(config = None, help = None, version = None, contact = None, packagename = None, system = None, game = False):
616  global initialized
617  assert not initialized
618  global pname
619  if packagename is not None:
620  pname = packagename
621  global is_system
622  global is_game
623  is_game = game
624  if config is not None:
625  print('Warning: using the config parameter for fhs.init() is DEPRECATED! Use option() instead.', file = sys.stderr)
626  for key in config:
627  option(key, 'no help for this option', default = config[key])
628  global XDG_RUNTIME_DIR
629  global _values, _present
630  global _info
631 
634  _info = {'help': help, 'version': version, 'contact': contact}
635  # If these default options are passed by the user, this will raise an exception.
636  first_options = {}
637  option_order = []
638  option('help', 'Show this help text', short = None if any(_options[o]['short'] == 'h' for o in _options) else 'h', argtype = bool, options = first_options, option_order = option_order)
639  option('version', 'Show version information', short = None if any(_options[o]['short'] == 'v' for o in _options) else 'v', argtype = bool, options = first_options, option_order = option_order)
640  option('configfile', 'Use this file for loading and/or saving commandline configuration', default = 'commandline', options = first_options, option_order = option_order)
641  option('saveconfig', 'Save active commandline configuration as default or to the named file', optional = True, default = None, noarg = '', argtype = str, options = first_options, option_order = option_order)
642  if system is None:
643  option('system', 'Use only system paths', argtype = bool, options = first_options, option_order = option_order)
644  else:
645  is_system = system
646  options = first_options.copy()
647  options.update(_options)
648  option_order += _option_order
649  try:
650  _values, _present = parse_args(sys.argv, options, extra = True)
651  except ValueError as err:
652  # Error parsing options.
653  print('Error parsing arguments: %s' % str(err))
654  help_text(help, options, option_order)
655  sys.exit(1)
656  if _values['help']:
657  help_text(help, options, option_order)
658  sys.exit(1)
659  _values.pop('help')
660  if _values['version']:
661  version_text()
662  sys.exit(1)
663  _values.pop('version')
664  configfile = _values.pop('configfile')
665  saveconfig = _values.pop('saveconfig')
666  if system is None:
667  is_system = _values['system']
668 
669  initialized = True
670  if saveconfig == '':
671  saveconfig = configfile
672  load_config(configfile, _values, _present, options)
673  if saveconfig is not None:
674  save_config({key: _values[key] for key in _values if _present[key]}, saveconfig, packagename)
675  # Split out the module options into their own object.
676  for module in _module_config:
677  _module_values[module] = {key: _values.pop(module + '-' + key) for key in _module_config[module]}
678  _module_present[module] = {key: _present.pop(module + '-' + key) for key in _module_config[module]}
679  # system may have been updated. Record the new value. Do this after
680  # save_config, because it should save in the location where read_config
681  # searches for it.
682  if system is None:
683  is_system = _values.pop('system')
684  @atexit.register
685  def clean_temps():
686  for f in _tempfiles:
687  try:
688  os.unlink(f)
689  except:
690  shutil.rmtree(f, ignore_errors = True)
691  if XDG_RUNTIME_DIR is None:
692  XDG_RUNTIME_DIR = write_temp(dir = True)
693  return _values
694 # }}}
695 
696 
707 def get_config(extra = False):
708  if not initialized:
709  print('Warning: init() should be called before get_config() to set program information', file = sys.stderr)
710  init()
711  if extra:
712  return _values, _present;
713  else:
714  return _values;
715 # }}}
716 # }}}
717 
718 # Module commandline argument handling. {{{
719 
726 def module_info(modulename, desc, version, contact):
727  assert not initialized
728  if modulename in _module_info:
729  print('Warning: duplicate registration of information for module %s' % modulename, file = sys.stderr)
730  return
731  _module_info[modulename] = {'desc': desc, 'version': version, 'contact': contact}
732  _module_config[modulename] = set()
733 # }}}
734 
735 
740 def module_option(modulename, name, help, short = None, multiple = False, optional = False, default = None, noarg = None, argtype = None, options = None, option_order = None):
741  assert not initialized
742  assert modulename in _module_info
743  _module_config[modulename].add(name)
744  return option(modulename + '-' + name, help, short, multiple, optional, default, noarg, argtype, modulename, options, option_order)
745 # }}}
746 
747 
757 def module_init(modulename, config):
758  print('Warning: module %s uses module_init() which is DEPRECATED! It should use module_option() instead.' % modulename, file = sys.stderr)
759  assert not initialized
760  module_info(modulename, 'no information about this module available', 'unknown', None)
761  for key in config:
762  module_option(modulename, key, 'no help for this module option', default = config[key])
763 # }}}
764 
765 
776 def module_get_config(modulename, extra = False):
777  if not initialized:
778  init()
779  if extra:
780  return _module_values[modulename], _module_present[modulename];
781  else:
782  return _module_values[modulename];
783 # }}}
784 # }}}
785 # }}}
786 
787 # Runtime files. {{{
788 
789 XDG_RUNTIME_DIR = os.getenv('XDG_RUNTIME_DIR')
790 def _runtime_get(name, packagename, dir):
791  assert initialized
792  if name is None:
793  if dir:
794  name = packagename or pname
795  else:
796  name = (packagename or pname) + os.extsep + 'txt'
797  else:
798  name = os.path.join(packagename or pname, name)
799  d = '/run' if is_system else XDG_RUNTIME_DIR
800  target = os.path.join(d, name)
801  d = target if dir else os.path.dirname(target)
802  return d, target
803 
804 
812 def write_runtime(name = None, text = True, dir = False, opened = True, packagename = None):
813  d, target = _runtime_get(name, packagename, dir)
814  if opened and not os.path.exists(d):
815  os.makedirs(d)
816  return open(target, 'w+' if text else 'w+b') if opened and not dir else target
817 
818 
826 def read_runtime(name = None, text = True, dir = False, opened = True, packagename = None):
827  d, target = _runtime_get(name, packagename, dir)
828  if os.path.exists(target) and (dir if os.path.isdir(target) else not dir):
829  return open(target, 'r' if text else 'rb') if opened and not dir else target
830  return None
831 
832 
839 def remove_runtime(name = None, dir = False, packagename = None):
840  assert initialized
841  if dir:
842  shutil.rmtree(read_runtime(name, False, True, False, packagename), ignore_errors = False)
843  else:
844  os.unlink(read_runtime(name, False, False, False, packagename))
845 # }}}
846 
847 # Temp files. {{{
848 class _TempFile:
849  def __init__(self, f, name):
850  # Avoid calling file.__setattr__.
851  super().__setattr__('_file', f)
852  super().__setattr__('filename', name)
853  def remove(self):
854  assert initialized
855  assert self.filename is not None
856  self.close()
857  os.unlink(self.filename)
858  _tempfiles.remove(self.filename)
859  super().__setattr__('_file', None)
860  super().__setattr__('filename', None)
861  def __getattr__(self, k):
862  return getattr(self._file, k)
863  def __setattr__(self, k, v):
864  return setattr(self._file, k, v)
865  def __enter__(self):
866  return self
867  def __exit__(self, exc_type, exc_value, traceback):
868  self.remove()
869  return False
870 
871 
892 def write_temp(dir = False, text = True, packagename = None):
893  assert initialized
894  if dir:
895  ret = tempfile.mkdtemp(prefix = (packagename or pname) + '-')
896  _tempfiles.append(ret)
897  else:
898  f = tempfile.mkstemp(text = text, prefix = (packagename or pname) + '-')
899  _tempfiles.append(f[1])
900  ret = _TempFile(os.fdopen(f[0], 'w+' if text else 'w+b'), f[1])
901  return ret
902 
903 
910 def remove_temp(name):
911  assert initialized
912  assert name in _tempfiles
913  _tempfiles.remove(name)
914  shutil.rmtree(name, ignore_errors = False)
915 # }}}
916 
917 # Data files. {{{
918 
919 XDG_DATA_HOME = os.getenv('XDG_DATA_HOME', os.path.join(HOME, '.local', 'share'))
920 
921 XDG_DATA_DIRS = os.getenv('XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(':')
922 
923 
931 def write_data(name = None, text = True, dir = False, opened = True, packagename = None):
932  assert initialized
933  if name is None:
934  if dir:
935  filename = packagename or pname
936  else:
937  filename = (packagename or pname) + os.extsep + 'dat'
938  else:
939  filename = name if is_system else os.path.join(packagename or pname, name)
940  if is_system:
941  if is_game:
942  if packagename and packagename != pname:
943  d = os.path.join('/var/games', pname, packagename)
944  else:
945  d = os.path.join('/var/games', pname)
946  else:
947  if packagename and packagename != pname:
948  d = os.path.join('/var/lib', pname, packagename)
949  else:
950  d = os.path.join('/var/lib', pname)
951  else:
952  d = XDG_DATA_HOME
953  target = os.path.join(d, filename)
954  if dir:
955  if opened and not os.path.exists(target):
956  os.makedirs(target)
957  return target
958  else:
959  d = os.path.dirname(target)
960  if opened and not os.path.exists(d):
961  os.makedirs(d)
962  return open(target, 'w+' if text else 'w+b') if opened else target
963 
964 
972 def read_data(name = None, text = True, dir = False, multiple = False, opened = True, packagename = None):
973  assert initialized
974  if name is None:
975  if dir:
976  filename = packagename or pname
977  else:
978  filename = (packagename or pname) + os.extsep + 'dat'
979  else:
980  filename = name
981  seen = set()
982  target = []
983  if not is_system:
984  t = os.path.join(XDG_DATA_HOME, filename if name is None else os.path.join(packagename or pname, name))
985  if os.path.realpath(t) not in seen and os.path.exists(t) and (dir if os.path.isdir(t) else not dir):
986  r = t if dir or not opened else open(t, 'r' if text else 'rb')
987  if not multiple:
988  return r
989  seen.add(os.path.realpath(t))
990  target.append(r)
991  dirs = ['/var/local/lib', '/var/lib', '/usr/local/lib', '/usr/lib', '/usr/local/share', '/usr/share']
992  if is_game:
993  dirs = ['/var/local/games', '/var/games', '/usr/local/lib/games', '/usr/lib/games', '/usr/local/share/games', '/usr/share/games'] + dirs
994  if not is_system:
995  for d in XDG_DATA_DIRS:
996  dirs.insert(0, d)
997  if packagename and packagename != pname:
998  dirs = [os.path.join(x, pname, packagename) for x in dirs] + [os.path.join(x, packagename) for x in dirs]
999  else:
1000  dirs = [os.path.join(x, pname) for x in dirs]
1001  if not is_system:
1002  dirs.insert(0, packagename or pname)
1003  dirs.insert(0, os.path.curdir)
1004  dirs.insert(0, _base)
1005  for d in dirs:
1006  t = os.path.join(d, filename)
1007  if os.path.realpath(t) not in seen and os.path.exists(t) and (dir if os.path.isdir(t) else not dir):
1008  r = t if dir or not opened else open(t, 'r' if text else 'rb')
1009  if not multiple:
1010  return r
1011  seen.add(os.path.realpath(t))
1012  target.append(r)
1013  if multiple:
1014  return target
1015  else:
1016  return None
1017 
1018 
1024 def remove_data(name = None, dir = False, packagename = None):
1025  assert initialized
1026  if dir:
1027  shutil.rmtree(read_data(name, False, True, False, False, packagename), ignore_errors = False)
1028  else:
1029  os.unlink(read_data(name, False, False, False, False, packagename))
1030 # }}}
1031 
1032 # Cache files. {{{
1033 
1034 XDG_CACHE_HOME = os.getenv('XDG_CACHE_HOME', os.path.join(HOME, '.cache'))
1035 
1036 
1044 def write_cache(name = None, text = True, dir = False, opened = True, packagename = None):
1045  assert initialized
1046  if name is None:
1047  if dir:
1048  filename = packagename or pname
1049  else:
1050  filename = (packagename or pname) + os.extsep + 'dat'
1051  else:
1052  filename = name if is_system else os.path.join(packagename or pname, name)
1053  d = os.path.join('/var/cache', packagename or pname) if is_system else XDG_CACHE_HOME
1054  target = os.path.join(d, filename)
1055  if dir:
1056  if opened and not os.path.exists(target):
1057  os.makedirs(target)
1058  return target
1059  else:
1060  d = os.path.dirname(target)
1061  if opened and not os.path.exists(d):
1062  os.makedirs(d)
1063  return open(target, 'w+' if text else 'w+b') if opened and not dir else target
1064 
1065 
1073 def read_cache(name = None, text = True, dir = False, opened = True, packagename = None):
1074  assert initialized
1075  if name is None:
1076  if dir:
1077  filename = packagename or pname
1078  else:
1079  filename = (packagename or pname) + os.extsep + 'dat'
1080  else:
1081  filename = os.path.join(packagename or pname, name)
1082  target = os.path.join(XDG_CACHE_HOME, filename)
1083  if not os.path.exists(target):
1084  if name is None:
1085  filename = os.path.join(packagename or pname, packagename or pname + os.extsep + 'dat')
1086  d = '/var/cache'
1087  target = os.path.join(d, filename)
1088  if not os.path.exists(target):
1089  return None
1090  return open(target, 'r' if text else 'rb') if opened and not dir else target
1091 
1092 
1098 def remove_cache(name = None, dir = False, packagename = None):
1099  assert initialized
1100  if dir:
1101  shutil.rmtree(read_cache(name, False, True, False, packagename), ignore_errors = False)
1102  else:
1103  os.unlink(read_cache(name, False, False, False, packagename))
1104 # }}}
1105 
1106 # Log files. {{{
1107 
1115 def write_log(name = None, packagename = None):
1116  assert initialized
1117  if not is_system:
1118  return sys.stderr
1119  if name is None:
1120  filename = (packagename or pname) + os.extsep + 'log'
1121  else:
1122  filename = os.path.join(packagename or pname, name)
1123  target = os.path.join('/var/log', filename)
1124  d = os.path.dirname(target)
1125  if not os.path.exists(d):
1126  os.makedirs(d)
1127  return open(target, 'a')
1128 # }}}
1129 
1130 # Spool files. {{{
1131 
1141 def write_spool(name = None, text = True, dir = False, opened = True, packagename = None):
1142  assert initialized
1143  if name is None:
1144  if dir:
1145  filename = packagename or pname
1146  else:
1147  filename = (packagename or pname) + os.extsep + 'dat'
1148  else:
1149  filename = os.path.join(packagename or pname, name)
1150  target = os.path.join('/var/spool' if is_system else os.path.join(XDG_CACHE_HOME, 'spool'), filename)
1151  d = os.path.dirname(target)
1152  if opened and not os.path.exists(d):
1153  os.makedirs(d)
1154  return open(target, 'w+' if text else 'w+b') if opened and not dir else target
1155 
1156 
1164 def read_spool(name = None, text = True, dir = False, opened = True, packagename = None):
1165  assert initialized
1166  if name is None:
1167  if dir:
1168  filename = packagename or pname
1169  else:
1170  filename = (packagename or pname) + os.extsep + 'dat'
1171  else:
1172  filename = os.path.join(packagename or pname, name)
1173  target = os.path.join('/var/spool' if is_system else os.path.join(XDG_CACHE_HOME, 'spool'), filename)
1174  if not os.path.exists(target):
1175  return None
1176  return open(target, 'r' if text else 'rb') if opened and not dir else target
1177 
1178 
1184 def remove_spool(name = None, dir = False, packagename = None):
1185  assert initialized
1186  if name is None:
1187  if dir:
1188  filename = packagename or pname
1189  else:
1190  filename = (packagename or pname) + os.extsep + 'dat'
1191  if dir:
1192  shutil.rmtree(read_spool(name, False, True, False, packagename), ignore_errors = False)
1193  else:
1194  os.unlink(read_spool(name, False, False, False, packagename))
1195 # }}}
1196 
1197 # Locks. {{{
1198 
1201 def lock(name = None, info = '', packagename = None):
1202  assert initialized
1203  # TODO
1204 
1205 
1208 def unlock(name = None, packagename = None):
1209  assert initialized
1210  # TODO
1211 # }}}
def write_data(name=None, text=True, dir=False, opened=True, packagename=None)
Open a data file for writing.
Definition: fhs.py:931
def read_cache(name=None, text=True, dir=False, opened=True, packagename=None)
Open a cache file for reading.
Definition: fhs.py:1073
def read_config(name=None, text=True, dir=False, multiple=False, opened=True, packagename=None)
Open a config file for reading.
Definition: fhs.py:196
def read_spool(name=None, text=True, dir=False, opened=True, packagename=None)
Open a spool file for reading.
Definition: fhs.py:1164
def module_init(modulename, config)
Add configuration for a module.
Definition: fhs.py:757
def parse_args(argv=None, options=None, extra=False)
Definition: fhs.py:489
def write_temp(dir=False, text=True, packagename=None)
Open a temporary file for writing.
Definition: fhs.py:892
def save_config(config, name=None, packagename=None)
Save a dict as a configuration file.
Definition: fhs.py:412
def read_runtime(name=None, text=True, dir=False, opened=True, packagename=None)
Open a runtime file for reading.
Definition: fhs.py:826
def write_config(name=None, text=True, dir=False, opened=True, packagename=None)
Open a config file for writing.
Definition: fhs.py:160
def write_cache(name=None, text=True, dir=False, opened=True, packagename=None)
Open a cache file for writing.
Definition: fhs.py:1044
def remove_cache(name=None, dir=False, packagename=None)
Remove a cache file.
Definition: fhs.py:1098
def unlock(name=None, packagename=None)
Release a lock.
Definition: fhs.py:1208
def remove_data(name=None, dir=False, packagename=None)
Remove a data file.
Definition: fhs.py:1024
def read_data(name=None, text=True, dir=False, multiple=False, opened=True, packagename=None)
Open a data file for reading.
Definition: fhs.py:972
def write_runtime(name=None, text=True, dir=False, opened=True, packagename=None)
Open a runtime file for writing.
Definition: fhs.py:812
def init(config=None, help=None, version=None, contact=None, packagename=None, system=None, game=False)
Initialize the module.
Definition: fhs.py:615
def version_text()
Definition: fhs.py:351
def load_config(filename, values=None, present=None, options=None)
Definition: fhs.py:367
def remove_temp(name)
Remove a temporary directory.
Definition: fhs.py:910
def decode_value(value, argtype)
Parse a string value into its proper type.
Definition: fhs.py:288
def module_option(modulename, name, help, short=None, multiple=False, optional=False, default=None, noarg=None, argtype=None, options=None, option_order=None)
Register a commandline option for a module.
Definition: fhs.py:740
def write_log(name=None, packagename=None)
Open a log file for writing.
Definition: fhs.py:1115
def lock(name=None, info='', packagename=None)
Acquire a lock.
Definition: fhs.py:1201
def module_info(modulename, desc, version, contact)
Register information about a module.
Definition: fhs.py:726
def help_text(main, options, option_order)
Definition: fhs.py:313
def module_get_config(modulename, extra=False)
Retrieve module configuration.
Definition: fhs.py:776
def get_config(extra=False)
Retrieve commandline configuration.
Definition: fhs.py:707
def write_spool(name=None, text=True, dir=False, opened=True, packagename=None)
Open a spool file for writing.
Definition: fhs.py:1141
def remove_runtime(name=None, dir=False, packagename=None)
Remove a reuntime file or directory.
Definition: fhs.py:839
def remove_config(name=None, dir=False, packagename=None)
Remove a config file.
Definition: fhs.py:247
def remove_spool(name=None, dir=False, packagename=None)
Remove a spool file.
Definition: fhs.py:1184
def encode_value(value)
Encode a value into a string which can be stored in a config file.
Definition: fhs.py:305
def option(name, help, short=None, multiple=False, optional=False, default=None, noarg=None, argtype=None, module=None, options=None, option_order=None)
Register commandline argument.
Definition: fhs.py:448