Skip to content

Commit 904b23e

Browse files
committed
feat(console): add shell component with pipeline and operator support
Add new esp_shell component providing Unix-like shell functionality: - Pipeline support with '|' operator for command chaining - File output with support for '>' or '>>' for append. - Multiple command operators: ';' (continue), '&&' (break on fail), '||' (break on success) - Asynchronous command execution using console task API - Real-time I/O handling with select() for stdin/stdout/stderr - Proper resource management and error handling - Support for Ctrl+D EOF detection, will support escaping of if a program like grep Enables complex command sequences and data flow between commands similar to traditional Unix shells.
1 parent 177c199 commit 904b23e

File tree

3 files changed

+353
-1
lines changed

3 files changed

+353
-1
lines changed

components/console/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ set(srcs "commands.c"
44
"esp_console_common.c"
55
"esp_console_repl_internal.c"
66
"split_argv.c"
7-
"linenoise/linenoise.c")
7+
"linenoise/linenoise.c"
8+
"shell.c")
89

910
set(requires vfs)
1011

components/console/esp_shell.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
#pragma once
7+
8+
#include <stdbool.h>
9+
10+
esp_err_t esp_shell_run(char *command_line, int *cmd_ret);

components/console/shell.c

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
#include <errno.h>
7+
#include <fcntl.h>
8+
#include <inttypes.h>
9+
#include <string.h>
10+
#include <sys/select.h>
11+
#include <unistd.h>
12+
13+
#include "esp_console.h"
14+
#include "esp_err.h"
15+
#include "esp_shell.h"
16+
17+
#include "freertos/task.h"
18+
#include "linenoise/linenoise.h"
19+
20+
#define READ_BUFFER_SIZE 256
21+
#define MAX_TASK_DEPTH 5
22+
#define MAX(a,b) ((a) > (b) ? (a) : (b))
23+
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0]))
24+
25+
typedef struct {
26+
const char *command_line;
27+
esp_console_task_handle_t *task_handle;
28+
29+
int stdout_pipe[2];
30+
} cli_task_stack_entry_t;
31+
32+
/*
33+
* This function runs a single command line pipeline with optional output redirection.
34+
* It handles the actual pipeline execution and I/O multiplexing.
35+
*/
36+
static esp_err_t esp_shell_run_pipeline(char *command_line, FILE *output_file, int *cmd_ret)
37+
{
38+
cli_task_stack_entry_t cli_stack[MAX_TASK_DEPTH];
39+
int stack_index = 0;
40+
*cmd_ret = 0;
41+
esp_err_t err = ESP_OK;
42+
43+
// Split by '|' to get individual commands in pipeline
44+
char *saveptr = NULL;
45+
char *token = strtok_r(command_line, "|", &saveptr);
46+
47+
// Next task should read from the previous task's stdout
48+
int input_fd = fileno(stdin);
49+
50+
while (token) {
51+
52+
// Skip leading spaces.
53+
while (*token == ' ') {
54+
token++;
55+
}
56+
57+
// Skip empty tokens
58+
if (*token == '\0') {
59+
token = strtok_r(NULL, "|", &saveptr);
60+
continue;
61+
}
62+
63+
if (stack_index >= ARRAY_SIZE(cli_stack)) {
64+
fprintf(stderr, "Command stack overflow\n");
65+
err = ESP_FAIL;
66+
break;
67+
}
68+
69+
// Prepare next task entry, from here we increment stack_index so we need to be careful on error handling
70+
cli_task_stack_entry_t *task = &cli_stack[stack_index++];
71+
*task = (cli_task_stack_entry_t){
72+
.command_line = token,
73+
.task_handle = NULL,
74+
.stdout_pipe = { -1, -1 },
75+
};
76+
77+
// Create pipes for stdout
78+
if (pipe(task->stdout_pipe) != 0) {
79+
fprintf(stderr, "Failed to create output pipe: %s\n", strerror(errno));
80+
err = ESP_ERR_NO_MEM;
81+
stack_index--;
82+
break;
83+
}
84+
85+
// If successful, we account on this function to close stdin and stdout when task is completed
86+
err = esp_console_run_on_task(token, input_fd != fileno(stdin) ? fdopen(input_fd, "r") : NULL, fdopen(task->stdout_pipe[1], "w"), NULL,
87+
&task->task_handle);
88+
89+
if (err != ESP_OK) {
90+
switch(err) {
91+
case ESP_ERR_NOT_FOUND:
92+
fprintf(stderr, "Unrecognized command '%s'\n", token);
93+
break;
94+
default:
95+
fprintf(stderr, "Command '%s' Internal error: %s\n", token, esp_err_to_name(err));
96+
break;
97+
}
98+
close(task->stdout_pipe[0]);
99+
close(task->stdout_pipe[1]);
100+
stack_index--;
101+
break;
102+
}
103+
104+
input_fd = task->stdout_pipe[0]; // Pipe stdout from previous task to next task's stdin
105+
token = strtok_r(NULL, "|", &saveptr);
106+
}
107+
108+
// As long as there are tasks running, monitor their output and stdin
109+
while (stack_index > 0) {
110+
cli_task_stack_entry_t *tail_entry = &cli_stack[stack_index - 1];
111+
112+
// Check for completed tasks, clean up and get exit codes
113+
if (!esp_console_task_is_running(tail_entry->task_handle)) {
114+
115+
// Use the wait function to get exit code, it will be instant if already terminated (because of exception)
116+
int exit_code;
117+
esp_console_wait_task(tail_entry->task_handle, &exit_code);
118+
119+
if (exit_code == 0) {
120+
//printf("Command '%s' executed successfully\n", tail_entry->command_line);
121+
} else {
122+
fprintf(stderr, "Command '%s' returned exit code: %d\n", tail_entry->command_line, exit_code);
123+
// Accumulate non-zero exit codes for now, so we report failure if any command fails
124+
*cmd_ret += exit_code;
125+
}
126+
127+
esp_console_task_free(tail_entry->task_handle);
128+
stack_index--;
129+
130+
// Re-evaluate next iteration
131+
continue;
132+
}
133+
134+
135+
fd_set readfds;
136+
fd_set exceptfds;
137+
struct timeval tv;
138+
FD_ZERO(&readfds);
139+
FD_ZERO(&exceptfds);
140+
FD_SET(fileno(stdin), &readfds);
141+
142+
143+
FD_SET(input_fd, &readfds); // For reading output
144+
FD_SET(input_fd, &exceptfds); // For detecting closed pipe
145+
int max_fd = fileno(stdin);
146+
max_fd = MAX(max_fd, input_fd);
147+
148+
// Set timeout to avoid blocking forever
149+
tv.tv_sec = 5;
150+
tv.tv_usec = 0; // 1000000; // 100ms
151+
152+
int ret = select(max_fd + 1, &readfds, NULL, &exceptfds, &tv);
153+
154+
if (ret > 0) {
155+
// Select loop while task is running
156+
char read_buffer[READ_BUFFER_SIZE];
157+
158+
// Check stdout from last task in pipeline
159+
if (FD_ISSET(input_fd, &readfds)) {
160+
ssize_t n = read(input_fd, read_buffer, READ_BUFFER_SIZE - 1);
161+
if (n > 0) {
162+
if (output_file) {
163+
// Redirect to file
164+
fwrite(read_buffer, 1, n, output_file);
165+
fflush(output_file);
166+
} else {
167+
// Normal output to stdout
168+
fwrite(read_buffer, 1, n, stdout);
169+
fflush(stdout);
170+
}
171+
}
172+
}
173+
174+
} else if (ret < 0 && errno != EINTR) {
175+
fprintf(stderr, "select() error: %s\n", strerror(errno));
176+
break;
177+
}
178+
}
179+
180+
181+
if (input_fd != -1 && input_fd != fileno(stdin)) {
182+
close(input_fd);
183+
}
184+
185+
return err;
186+
}
187+
188+
/*
189+
* This function runs a single command line, which may be part of a pipeline.
190+
* It sets up output redirection and delegates pipeline execution to esp_shell_run_pipeline.
191+
*/
192+
static esp_err_t esp_shell_run_single(char *command_line, int *cmd_ret)
193+
{
194+
// Check for output redirection (> or >> filename) first
195+
char *redirect_pos = NULL;
196+
char *append_pos = strstr(command_line, ">>");
197+
char *write_pos = strstr(command_line, ">");
198+
bool append_mode = false;
199+
FILE *output_file = NULL;
200+
char *filename = NULL;
201+
202+
// Check for >> first (longer pattern), then >
203+
if (append_pos) {
204+
redirect_pos = append_pos;
205+
append_mode = true;
206+
} else if (write_pos) {
207+
redirect_pos = write_pos;
208+
append_mode = false;
209+
}
210+
211+
if (redirect_pos) {
212+
// Null-terminate the command part before '>' or '>>'
213+
*redirect_pos = '\0';
214+
215+
// Find the filename after '>' or '>>'
216+
filename = redirect_pos + (append_mode ? 2 : 1);
217+
while (*filename == ' ') {
218+
filename++; // Skip spaces
219+
}
220+
221+
// Remove trailing spaces/newlines from filename
222+
char *end = filename + strlen(filename) - 1;
223+
while (end > filename && (*end == ' ' || *end == '\n' || *end == '\r')) {
224+
*end-- = '\0';
225+
}
226+
227+
if (*filename != '\0') {
228+
output_file = fopen(filename, append_mode ? "a" : "w");
229+
if (!output_file) {
230+
fprintf(stderr, "Failed to open file '%s' for %s: %s\n", filename,
231+
append_mode ? "appending" : "writing", strerror(errno));
232+
return ESP_FAIL;
233+
}
234+
} else {
235+
fprintf(stderr, "No filename specified after '%s'\n", append_mode ? ">>" : ">");
236+
return ESP_FAIL;
237+
}
238+
}
239+
240+
// Run the pipeline with the specified output file (or NULL for stdout)
241+
esp_err_t result = esp_shell_run_pipeline(command_line, output_file, cmd_ret);
242+
243+
// Close output file if it was opened
244+
if (output_file) {
245+
fclose(output_file);
246+
}
247+
248+
return result;
249+
}
250+
251+
/*
252+
* This function runs a command line, which may contain multiple commands separated by ';', '&&', or '||'.
253+
* ';' - Execute commands sequentially, continue on failure
254+
* '&&' - Execute commands sequentially, break on failure
255+
* '||' - Execute commands sequentially, skip next command on success
256+
*/
257+
esp_err_t esp_shell_run(char *command_line, int *cmd_ret)
258+
{
259+
char *current_pos = command_line;
260+
*cmd_ret = 0;
261+
enum { OP_SEMICOLON, OP_AND, OP_OR } operator_type;
262+
263+
while (*current_pos != '\0') {
264+
// Find the next separator (';', '&&', or '||')
265+
char *separator_pos = NULL;
266+
operator_type = OP_SEMICOLON; // Default to semicolon
267+
268+
// Look for '&&' and '||' first (longer patterns)
269+
char *and_pos = strstr(current_pos, "&&");
270+
char *or_pos = strstr(current_pos, "||");
271+
char *semicolon_pos = strchr(current_pos, ';');
272+
273+
// Find the earliest separator
274+
char *earliest_pos = NULL;
275+
if (and_pos && (!earliest_pos || and_pos < earliest_pos)) {
276+
earliest_pos = and_pos;
277+
operator_type = OP_AND;
278+
}
279+
if (or_pos && (!earliest_pos || or_pos < earliest_pos)) {
280+
earliest_pos = or_pos;
281+
operator_type = OP_OR;
282+
}
283+
if (semicolon_pos && (!earliest_pos || semicolon_pos < earliest_pos)) {
284+
earliest_pos = semicolon_pos;
285+
operator_type = OP_SEMICOLON;
286+
}
287+
288+
separator_pos = earliest_pos;
289+
290+
// Extract the current command
291+
char *command_end = separator_pos ? separator_pos : current_pos + strlen(current_pos);
292+
size_t command_len = command_end - current_pos;
293+
294+
// Create a null-terminated copy of the command
295+
char command_buffer[command_len + 1];
296+
strncpy(command_buffer, current_pos, command_len);
297+
command_buffer[command_len] = '\0';
298+
299+
// Skip leading spaces
300+
char *command = command_buffer;
301+
while (*command == ' ') {
302+
command++;
303+
}
304+
305+
// Skip trailing spaces
306+
char *end = command + strlen(command) - 1;
307+
while (end > command && *end == ' ') {
308+
*end-- = '\0';
309+
}
310+
311+
// Skip empty commands
312+
if (*command != '\0') {
313+
esp_err_t ret = esp_shell_run_single(command, cmd_ret);
314+
bool command_failed = (ret != ESP_OK || *cmd_ret != 0);
315+
316+
// Handle failure/success based on operator type
317+
if (operator_type == OP_AND) {
318+
// '&&' operator: break on failure
319+
if (command_failed) {
320+
return ret != ESP_OK ? ret : ESP_OK;
321+
}
322+
} else if (operator_type == OP_OR) {
323+
// '||' operator: break on success
324+
if (!command_failed) {
325+
return ESP_OK;
326+
}
327+
}
328+
// ';' operator: continue regardless (do nothing)
329+
}
330+
331+
// Move to next command
332+
if (separator_pos) {
333+
int separator_len = (operator_type == OP_AND || operator_type == OP_OR) ? 2 : 1;
334+
current_pos = separator_pos + separator_len;
335+
} else {
336+
break;
337+
}
338+
}
339+
340+
return ESP_OK;
341+
}

0 commit comments

Comments
 (0)