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:
commit
22795248eb
@ -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
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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`.
|
||||
*
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
*
|
||||
|
13
tests/sys/shell_scripting/Makefile
Normal file
13
tests/sys/shell_scripting/Makefile
Normal 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
|
4
tests/sys/shell_scripting/Makefile.ci
Normal file
4
tests/sys/shell_scripting/Makefile.ci
Normal file
@ -0,0 +1,4 @@
|
||||
BOARD_INSUFFICIENT_MEMORY := \
|
||||
atmega8 \
|
||||
nucleo-l011k4 \
|
||||
#
|
94
tests/sys/shell_scripting/main.c
Normal file
94
tests/sys/shell_scripting/main.c
Normal 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;
|
||||
}
|
14
tests/sys/shell_scripting/tests/01-run.py
Executable file
14
tests/sys/shell_scripting/tests/01-run.py
Executable 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))
|
Loading…
Reference in New Issue
Block a user