1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

Merge pull request #20651 from benpicco/shell_scripting

sys/shell: add support for running a batch of commands from a file
This commit is contained in:
benpicco 2024-05-15 09:35:04 +00:00 committed by GitHub
commit 22795248eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 301 additions and 10 deletions

View File

@ -225,6 +225,33 @@ static inline void shell_run(const shell_command_t *commands,
shell_run_forever(commands, line_buf, len);
}
/**
* @brief Parse and run a line of text as a shell command with
* arguments.
*
* @param[in] commands ptr to array of command structs
* @param[in] line The input line to parse
*
* @returns return value of the found command
* @returns -ENOEXEC if no valid command could be found
*/
int shell_handle_input_line(const shell_command_t *commands, char *line);
/**
* @brief Read shell commands from a file and run them.
*
* @note This requires the `vfs` module.
*
* @param[in] commands ptr to array of command structs
* @param[in] filename file to read shell commands from
* @param[out] line_nr line on which an error occurred, may be NULL
*
* @returns 0 if all commands were executed successful
* error return of failed command otherwise
*/
int shell_parse_file(const shell_command_t *commands,
const char *filename, unsigned *line_nr);
#ifndef __cplusplus
/**
* @brief Define shell command

View File

@ -841,6 +841,20 @@ int vfs_open(const char *name, int flags, mode_t mode);
*/
ssize_t vfs_read(int fd, void *dest, size_t count);
/**
* @brief Read a line from an open text file
*
* Reads from a file until a `\r` or `\n` character is found.
*
* @param[in] fd fd number obtained from vfs_open
* @param[out] dest destination buffer to hold the line
* @param[in] count maximum number of characters to read
*
* @return number of bytes read on success
* @return <0 on error
*/
ssize_t vfs_readline(int fd, char *dest, size_t count);
/**
* @brief Write bytes to an open file
*

View File

@ -20,6 +20,9 @@
#ifndef VFS_UTIL_H
#define VFS_UTIL_H
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
@ -115,6 +118,15 @@ int vfs_file_sha256(const char* file, void *digest,
*/
int vfs_is_dir(const char *path);
/**
@brief Checks if @p path is a file and can be read.
*
* @param[in] path Path to check
*
* @return true if the file exists, false otherwise
*/
bool vfs_file_exists(const char *path);
/**
* @brief Behaves like `rm -r @p root`.
*

View File

@ -41,6 +41,11 @@
#include "shell.h"
#include "shell_lock.h"
#ifdef MODULE_VFS
#include <fcntl.h>
#include "vfs.h"
#endif
/* define shell command cross file array */
XFA_INIT_CONST(shell_command_xfa_t*, shell_commands_xfa);
@ -206,7 +211,7 @@ static void print_help(const shell_command_t *command_list)
*
*
*/
static void handle_input_line(const shell_command_t *command_list, char *line)
int shell_handle_input_line(const shell_command_t *command_list, char *line)
{
/* first we need to calculate the number of arguments */
int argc = 0;
@ -297,11 +302,11 @@ static void handle_input_line(const shell_command_t *command_list, char *line)
if (pstate != PARSE_BLANK && pstate != PARSE_UNQUOTED) {
printf("shell: incorrect quoting\n");
return;
return -EINVAL;
}
if (argc == 0) {
return;
return 0;
}
/* then we fill the argv array */
@ -327,19 +332,23 @@ static void handle_input_line(const shell_command_t *command_list, char *line)
shell_pre_command_hook(argc, argv);
int res = handler(argc, argv);
shell_post_command_hook(res, argc, argv);
return res;
}
else {
handler(argc, argv);
return handler(argc, argv);
}
}
else {
if (strcmp("help", argv[0]) == 0) {
print_help(command_list);
return 0;
}
else {
printf("shell: command not found: %s\n", argv[0]);
}
}
return -ENOEXEC;
}
__attribute__((weak)) void shell_post_readline_hook(void)
@ -518,10 +527,51 @@ void shell_run_once(const shell_command_t *shell_commands,
break;
default:
handle_input_line(shell_commands, line_buf);
shell_handle_input_line(shell_commands, line_buf);
break;
}
print_prompt();
}
}
#ifdef MODULE_VFS
int shell_parse_file(const shell_command_t *shell_commands,
const char *filename, unsigned *line_nr)
{
char buffer[SHELL_DEFAULT_BUFSIZE];
if (line_nr) {
*line_nr = 0;
}
int res, fd = vfs_open(filename, O_RDONLY, 0);
if (fd < 0) {
printf("Can't open %s\n", filename);
return fd;
}
while (1) {
res = vfs_readline(fd, buffer, sizeof(buffer));
if (line_nr) {
*line_nr += 1;
}
/* error reading line */
if (res < 0) {
break;
}
/* skip comment and empty lines */
if (buffer[0] == '#') {
continue;
}
res = shell_handle_input_line(shell_commands, buffer);
if (res) {
break;
}
}
vfs_close(fd);
return res;
}
#endif

View File

