1 | /* |
2 | This file is part of the KDE libraries |
3 | SPDX-FileCopyrightText: 2000 David Smith <dsmith@algonet.se> |
4 | |
5 | SPDX-License-Identifier: LGPL-2.0-or-later |
6 | */ |
7 | |
8 | #include "kshellcompletion.h" |
9 | |
10 | #include <KCompletion> |
11 | #include <KCompletionMatches> |
12 | #include <stdlib.h> |
13 | |
14 | class KShellCompletionPrivate |
15 | { |
16 | public: |
17 | KShellCompletionPrivate() |
18 | : m_word_break_char(QLatin1Char(' ')) |
19 | , m_quote_char1(QLatin1Char('\"')) |
20 | , m_quote_char2(QLatin1Char('\'')) |
21 | , m_escape_char(QLatin1Char('\\')) |
22 | { |
23 | } |
24 | |
25 | void splitText(const QString &text, QString &text_start, QString &text_compl) const; |
26 | bool quoteText(QString *text, bool force, bool skip_last) const; |
27 | QString unquote(const QString &text) const; |
28 | |
29 | QString m_text_start; // part of the text that was not completed |
30 | QString m_text_compl; // part of the text that was completed (unchanged) |
31 | |
32 | QChar m_word_break_char; |
33 | QChar m_quote_char1; |
34 | QChar m_quote_char2; |
35 | QChar m_escape_char; |
36 | }; |
37 | |
38 | KShellCompletion::KShellCompletion() |
39 | : KUrlCompletion() |
40 | , d(new KShellCompletionPrivate) |
41 | { |
42 | } |
43 | |
44 | KShellCompletion::~KShellCompletion() = default; |
45 | |
46 | /* |
47 | * makeCompletion() |
48 | * |
49 | * Entry point for file name completion |
50 | */ |
51 | QString KShellCompletion::makeCompletion(const QString &text) |
52 | { |
53 | // Split text at the last unquoted space |
54 | // |
55 | d->splitText(text, text_start&: d->m_text_start, text_compl&: d->m_text_compl); |
56 | |
57 | // Remove quotes from the text to be completed |
58 | // |
59 | QString tmp = d->unquote(text: d->m_text_compl); |
60 | d->m_text_compl = tmp; |
61 | |
62 | // Do exe-completion if there was no unquoted space |
63 | // |
64 | const bool is_exe_completion = !d->m_text_start.contains(c: d->m_word_break_char); |
65 | |
66 | setMode(is_exe_completion ? ExeCompletion : FileCompletion); |
67 | |
68 | // Make completion on the last part of text |
69 | // |
70 | return KUrlCompletion::makeCompletion(text: d->m_text_compl); |
71 | } |
72 | |
73 | /* |
74 | * postProcessMatch, postProcessMatches |
75 | * |
76 | * Called by KCompletion before emitting match() and matches() |
77 | * |
78 | * Add add the part of the text that was not completed |
79 | * Add quotes when needed |
80 | */ |
81 | void KShellCompletion::postProcessMatch(QString *match) const |
82 | { |
83 | KUrlCompletion::postProcessMatch(match); |
84 | |
85 | if (match->isNull()) { |
86 | return; |
87 | } |
88 | |
89 | if (match->endsWith(c: QLatin1Char('/'))) { |
90 | d->quoteText(text: match, force: false, skip_last: true); // don't quote the trailing '/' |
91 | } else { |
92 | d->quoteText(text: match, force: false, skip_last: false); // quote the whole text |
93 | } |
94 | |
95 | match->prepend(s: d->m_text_start); |
96 | } |
97 | |
98 | void KShellCompletion::postProcessMatches(QStringList *matches) const |
99 | { |
100 | KUrlCompletion::postProcessMatches(matches); |
101 | |
102 | for (QString &match : *matches) { |
103 | if (!match.isNull()) { |
104 | if (match.endsWith(c: QLatin1Char('/'))) { |
105 | d->quoteText(text: &match, force: false, skip_last: true); // don't quote trailing '/' |
106 | } else { |
107 | d->quoteText(text: &match, force: false, skip_last: false); // quote the whole text |
108 | } |
109 | |
110 | match.prepend(s: d->m_text_start); |
111 | } |
112 | } |
113 | } |
114 | |
115 | void KShellCompletion::postProcessMatches(KCompletionMatches *matches) const |
116 | { |
117 | KUrlCompletion::postProcessMatches(matches); |
118 | |
119 | for (auto &match : *matches) { |
120 | QString &matchString = match.value(); |
121 | if (!matchString.isNull()) { |
122 | if (matchString.endsWith(c: QLatin1Char('/'))) { |
123 | d->quoteText(text: &matchString, force: false, skip_last: true); // don't quote trailing '/' |
124 | } else { |
125 | d->quoteText(text: &matchString, force: false, skip_last: false); // quote the whole text |
126 | } |
127 | |
128 | matchString.prepend(s: d->m_text_start); |
129 | } |
130 | } |
131 | } |
132 | |
133 | /* |
134 | * splitText |
135 | * |
136 | * Split text at the last unquoted space |
137 | * |
138 | * text_start = [out] text at the left, including the space |
139 | * text_compl = [out] text at the right |
140 | */ |
141 | void KShellCompletionPrivate::splitText(const QString &text, QString &text_start, QString &text_compl) const |
142 | { |
143 | bool in_quote = false; |
144 | bool escaped = false; |
145 | QChar p_last_quote_char; |
146 | int last_unquoted_space = -1; |
147 | |
148 | for (int pos = 0; pos < text.length(); pos++) { |
149 | int end_space_len = 0; |
150 | |
151 | if (escaped) { |
152 | escaped = false; |
153 | } else if (in_quote && text[pos] == p_last_quote_char) { |
154 | in_quote = false; |
155 | } else if (!in_quote && text[pos] == m_quote_char1) { |
156 | p_last_quote_char = m_quote_char1; |
157 | in_quote = true; |
158 | } else if (!in_quote && text[pos] == m_quote_char2) { |
159 | p_last_quote_char = m_quote_char2; |
160 | in_quote = true; |
161 | } else if (text[pos] == m_escape_char) { |
162 | escaped = true; |
163 | } else if (!in_quote && text[pos] == m_word_break_char) { |
164 | end_space_len = 1; |
165 | |
166 | while (pos + 1 < text.length() && text[pos + 1] == m_word_break_char) { |
167 | end_space_len++; |
168 | pos++; |
169 | } |
170 | |
171 | if (pos + 1 == text.length()) { |
172 | break; |
173 | } |
174 | |
175 | last_unquoted_space = pos; |
176 | } |
177 | } |
178 | |
179 | text_start = text.left(n: last_unquoted_space + 1); |
180 | |
181 | // the last part without trailing blanks |
182 | text_compl = text.mid(position: last_unquoted_space + 1); |
183 | } |
184 | |
185 | /* |
186 | * quoteText() |
187 | * |
188 | * Add quotations to 'text' if needed or if 'force' = true |
189 | * Returns true if quotes were added |
190 | * |
191 | * skip_last => ignore the last character (we add a space or '/' to all filenames) |
192 | */ |
193 | bool KShellCompletionPrivate::quoteText(QString *text, bool force, bool skip_last) const |
194 | { |
195 | int pos = 0; |
196 | |
197 | if (!force) { |
198 | pos = text->indexOf(c: m_word_break_char); |
199 | if (skip_last && (pos == (int)(text->length()) - 1)) { |
200 | pos = -1; |
201 | } |
202 | } |
203 | |
204 | if (!force && pos == -1) { |
205 | pos = text->indexOf(c: m_quote_char1); |
206 | if (skip_last && (pos == (int)(text->length()) - 1)) { |
207 | pos = -1; |
208 | } |
209 | } |
210 | |
211 | if (!force && pos == -1) { |
212 | pos = text->indexOf(c: m_quote_char2); |
213 | if (skip_last && (pos == (int)(text->length()) - 1)) { |
214 | pos = -1; |
215 | } |
216 | } |
217 | |
218 | if (!force && pos == -1) { |
219 | pos = text->indexOf(c: m_escape_char); |
220 | if (skip_last && (pos == (int)(text->length()) - 1)) { |
221 | pos = -1; |
222 | } |
223 | } |
224 | |
225 | if (force || (pos >= 0)) { |
226 | // Escape \ in the string |
227 | text->replace(c: m_escape_char, after: QString(m_escape_char) + m_escape_char); |
228 | |
229 | // Escape " in the string |
230 | text->replace(c: m_quote_char1, after: QString(m_escape_char) + m_quote_char1); |
231 | |
232 | // " at the beginning |
233 | text->insert(i: 0, c: m_quote_char1); |
234 | |
235 | // " at the end |
236 | if (skip_last) { |
237 | text->insert(i: text->length() - 1, c: m_quote_char1); |
238 | } else { |
239 | text->insert(i: text->length(), c: m_quote_char1); |
240 | } |
241 | |
242 | return true; |
243 | } |
244 | |
245 | return false; |
246 | } |
247 | |
248 | /* |
249 | * unquote |
250 | * |
251 | * Remove quotes and return the result in a new string |
252 | * |
253 | */ |
254 | QString KShellCompletionPrivate::unquote(const QString &text) const |
255 | { |
256 | bool in_quote = false; |
257 | bool escaped = false; |
258 | QChar p_last_quote_char; |
259 | QString result; |
260 | |
261 | for (const QChar ch : text) { |
262 | if (escaped) { |
263 | escaped = false; |
264 | result.insert(i: result.length(), c: ch); |
265 | } else if (in_quote && ch == p_last_quote_char) { |
266 | in_quote = false; |
267 | } else if (!in_quote && ch == m_quote_char1) { |
268 | p_last_quote_char = m_quote_char1; |
269 | in_quote = true; |
270 | } else if (!in_quote && ch == m_quote_char2) { |
271 | p_last_quote_char = m_quote_char2; |
272 | in_quote = true; |
273 | } else if (ch == m_escape_char) { |
274 | escaped = true; |
275 | result.insert(i: result.length(), c: ch); |
276 | } else { |
277 | result.insert(i: result.length(), c: ch); |
278 | } |
279 | } |
280 | |
281 | return result; |
282 | } |
283 | |
284 | #include "moc_kshellcompletion.cpp" |
285 | |