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
14class KShellCompletionPrivate
15{
16public:
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
38KShellCompletion::KShellCompletion()
39 : KUrlCompletion()
40 , d(new KShellCompletionPrivate)
41{
42}
43
44KShellCompletion::~KShellCompletion() = default;
45
46/*
47 * makeCompletion()
48 *
49 * Entry point for file name completion
50 */
51QString 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 */
81void 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
98void 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
115void 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 */
141void 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 */
193bool 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 */
254QString 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

source code of kio/src/widgets/kshellcompletion.cpp