Skip to content

Commit db89c89

Browse files
committed
Ticket #73: do not intercept 'cd' in compound commands
Skip internal cd handling when the command line contains unescaped ';', '&&', or '||', letting the shell execute it instead. Signed-off-by: Marek Libra <marek.libra@gmail.com>
1 parent 4115fb9 commit db89c89

4 files changed

Lines changed: 190 additions & 2 deletions

File tree

src/filemanager/command.c

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,73 @@ static const input_colors_t command_colors = {
7979
/*** file scope functions ************************************************************************/
8080
/* --------------------------------------------------------------------------------------------- */
8181

82+
/**
83+
* Checks if a command string contains unquoted shell metacharacters (;, |, &).
84+
* Handles single quotes, double quotes, and backslash escapes.
85+
*/
86+
87+
gboolean
88+
has_shell_metacharacters (const char *cmd)
89+
{
90+
gboolean in_single_quote = FALSE;
91+
gboolean in_double_quote = FALSE;
92+
const char *p;
93+
GError *error = NULL;
94+
95+
if (cmd[0] == '\0')
96+
return FALSE;
97+
98+
if (!g_shell_parse_argv (cmd, NULL, NULL, &error))
99+
{
100+
g_error_free (error);
101+
return TRUE;
102+
}
103+
104+
for (p = cmd; *p != '\0'; p++)
105+
{
106+
if (in_single_quote)
107+
{
108+
if (*p == '\'')
109+
in_single_quote = FALSE;
110+
continue;
111+
}
112+
113+
if (in_double_quote)
114+
{
115+
if (*p == '\\' && p[1] != '\0')
116+
p++;
117+
else if (*p == '"')
118+
in_double_quote = FALSE;
119+
continue;
120+
}
121+
122+
if (*p == '\\' && p[1] != '\0')
123+
{
124+
p++;
125+
continue;
126+
}
127+
128+
if (*p == '\'')
129+
{
130+
in_single_quote = TRUE;
131+
continue;
132+
}
133+
134+
if (*p == '"')
135+
{
136+
in_double_quote = TRUE;
137+
continue;
138+
}
139+
140+
if (*p == ';' || *p == '|' || *p == '&')
141+
return TRUE;
142+
}
143+
144+
return FALSE;
145+
}
146+
147+
/* --------------------------------------------------------------------------------------------- */
148+
82149
/** Handle Enter on the command line
83150
*
84151
* @param lc_cmdline string for handling
@@ -102,7 +169,8 @@ enter (WInput *lc_cmdline)
102169
if (*cmd == '\0')
103170
return MSG_HANDLED;
104171

105-
if (strncmp (cmd, "cd", 2) == 0 && (cmd[2] == '\0' || whitespace (cmd[2])))
172+
if (strncmp (cmd, "cd", 2) == 0 && (cmd[2] == '\0' || whitespace (cmd[2]))
173+
&& !has_shell_metacharacters (cmd))
106174
{
107175
cd_to (cmd + 2);
108176
input_clean (lc_cmdline);

src/filemanager/command.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern WInput *cmdline;
2121

2222
WInput *command_new (int y, int x, int len);
2323
void command_insert (WInput *in, const char *text, gboolean insert_extra_space);
24+
gboolean has_shell_metacharacters (const char *cmd);
2425

2526
/*** inline functions ****************************************************************************/
2627
#endif

tests/src/filemanager/Makefile.am

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ TESTS = \
2121
exec_get_export_variables_ext \
2222
ext__exec_make_shell_string \
2323
filegui_is_wildcarded \
24-
get_random_hint
24+
get_random_hint \
25+
has_shell_metacharacters
2526

2627
check_PROGRAMS = $(TESTS)
2728

@@ -42,3 +43,6 @@ get_random_hint_SOURCES = \
4243

4344
filegui_is_wildcarded_SOURCES = \
4445
filegui_is_wildcarded.c
46+
47+
has_shell_metacharacters_SOURCES = \
48+
has_shell_metacharacters.c
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
src/filemanager - tests for has_shell_metacharacters() function
3+
4+
Copyright (C) 2026
5+
Free Software Foundation, Inc.
6+
7+
This file is part of the Midnight Commander.
8+
9+
The Midnight Commander is free software: you can redistribute it
10+
and/or modify it under the terms of the GNU General Public License as
11+
published by the Free Software Foundation, either version 3 of the License,
12+
or (at your option) any later version.
13+
14+
The Midnight Commander is distributed in the hope that it will be useful,
15+
but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
GNU General Public License for more details.
18+
19+
You should have received a copy of the GNU General Public License
20+
along with this program. If not, see <https://www.gnu.org/licenses/>.
21+
*/
22+
23+
#define TEST_SUITE_NAME "/src/filemanager"
24+
25+
#include "tests/mctest.h"
26+
27+
#include "src/filemanager/command.h"
28+
29+
/* --------------------------------------------------------------------------------------------- */
30+
31+
/* @DataSource("test_has_shell_metacharacters_ds") */
32+
static const struct test_has_shell_metacharacters_ds
33+
{
34+
const char *input_value;
35+
gboolean expected_result;
36+
} test_has_shell_metacharacters_ds[] = {
37+
{ "", FALSE },
38+
{ "cd", FALSE },
39+
{ "cd /tmp", FALSE },
40+
{ "cd ..", FALSE },
41+
{ "cd ~/projects", FALSE },
42+
{ "cd $HOME", FALSE },
43+
{ "cd -", FALSE },
44+
{ "cd /path/to/dir", FALSE },
45+
46+
{ "cd ..; make", TRUE },
47+
{ "cd ..; make; cd -", TRUE },
48+
{ "cd /tmp; ls", TRUE },
49+
{ ";", TRUE },
50+
{ "cd foo;bar", TRUE },
51+
52+
{ "cd .. && make", TRUE },
53+
{ "cd /tmp && ls && pwd", TRUE },
54+
{ "&&", TRUE },
55+
56+
{ "cd .. || echo fail", TRUE },
57+
{ "||", TRUE },
58+
59+
/* single & and | are shell metacharacters too */
60+
{ "cd &", TRUE },
61+
{ "cd foo | bar", TRUE },
62+
{ "cd a&b", TRUE },
63+
64+
/* escaped metacharacters should NOT trigger */
65+
{ "cd foo\\;bar", FALSE },
66+
{ "cd path\\;with\\;semicolons", FALSE },
67+
68+
/* escaped semicolon at start of string */
69+
{ "\\;", FALSE },
70+
71+
/* mix: escaped semicolons plus a real one */
72+
{ "cd foo\\;bar; make", TRUE },
73+
74+
/* quoted metacharacters should NOT trigger */
75+
{ "cd 'foo;bar'", FALSE },
76+
{ "cd \"foo;bar\"", FALSE },
77+
{ "cd 'foo|bar'", FALSE },
78+
{ "cd 'foo&bar'", FALSE },
79+
80+
/* unbalanced quotes should trigger (let the shell report the error) */
81+
{ "cd 'foo", TRUE },
82+
};
83+
84+
/* @Test(dataSource = "test_has_shell_metacharacters_ds") */
85+
START_PARAMETRIZED_TEST (test_has_shell_metacharacters, test_has_shell_metacharacters_ds)
86+
{
87+
// given
88+
gboolean actual_result;
89+
90+
// when
91+
actual_result = has_shell_metacharacters (data->input_value);
92+
93+
// then
94+
ck_assert_int_eq (actual_result, data->expected_result);
95+
}
96+
END_PARAMETRIZED_TEST
97+
98+
/* --------------------------------------------------------------------------------------------- */
99+
100+
int
101+
main (void)
102+
{
103+
TCase *tc_core;
104+
105+
tc_core = tcase_create ("Core");
106+
107+
// Add new tests here: ***************
108+
mctest_add_parameterized_test (tc_core, test_has_shell_metacharacters,
109+
test_has_shell_metacharacters_ds);
110+
// ***********************************
111+
112+
return mctest_run_all (tc_core);
113+
}
114+
115+
/* --------------------------------------------------------------------------------------------- */

0 commit comments

Comments
 (0)