@ -317,28 +317,80 @@ int vfs_open(const char *name, int flags, mode_t mode)
return fd;
}
ssize_t vfs_read(int fd, void *dest, size_t count)
static inline int _prep_read(int fd, const void *dest, vfs_file_t **filp)
{
DEBUG("vfs_read: %d, %p, %" PRIuSIZE "\n", fd, dest, count);
if (dest == NULL) {
return -EFAULT;
}
int res = _fd_is_valid(fd);
if (res < 0) {
return res;
}
vfs_file_t *filp = &_vfs_open_files[fd];
if (((filp->flags & O_ACCMODE) != O_RDONLY) & ((filp->flags & O_ACCMODE) != O_RDWR)) {
*filp = &_vfs_open_files[fd];
if ((((*filp)->flags & O_ACCMODE) != O_RDONLY) &&
(((*filp)->flags & O_ACCMODE) != O_RDWR)) {
/* File not open for reading */
return -EBADF;
}
if (filp->f_op->read == NULL) {
if ((*filp)->f_op->read == NULL) {
/* driver does not implement read() */
return -EINVAL;
}
return 0;
}
ssize_t vfs_read(int fd, void *dest, size_t count)
{
DEBUG("vfs_read: %d, %p, %" PRIuSIZE "\n", fd, dest, count);
vfs_file_t *filp = NULL;
int res = _prep_read(fd, dest, &filp);
if (res) {
DEBUG("vfs_read: can't open file - %d\n", res);
return res;
}
return filp->f_op->read(filp, dest, count);
}
ssize_t vfs_readline(int fd, char *dst, size_t len_max)
{
DEBUG("vfs_readline: %d, %p, %" PRIuSIZE "\n", fd, (void *)dst, len_max);
vfs_file_t *filp = NULL;
int res = _prep_read(fd, dst, &filp);
if (res) {
DEBUG("vfs_readline: can't open file - %d\n", res);
return res;
}
const char *start = dst;
while (len_max) {
int res = filp->f_op->read(filp, dst, 1);
if (res < 0) {
break;
}
if (*dst == '\r' || *dst == '\n' || res == 0) {
*dst = 0;
++dst;
break;
} else {
--len_max;
++dst;
}
}
if (len_max == 0) {
return -E2BIG;
}
return dst - start;
}
ssize_t vfs_write(int fd, const void *src, size_t count)
{
DEBUG_NOT_STDOUT(fd, "vfs_write: %d, %p, %" PRIuSIZE "\n", fd, src, count);

View File

@ -171,6 +171,17 @@ int vfs_is_dir(const char *path)
return ((stat.st_mode & S_IFMT) == S_IFDIR);
}
bool vfs_file_exists(const char *path)
{
int res = vfs_open(path, O_RDONLY, 0);
if (res < 0) {
return false;
}
vfs_close(res);
return true;
}
/**
* @brief Removes additional "/" slashes from @p path
*

View File

@ -0,0 +1,13 @@
include ../Makefile.sys_common
USEMODULE += shell
USEMODULE += vfs_default
USEMODULE += vfs_util
USEMODULE += ztimer_msec
DISABLE_MODULE += test_utils_interactive_sync
# only boards with MTD are eligible
TEST_ON_CI_WHITELIST += native native64
include $(RIOTBASE)/Makefile.include

View File

@ -0,0 +1,4 @@
BOARD_INSUFFICIENT_MEMORY := \
atmega8 \
nucleo-l011k4 \
#

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2024 ML!PA Consulting GmbH
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup tests
* @{
*
* @file
* @brief Shell Scripting Test Application
*
* @author Benjamin Valentin <benjamin.valentin@ml-pa.com>
* @}
*/
#include <stdlib.h>
#include "shell.h"
#include "vfs_default.h"
#include "vfs_util.h"
#include "ztimer.h"
#ifndef SCRIPT_FILE
#define SCRIPT_FILE VFS_DEFAULT_DATA "/script.sh"
#endif
static int cmd_msleep(int argc, char **argv)
{
if (argc != 2) {
return -1;
}
ztimer_sleep(ZTIMER_MSEC, atoi(argv[1]));
return 0;
}
static int cmd_echo(int argc, char **argv)
{
for (int i = 1; i < argc; ++i) {
printf("%s ", argv[i]);
}
puts("");
return 0;
}
static int cmd_add(int argc, char **argv)
{
int sum = 0;
for (int i = 1; i < argc; ++i) {
sum += atoi(argv[i]);
}
printf("%d\n", sum);
return 0;
}
static const shell_command_t shell_commands[] = {
{ "msleep", "sleep for a number of ms", cmd_msleep },
{ "echo", "echo parameters to console", cmd_echo },
{ "add", "add up a list of numbers", cmd_add },
{ NULL, NULL, NULL }
};
static int _create_script(void)
{
const char file[] = {
"# this is a comment\n"
"msleep 500\n"
"echo Hello RIOT!\n"
"\n"
"add 10 23 9\n"
"01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
};
return vfs_file_from_buffer(SCRIPT_FILE, file, sizeof(file) - 1);
}
int main(void)
{
_create_script();
unsigned line;
int res = shell_parse_file(shell_commands, SCRIPT_FILE, &line);
if (res) {
fprintf(stderr, "%s:%u: error %d\n", SCRIPT_FILE, line, res);
}
return res;
}

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
import sys
from testrunner import run
def testfunc(child):
child.expect_exact('Hello RIOT!')
child.expect_exact('42')
child.expect_exact('/nvm0/script.sh:6: error -7')
if __name__ == "__main__":
sys.exit(run(testfunc))