1 /*
2 Command line widget.
3 This widget is derived from the WInput widget, it's used to cope
4 with all the magic of the command input line, we depend on some
5 help from the program's callback.
6
7 Copyright (C) 1995-2026
8 Free Software Foundation, Inc.
9
10 Written by:
11 Slava Zanko <slavazanko@gmail.com>, 2013
12 Andrew Borodin <aborodin@vmail.ru>, 2011-2022
13 Marek Libra <marek.libra@gmail.com>, 2026
14
15 This file is part of the Midnight Commander.
16
17 The Midnight Commander is free software: you can redistribute it
18 and/or modify it under the terms of the GNU General Public License as
19 published by the Free Software Foundation, either version 3 of the License,
20 or (at your option) any later version.
21
22 The Midnight Commander is distributed in the hope that it will be useful,
23 but WITHOUT ANY WARRANTY; without even the implied warranty of
24 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 GNU General Public License for more details.
26
27 You should have received a copy of the GNU General Public License
28 along with this program. If not, see <https://www.gnu.org/licenses/>.
29 */
30
31 /** \file command.c
32 * \brief Source: command line widget
33 */
34
35 #include <config.h>
36
37 #include <stdlib.h>
38 #include <string.h>
39
40 #include "lib/global.h"
41 #include "lib/vfs/vfs.h" // vfs_current_is_local()
42 #include "lib/skin.h"
43 #include "lib/util.h" // whitespace()
44 #include "lib/widget.h"
45
46 #include "src/setup.h" // quit
47 #ifdef ENABLE_SUBSHELL
48 #include "src/subshell/subshell.h"
49 #endif
50 #include "src/execute.h" // shell_execute()
51 #include "src/usermenu.h" // expand_format()
52
53 #include "filemanager.h" // quiet_quit_cmd(), layout.h
54 #include "cd.h" // cd_to()
55
56 #include "command.h"
57
58 /*** global variables ****************************************************************************/
59
60 /* This holds the command line */
61 WInput *cmdline;
62
63 /*** file scope macro definitions ****************************************************************/
64
65 /*** file scope type declarations ****************************************************************/
66
67 /*** forward declarations (file scope functions) *************************************************/
68
69 #ifdef HAVE_TESTS
70 MC_TESTABLE gboolean command_has_unquoted_metacharacters (const char *cmd);
71 #endif
72
73 /*** file scope variables ************************************************************************/
74
75 /* Color styles command line */
76 static const input_colors_t command_colors = {
77 [INPUT_COLOR_MAIN] = CORE_COMMANDLINE_COLOR,
78 [INPUT_COLOR_MARK] = CORE_COMMANDLINE_MARK_COLOR,
79 [INPUT_COLOR_UNCHANGED] = CORE_COMMANDLINE_COLOR,
80 [INPUT_COLOR_HISTORY] = CORE_COMMAND_HISTORY_COLOR,
81 };
82
83 /* --------------------------------------------------------------------------------------------- */
84 /*** file scope functions ************************************************************************/
85 /* --------------------------------------------------------------------------------------------- */
86 /**
87 * Detect shell syntax that the internal cd handler cannot process.
88 *
89 * Returns TRUE when any of the following appears outside of single quotes
90 * and backslash escaping:
91 * - Command separators/operators: ; | &
92 * - Command substitution: $( or ` (including inside double quotes,
93 * where the shell still expands them)
94 *
95 * Note: plain parameter expansion ($VAR, ${VAR}) is intentionally excluded
96 * because the internal cd handler expands environment variables itself.
97 *
98 * Motivation: the internal cd handler cannot expand command substitutions
99 * nor execute compound commands. When any of these constructs are present,
100 * the entire command line must be passed to the shell.
101 *
102 * Only single quotes and backslash fully suppress detection. Double quotes
103 * suppress ; | & but NOT $( and ` because the shell expands command
104 * substitution inside double quotes.
105 *
106 * Parsing strategy: forward scan tracking quote state.
107 * We treat $( and ` as immediate indicators rather than parsing their
108 * contents, because internal cd cannot handle them regardless
109 * of what is inside.
110 */
111
112 MC_TESTABLE gboolean
113 command_has_unquoted_metacharacters (const char *cmd)
/* ![[previous]](../icons/n_left.png)
![[next]](../icons/right.png)
![[first]](../icons/n_first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
114 {
115 gboolean in_single_quote = FALSE;
116 gboolean in_double_quote = FALSE;
117
118 for (const char *p = cmd; *p != '\0'; p++)
119 {
120 if (in_single_quote)
121 {
122 if (*p == '\'')
123 in_single_quote = FALSE;
124 continue;
125 }
126
127 if (*p == '\\' && p[1] != '\0')
128 {
129 p++;
130 continue;
131 }
132
133 if (*p == '`')
134 return TRUE;
135
136 if (*p == '$' && p[1] == '(')
137 return TRUE;
138
139 if (in_double_quote)
140 {
141 if (*p == '"')
142 in_double_quote = FALSE;
143 continue;
144 }
145
146 if (*p == '\'')
147 {
148 in_single_quote = TRUE;
149 continue;
150 }
151
152 if (*p == '"')
153 {
154 in_double_quote = TRUE;
155 continue;
156 }
157
158 if (*p == ';' || *p == '|' || *p == '&')
159 return TRUE;
160 }
161
162 return FALSE;
163 }
164
165 /* --------------------------------------------------------------------------------------------- */
166
167 /** Handle Enter on the command line
168 *
169 * @param lc_cmdline string for handling
170 * @return MSG_HANDLED on success else MSG_NOT_HANDLED
171 */
172
173 static cb_ret_t
174 enter (WInput *lc_cmdline)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
175 {
176 const char *cmd;
177
178 if (!command_prompt)
179 return MSG_HANDLED;
180
181 cmd = input_get_ctext (lc_cmdline);
182
183 // Any initial whitespace should be removed at this point
184 while (whiteness (*cmd))
185 cmd++;
186
187 if (*cmd == '\0')
188 return MSG_HANDLED;
189
190 if (strncmp (cmd, "cd", 2) == 0 && (cmd[2] == '\0' || whitespace (cmd[2]))
191 && !command_has_unquoted_metacharacters (cmd))
192 {
193 cd_to (cmd + 2);
194 input_clean (lc_cmdline);
195 return MSG_HANDLED;
196 }
197 else if (strcmp (cmd, "exit") == 0)
198 {
199 input_assign_text (lc_cmdline, "");
200 if (!quiet_quit_cmd (FALSE))
201 return MSG_NOT_HANDLED;
202 }
203 else
204 {
205 GString *command;
206 size_t i;
207
208 if (!vfs_current_is_local ())
209 {
210 message (D_ERROR, MSG_ERROR, _ ("Cannot execute commands on non-local filesystems"));
211 return MSG_NOT_HANDLED;
212 }
213 #ifdef ENABLE_SUBSHELL
214 /* Check this early before we clean command line
215 * (will be checked again by shell_execute) */
216 if (mc_global.tty.use_subshell && subshell_state != INACTIVE)
217 {
218 message (D_ERROR, MSG_ERROR, _ ("The shell is already running a command"));
219 return MSG_NOT_HANDLED;
220 }
221 #endif
222 command = g_string_sized_new (32);
223
224 for (i = 0; cmd[i] != '\0'; i++)
225 {
226 if (cmd[i] != '%')
227 g_string_append_c (command, cmd[i]);
228 else
229 {
230 GString *s;
231
232 s = expand_format (NULL, cmd[++i], TRUE);
233 if (s != NULL)
234 {
235 mc_g_string_concat (command, s);
236 g_string_free (s, TRUE);
237 }
238 }
239 }
240
241 input_clean (lc_cmdline);
242 shell_execute (command->str, 0);
243 g_string_free (command, TRUE);
244
245 #ifdef ENABLE_SUBSHELL
246 if ((quit & SUBSHELL_EXIT) != 0)
247 {
248 if (quiet_quit_cmd (FALSE))
249 return MSG_HANDLED;
250
251 quit = 0;
252 // restart subshell
253 if (mc_global.tty.use_subshell)
254 init_subshell ();
255 }
256
257 if (mc_global.tty.use_subshell)
258 do_load_prompt ();
259 #endif
260 }
261 return MSG_HANDLED;
262 }
263
264 /* --------------------------------------------------------------------------------------------- */
265
266 /**
267 * Default command line callback
268 *
269 * @param w Widget object
270 * @param msg message for handling
271 * @param parm extra parameter such as key code
272 *
273 * @return MSG_NOT_HANDLED on fail else MSG_HANDLED
274 */
275
276 static cb_ret_t
277 command_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
278 {
279 switch (msg)
280 {
281 case MSG_KEY:
282 // Special case: we handle the enter key
283 if (parm == '\n')
284 return enter (INPUT (w));
285 MC_FALLTHROUGH;
286
287 default:
288 return input_callback (w, sender, msg, parm, data);
289 }
290 }
291
292 /* --------------------------------------------------------------------------------------------- */
293 /*** public functions ****************************************************************************/
294 /* --------------------------------------------------------------------------------------------- */
295
296 WInput *
297 command_new (int y, int x, int cols)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
298 {
299 WInput *cmd;
300 Widget *w;
301
302 cmd = input_new (y, x, command_colors, cols, "", "cmdline",
303 INPUT_COMPLETE_FILENAMES | INPUT_COMPLETE_VARIABLES | INPUT_COMPLETE_USERNAMES
304 | INPUT_COMPLETE_HOSTNAMES | INPUT_COMPLETE_CD | INPUT_COMPLETE_COMMANDS
305 | INPUT_COMPLETE_SHELL_ESC);
306 w = WIDGET (cmd);
307 // Don't set WOP_SELECTABLE up, otherwise panels will be unselected
308 widget_set_options (w, WOP_SELECTABLE, FALSE);
309 // Add our hooks
310 w->callback = command_callback;
311
312 return cmd;
313 }
314
315 /* --------------------------------------------------------------------------------------------- */
316 /**
317 * Insert quoted text in input line. The function is meant for the
318 * command line, so the percent sign is quoted as well.
319 *
320 * @param in WInput object
321 * @param text string for insertion
322 * @param insert_extra_space add extra space
323 */
324
325 void
326 command_insert (WInput *in, const char *text, gboolean insert_extra_space)
/* ![[previous]](../icons/left.png)
![[next]](../icons/n_right.png)
![[first]](../icons/first.png)
![[last]](../icons/n_last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
327 {
328 GString *quoted_text;
329
330 quoted_text = name_quote (text, TRUE);
331 if (quoted_text != NULL)
332 {
333 input_insert (in, quoted_text->str, insert_extra_space);
334 g_string_free (quoted_text, TRUE);
335 }
336 }
337
338 /* --------------------------------------------------------------------------------------------- */