14360: Merge branch 'master'
[arvados.git] / crunch_scripts / crunchutil / subst.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4
5 import glob
6 import os
7 import re
8 import stat
9
10 BACKSLASH_ESCAPE_RE = re.compile(r'\\(.)')
11
12 class SubstitutionError(Exception):
13     pass
14
15 def search(c):
16     DEFAULT = 0
17     DOLLAR = 1
18
19     i = 0
20     state = DEFAULT
21     start = None
22     depth = 0
23     while i < len(c):
24         if c[i] == '\\':
25             i += 1
26         elif state == DEFAULT:
27             if c[i] == '$':
28                 state = DOLLAR
29                 if depth == 0:
30                     start = i
31             elif c[i] == ')':
32                 if depth == 1:
33                     return [start, i]
34                 if depth > 0:
35                     depth -= 1
36         elif state == DOLLAR:
37             if c[i] == '(':
38                 depth += 1
39             state = DEFAULT
40         i += 1
41     if depth != 0:
42         raise SubstitutionError("Substitution error, mismatched parentheses {}".format(c))
43     return None
44
45 def sub_file(v):
46     path = os.path.join(os.environ['TASK_KEEPMOUNT'], v)
47     st = os.stat(path)
48     if st and stat.S_ISREG(st.st_mode):
49         return path
50     else:
51         raise SubstitutionError("$(file {}) is not accessible or is not a regular file".format(path))
52
53 def sub_dir(v):
54     d = os.path.dirname(v)
55     if d == '':
56         d = v
57     path = os.path.join(os.environ['TASK_KEEPMOUNT'], d)
58     st = os.stat(path)
59     if st and stat.S_ISDIR(st.st_mode):
60         return path
61     else:
62         raise SubstitutionError("$(dir {}) is not accessible or is not a directory".format(path))
63
64 def sub_basename(v):
65     return os.path.splitext(os.path.basename(v))[0]
66
67 def sub_glob(v):
68     l = glob.glob(v)
69     if len(l) == 0:
70         raise SubstitutionError("$(glob {}) no match found".format(v))
71     else:
72         return l[0]
73
74 default_subs = {"file ": sub_file,
75                 "dir ": sub_dir,
76                 "basename ": sub_basename,
77                 "glob ": sub_glob}
78
79 def do_substitution(p, c, subs=default_subs):
80     while True:
81         m = search(c)
82         if m is None:
83             return BACKSLASH_ESCAPE_RE.sub(r'\1', c)
84
85         v = do_substitution(p, c[m[0]+2 : m[1]])
86         var = True
87         for sub in subs:
88             if v.startswith(sub):
89                 r = subs[sub](v[len(sub):])
90                 var = False
91                 break
92         if var:
93             if v in p:
94                 r = p[v]
95             else:
96                 raise SubstitutionError("Unknown variable or function '%s' while performing substitution on '%s'" % (v, c))
97             if r is None:
98                 raise SubstitutionError("Substitution for '%s' is null while performing substitution on '%s'" % (v, c))
99             if not isinstance(r, basestring):
100                 raise SubstitutionError("Substitution for '%s' must be a string while performing substitution on '%s'" % (v, c))
101
102         c = c[:m[0]] + r + c[m[1]+1:]