1"""
2Core functions to be used in Python scripts.
3
4Usage:
5
6::
7
8    from grass.script import core as grass
9    grass.parser()
10
11(C) 2008-2020 by the GRASS Development Team
12This program is free software under the GNU General Public
13License (>=v2). Read the file COPYING that comes with GRASS
14for details.
15
16.. sectionauthor:: Glynn Clements
17.. sectionauthor:: Martin Landa <landa.martin gmail.com>
18.. sectionauthor:: Michael Barton <michael.barton asu.edu>
19"""
20from __future__ import absolute_import, print_function
21
22import os
23import sys
24import atexit
25import subprocess
26import shutil
27import codecs
28import string
29import random
30import pipes
31import types as python_types
32
33from .utils import KeyValue, parse_key_val, basename, encode, decode
34from grass.exceptions import ScriptError, CalledModuleError
35
36# PY2/PY3 compat
37if sys.version_info.major > 2:
38    unicode = str
39
40# subprocess wrapper that uses shell on Windows
41class Popen(subprocess.Popen):
42    _builtin_exts = set(['.com', '.exe', '.bat', '.cmd'])
43
44    @staticmethod
45    def _escape_for_shell(arg):
46        # TODO: what are cmd.exe's parsing rules?
47        return arg
48
49    def __init__(self, args, **kwargs):
50        if (sys.platform == 'win32'
51            and isinstance(args, list)
52            and not kwargs.get('shell', False)
53            and kwargs.get('executable') is None):
54            cmd = shutil_which(args[0])
55            if cmd is None:
56                raise OSError(_("Cannot find the executable {0}")
57                              .format(args[0]))
58            args = [cmd] + args[1:]
59            name, ext = os.path.splitext(cmd)
60            if ext.lower() not in self._builtin_exts:
61                kwargs['shell'] = True
62                args = [self._escape_for_shell(arg) for arg in args]
63        subprocess.Popen.__init__(self, args, **kwargs)
64
65PIPE = subprocess.PIPE
66STDOUT = subprocess.STDOUT
67
68
69raise_on_error = False  # raise exception instead of calling fatal()
70_capture_stderr = False  # capture stderr of subprocesses if possible
71
72
73def call(*args, **kwargs):
74    return Popen(*args, **kwargs).wait()
75
76# GRASS-oriented interface to subprocess module
77
78_popen_args = ["bufsize", "executable", "stdin", "stdout", "stderr",
79               "preexec_fn", "close_fds", "cwd", "env",
80               "universal_newlines", "startupinfo", "creationflags"]
81
82
83def _make_val(val):
84    """Convert value to unicode"""
85    if isinstance(val, (bytes, str, unicode)):
86        return decode(val)
87    if isinstance(val, (int, float)):
88        return unicode(val)
89    try:
90        return ",".join(map(_make_val, iter(val)))
91    except TypeError:
92        pass
93    return unicode(val)
94
95
96def _make_unicode(val, enc):
97    """Convert value to unicode with given encoding
98
99    :param val: value to be converted
100    :param enc: encoding to be used
101    """
102    if val is None or enc is None:
103        return val
104    else:
105        if enc == 'default':
106            return decode(val)
107        else:
108            return decode(val, encoding=enc)
109
110
111def get_commands():
112    """Create list of available GRASS commands to use when parsing
113    string from the command line
114
115    :return: list of commands (set) and directory of scripts (collected
116             by extension - MS Windows only)
117
118    >>> cmds = list(get_commands()[0])
119    >>> cmds.sort()
120    >>> cmds[:5]
121    ['d.barscale', 'd.colorlist', 'd.colortable', 'd.correlate', 'd.erase']
122
123    """
124    gisbase = os.environ['GISBASE']
125    cmd = list()
126    scripts = {'.py': list()} if sys.platform == 'win32' else {}
127
128    def scan(gisbase, directory):
129        dir_path = os.path.join(gisbase, directory)
130        if os.path.exists(dir_path):
131            for fname in os.listdir(os.path.join(gisbase, directory)):
132                if scripts:  # win32
133                    name, ext = os.path.splitext(fname)
134                    if ext != '.manifest':
135                        cmd.append(name)
136                    if ext in scripts.keys():
137                        scripts[ext].append(name)
138                else:
139                    cmd.append(fname)
140
141    for directory in ('bin', 'scripts'):
142        scan(gisbase, directory)
143
144    # scan gui/scripts/
145    gui_path = os.path.join(gisbase, 'etc', 'gui', 'scripts')
146    if os.path.exists(gui_path):
147        os.environ["PATH"] = os.getenv("PATH") + os.pathsep + gui_path
148        cmd = cmd + os.listdir(gui_path)
149
150    return set(cmd), scripts
151
152# TODO: Please replace this function with shutil.which() before 8.0 comes out
153# replacement for which function from shutil (not available in all versions)
154# from http://hg.python.org/cpython/file/6860263c05b3/Lib/shutil.py#l1068
155# added because of Python scripts running Python scripts on MS Windows
156# see also ticket #2008 which is unrelated but same function was proposed
157def shutil_which(cmd, mode=os.F_OK | os.X_OK, path=None):
158    """Given a command, mode, and a PATH string, return the path which
159    conforms to the given mode on the PATH, or None if there is no such
160    file.
161
162    `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
163    of os.environ.get("PATH"), or can be overridden with a custom search
164    path.
165
166    :param cmd: the command
167    :param mode:
168    :param path:
169
170    """
171    # Check that a given file can be accessed with the correct mode.
172    # Additionally check that `file` is not a directory, as on Windows
173    # directories pass the os.access check.
174    def _access_check(fn, mode):
175        return (os.path.exists(fn) and os.access(fn, mode)
176                and not os.path.isdir(fn))
177
178    # If we're given a path with a directory part, look it up directly rather
179    # than referring to PATH directories. This includes checking relative to the
180    # current directory, e.g. ./script
181    if os.path.dirname(cmd):
182        if _access_check(cmd, mode):
183            return cmd
184        return None
185
186    if path is None:
187        path = os.environ.get("PATH", os.defpath)
188    if not path:
189        return None
190    path = path.split(os.pathsep)
191
192    if sys.platform == "win32":
193        # The current directory takes precedence on Windows.
194        if not os.curdir in path:
195            path.insert(0, os.curdir)
196
197        # PATHEXT is necessary to check on Windows (force lowercase)
198        pathext = list(map(lambda x: x.lower(),
199                           os.environ.get("PATHEXT", "").split(os.pathsep)))
200        if '.py' not in pathext:
201            # we assume that PATHEXT contains always '.py'
202            pathext.insert(0, '.py')
203        # See if the given file matches any of the expected path extensions.
204        # This will allow us to short circuit when given "python3.exe".
205        # If it does match, only test that one, otherwise we have to try
206        # others.
207        if any(cmd.lower().endswith(ext) for ext in pathext):
208            files = [cmd]
209        else:
210            files = [cmd + ext for ext in pathext]
211    else:
212        # On other platforms you don't have things like PATHEXT to tell you
213        # what file suffixes are executable, so just pass on cmd as-is.
214        files = [cmd]
215
216    seen = set()
217    for dir in path:
218        normdir = os.path.normcase(dir)
219        if not normdir in seen:
220            seen.add(normdir)
221            for thefile in files:
222                name = os.path.join(dir, thefile)
223                if _access_check(name, mode):
224                    return name
225    return None
226
227if sys.version_info.major > 2:
228    shutil_which = shutil.which
229
230# Added because of scripts calling scripts on MS Windows.
231# Module name (here cmd) differs from the file name (does not have extension).
232# Additionally, we don't run scripts using system executable mechanism,
233# so we need the full path name.
234# However, scripts are on the PATH and '.PY' in in PATHEXT, so we can use
235# shutil.which to get the full file path. Addons are on PATH too.
236# An alternative to which function call would be to check the script path and
237# addons path. This is proposed improvement for the future.
238# Another alternative is to check some global list of scripts but this list
239# needs to be created first. The question is what is less expensive.
240# Note that getting the full path is only part of the solution,
241# the other part is to use the right Python as an executable and pass the full
242# script path as a parameter.
243# Nevertheless, it is unclear on which places which extensions are added.
244# This function also could skip the check for platform but depends
245# how will be used, this is most general but not most effective.
246def get_real_command(cmd):
247    """Returns the real file command for a module (cmd)
248
249    For Python scripts on MS Windows it returns full path to the script
250    and adds a '.py' extension.
251    For other cases it just returns a module (name).
252    So, you can just use this function for all without further check.
253
254    >>> get_real_command('g.region')
255    'g.region'
256
257    :param cmd: the command
258    """
259    if sys.platform == 'win32':
260        # we in fact expect pure module name (without extension)
261        # so, lets remove extension
262        if os.path.splitext(cmd)[1] == '.py':
263            cmd = cmd[:-3]
264        # PATHEXT is necessary to check on Windows (force lowercase)
265        pathext = list(map(lambda x: x.lower(),
266                           os.environ['PATHEXT'].split(os.pathsep)))
267        if '.py' not in pathext:
268            # we assume that PATHEXT contains always '.py'
269            os.environ['PATHEXT'] = '.py;' + os.environ['PATHEXT']
270        full_path = shutil_which(cmd + '.py')
271        if full_path:
272            return full_path
273
274    return cmd
275
276
277def make_command(prog, flags="", overwrite=False, quiet=False, verbose=False,
278                 superquiet=False, errors=None, **options):
279    """Return a list of strings suitable for use as the args parameter to
280    Popen() or call(). Example:
281
282
283    >>> make_command("g.message", flags = 'w', message = 'this is a warning')
284    ['g.message', '-w', 'message=this is a warning']
285
286
287    :param str prog: GRASS module
288    :param str flags: flags to be used (given as a string)
289    :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
290    :param bool quiet: True to run quietly (<tt>--q</tt>)
291    :param bool verbose: True to run verbosely (<tt>--v</tt>)
292    :param options: module's parameters
293
294    :return: list of arguments
295    """
296    args = [_make_val(prog)]
297    if overwrite:
298        args.append("--o")
299    if quiet:
300        args.append("--q")
301    if verbose:
302        args.append("--v")
303    if superquiet:
304        args.append("--qq")
305    if flags:
306        flags = _make_val(flags)
307        if '-' in flags:
308            raise ScriptError("'-' is not a valid flag")
309        args.append("-" + flags)
310    for opt, val in options.items():
311        if opt in _popen_args:
312            continue
313        # convert string to bytes
314        if val is not None:
315            if opt.startswith('_'):
316                opt = opt[1:]
317                warning(_("To run the module <%s> add underscore at the end"
318                    " of the option <%s> to avoid conflict with Python"
319                    " keywords. Underscore at the beginning is"
320                    " depreciated in GRASS GIS 7.0 and will be removed"
321                    " in version 7.1.") % (prog, opt))
322            elif opt.endswith('_'):
323                opt = opt[:-1]
324            args.append(opt + '=' + _make_val(val))
325    return args
326
327
328def handle_errors(returncode, result, args, kwargs):
329    if returncode == 0:
330        return result
331    handler = kwargs.get('errors', 'raise')
332    if handler.lower() == 'ignore':
333        return result
334    elif handler.lower() == 'status':
335        return returncode
336    elif handler.lower() == 'exit':
337        sys.exit(1)
338    else:
339        # TODO: construction of the whole command is far from perfect
340        args = make_command(*args, **kwargs)
341        code = ' '.join(args)
342        raise CalledModuleError(module=None, code=code,
343                                returncode=returncode)
344
345def start_command(prog, flags="", overwrite=False, quiet=False,
346                  verbose=False, superquiet=False, **kwargs):
347    """Returns a Popen object with the command created by make_command.
348    Accepts any of the arguments which Popen() accepts apart from "args"
349    and "shell".
350
351    >>> p = start_command("g.gisenv", stdout=subprocess.PIPE)
352    >>> print(p)  # doctest: +ELLIPSIS
353    <...Popen object at 0x...>
354    >>> print(p.communicate()[0])  # doctest: +SKIP
355    GISDBASE='/opt/grass-data';
356    LOCATION_NAME='spearfish60';
357    MAPSET='glynn';
358    GUI='text';
359    MONITOR='x0';
360
361    If the module parameter is the same as Python keyword, add
362    underscore at the end of the parameter. For example, use
363    ``lambda_=1.6`` instead of ``lambda=1.6``.
364
365    :param str prog: GRASS module
366    :param str flags: flags to be used (given as a string)
367    :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
368    :param bool quiet: True to run quietly (<tt>--q</tt>)
369    :param bool verbose: True to run verbosely (<tt>--v</tt>)
370    :param kwargs: module's parameters
371
372    :return: Popen object
373    """
374    if 'encoding' in kwargs.keys():
375        encoding = kwargs.pop('encoding')
376
377    options = {}
378    popts = {}
379    for opt, val in kwargs.items():
380        if opt in _popen_args:
381            popts[opt] = val
382        else:
383            options[opt] = val
384
385    args = make_command(prog, flags, overwrite, quiet, verbose, **options)
386
387    if debug_level() > 0:
388        sys.stderr.write("D1/{}: {}.start_command(): {}\n".format(
389            debug_level(), __name__,
390            ' '.join(args))
391        )
392        sys.stderr.flush()
393    return Popen(args, **popts)
394
395
396def run_command(*args, **kwargs):
397    """Execute a module synchronously
398
399    This function passes all arguments to ``start_command()``,
400    then waits for the process to complete. It is similar to
401    ``subprocess.check_call()``, but with the ``make_command()``
402    interface.
403
404    For backward compatibility, the function returns exit code
405    by default but only if it is equal to zero. An exception is raised
406    in case of an non-zero return code.
407
408    >>> run_command('g.region', raster='elevation')
409    0
410
411    See :func:`start_command()` for details about parameters and usage.
412
413    ..note::
414        You should ignore the return value of this function unless, you
415        change the default behavior using *errors* parameter.
416
417    :param *args: unnamed arguments passed to ``start_command()``
418    :param **kwargs: named arguments passed to ``start_command()``
419
420    :returns: 0 with default parameters for backward compatibility only
421
422    :raises: ``CalledModuleError`` when module returns non-zero return code
423    """
424    encoding = 'default'
425    if 'encoding' in kwargs:
426        encoding = kwargs['encoding']
427
428    if _capture_stderr and 'stderr' not in kwargs.keys():
429        kwargs['stderr'] = PIPE
430    ps = start_command(*args, **kwargs)
431    if _capture_stderr:
432        stdout, stderr = ps.communicate()
433        if encoding is not None:
434            stdout = _make_unicode(stdout, encoding)
435            stderr = _make_unicode(stderr, encoding)
436        returncode = ps.poll()
437        if returncode:
438            sys.stderr.write(stderr)
439    else:
440        returncode = ps.wait()
441    return handle_errors(returncode, returncode, args, kwargs)
442
443
444def pipe_command(*args, **kwargs):
445    """Passes all arguments to start_command(), but also adds
446    "stdout = PIPE". Returns the Popen object.
447
448    >>> p = pipe_command("g.gisenv")
449    >>> print(p)  # doctest: +ELLIPSIS
450    <....Popen object at 0x...>
451    >>> print(p.communicate()[0])  # doctest: +SKIP
452    GISDBASE='/opt/grass-data';
453    LOCATION_NAME='spearfish60';
454    MAPSET='glynn';
455    GUI='text';
456    MONITOR='x0';
457
458    :param list args: list of unnamed arguments (see start_command() for details)
459    :param list kwargs: list of named arguments (see start_command() for details)
460
461    :return: Popen object
462    """
463    kwargs['stdout'] = PIPE
464    return start_command(*args, **kwargs)
465
466
467def feed_command(*args, **kwargs):
468    """Passes all arguments to start_command(), but also adds
469    "stdin = PIPE". Returns the Popen object.
470
471    :param list args: list of unnamed arguments (see start_command() for details)
472    :param list kwargs: list of named arguments (see start_command() for details)
473
474    :return: Popen object
475    """
476    kwargs['stdin'] = PIPE
477    return start_command(*args, **kwargs)
478
479
480def read_command(*args, **kwargs):
481    """Passes all arguments to pipe_command, then waits for the process to
482    complete, returning its stdout (i.e. similar to shell `backticks`).
483
484    :param list args: list of unnamed arguments (see start_command() for details)
485    :param list kwargs: list of named arguments (see start_command() for details)
486
487    :return: stdout
488    """
489    encoding = 'default'
490    if 'encoding' in kwargs:
491        encoding = kwargs['encoding']
492
493    if _capture_stderr and 'stderr' not in kwargs.keys():
494        kwargs['stderr'] = PIPE
495    process = pipe_command(*args, **kwargs)
496    stdout, stderr = process.communicate()
497    if encoding is not None:
498        stdout = _make_unicode(stdout, encoding)
499        stderr = _make_unicode(stderr, encoding)
500    returncode = process.poll()
501    if _capture_stderr and returncode:
502        sys.stderr.write(stderr)
503    return handle_errors(returncode, stdout, args, kwargs)
504
505
506def parse_command(*args, **kwargs):
507    """Passes all arguments to read_command, then parses the output
508    by parse_key_val().
509
510    Parsing function can be optionally given by <em>parse</em> parameter
511    including its arguments, e.g.
512
513    ::
514
515        parse_command(..., parse = (grass.parse_key_val, { 'sep' : ':' }))
516
517    or you can simply define <em>delimiter</em>
518
519    ::
520
521        parse_command(..., delimiter = ':')
522
523    :param args: list of unnamed arguments (see start_command() for details)
524    :param kwargs: list of named arguments (see start_command() for details)
525
526    :return: parsed module output
527    """
528    parse = None
529    parse_args = {}
530    if 'parse' in kwargs:
531        if isinstance(kwargs['parse'], tuple):
532            parse = kwargs['parse'][0]
533            parse_args = kwargs['parse'][1]
534        del kwargs['parse']
535
536    if 'delimiter' in kwargs:
537        parse_args = {'sep': kwargs['delimiter']}
538        del kwargs['delimiter']
539
540    if not parse:
541        parse = parse_key_val  # use default fn
542
543    res = read_command(*args, **kwargs)
544
545    return parse(res, **parse_args)
546
547
548def write_command(*args, **kwargs):
549    """Execute a module with standard input given by *stdin* parameter.
550
551    Passes all arguments to ``feed_command()``, with the string specified
552    by the *stdin* argument fed to the process' standard input.
553
554    >>> gscript.write_command(
555    ...    'v.in.ascii', input='-',
556    ...    stdin='%s|%s' % (635818.8, 221342.4),
557    ...    output='view_point')
558    0
559
560    See ``start_command()`` for details about parameters and usage.
561
562    :param *args: unnamed arguments passed to ``start_command()``
563    :param **kwargs: named arguments passed to ``start_command()``
564
565    :returns: 0 with default parameters for backward compatibility only
566
567    :raises: ``CalledModuleError`` when module returns non-zero return code
568    """
569    encoding = 'default'
570    if 'encoding' in kwargs:
571        encoding = kwargs['encoding']
572    # TODO: should we delete it from kwargs?
573    stdin = kwargs['stdin']
574    if encoding is None or encoding == 'default':
575        stdin = encode(stdin)
576    else:
577        stdin = encode(stdin, encoding=encoding)
578    if _capture_stderr and 'stderr' not in kwargs.keys():
579        kwargs['stderr'] = PIPE
580    process = feed_command(*args, **kwargs)
581    unused, stderr = process.communicate(stdin)
582    if encoding is not None:
583        unused = _make_unicode(unused, encoding)
584        stderr = _make_unicode(stderr, encoding)
585    returncode = process.poll()
586    if _capture_stderr and returncode:
587        sys.stderr.write(stderr)
588    return handle_errors(returncode, returncode, args, kwargs)
589
590
591def exec_command(prog, flags="", overwrite=False, quiet=False, verbose=False,
592                 superquiet=False, env=None, **kwargs):
593    """Interface to os.execvpe(), but with the make_command() interface.
594
595    :param str prog: GRASS module
596    :param str flags: flags to be used (given as a string)
597    :param bool overwrite: True to enable overwriting the output (<tt>--o</tt>)
598    :param bool quiet: True to run quietly (<tt>--q</tt>)
599    :param bool verbose: True to run verbosely (<tt>--v</tt>)
600    :param env: directory with environmental variables
601    :param list kwargs: module's parameters
602
603    """
604    args = make_command(prog, flags, overwrite, quiet, verbose, **kwargs)
605
606    if env is None:
607        env = os.environ
608    os.execvpe(prog, args, env)
609
610# interface to g.message
611
612
613def message(msg, flag=None):
614    """Display a message using `g.message`
615
616    :param str msg: message to be displayed
617    :param str flag: flags (given as string)
618    """
619    run_command("g.message", flags=flag, message=msg, errors='ignore')
620
621
622def debug(msg, debug=1):
623    """Display a debugging message using `g.message -d`
624
625    :param str msg: debugging message to be displayed
626    :param str debug: debug level (0-5)
627    """
628    if debug_level() >= debug:
629        # TODO: quite a random hack here, do we need it somewhere else too?
630        if sys.platform == "win32":
631            msg = msg.replace('&', '^&')
632
633        run_command("g.message", flags='d', message=msg, debug=debug)
634
635def verbose(msg):
636    """Display a verbose message using `g.message -v`
637
638    :param str msg: verbose message to be displayed
639    """
640    message(msg, flag='v')
641
642
643def info(msg):
644    """Display an informational message using `g.message -i`
645
646    :param str msg: informational message to be displayed
647    """
648    message(msg, flag='i')
649
650
651def percent(i, n, s):
652    """Display a progress info message using `g.message -p`
653
654    ::
655
656        message(_("Percent complete..."))
657        n = 100
658        for i in range(n):
659            percent(i, n, 1)
660        percent(1, 1, 1)
661
662    :param int i: current item
663    :param int n: total number of items
664    :param int s: increment size
665    """
666    message("%d %d %d" % (i, n, s), flag='p')
667
668
669def warning(msg):
670    """Display a warning message using `g.message -w`
671
672    :param str msg: warning message to be displayed
673    """
674    message(msg, flag='w')
675
676
677def error(msg):
678    """Display an error message using `g.message -e`
679
680    This function does not end the execution of the program.
681    The right action after the error is up to the caller.
682    For error handling using the standard mechanism use :func:`fatal()`.
683
684    :param str msg: error message to be displayed
685    """
686    message(msg, flag='e')
687
688
689def fatal(msg):
690    """Display an error message using `g.message -e`, then abort or raise
691
692    Raises exception when module global raise_on_error is 'True', abort
693    (calls exit) otherwise.
694    Use :func:`set_raise_on_error()` to set the behavior.
695
696    :param str msg: error message to be displayed
697    """
698    global raise_on_error
699    if raise_on_error:
700        raise ScriptError(msg)
701
702    error(msg)
703    sys.exit(1)
704
705
706def set_raise_on_error(raise_exp=True):
707    """Define behaviour on fatal error (fatal() called)
708
709    :param bool raise_exp: True to raise ScriptError instead of calling
710                           sys.exit(1) in fatal()
711
712    :return: current status
713    """
714    global raise_on_error
715    tmp_raise = raise_on_error
716    raise_on_error = raise_exp
717    return tmp_raise
718
719
720def get_raise_on_error():
721    """Return True if a ScriptError exception is raised instead of calling
722       sys.exit(1) in case a fatal error was invoked with fatal()
723    """
724    global raise_on_error
725    return raise_on_error
726
727
728# TODO: solve also warnings (not printed now)
729def set_capture_stderr(capture=True):
730    """Enable capturing standard error output of modules and print it.
731
732    By default, standard error output (stderr) of child processes shows
733    in the same place as output of the parent process. This may not
734    always be the same place as ``sys.stderr`` is written.
735    After calling this function, functions in the ``grass.script``
736    package will capture the stderr of child processes and pass it
737    to ``sys.stderr`` if there is an error.
738
739    .. note::
740
741        This is advantages for interactive shells such as the one in GUI
742        and interactive notebooks such as Jupyer Notebook.
743
744    The capturing can be applied only in certain cases, for example
745    in case of run_command() it is applied because run_command() nor
746    its callers do not handle the streams, however feed_command()
747    cannot do capturing because its callers handle the streams.
748
749    The previous state is returned. Passing ``False`` disables the
750    capturing.
751
752    .. versionadded:: 7.4
753    """
754    global _capture_stderr
755    tmp = _capture_stderr
756    _capture_stderr = capture
757    return tmp
758
759def get_capture_stderr():
760    """Return True if stderr is captured, False otherwise.
761
762    See set_capture_stderr().
763    """
764    global _capture_stderr
765    return _capture_stderr
766
767# interface to g.parser
768
769
770def _parse_opts(lines):
771    options = {}
772    flags = {}
773    for line in lines:
774        if not line:
775            break
776        try:
777            [var, val] = line.split(b'=', 1)
778            [var, val] = [decode(var), decode(val)]
779        except:
780            raise SyntaxError("invalid output from g.parser: %s" % line)
781
782        if var.startswith('flag_'):
783            flags[var[5:]] = bool(int(val))
784        elif var.startswith('opt_'):
785            options[var[4:]] = val
786        elif var in ['GRASS_OVERWRITE', 'GRASS_VERBOSE']:
787            os.environ[var] = val
788        else:
789            raise SyntaxError("invalid output from g.parser: %s" % line)
790
791    return (options, flags)
792
793
794def parser():
795    """Interface to g.parser, intended to be run from the top-level, e.g.:
796
797    ::
798
799        if __name__ == "__main__":
800            options, flags = grass.parser()
801            main()
802
803    Thereafter, the global variables "options" and "flags" will be
804    dictionaries containing option/flag values, keyed by lower-case
805    option/flag names. The values in "options" are strings, those in
806    "flags" are Python booleans.
807
808    Overview table of parser standard options:
809    https://grass.osgeo.org/grass78/manuals/parser_standard_options.html
810    """
811    if not os.getenv("GISBASE"):
812        print("You must be in GRASS GIS to run this program.", file=sys.stderr)
813        sys.exit(1)
814
815    cmdline = [basename(sys.argv[0])]
816    cmdline += [pipes.quote(a) for a in sys.argv[1:]]
817    os.environ['CMDLINE'] = ' '.join(cmdline)
818
819    argv = sys.argv[:]
820    name = argv[0]
821    if not os.path.isabs(name):
822        if os.sep in name or (os.altsep and os.altsep in name):
823            argv[0] = os.path.abspath(name)
824        else:
825            argv[0] = os.path.join(sys.path[0], name)
826
827    prog = "g.parser.exe" if sys.platform == "win32" else "g.parser"
828    p = subprocess.Popen([prog, '-n'] + argv, stdout=subprocess.PIPE)
829    s = p.communicate()[0]
830    lines = s.split(b'\0')
831
832    if not lines or lines[0] != b"@ARGS_PARSED@":
833        stdout = os.fdopen(sys.stdout.fileno(), 'wb')
834        stdout.write(s)
835        sys.exit(p.returncode)
836    return _parse_opts(lines[1:])
837
838# interface to g.tempfile
839
840
841def tempfile(create=True):
842    """Returns the name of a temporary file, created with g.tempfile.
843
844    :param bool create: True to create a file
845
846    :return: path to a tmp file
847    """
848    flags = ''
849    if not create:
850        flags += 'd'
851
852    return read_command("g.tempfile", flags=flags, pid=os.getpid()).strip()
853
854
855def tempdir():
856    """Returns the name of a temporary dir, created with g.tempfile."""
857    tmp = tempfile(create=False)
858    os.mkdir(tmp)
859
860    return tmp
861
862
863def tempname(length, lowercase=False):
864    """Generate a GRASS and SQL compliant random name starting with tmp_
865    followed by a random part of length "length"
866
867    :param int length: length of the random part of the name to generate
868    :param bool lowercase: use only lowercase characters to generate name
869    :returns: String with a random name of length "length" starting with a letter
870    :rtype: str
871
872    :Example:
873
874    >>> tempname(12)
875    'tmp_MxMa1kAS13s9'
876    """
877
878    chars = string.ascii_lowercase + string.digits
879    if not lowercase:
880        chars += string.ascii_uppercase
881    random_part = ''.join(random.choice(chars) for _ in range(length))
882    randomname = 'tmp_' + random_part
883
884    return randomname
885
886
887def _compare_projection(dic):
888    """Check if projection has some possibility of duplicate names like
889    Universal Transverse Mercator and Universe Transverse Mercator and
890    unify them
891
892    :param dic: The dictionary containing information about projection
893
894    :return: The dictionary with the new values if needed
895
896    """
897    # the lookup variable is a list of list, each list contains all the
898    # possible name for a projection system
899    lookup = [['Universal Transverse Mercator', 'Universe Transverse Mercator']]
900    for lo in lookup:
901        for n in range(len(dic['name'])):
902            if dic['name'][n] in lo:
903                dic['name'][n] = lo[0]
904    return dic
905
906
907def _compare_units(dic):
908    """Check if units has some possibility of duplicate names like
909    meter and metre and unify them
910
911    :param dic: The dictionary containing information about units
912
913    :return: The dictionary with the new values if needed
914
915    """
916    # the lookup variable is a list of list, each list contains all the
917    # possible name for a units
918    lookup = [['meter', 'metre'], ['meters', 'metres'], ['kilometer',
919              'kilometre'], ['kilometers', 'kilometres']]
920    for l in lookup:
921        for n in range(len(dic['unit'])):
922            if dic['unit'][n].lower() in l:
923                dic['unit'][n] = l[0]
924        for n in range(len(dic['units'])):
925            if dic['units'][n].lower() in l:
926                dic['units'][n] = l[0]
927    return dic
928
929
930def _text_to_key_value_dict(filename, sep=":", val_sep=",", checkproj=False,
931                            checkunits=False):
932    """Convert a key-value text file, where entries are separated by newlines
933    and the key and value are separated by `sep', into a key-value dictionary
934    and discover/use the correct data types (float, int or string) for values.
935
936    :param str filename: The name or name and path of the text file to convert
937    :param str sep: The character that separates the keys and values, default
938                    is ":"
939    :param str val_sep: The character that separates the values of a single
940                        key, default is ","
941    :param bool checkproj: True if it has to check some information about
942                           projection system
943    :param bool checkproj: True if it has to check some information about units
944
945    :return: The dictionary
946
947    A text file with this content:
948    ::
949
950        a: Hello
951        b: 1.0
952        c: 1,2,3,4,5
953        d : hello,8,0.1
954
955    Will be represented as this dictionary:
956
957    ::
958
959        {'a': ['Hello'], 'c': [1, 2, 3, 4, 5], 'b': [1.0], 'd': ['hello', 8, 0.1]}
960
961    """
962    text = open(filename, "r").readlines()
963    kvdict = KeyValue()
964
965    for line in text:
966        if line.find(sep) >= 0:
967            key, value = line.split(sep)
968            key = key.strip()
969            value = value.strip()
970        else:
971            # Jump over empty values
972            continue
973        values = value.split(val_sep)
974        value_list = []
975
976        for value in values:
977            not_float = False
978            not_int = False
979
980            # Convert values into correct types
981            # We first try integer then float
982            try:
983                value_converted = int(value)
984            except:
985                not_int = True
986            if not_int:
987                try:
988                    value_converted = float(value)
989                except:
990                    not_float = True
991
992            if not_int and not_float:
993                value_converted = value.strip()
994
995            value_list.append(value_converted)
996
997        kvdict[key] = value_list
998    if checkproj:
999        kvdict = _compare_projection(kvdict)
1000    if checkunits:
1001        kvdict = _compare_units(kvdict)
1002    return kvdict
1003
1004
1005def compare_key_value_text_files(filename_a, filename_b, sep=":",
1006                                 val_sep=",", precision=0.000001,
1007                                 proj=False, units=False):
1008    """Compare two key-value text files
1009
1010    This method will print a warning in case keys that are present in the first
1011    file are not present in the second one.
1012    The comparison method tries to convert the values into their native format
1013    (float, int or string) to allow correct comparison.
1014
1015    An example key-value text file may have this content:
1016
1017    ::
1018
1019        a: Hello
1020        b: 1.0
1021        c: 1,2,3,4,5
1022        d : hello,8,0.1
1023
1024    :param str filename_a: name of the first key-value text file
1025    :param str filenmae_b: name of the second key-value text file
1026    :param str sep: character that separates the keys and values, default is ":"
1027    :param str val_sep: character that separates the values of a single key, default is ","
1028    :param double precision: precision with which the floating point values are compared
1029    :param bool proj: True if it has to check some information about projection system
1030    :param bool units: True if it has to check some information about units
1031
1032    :return: True if full or almost identical, False if different
1033    """
1034    dict_a = _text_to_key_value_dict(filename_a, sep, checkproj=proj,
1035                                     checkunits=units)
1036    dict_b = _text_to_key_value_dict(filename_b, sep, checkproj=proj,
1037                                     checkunits=units)
1038
1039    if sorted(dict_a.keys()) != sorted(dict_b.keys()):
1040        return False
1041
1042    # We compare matching keys
1043    for key in dict_a.keys():
1044        # Floating point values must be handled separately
1045        if isinstance(dict_a[key], float) and isinstance(dict_b[key], float):
1046            if abs(dict_a[key] - dict_b[key]) > precision:
1047                return False
1048        elif isinstance(dict_a[key], float) or isinstance(dict_b[key], float):
1049            warning(_("Mixing value types. Will try to compare after "
1050                      "integer conversion"))
1051            return int(dict_a[key]) == int(dict_b[key])
1052        elif key == "+towgs84":
1053            # We compare the sum of the entries
1054            if abs(sum(dict_a[key]) - sum(dict_b[key])) > precision:
1055                return False
1056        else:
1057            if dict_a[key] != dict_b[key]:
1058                return False
1059    return True
1060
1061# interface to g.gisenv
1062
1063
1064def gisenv(env=None):
1065    """Returns the output from running g.gisenv (with no arguments), as a
1066    dictionary. Example:
1067
1068    >>> env = gisenv()
1069    >>> print(env['GISDBASE'])  # doctest: +SKIP
1070    /opt/grass-data
1071
1072    :param env run with different environment
1073    :return: list of GRASS variables
1074    """
1075    s = read_command("g.gisenv", flags='n', env=env)
1076    return parse_key_val(s)
1077
1078# interface to g.region
1079
1080
1081def locn_is_latlong():
1082    """Tests if location is lat/long. Value is obtained
1083    by checking the "g.region -pu" projection code.
1084
1085    :return: True for a lat/long region, False otherwise
1086    """
1087    s = read_command("g.region", flags='pu')
1088    kv = parse_key_val(s, ':')
1089    if kv['projection'].split(' ')[0] == '3':
1090        return True
1091    else:
1092        return False
1093
1094
1095def region(region3d=False, complete=False, env=None):
1096    """Returns the output from running "g.region -gu", as a
1097    dictionary. Example:
1098
1099    :param bool region3d: True to get 3D region
1100    :param bool complete:
1101    :param env env
1102
1103    >>> curent_region = region()
1104    >>> # obtain n, s, e and w values
1105    >>> [curent_region[key] for key in "nsew"]  # doctest: +ELLIPSIS
1106    [..., ..., ..., ...]
1107    >>> # obtain ns and ew resulutions
1108    >>> (curent_region['nsres'], curent_region['ewres'])  # doctest: +ELLIPSIS
1109    (..., ...)
1110
1111    :return: dictionary of region values
1112    """
1113    flgs = 'gu'
1114    if region3d:
1115        flgs += '3'
1116    if complete:
1117        flgs += 'cep'
1118
1119    s = read_command("g.region", flags=flgs, env=env)
1120    reg = parse_key_val(s, val_type=float)
1121    for k in ['projection', 'zone', 'rows',  'cols',  'cells',
1122              'rows3', 'cols3', 'cells3', 'depths']:
1123        if k not in reg:
1124            continue
1125        reg[k] = int(reg[k])
1126
1127    return reg
1128
1129
1130def region_env(region3d=False, flags=None, env=None, **kwargs):
1131    """Returns region settings as a string which can used as
1132    GRASS_REGION environmental variable.
1133
1134    If no 'kwargs' are given then the current region is used. Note
1135    that this function doesn't modify the current region!
1136
1137    See also :func:`use_temp_region()` for alternative method how to define
1138    temporary region used for raster-based computation.
1139
1140    :param bool region3d: True to get 3D region
1141    :param string flags: for example 'a'
1142    :param env: different environment than current
1143    :param kwargs: g.region's parameters like 'raster', 'vector' or 'region'
1144
1145    ::
1146
1147        os.environ['GRASS_REGION'] = grass.region_env(region='detail')
1148        grass.mapcalc('map=1', overwrite=True)
1149        os.environ.pop('GRASS_REGION')
1150
1151    :return: string with region values
1152    :return: empty string on error
1153    """
1154    # read proj/zone from WIND file
1155    gis_env = gisenv(env)
1156    windfile = os.path.join(gis_env['GISDBASE'], gis_env['LOCATION_NAME'],
1157                            gis_env['MAPSET'], "WIND")
1158    with open(windfile, "r") as fd:
1159        grass_region = ''
1160        for line in fd.readlines():
1161            key, value = map(lambda x: x.strip(), line.split(":", 1))
1162            if kwargs and key not in ('proj', 'zone'):
1163                continue
1164            if not kwargs and not region3d and \
1165                    key in ('top', 'bottom', 'cols3', 'rows3',
1166                            'depths', 'e-w resol3', 'n-s resol3', 't-b resol'):
1167                continue
1168
1169            grass_region += '%s: %s;' % (key, value)
1170
1171    if not kwargs:  # return current region
1172        return grass_region
1173
1174    # read other values from `g.region -gu`
1175    flgs = 'ug'
1176    if region3d:
1177        flgs += '3'
1178    if flags:
1179        flgs += flags
1180
1181    s = read_command('g.region', flags=flgs, env=env, **kwargs)
1182    if not s:
1183        return ''
1184    reg = parse_key_val(s)
1185
1186    kwdata = [('north',     'n'),
1187              ('south',     's'),
1188              ('east',      'e'),
1189              ('west',      'w'),
1190              ('cols',      'cols'),
1191              ('rows',      'rows'),
1192              ('e-w resol', 'ewres'),
1193              ('n-s resol', 'nsres')]
1194    if region3d:
1195        kwdata += [('top',        't'),
1196                   ('bottom',     'b'),
1197                   ('cols3',      'cols3'),
1198                   ('rows3',      'rows3'),
1199                   ('depths',     'depths'),
1200                   ('e-w resol3', 'ewres3'),
1201                   ('n-s resol3', 'nsres3'),
1202                   ('t-b resol',  'tbres')]
1203
1204    for wkey, rkey in kwdata:
1205        grass_region += '%s: %s;' % (wkey, reg[rkey])
1206
1207    return grass_region
1208
1209
1210def use_temp_region():
1211    """Copies the current region to a temporary region with "g.region save=",
1212    then sets WIND_OVERRIDE to refer to that region. Installs an atexit
1213    handler to delete the temporary region upon termination.
1214    """
1215    name = "tmp.%s.%d" % (os.path.basename(sys.argv[0]), os.getpid())
1216    run_command("g.region", save=name, overwrite=True)
1217    os.environ['WIND_OVERRIDE'] = name
1218    atexit.register(del_temp_region)
1219
1220
1221def del_temp_region():
1222    """Unsets WIND_OVERRIDE and removes any region named by it."""
1223    try:
1224        name = os.environ.pop('WIND_OVERRIDE')
1225        run_command("g.remove", flags='f', quiet=True, type='region', name=name)
1226    except:
1227        pass
1228
1229# interface to g.findfile
1230
1231
1232def find_file(name, element='cell', mapset=None):
1233    """Returns the output from running g.findfile as a
1234    dictionary. Example:
1235
1236    >>> result = find_file('elevation', element='cell')
1237    >>> print(result['fullname'])
1238    elevation@PERMANENT
1239    >>> print(result['file'])  # doctest: +ELLIPSIS
1240    /.../PERMANENT/cell/elevation
1241
1242
1243    :param str name: file name
1244    :param str element: element type (default 'cell')
1245    :param str mapset: mapset name (default all mapsets in search path)
1246
1247    :return: parsed output of g.findfile
1248    """
1249    if element == 'raster' or element == 'rast':
1250        verbose(_('Element type should be "cell" and not "%s"') % element)
1251        element = 'cell'
1252    # g.findfile returns non-zero when file was not found
1253    # se we ignore return code and just focus on stdout
1254    process = start_command('g.findfile', flags='n',
1255                            element=element, file=name, mapset=mapset,
1256                            stdout=PIPE)
1257    stdout = process.communicate()[0]
1258    return parse_key_val(stdout)
1259
1260# interface to g.list
1261
1262
1263def list_strings(type, pattern=None, mapset=None, exclude=None, flag=''):
1264    """List of elements as strings.
1265
1266    Returns the output from running g.list, as a list of qualified
1267    names.
1268
1269    :param str type: element type (raster, vector, raster_3d, region, ...)
1270    :param str pattern: pattern string
1271    :param str mapset: mapset name (if not given use search path)
1272    :param str exclude: pattern string to exclude maps from the research
1273    :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
1274                     or '' (glob pattern)
1275
1276    :return: list of elements
1277    """
1278    if type == 'cell':
1279        verbose(_('Element type should be "raster" and not "%s"') % type)
1280
1281    result = list()
1282    for line in read_command("g.list",
1283                             quiet=True,
1284                             flags='m' + flag,
1285                             type=type,
1286                             pattern=pattern,
1287                             exclude=exclude,
1288                             mapset=mapset).splitlines():
1289        result.append(line.strip())
1290
1291    return result
1292
1293
1294def list_pairs(type, pattern=None, mapset=None, exclude=None, flag=''):
1295    """List of elements as pairs
1296
1297    Returns the output from running g.list, as a list of
1298    (name, mapset) pairs
1299
1300    :param str type: element type (raster, vector, raster_3d, region, ...)
1301    :param str pattern: pattern string
1302    :param str mapset: mapset name (if not given use search path)
1303    :param str exclude: pattern string to exclude maps from the research
1304    :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
1305                     or '' (glob pattern)
1306
1307    :return: list of elements
1308    """
1309    return [tuple(map.split('@', 1)) for map in list_strings(type, pattern,
1310                                                              mapset, exclude,
1311                                                              flag)]
1312
1313
1314def list_grouped(type, pattern=None, check_search_path=True, exclude=None,
1315                 flag=''):
1316    """List of elements grouped by mapsets.
1317
1318    Returns the output from running g.list, as a dictionary where the
1319    keys are mapset names and the values are lists of maps in that
1320    mapset. Example:
1321
1322    >>> list_grouped('vect', pattern='*roads*')['PERMANENT']
1323    ['railroads', 'roadsmajor']
1324
1325    :param str type: element type (raster, vector, raster_3d, region, ...) or list of elements
1326    :param str pattern: pattern string
1327    :param str check_search_path: True to add mapsets for the search path
1328                                  with no found elements
1329    :param str exclude: pattern string to exclude maps from the research
1330    :param str flag: pattern type: 'r' (basic regexp), 'e' (extended regexp),
1331                                    or '' (glob pattern)
1332
1333    :return: directory of mapsets/elements
1334    """
1335    if isinstance(type, str) or len(type) == 1:
1336        types = [type]
1337        store_types = False
1338    else:
1339        types = type
1340        store_types = True
1341        flag += 't'
1342    for i in range(len(types)):
1343        if types[i] == 'cell':
1344            verbose(_('Element type should be "raster" and not "%s"') % types[i])
1345            types[i] = 'raster'
1346    result = {}
1347    if check_search_path:
1348        for mapset in mapsets(search_path=True):
1349            if store_types:
1350                result[mapset] = {}
1351            else:
1352                result[mapset] = []
1353
1354    mapset = None
1355    for line in read_command("g.list", quiet=True, flags="m" + flag,
1356                             type=types, pattern=pattern, exclude=exclude).splitlines():
1357        try:
1358            name, mapset = line.split('@')
1359        except ValueError:
1360            warning(_("Invalid element '%s'") % line)
1361            continue
1362
1363        if store_types:
1364            type_, name = name.split('/')
1365            if mapset in result:
1366                if type_ in result[mapset]:
1367                    result[mapset][type_].append(name)
1368                else:
1369                    result[mapset][type_] = [name, ]
1370            else:
1371                result[mapset] = {type_: [name, ]}
1372        else:
1373            if mapset in result:
1374                result[mapset].append(name)
1375            else:
1376                result[mapset] = [name, ]
1377
1378    return result
1379
1380# color parsing
1381
1382named_colors = {
1383    "white":   (1.00, 1.00, 1.00),
1384    "black":   (0.00, 0.00, 0.00),
1385    "red":     (1.00, 0.00, 0.00),
1386    "green":   (0.00, 1.00, 0.00),
1387    "blue":    (0.00, 0.00, 1.00),
1388    "yellow":  (1.00, 1.00, 0.00),
1389    "magenta": (1.00, 0.00, 1.00),
1390    "cyan":    (0.00, 1.00, 1.00),
1391    "aqua":    (0.00, 0.75, 0.75),
1392    "grey":    (0.75, 0.75, 0.75),
1393    "gray":    (0.75, 0.75, 0.75),
1394    "orange":  (1.00, 0.50, 0.00),
1395    "brown":   (0.75, 0.50, 0.25),
1396    "purple":  (0.50, 0.00, 1.00),
1397    "violet":  (0.50, 0.00, 1.00),
1398    "indigo":  (0.00, 0.50, 1.00)}
1399
1400
1401def parse_color(val, dflt=None):
1402    """Parses the string "val" as a GRASS colour, which can be either one of
1403    the named colours or an R:G:B tuple e.g. 255:255:255. Returns an
1404    (r,g,b) triple whose components are floating point values between 0
1405    and 1. Example:
1406
1407    >>> parse_color("red")
1408    (1.0, 0.0, 0.0)
1409    >>> parse_color("255:0:0")
1410    (1.0, 0.0, 0.0)
1411
1412    :param val: color value
1413    :param dflt: default color value
1414
1415    :return: tuple RGB
1416    """
1417    if val in named_colors:
1418        return named_colors[val]
1419
1420    vals = val.split(':')
1421    if len(vals) == 3:
1422        return tuple(float(v) / 255 for v in vals)
1423
1424    return dflt
1425
1426# check GRASS_OVERWRITE
1427
1428
1429def overwrite():
1430    """Return True if existing files may be overwritten"""
1431    owstr = 'GRASS_OVERWRITE'
1432    return owstr in os.environ and os.environ[owstr] != '0'
1433
1434# check GRASS_VERBOSE
1435
1436
1437def verbosity():
1438    """Return the verbosity level selected by GRASS_VERBOSE
1439
1440    Currently, there are 5 levels of verbosity:
1441    -1 nothing will be printed (also fatal errors and warnings will be discarded)
1442
1443    0 only errors and warnings are printed, triggered by "--q" or "--quiet" flag.
1444
1445    1 progress information (percent) and important messages will be printed
1446
1447    2 all messages will be printed
1448
1449    3 also verbose messages will be printed. Triggered by "--v" or "--verbose" flag.
1450    """
1451    vbstr = os.getenv('GRASS_VERBOSE')
1452    if vbstr:
1453        return int(vbstr)
1454    else:
1455        return 2
1456
1457## various utilities, not specific to GRASS
1458
1459def find_program(pgm, *args):
1460    """Attempt to run a program, with optional arguments.
1461
1462    You must call the program in a way that will return a successful
1463    exit code. For GRASS modules this means you need to pass it some
1464    valid CLI option, like "--help". For other programs a common
1465    valid do-little option is usually "--version".
1466
1467    Example:
1468
1469    >>> find_program('r.sun', '--help')
1470    True
1471    >>> find_program('ls', '--version')
1472    True
1473
1474    :param str pgm: program name
1475    :param args: list of arguments
1476
1477    :return: False if the attempt failed due to a missing executable
1478            or non-zero return code
1479    :return: True otherwise
1480    """
1481    nuldev = open(os.devnull, 'w+')
1482    try:
1483        # TODO: the doc or impl is not correct, any return code is accepted
1484        call([pgm] + list(args), stdin = nuldev, stdout = nuldev, stderr = nuldev)
1485        found = True
1486    except:
1487        found = False
1488    nuldev.close()
1489
1490    return found
1491
1492# interface to g.mapsets
1493
1494
1495def mapsets(search_path=False):
1496    """List available mapsets
1497
1498    :param bool search_path: True to list mapsets only in search path
1499
1500    :return: list of mapsets
1501    """
1502    if search_path:
1503        flags = 'p'
1504    else:
1505        flags = 'l'
1506    mapsets = read_command('g.mapsets',
1507                           flags=flags,
1508                           sep='newline',
1509                           quiet=True)
1510    if not mapsets:
1511        fatal(_("Unable to list mapsets"))
1512
1513    return mapsets.splitlines()
1514
1515# interface to `g.proj -c`
1516
1517
1518def create_location(dbase, location, epsg=None, proj4=None, filename=None,
1519                    wkt=None, datum=None, datum_trans=None, desc=None,
1520                    overwrite=False):
1521    """Create new location
1522
1523    Raise ScriptError on error.
1524
1525    :param str dbase: path to GRASS database
1526    :param str location: location name to create
1527    :param epsg: if given create new location based on EPSG code
1528    :param proj4: if given create new location based on Proj4 definition
1529    :param str filename: if given create new location based on georeferenced file
1530    :param str wkt: if given create new location based on WKT definition
1531                    (path to PRJ file)
1532    :param datum: GRASS format datum code
1533    :param datum_trans: datum transformation parameters (used for epsg and proj4)
1534    :param desc: description of the location (creates MYNAME file)
1535    :param bool overwrite: True to overwrite location if exists(WARNING:
1536                           ALL DATA from existing location ARE DELETED!)
1537    """
1538    gisdbase = None
1539    if epsg or proj4 or filename or wkt:
1540        # FIXME: changing GISDBASE mid-session is not background-job safe
1541        gisdbase = gisenv()['GISDBASE']
1542        run_command('g.gisenv', set='GISDBASE=%s' % dbase)
1543    # create dbase if not exists
1544    if not os.path.exists(dbase):
1545            os.mkdir(dbase)
1546
1547    # check if location already exists
1548    if os.path.exists(os.path.join(dbase, location)):
1549        if not overwrite:
1550            warning(_("Location <%s> already exists. Operation canceled.") % location)
1551            return
1552        else:
1553            warning(_("Location <%s> already exists and will be overwritten") % location)
1554            shutil.rmtree(os.path.join(dbase, location))
1555
1556    kwargs = dict()
1557    if datum:
1558        kwargs['datum'] = datum
1559    if datum_trans:
1560        kwargs['datum_trans'] = datum_trans
1561
1562    if epsg:
1563        ps = pipe_command('g.proj', quiet=True, flags='t', epsg=epsg,
1564                          location=location, stderr=PIPE, **kwargs)
1565    elif proj4:
1566        ps = pipe_command('g.proj', quiet=True, flags='t', proj4=proj4,
1567                          location=location, stderr=PIPE, **kwargs)
1568    elif filename:
1569        ps = pipe_command('g.proj', quiet=True, georef=filename,
1570                          location=location, stderr=PIPE)
1571    elif wkt:
1572        ps = pipe_command('g.proj', quiet=True, wkt=wkt, location=location,
1573                          stderr=PIPE)
1574    else:
1575        _create_location_xy(dbase, location)
1576
1577    if epsg or proj4 or filename or wkt:
1578        error = ps.communicate()[1]
1579        run_command('g.gisenv', set='GISDBASE=%s' % gisdbase)
1580
1581        if ps.returncode != 0 and error:
1582            raise ScriptError(repr(error))
1583
1584    try:
1585        fd = codecs.open(os.path.join(dbase, location, 'PERMANENT', 'MYNAME'),
1586                         encoding='utf-8', mode='w')
1587        if desc:
1588            fd.write(desc + os.linesep)
1589        else:
1590            fd.write(os.linesep)
1591        fd.close()
1592    except OSError as e:
1593        raise ScriptError(repr(e))
1594
1595
1596def _create_location_xy(database, location):
1597    """Create unprojected location
1598
1599    Raise ScriptError on error.
1600
1601    :param database: GRASS database where to create new location
1602    :param location: location name
1603    """
1604    cur_dir = os.getcwd()
1605    try:
1606        os.chdir(database)
1607        os.mkdir(location)
1608        os.mkdir(os.path.join(location, 'PERMANENT'))
1609
1610        # create DEFAULT_WIND and WIND files
1611        regioninfo = ['proj:       0',
1612                      'zone:       0',
1613                      'north:      1',
1614                      'south:      0',
1615                      'east:       1',
1616                      'west:       0',
1617                      'cols:       1',
1618                      'rows:       1',
1619                      'e-w resol:  1',
1620                      'n-s resol:  1',
1621                      'top:        1',
1622                      'bottom:     0',
1623                      'cols3:      1',
1624                      'rows3:      1',
1625                      'depths:     1',
1626                      'e-w resol3: 1',
1627                      'n-s resol3: 1',
1628                      't-b resol:  1']
1629
1630        defwind = open(os.path.join(location,
1631                                    "PERMANENT", "DEFAULT_WIND"), 'w')
1632        for param in regioninfo:
1633            defwind.write(param + '%s' % os.linesep)
1634        defwind.close()
1635
1636        shutil.copy(os.path.join(location, "PERMANENT", "DEFAULT_WIND"),
1637                    os.path.join(location, "PERMANENT", "WIND"))
1638
1639        os.chdir(cur_dir)
1640    except OSError as e:
1641        raise ScriptError(repr(e))
1642
1643# interface to g.version
1644
1645
1646def version():
1647    """Get GRASS version as dictionary
1648
1649    ::
1650
1651        >>> print(version())
1652        {'proj4': '4.8.0', 'geos': '3.3.5', 'libgis_revision': '52468',
1653         'libgis_date': '2012-07-27 22:53:30 +0200 (Fri, 27 Jul 2012)',
1654         'version': '7.0.svn', 'date': '2012', 'gdal': '2.0dev',
1655         'revision': '53670'}
1656
1657    """
1658    data = parse_command('g.version', flags='rge', errors='ignore')
1659    for k, v in data.items():
1660        data[k.strip()] = v.replace('"', '').strip()
1661
1662    return data
1663
1664# get debug_level
1665_debug_level = None
1666
1667
1668def debug_level(force=False):
1669    global _debug_level
1670    if not force and _debug_level is not None:
1671        return _debug_level
1672    _debug_level = 0
1673    if find_program('g.gisenv', '--help'):
1674        try:
1675            _debug_level = int(gisenv().get('DEBUG', 0))
1676            if _debug_level < 0 or _debug_level > 5:
1677                raise ValueError(_("Debug level {0}").format(_debug_level))
1678        except ValueError as e:
1679            _debug_level = 0
1680            sys.stderr.write(_("WARNING: Ignoring unsupported debug level (must be >=0 and <=5). {0}\n").format(e))
1681
1682    return _debug_level
1683
1684
1685def legal_name(s):
1686    """Checks if the string contains only allowed characters.
1687
1688    This is the Python implementation of :func:`G_legal_filename()` function.
1689
1690    ..note::
1691
1692        It is not clear when exactly use this function, but it might be
1693        useful anyway for checking map names and column names.
1694    """
1695    if not s or s[0] == '.':
1696        warning(_("Illegal filename <%s>. Cannot be 'NULL' or start with " \
1697                  "'.'.") % s)
1698        return False
1699
1700    illegal = [c
1701               for c in s
1702               if c in '/"\'@,=*~' or c <= ' ' or c >= '\177']
1703    if illegal:
1704        illegal = ''.join(sorted(set(illegal)))
1705        warning(_("Illegal filename <%(s)s>. <%(il)s> not allowed.\n") % {
1706        's': s, 'il': illegal})
1707        return False
1708
1709    return True
1710
1711
1712def create_environment(gisdbase, location, mapset):
1713    """Creates environment to be passed in run_command for example.
1714    Returns tuple with temporary file path and the environment. The user
1715    of this function is responsile for deleting the file."""
1716    tmp_gisrc_file = tempfile()
1717    with open(tmp_gisrc_file, 'w') as f:
1718        f.write('MAPSET: {mapset}\n'.format(mapset=mapset))
1719        f.write('GISDBASE: {g}\n'.format(g=gisdbase))
1720        f.write('LOCATION_NAME: {l}\n'.format(l=location))
1721        f.write('GUI: text\n')
1722    env = os.environ.copy()
1723    env['GISRC'] = tmp_gisrc_file
1724    return tmp_gisrc_file, env
1725
1726
1727if __name__ == '__main__':
1728    import doctest
1729    doctest.testmod()
1730