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 if (escaped) {
150 escaped = false;
151 } else if (in_quote && text[pos] == p_last_quote_char) {
152 in_quote = false;
153 } else if (!in_quote && text[pos] == m_quote_char1) {
154 p_last_quote_char = m_quote_char1;
155 in_quote = true;
156 } else if (!in_quote && text[pos] == m_quote_char2) {
157 p_last_quote_char = m_quote_char2;
158 in_quote = true;
159 } else if (text[pos] == m_escape_char) {
160 escaped = true;
161 } else if (!in_quote && text[pos] == m_word_break_char) {
162 while (pos + 1 < text.length() && text[pos + 1] == m_word_break_char) {
163 pos++;
164 }
165
166 if (pos + 1 == text.length()) {
167 break;
168 }
169
170 last_unquoted_space = pos;
171 }
172 }
173
174 text_start = text.left(n: last_unquoted_space + 1);
175
176 // the last part without trailing blanks
177 text_compl = text.mid(position: last_unquoted_space + 1);
178}
179
180/*
181 * quoteText()
182 *
183 * Add quotations to 'text' if needed or if 'force' = true
184 * Returns true if quotes were added
185 *
186 * skip_last => ignore the last character (we add a space or '/' to all filenames)
187 */
188bool KShellCompletionPrivate::quoteText(QString *text, bool force, bool skip_last) const
189{
190 int pos = 0;
191
192 if (!force) {
193 pos = text->indexOf(ch: m_word_break_char);
194 if (skip_last && (pos == (int)(text->length()) - 1)) {
195 pos = -1;
196 }
197 }
198
199 if (!force && pos == -1) {
200 pos = text->indexOf(ch: m_quote_char1);
201 if (skip_last && (pos == (int)(text->length()) - 1)) {
202 pos = -1;
203 }
204 }
205
206 if (!force && pos == -1) {
207 pos = text->indexOf(ch: m_quote_char2);
208 if (skip_last && (pos == (int)(text->length()) - 1)) {
209 pos = -1;
210 }
211 }
212
213 if (!force && pos == -1) {
214 pos = text->indexOf(ch: m_escape_char);
215 if (skip_last && (pos == (int)(text->length()) - 1)) {
216 pos = -1;
217 }
218 }
219
220 if (force || (pos >= 0)) {
221 // Escape \ in the string
222 text->replace(c: m_escape_char, after: QString(m_escape_char) + m_escape_char);
223
224 // Escape " in the string
225 text->replace(c: m_quote_char1, after: QString(m_escape_char) + m_quote_char1);
226
227 // " at the beginning
228 text->insert(i: 0, c: m_quote_char1);
229
230 // " at the end
231 if (skip_last) {
232 text->insert(i: text->length() - 1, c: m_quote_char1);
233 } else {
234 text->insert(i: text->length(), c: m_quote_char1);
235 }
236
237 return true;
238 }
239
240 return false;
241}
242
243/*
244 * unquote
245 *
246 * Remove quotes and return the result in a new string
247 *
248 */
249QString KShellCompletionPrivate::unquote(const QString &text) const
250{
251 bool in_quote = false;
252 bool escaped = false;
253 QChar p_last_quote_char;
254 QString result;
255
256 for (const QChar ch : text) {
257 if (escaped) {
258 escaped = false;
259 result.insert(i: result.length(), c: ch);
260 } else if (in_quote && ch == p_last_quote_char) {
261 in_quote = false;
262 } else if (!in_quote && ch == m_quote_char1) {
263 p_last_quote_char = m_quote_char1;
264 in_quote = true;
265 } else if (!in_quote && ch == m_quote_char2) {
266 p_last_quote_char = m_quote_char2;
267 in_quote = true;
268 } else if (ch == m_escape_char) {
269 escaped = true;
270 result.insert(i: result.length(), c: ch);
271 } else {
272 result.insert(i: result.length(), c: ch);
273 }
274 }
275
276 return result;
277}
278
279#include "moc_kshellcompletion.cpp"
280

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