]> arthur.barton.de Git - bup.git/blob - lib/bup/shquote.py
a3cd4c4846114d17be481a398b92a84abf047c6c
[bup.git] / lib / bup / shquote.py
1 import re
2
3 q = "'"
4 qq = '"'
5
6
7 class QuoteError(Exception):
8     pass
9
10
11 def _quotesplit(line):
12     inquote = None
13     inescape = None
14     wordstart = 0
15     word = ''
16     for i in range(len(line)):
17         c = line[i]
18         if inescape:
19             if inquote == q and c != q:
20                 word += '\\'  # single-q backslashes can only quote single-q
21             word += c
22             inescape = False
23         elif c == '\\':
24             inescape = True
25         elif c == inquote:
26             inquote = None
27             # this is un-sh-like, but do it for sanity when autocompleting
28             yield (wordstart, word)
29             word = ''
30             wordstart = i+1
31         elif not inquote and not word and (c == q or c == qq):
32             # the 'not word' constraint on this is un-sh-like, but do it
33             # for sanity when autocompleting
34             inquote = c
35             wordstart = i
36         elif not inquote and c in [' ', '\n', '\r', '\t']:
37             if word:
38                 yield (wordstart, word)
39             word = ''
40             wordstart = i+1
41         else:
42             word += c
43     if word:
44         yield (wordstart, word)
45     if inquote or inescape or word:
46         raise QuoteError()
47
48
49 def quotesplit(line):
50     """Split 'line' into a list of offset,word tuples.
51
52     The words are produced after removing doublequotes, singlequotes, and
53     backslash escapes.
54
55     Note that this implementation isn't entirely sh-compatible.  It only
56     dequotes words that *start* with a quote character, that is, a string like
57        hello"world"
58     will not have its quotes removed, while a string like
59        hello "world"
60     will be turned into [(0, 'hello'), (6, 'world')] (ie. quotes removed).
61     """
62     l = []
63     try:
64         for i in _quotesplit(line):
65             l.append(i)
66     except QuoteError:
67         pass
68     return l
69
70
71 def unfinished_word(line):
72     """Returns the quotechar,word of any unfinished word at the end of 'line'.
73
74     You can use this to determine if 'line' is a completely parseable line
75     (ie. one that quotesplit() will finish successfully) or if you need
76     to read more bytes first.
77
78     Args:
79       line: an input string
80     Returns:
81       quotechar,word: the initial quote char (or None), and the partial word.
82     """
83     try:
84         for (wordstart,word) in _quotesplit(line):
85             pass
86     except QuoteError:
87         firstchar = line[wordstart]
88         if firstchar in [q, qq]:
89             return (firstchar, word)
90         else:
91             return (None, word)
92     else:
93         return (None, '')
94
95
96 def quotify(qtype, word, terminate):
97     """Return a string corresponding to given word, quoted using qtype.
98
99     The resulting string is dequotable using quotesplit() and can be
100     joined with other quoted strings by adding arbitrary whitespace
101     separators.
102
103     Args:
104       qtype: one of '', shquote.qq, or shquote.q
105       word: the string to quote.  May contain arbitrary characters.
106       terminate: include the trailing quote character, if any.
107     Returns:
108       The quoted string.
109     """
110     if qtype == qq:
111         return qq + word.replace(qq, '\\"') + (terminate and qq or '')
112     elif qtype == q:
113         return q + word.replace(q, "\\'") + (terminate and q or '')
114     else:
115         return re.sub(r'([\"\' \t\n\r])', r'\\\1', word)
116
117
118 def quotify_list(words):
119   """Return a minimally-quoted string produced by quoting each word.
120
121   This calculates the qtype for each word depending on whether the word
122   already includes singlequote characters, doublequote characters, both,
123   or neither.
124
125   Args:
126     words: the list of words to quote.
127   Returns:
128     The resulting string, with quoted words separated by ' '.
129   """
130   wordout = []
131   for word in words:
132     qtype = q
133     if word and not re.search(r'[\s\"\']', word):
134       qtype = ''
135     elif q in word and qq not in word:
136       qtype = qq
137     wordout.append(quotify(qtype, word, True))
138   return ' '.join(wordout)
139
140
141 def what_to_add(qtype, origword, newword, terminate):
142     """Return a qtype that is needed to finish a partial word.
143
144     For example, given an origword of '\"frog' and a newword of '\"frogston',
145     returns either:
146        terminate=False: 'ston'
147        terminate=True:  'ston\"'
148
149     This is useful when calculating tab completion strings for readline.
150
151     Args:
152       qtype: the type of quoting to use (ie. the first character of origword)
153       origword: the original word that needs completion.
154       newword: the word we want it to be after completion.  Must start with
155         origword.
156       terminate: true if we should add the actual quote character at the end.
157     Returns:
158       The string to append to origword to produce (quoted) newword.
159     """
160     if not newword.startswith(origword):
161         return ''
162     else:
163         qold = quotify(qtype, origword, terminate=False)
164         return quotify(qtype, newword, terminate=terminate)[len(qold):]