mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 02:45:05 +00:00
Shell and terminal improved (#664)
- Refactored shell parsing so that the callstack doesn't increase when script complexity increases. - Improved terminal input handling: input delay from +- 100 ms to at most about 16 ms.
This commit is contained in:
parent
33898ddc4e
commit
0580eb6104
@ -27,7 +27,6 @@
|
||||
- Improve Setup: Show "Step done" screen
|
||||
- Make it more clear to end-users that an SD card is required to run Tactility
|
||||
- Warn if file operations are done from prohibited tasks (e.g. lvgl task)
|
||||
- Shell interpreter works recursive-descent algorithm, which requires a big stack size. Can we optimize this?
|
||||
- stopAppFromToolbar() in Tactility.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
|
||||
- Consider not unpacking `.app` files and executing them directly. Might want to cache file offsets. Cache file must be pinned to app version.
|
||||
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
|
||||
|
||||
@ -333,9 +333,9 @@ int chdir(const char* path) {
|
||||
// wraps above can't reach, so these redirect calls to printf/fprintf/etc directly. POSIX-only:
|
||||
// newlib's stdio already goes through the wrappable syscall stubs.
|
||||
//
|
||||
// Scoped to printf/getc: fread/fwrite are used sitewide for real file I/O, so wrapping them would
|
||||
// be a correctness risk for unrelated code. putc/getc are macros, not real calls, so wrapping
|
||||
// those symbols wouldn't reliably intercept them.
|
||||
// Only the process' original stdin/stdout/stderr are routed to the app's fds; every other stream
|
||||
// (real file I/O, including fwrite() to a file) goes straight to libc. fread() is not wrapped.
|
||||
// putc/getc are macros, not real calls, so wrapping those symbols wouldn't reliably intercept them.
|
||||
|
||||
#if !defined(ESP_PLATFORM)
|
||||
|
||||
@ -349,6 +349,7 @@ extern "C" {
|
||||
int __real_vfprintf(FILE* stream, const char* format, va_list args);
|
||||
int __real_fputs(const char* s, FILE* stream);
|
||||
int __real_fputc(int c, FILE* stream);
|
||||
size_t __real_fwrite(const void* data, size_t size, size_t count, FILE* stream);
|
||||
int __real_fgetc(FILE* stream);
|
||||
char* __real_fgets(char* buffer, int size, FILE* stream);
|
||||
}
|
||||
@ -372,6 +373,11 @@ int __real_fputc(int c, FILE* stream) {
|
||||
return real(c, stream);
|
||||
}
|
||||
|
||||
size_t __real_fwrite(const void* data, size_t size, size_t count, FILE* stream) {
|
||||
static auto real = reinterpret_cast<size_t (*)(const void*, size_t, size_t, FILE*)>(dlsym(RTLD_NEXT, "fwrite"));
|
||||
return real(data, size, count, stream);
|
||||
}
|
||||
|
||||
int __real_fgetc(FILE* stream) {
|
||||
static auto real = reinterpret_cast<int (*)(FILE*)>(dlsym(RTLD_NEXT, "fgetc"));
|
||||
return real(stream);
|
||||
@ -386,7 +392,8 @@ char* __real_fgets(char* buffer, int size, FILE* stream) {
|
||||
|
||||
namespace {
|
||||
|
||||
void writeAllTo(int fd, const void* data, size_t size) {
|
||||
/** @return the number of bytes written, less than `size` if the fd stopped accepting data */
|
||||
size_t writeAllTo(int fd, const void* data, size_t size) {
|
||||
const auto* bytes = static_cast<const char*>(data);
|
||||
size_t remaining = size;
|
||||
while (remaining > 0) {
|
||||
@ -397,6 +404,7 @@ void writeAllTo(int fd, const void* data, size_t size) {
|
||||
bytes += written;
|
||||
remaining -= static_cast<size_t>(written);
|
||||
}
|
||||
return size - remaining;
|
||||
}
|
||||
|
||||
int formatTo(int fd, const char* format, va_list args) {
|
||||
@ -425,9 +433,16 @@ int readOneFromStdin(char& out) {
|
||||
return static_cast<int>(app_io_read(STDIN_FILENO, &out, 1));
|
||||
}
|
||||
|
||||
// The process' own streams, captured before anything can reassign stdin/stdout/stderr. A caller
|
||||
// that points stdout at a file (e.g. the shell's redirection) must get real file I/O, so only these
|
||||
// original streams are routed to the app's fds.
|
||||
FILE* const originalStdin = stdin;
|
||||
FILE* const originalStdout = stdout;
|
||||
FILE* const originalStderr = stderr;
|
||||
|
||||
int targetFdOf(FILE* stream) {
|
||||
if (stream == stdout) return STDOUT_FILENO;
|
||||
if (stream == stderr) return STDERR_FILENO;
|
||||
if (stream == originalStdout) return STDOUT_FILENO;
|
||||
if (stream == originalStderr) return STDERR_FILENO;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -435,14 +450,17 @@ int targetFdOf(FILE* stream) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vfprintf(FILE* stream, const char* format, va_list args);
|
||||
int __wrap_fputc(int c, FILE* stream);
|
||||
|
||||
int __wrap_vprintf(const char* format, va_list args) {
|
||||
return formatTo(STDOUT_FILENO, format, args);
|
||||
return __wrap_vfprintf(stdout, format, args);
|
||||
}
|
||||
|
||||
int __wrap_printf(const char* format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int result = formatTo(STDOUT_FILENO, format, args);
|
||||
int result = __wrap_vfprintf(stdout, format, args);
|
||||
va_end(args);
|
||||
return result;
|
||||
}
|
||||
@ -465,6 +483,9 @@ int __wrap_fprintf(FILE* stream, const char* format, ...) {
|
||||
}
|
||||
|
||||
int __wrap_puts(const char* s) {
|
||||
if (targetFdOf(stdout) < 0) {
|
||||
return __real_fputs(s, stdout) < 0 ? EOF : __real_fputc('\n', stdout);
|
||||
}
|
||||
writeAllTo(STDOUT_FILENO, s, strlen(s));
|
||||
writeAllTo(STDOUT_FILENO, "\n", 1);
|
||||
return 0;
|
||||
@ -480,9 +501,7 @@ int __wrap_fputs(const char* s, FILE* stream) {
|
||||
}
|
||||
|
||||
int __wrap_putchar(int c) {
|
||||
auto ch = static_cast<char>(c);
|
||||
writeAllTo(STDOUT_FILENO, &ch, 1);
|
||||
return c;
|
||||
return __wrap_fputc(c, stdout);
|
||||
}
|
||||
|
||||
int __wrap_fputc(int c, FILE* stream) {
|
||||
@ -495,20 +514,31 @@ int __wrap_fputc(int c, FILE* stream) {
|
||||
return __real_fputc(c, stream);
|
||||
}
|
||||
|
||||
int __wrap_getchar() {
|
||||
size_t __wrap_fwrite(const void* data, size_t size, size_t count, FILE* stream) {
|
||||
int fd = targetFdOf(stream);
|
||||
if (fd < 0) {
|
||||
return __real_fwrite(data, size, count, stream);
|
||||
}
|
||||
if (size == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
return writeAllTo(fd, data, size * count) / size;
|
||||
}
|
||||
|
||||
int __wrap_fgetc(FILE* stream) {
|
||||
if (stream != originalStdin) {
|
||||
return __real_fgetc(stream);
|
||||
}
|
||||
char c;
|
||||
return readOneFromStdin(c) == 1 ? static_cast<unsigned char>(c) : EOF;
|
||||
}
|
||||
|
||||
int __wrap_fgetc(FILE* stream) {
|
||||
if (stream == stdin) {
|
||||
return __wrap_getchar();
|
||||
}
|
||||
return __real_fgetc(stream);
|
||||
int __wrap_getchar() {
|
||||
return __wrap_fgetc(stdin);
|
||||
}
|
||||
|
||||
char* __wrap_fgets(char* buffer, int size, FILE* stream) {
|
||||
if (stream != stdin) {
|
||||
if (stream != originalStdin) {
|
||||
return __real_fgets(buffer, size, stream);
|
||||
}
|
||||
if (size <= 0) {
|
||||
@ -544,6 +574,7 @@ TT_DYLD_INTERPOSE(__wrap_puts, puts)
|
||||
TT_DYLD_INTERPOSE(__wrap_fputs, fputs)
|
||||
TT_DYLD_INTERPOSE(__wrap_putchar, putchar)
|
||||
TT_DYLD_INTERPOSE(__wrap_fputc, fputc)
|
||||
TT_DYLD_INTERPOSE(__wrap_fwrite, fwrite)
|
||||
TT_DYLD_INTERPOSE(__wrap_getchar, getchar)
|
||||
TT_DYLD_INTERPOSE(__wrap_fgetc, fgetc)
|
||||
TT_DYLD_INTERPOSE(__wrap_fgets, fgets)
|
||||
@ -591,6 +622,10 @@ int fputc(int c, FILE* stream) {
|
||||
return __wrap_fputc(c, stream);
|
||||
}
|
||||
|
||||
size_t fwrite(const void* data, size_t size, size_t count, FILE* stream) {
|
||||
return __wrap_fwrite(data, size, count, stream);
|
||||
}
|
||||
|
||||
int getchar() {
|
||||
return __wrap_getchar();
|
||||
}
|
||||
|
||||
@ -58,8 +58,9 @@ void forEachCommand(void* context, void (*callback)(const Command& command, void
|
||||
/**
|
||||
* Completes the final word of `line`.
|
||||
*
|
||||
* The first word completes against command names, any later word against filesystem paths — which
|
||||
* is why this lives here rather than in LineEditor: it needs both the command table and ShellFs.
|
||||
* Every word completes against filesystem paths, and a first word without a '/' against command
|
||||
* names as well. That is why this lives here rather than in LineEditor: it needs both the command
|
||||
* table and ShellFs.
|
||||
*
|
||||
* On a unique match, `outSuffix` receives the text to append. On several, the common prefix shared
|
||||
* by all of them is returned instead (which may be empty), and the candidates are printed.
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// ---- shell state -----------------------------------------------------------
|
||||
|
||||
struct node; // forward (AST body of a function)
|
||||
@ -118,13 +122,6 @@ void sh_fields_init(sh_fields *f);
|
||||
void sh_fields_push(sh_fields *f, const char *s);
|
||||
void sh_fields_free(sh_fields *f);
|
||||
|
||||
// Expand one raw word token into zero or more fields (word splitting applies to
|
||||
// unquoted expansions). Appends to `out`.
|
||||
void sh_expand_word(sh_state *st, const char *raw, sh_fields *out);
|
||||
// Expand a raw word to a single string (no splitting) - for redirect targets
|
||||
// and assignment values. Caller frees the returned string.
|
||||
char *sh_expand_single(sh_state *st, const char *raw);
|
||||
// Expand a here-doc body: parameter/command expansion and backslash escaping of
|
||||
// $ ` \ (double-quote semantics), preserving newlines and literal quotes.
|
||||
// Caller frees the returned string.
|
||||
char *sh_expand_heredoc(sh_state *st, const char *raw);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -3,9 +3,17 @@
|
||||
|
||||
#include "sh.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Evaluate arithmetic expression `expr` (signed long, C/POSIX operator set).
|
||||
// Returns 0 on success and writes *out; nonzero on error (division by zero,
|
||||
// bad token, etc.). `st` is used to read/write shell variables. On error, an
|
||||
// English message is left in *errmsg (points at a static buffer; may be NULL
|
||||
// on success). Pass errmsg=NULL to ignore.
|
||||
int sh_arith_eval(sh_state *st, const char *expr, long *out, const char **errmsg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
#pragma once
|
||||
#include "sh.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// If argv[0] names a shell builtin, run it, store its status in *status, and
|
||||
// return 1. Otherwise return 0 (caller should try an external command).
|
||||
// `eval` and `source`/`.` run nested programs, so the executor handles those itself.
|
||||
int sh_run_builtin(sh_state *st, int argc, char **argv, int *status);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
// ${v#..}/${v%..} strip operators and, later, by `case` and pathname globbing.
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Match string `s` against shell pattern `p` using '*' (any run, incl. empty)
|
||||
// and '?' (exactly one char). Backslash escapes the next metacharacter.
|
||||
// Returns 1 on match, 0 otherwise.
|
||||
@ -22,3 +26,7 @@ char *sh_strip_suffix(const char *s, const char *p, int longest);
|
||||
// the word literal, dash-style.
|
||||
#include "sh.h"
|
||||
int sh_glob_pathnames(const char *pat, sh_fields *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
T_WORD, // a raw word, quotes preserved (resolved later by sh_expand)
|
||||
T_SEMI, // ;
|
||||
@ -38,3 +42,7 @@ typedef struct sh_toklist {
|
||||
// Tokenize src into tl. Returns 0 on success, -1 on lex error.
|
||||
int sh_lex(const char *src, sh_toklist *tl);
|
||||
void sh_toklist_free(sh_toklist *tl);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
475
Tactility/Private/Tactility/app/shell/shell/sh_machine.h
Normal file
475
Tactility/Private/Tactility/app/shell/shell/sh_machine.h
Normal file
@ -0,0 +1,475 @@
|
||||
// Interpreter execution machine.
|
||||
//
|
||||
// Execution and expansion run on an explicit stack of frames instead of the
|
||||
// native call stack. Each frame is the state of one unfinished construct (a
|
||||
// loop, a command, a word being expanded, ...). The machine repeatedly steps
|
||||
// the top frame: a frame either pushes a child frame and waits for its result,
|
||||
// or finishes and hands its result to the frame below. The native stack stays
|
||||
// the same size regardless of how deeply a script nests.
|
||||
//
|
||||
// Frames are stored in chunks that never move, so a frame may hand a child a
|
||||
// pointer to one of its own members. A frame must have no side effects before
|
||||
// its first step(): frames are moved into place, and a moved-from frame is
|
||||
// destroyed without ever having run.
|
||||
#pragma once
|
||||
|
||||
#include "sh.h"
|
||||
#include "sh_parse.h"
|
||||
#include "sh_port.h"
|
||||
|
||||
#include <tactility/memory.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
// Maximum number of frames. Exceeding it (or running out of memory for frames) aborts the run with
|
||||
// "nesting too deep" instead of exhausting memory.
|
||||
#define SH_MAX_FRAMES 1024
|
||||
|
||||
namespace sh {
|
||||
|
||||
using Fields = std::vector<std::string>;
|
||||
|
||||
/** What a finished frame hands to the frame that pushed it. */
|
||||
struct Result {
|
||||
int status = 0;
|
||||
std::string text; // single-string expansions and command substitution output
|
||||
Fields fields; // word expansion
|
||||
};
|
||||
|
||||
/** The field currently being built during expansion. */
|
||||
struct FieldBuilder {
|
||||
std::string text;
|
||||
bool started = false;
|
||||
|
||||
void put(char c) {
|
||||
text.push_back(c);
|
||||
started = true;
|
||||
}
|
||||
/** Ends the field if it has begun. */
|
||||
void emit(Fields& out);
|
||||
/** Ends the field even when empty (an IFS delimiter boundary). */
|
||||
void emitForced(Fields& out);
|
||||
};
|
||||
|
||||
/** Control-flow flags that command substitution and pipeline stages keep local. */
|
||||
struct FlowFlags {
|
||||
int exiting, exitCode, brk, cont, returning, returnCode;
|
||||
|
||||
static FlowFlags save(const sh_state& st) {
|
||||
return { st.exiting, st.exit_code, st.brk, st.cont, st.returning, st.return_code };
|
||||
}
|
||||
void restore(sh_state& st) const {
|
||||
st.exiting = exiting; st.exit_code = exitCode;
|
||||
st.brk = brk; st.cont = cont;
|
||||
st.returning = returning; st.return_code = returnCode;
|
||||
}
|
||||
};
|
||||
|
||||
/** Redirections applied around a command. Restores the streams and removes here-doc files when destroyed. */
|
||||
struct Redirections {
|
||||
sh_redir_saved io {};
|
||||
bool active = false;
|
||||
std::vector<std::string> paths; // expanded filenames
|
||||
std::vector<std::string> tempFiles; // here-doc bodies
|
||||
|
||||
Redirections() = default;
|
||||
Redirections(Redirections&&) = default;
|
||||
~Redirections();
|
||||
void restore();
|
||||
};
|
||||
|
||||
class Machine;
|
||||
|
||||
enum class Step { Call, Return };
|
||||
|
||||
// region Frames
|
||||
|
||||
/** Lex, parse and execute a script. Also implements `eval` and `source`/`.`. */
|
||||
struct RunProgram {
|
||||
enum class Mode { Plain, Eval, Source };
|
||||
|
||||
std::string source;
|
||||
Mode mode = Mode::Plain;
|
||||
std::optional<Fields> positional; // Source: replaces $@ for the duration
|
||||
node* root = nullptr;
|
||||
bool started = false;
|
||||
char** savedPositional = nullptr;
|
||||
int savedPositionalCount = 0;
|
||||
bool positionalSwapped = false;
|
||||
sh_state* state = nullptr;
|
||||
|
||||
RunProgram(std::string source, Mode mode) : source(std::move(source)), mode(mode) {}
|
||||
RunProgram(RunProgram&&) = default;
|
||||
~RunProgram();
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step finish(Machine& m, int status);
|
||||
void restorePositional(sh_state& st);
|
||||
};
|
||||
|
||||
/** Any AST node: compound-command redirects, negation and errexit around the node's own frame. */
|
||||
struct ExecNode {
|
||||
node* n;
|
||||
int exempt;
|
||||
enum class Phase { Start, Redirected, Body } phase = Phase::Start;
|
||||
std::unique_ptr<Redirections> redirections; // only for a compound command with redirects
|
||||
|
||||
ExecNode(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step body(Machine& m);
|
||||
Step finish(Machine& m, int status);
|
||||
};
|
||||
|
||||
struct ExecList {
|
||||
node* n;
|
||||
int exempt;
|
||||
int index = 0;
|
||||
int status = 0;
|
||||
bool started = false;
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
struct ExecAndOr {
|
||||
node* n;
|
||||
int exempt;
|
||||
enum class Phase { Start, Left, Right } phase = Phase::Start;
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
struct ExecIf {
|
||||
node* n;
|
||||
int exempt;
|
||||
int clause = 0;
|
||||
enum class Phase { Start, Condition, Tail } phase = Phase::Start;
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
struct ExecWhile {
|
||||
node* n;
|
||||
int exempt;
|
||||
int status = 0;
|
||||
sh_state* state = nullptr; // set once loop_depth has been incremented
|
||||
enum class Phase { Start, Condition, Body } phase = Phase::Start;
|
||||
|
||||
ExecWhile(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
ExecWhile(ExecWhile&&) = default;
|
||||
~ExecWhile();
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
struct ExecFor {
|
||||
node* n;
|
||||
int exempt;
|
||||
Fields items;
|
||||
int word = 0;
|
||||
size_t item = 0;
|
||||
int status = 0;
|
||||
sh_state* state = nullptr; // set once loop_depth has been incremented
|
||||
enum class Phase { Start, Expanding, Body } phase = Phase::Start;
|
||||
|
||||
ExecFor(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
ExecFor(ExecFor&&) = default;
|
||||
~ExecFor();
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step next(Machine& m);
|
||||
};
|
||||
|
||||
struct ExecCase {
|
||||
node* n;
|
||||
int exempt;
|
||||
std::string subject;
|
||||
int clause = 0;
|
||||
int pattern = 0;
|
||||
enum class Phase { Start, Subject, Pattern, Tail } phase = Phase::Start;
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step nextPattern(Machine& m);
|
||||
};
|
||||
|
||||
/** `{ list; }` or a `( list )` subshell, which snapshots and restores the shell state. */
|
||||
struct ExecGroup {
|
||||
node* n;
|
||||
int exempt;
|
||||
bool started = false;
|
||||
sh_state* state = nullptr; // set once a subshell snapshot has been taken
|
||||
sh_var* savedVars = nullptr;
|
||||
char** savedPositional = nullptr;
|
||||
int savedPositionalCount = 0;
|
||||
char* savedArg0 = nullptr;
|
||||
std::string savedCwd;
|
||||
char* savedLogicalCwd = nullptr; // sh_state::cwd, which `cd` replaces
|
||||
FlowFlags savedFlow {};
|
||||
int savedErrexit = 0;
|
||||
int savedNounset = 0;
|
||||
|
||||
ExecGroup(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
ExecGroup(ExecGroup&&) = default;
|
||||
~ExecGroup();
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
/** A pipeline, emulated with temp files between stages. */
|
||||
struct ExecPipe {
|
||||
node* n;
|
||||
int exempt;
|
||||
std::string tempA;
|
||||
std::string tempB;
|
||||
int stage = 0;
|
||||
int status = 0;
|
||||
const char* previousOutput = nullptr;
|
||||
std::unique_ptr<sh_redir_saved> stageIo;
|
||||
bool stageRedirected = false;
|
||||
FlowFlags savedFlow {};
|
||||
bool started = false;
|
||||
|
||||
ExecPipe(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
ExecPipe(ExecPipe&&) = default;
|
||||
~ExecPipe();
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step startStage(Machine& m);
|
||||
};
|
||||
|
||||
/** A simple command: assignments, word expansion, redirects, then a function, builtin or external command. */
|
||||
struct ExecSimple {
|
||||
node* n;
|
||||
int exempt;
|
||||
enum class Phase {
|
||||
Start, PureAssign, PureRedirected, Words, EmptyAssign, PrefixAssign, Redirected, Ran
|
||||
} phase = Phase::Start;
|
||||
int index = 0;
|
||||
int emptyStatus = 0;
|
||||
Fields argv;
|
||||
std::unique_ptr<Redirections> redirections; // only for a command with redirects
|
||||
|
||||
struct Binding {
|
||||
std::string name;
|
||||
std::optional<std::string> value;
|
||||
std::optional<std::string> environment;
|
||||
};
|
||||
std::vector<Binding> bindings; // prefix assignments to undo afterwards
|
||||
sh_state* state = nullptr; // set while bindings are applied
|
||||
|
||||
ExecSimple(node* n, int exempt) : n(n), exempt(exempt) {}
|
||||
ExecSimple(ExecSimple&&) = default;
|
||||
~ExecSimple();
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step expandAssignment(Machine& m);
|
||||
void applyAssignment(Machine& m, const std::string& value);
|
||||
Step redirect(Machine& m, Phase next);
|
||||
Step run(Machine& m);
|
||||
Step finish(Machine& m, int status);
|
||||
void restoreBindings();
|
||||
};
|
||||
|
||||
/** A shell function call with its own positional parameters and `local` scope. */
|
||||
struct CallFunction {
|
||||
sh_func* function;
|
||||
Fields argv;
|
||||
int exempt;
|
||||
char** savedPositional = nullptr;
|
||||
int savedPositionalCount = 0;
|
||||
int previousReturning = 0;
|
||||
sh_state* state = nullptr; // set once the call has been entered
|
||||
|
||||
CallFunction(sh_func* function, Fields argv, int exempt) : function(function), argv(std::move(argv)), exempt(exempt) {}
|
||||
CallFunction(CallFunction&&) = default;
|
||||
~CallFunction();
|
||||
Step step(Machine& m);
|
||||
};
|
||||
|
||||
/** Expands a node's redirect words and applies them into the owner's Redirections. Status 0 on success, 1 on failure. */
|
||||
struct ApplyRedirects {
|
||||
node* n;
|
||||
Redirections* target;
|
||||
int index = 0;
|
||||
bool waiting = false;
|
||||
|
||||
struct Item {
|
||||
sh_rd_op op;
|
||||
int fd;
|
||||
int dupfd;
|
||||
int path; // index into target->paths or target->tempFiles, -1 for none
|
||||
bool temp;
|
||||
};
|
||||
std::vector<Item> items;
|
||||
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
void writeHeredoc(const std::string& body);
|
||||
};
|
||||
|
||||
/** `$(...)`: runs a command with stdout captured to a temp file, then reads the output back. */
|
||||
struct CommandSubst {
|
||||
std::string command;
|
||||
std::string tempFile;
|
||||
std::unique_ptr<sh_redir_saved> io;
|
||||
bool redirected = false;
|
||||
bool started = false;
|
||||
FlowFlags savedFlow {};
|
||||
|
||||
explicit CommandSubst(std::string command) : command(std::move(command)) {}
|
||||
CommandSubst(CommandSubst&&) = default;
|
||||
~CommandSubst();
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step finish(Machine& m, std::string output);
|
||||
};
|
||||
|
||||
/** Expands one raw word: into fields (Word), a single string (Single) or a here-doc body (Heredoc). */
|
||||
struct Expand {
|
||||
enum class Mode { Word, Single, Heredoc };
|
||||
|
||||
std::string raw;
|
||||
Mode mode;
|
||||
bool allowSplit = false;
|
||||
bool allowTilde = false;
|
||||
bool quotedContext = false;
|
||||
|
||||
FieldBuilder field;
|
||||
Fields out;
|
||||
size_t i = 0;
|
||||
bool started = false;
|
||||
bool inDoubleQuote = false;
|
||||
|
||||
enum class Pending { None, Arithmetic, CommandSubstitution, Brace, Backtick } pending = Pending::None;
|
||||
bool pendingSplit = false;
|
||||
|
||||
static Expand word(std::string raw);
|
||||
static Expand single(std::string raw, bool allowTilde = true, bool quotedContext = false);
|
||||
static Expand heredoc(std::string raw);
|
||||
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
char at(size_t k) const { return k < raw.size() ? raw[k] : '\0'; }
|
||||
void resume(Machine& m);
|
||||
bool scanWord(Machine& m);
|
||||
bool scanHeredoc(Machine& m);
|
||||
bool dollar(Machine& m, bool split, bool quoted);
|
||||
Step finish(Machine& m);
|
||||
};
|
||||
|
||||
/** The interior of a `${...}` expression, appending into the owning Expand's field. */
|
||||
struct ExpandBrace {
|
||||
std::string content;
|
||||
bool quoted;
|
||||
bool split;
|
||||
FieldBuilder* field;
|
||||
Fields* out;
|
||||
|
||||
enum class Phase { Start, Strip, Plus, Minus, Assign, Error } phase = Phase::Start;
|
||||
std::string name;
|
||||
bool longest = false;
|
||||
char stripKind = 0;
|
||||
std::string value; // the parameter's value when the operator doesn't need its word
|
||||
|
||||
Step step(Machine& m);
|
||||
|
||||
private:
|
||||
Step start(Machine& m);
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
using Frame = std::variant<RunProgram, ExecNode, ExecList, ExecAndOr, ExecIf, ExecWhile, ExecFor, ExecCase,
|
||||
ExecGroup, ExecPipe, ExecSimple, CallFunction, ApplyRedirects, CommandSubst,
|
||||
Expand, ExpandBrace>;
|
||||
|
||||
/** A stack of frames in fixed chunks, so frames never move once pushed. */
|
||||
class FrameStack {
|
||||
static constexpr size_t CHUNK_SIZE = 8;
|
||||
|
||||
struct Chunk {
|
||||
Chunk* previous;
|
||||
size_t count;
|
||||
alignas(Frame) unsigned char storage[CHUNK_SIZE * sizeof(Frame)];
|
||||
|
||||
Frame* at(size_t index) { return std::launder(reinterpret_cast<Frame*>(storage) + index); }
|
||||
};
|
||||
|
||||
Chunk* top = nullptr;
|
||||
Chunk* spare = nullptr; // one emptied chunk is kept, so a loop at a chunk boundary doesn't allocate every iteration
|
||||
size_t count = 0;
|
||||
|
||||
public:
|
||||
FrameStack() = default;
|
||||
FrameStack(const FrameStack&) = delete;
|
||||
FrameStack& operator=(const FrameStack&) = delete;
|
||||
~FrameStack();
|
||||
|
||||
/** @return false when no memory is available for the frame */
|
||||
bool push(Frame&& frame);
|
||||
void pop();
|
||||
Frame& back() { return *top->at(top->count - 1); }
|
||||
size_t size() const { return count; }
|
||||
bool empty() const { return count == 0; }
|
||||
};
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
explicit Machine(sh_state& st) : st(st) {}
|
||||
|
||||
sh_state& st;
|
||||
/** The result of the frame that finished most recently. */
|
||||
Result result;
|
||||
|
||||
/** Runs the root frame to completion and returns its status. */
|
||||
int run(Frame root);
|
||||
|
||||
/** Pushes a child frame, which runs before the calling frame is stepped again. */
|
||||
Step call(Frame frame) {
|
||||
pending.emplace(std::move(frame));
|
||||
return Step::Call;
|
||||
}
|
||||
|
||||
Step done(int status) {
|
||||
result = Result { .status = status, .text = {}, .fields = {} };
|
||||
return Step::Return;
|
||||
}
|
||||
|
||||
Step done(std::string text) {
|
||||
result = Result { .status = 0, .text = std::move(text), .fields = {} };
|
||||
return Step::Return;
|
||||
}
|
||||
|
||||
Step done(Fields fields) {
|
||||
result = Result { .status = 0, .text = {}, .fields = std::move(fields) };
|
||||
return Step::Return;
|
||||
}
|
||||
|
||||
private:
|
||||
FrameStack frames;
|
||||
std::optional<Frame> pending;
|
||||
};
|
||||
|
||||
// region Expansion helpers (sh_expand.cpp)
|
||||
|
||||
const char* get_ifs(sh_state* st);
|
||||
void append_val(FieldBuilder& b, Fields& out, const char* v, bool split, const char* ifs);
|
||||
const char* param_raw(sh_state* st, const char* name, char** owned);
|
||||
bool nounset_fire(sh_state* st, const char* name, const char* val);
|
||||
void expand_at_star(sh_state* st, bool star, bool quoted, FieldBuilder& b, Fields& out, bool split, const char* ifs);
|
||||
bool is_var_name(const char* name);
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace sh
|
||||
@ -1,6 +1,10 @@
|
||||
#pragma once
|
||||
#include "sh_lex.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
N_LIST, // sequence of and_or nodes (children), run in order
|
||||
N_ANDOR, // binary: left <op> right, op in { &&, || }
|
||||
@ -85,3 +89,7 @@ typedef struct node {
|
||||
// sets *errmsg to a static description.
|
||||
node *sh_parse(sh_toklist *tl, const char **errmsg);
|
||||
void sh_free_node(node *n);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -5,6 +5,10 @@
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Run an external (non-builtin) command with a fully-expanded argv.
|
||||
// Returns the command's exit status. Sets *found to 1 if the command existed,
|
||||
// 0 if it could not be located (so the core can report "not found").
|
||||
@ -52,3 +56,7 @@ void sh_port_tmpfile(int which, char *buf, int bufsz);
|
||||
// Working directory (cd / pwd builtins).
|
||||
int sh_port_chdir(const char *path); // 0 on success
|
||||
void sh_port_getcwd(char *buf, int bufsz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
/**
|
||||
* Launches the shell app and relays its stdio to/from vterm until it exits on its own or
|
||||
* `*stopRequested` asks it to (touch-to-exit); this is the same relationship a real terminal
|
||||
* emulator has to the shell it runs over a pty. Sets `*stopRequested` before returning either way,
|
||||
* so the caller's own I/O task loop (still running independently) winds down too.
|
||||
* `renderTask` is notified (xTaskNotifyGive) whenever shell output reaches vterm, so it can draw it right away.
|
||||
*/
|
||||
void runShell(int columns, int rows, volatile bool* stopRequested);
|
||||
void runShell(int columns, int rows, volatile bool* stopRequested, TaskHandle_t renderTask);
|
||||
|
||||
@ -67,6 +67,11 @@ void vterm_get_cursor(int vt_id, int *col, int *row, int *visible);
|
||||
bool vterm_take_dirty(void);
|
||||
void vterm_set_switch_callback(void (*cb)(int new_vt));
|
||||
|
||||
// Tactility addition: called after each byte is queued by vterm_send_input(), so a reader can wait
|
||||
// on its own event source instead of polling vterm_getchar(). Pass NULL to remove. Returns only
|
||||
// once a callback already in progress has finished, so the previous context may be released after.
|
||||
void vterm_set_input_callback(void (*cb)(void *context), void *context);
|
||||
|
||||
// Tactility addition: called just before a line scrolls off the top, while it is still readable
|
||||
// via vterm_get_direct_buffer(). Lets a caller keep scrollback history that vterm itself does not.
|
||||
// Pass NULL to remove.
|
||||
|
||||
@ -121,6 +121,15 @@ constexpr int RUN_APP_START_FAILED = -1;
|
||||
|
||||
bool lastByteWasNewline = true;
|
||||
|
||||
/** Copies what a child wrote to one of its output streams to `target`, which may be redirected. */
|
||||
void drainOutput(AppStream& stream, FILE* target, uint8_t* buffer, size_t size) {
|
||||
size_t n;
|
||||
while ((n = app_stream_read(&stream, buffer, size)) > 0) {
|
||||
fwrite(buffer, 1, n, target); // byte-counted: output may contain NUL bytes
|
||||
lastByteWasNewline = buffer[n - 1] == '\n';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an app instance to completion, piping its stdio to/from this task's own, like a shell
|
||||
* piping a child process. `context` must already have its location and arguments set.
|
||||
@ -133,17 +142,22 @@ bool lastByteWasNewline = true;
|
||||
int runApp(AppStartContext& context) {
|
||||
constexpr size_t STDIN_BUFFER_SIZE = 256;
|
||||
constexpr size_t STDOUT_BUFFER_SIZE = 1024;
|
||||
constexpr size_t STDERR_BUFFER_SIZE = 256;
|
||||
constexpr MemoryPolicy policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||
auto* stdinBuffer = static_cast<uint8_t*>(memory_alloc_with_policy(STDIN_BUFFER_SIZE, &policy));
|
||||
auto* stdoutBuffer = static_cast<uint8_t*>(memory_alloc_with_policy(STDOUT_BUFFER_SIZE, &policy));
|
||||
if (stdinBuffer == nullptr || stdoutBuffer == nullptr) {
|
||||
auto* stderrBuffer = static_cast<uint8_t*>(memory_alloc_with_policy(STDERR_BUFFER_SIZE, &policy));
|
||||
if (stdinBuffer == nullptr || stdoutBuffer == nullptr || stderrBuffer == nullptr) {
|
||||
memory_free(stdinBuffer);
|
||||
memory_free(stdoutBuffer);
|
||||
memory_free(stderrBuffer);
|
||||
return RUN_APP_START_FAILED;
|
||||
}
|
||||
|
||||
AppStream stdinStream {};
|
||||
AppStream stdoutStream {};
|
||||
// Kept apart from stdout, so `$(...)` captures only a child's stdout, like any shell
|
||||
AppStream stderrStream {};
|
||||
|
||||
TaskEventGroup eventGroup {};
|
||||
task_event_group_construct(&eventGroup);
|
||||
@ -158,7 +172,8 @@ int runApp(AppStartContext& context) {
|
||||
|
||||
AppStreamBinding bindings[] = {
|
||||
{ STDIN_FILENO, &stdinStream, stdinBuffer, STDIN_BUFFER_SIZE, &eventGroup, {}, -1 },
|
||||
{ STDOUT_FILENO, &stdoutStream, stdoutBuffer, STDOUT_BUFFER_SIZE, &eventGroup, windowSize, STDERR_FILENO },
|
||||
{ STDOUT_FILENO, &stdoutStream, stdoutBuffer, STDOUT_BUFFER_SIZE, &eventGroup, windowSize, -1 },
|
||||
{ STDERR_FILENO, &stderrStream, stderrBuffer, STDERR_BUFFER_SIZE, &eventGroup, windowSize, -1 },
|
||||
};
|
||||
|
||||
AppEventSubscription eventSub {};
|
||||
@ -172,8 +187,9 @@ int runApp(AppStartContext& context) {
|
||||
if (result != ERROR_NONE) {
|
||||
app_event_unsubscribe(&eventSub);
|
||||
task_event_group_destruct(&eventGroup);
|
||||
free(stdinBuffer);
|
||||
free(stdoutBuffer);
|
||||
memory_free(stdinBuffer);
|
||||
memory_free(stdoutBuffer);
|
||||
memory_free(stderrBuffer);
|
||||
return RUN_APP_START_FAILED;
|
||||
}
|
||||
|
||||
@ -207,11 +223,8 @@ int runApp(AppStartContext& context) {
|
||||
vTaskDelay(pdMS_TO_TICKS(APP_PUMP_INTERVAL_MS));
|
||||
}
|
||||
|
||||
size_t n;
|
||||
while ((n = app_stream_read(&stdoutStream, drain, sizeof(drain))) > 0) {
|
||||
printf("%.*s", static_cast<int>(n), reinterpret_cast<const char*>(drain));
|
||||
lastByteWasNewline = drain[n - 1] == '\n';
|
||||
}
|
||||
drainOutput(stdoutStream, stdout, drain, sizeof(drain));
|
||||
drainOutput(stderrStream, stderr, drain, sizeof(drain));
|
||||
|
||||
AppEvent event {};
|
||||
while (app_event_poll(&eventSub, &event) == ERROR_NONE) {
|
||||
@ -223,17 +236,15 @@ int runApp(AppStartContext& context) {
|
||||
}
|
||||
|
||||
// The child may have exited right after its last write, before the loop above's last read.
|
||||
size_t n;
|
||||
while ((n = app_stream_read(&stdoutStream, drain, sizeof(drain))) > 0) {
|
||||
printf("%.*s", static_cast<int>(n), reinterpret_cast<const char*>(drain));
|
||||
lastByteWasNewline = drain[n - 1] == '\n';
|
||||
}
|
||||
drainOutput(stdoutStream, stdout, drain, sizeof(drain));
|
||||
drainOutput(stderrStream, stderr, drain, sizeof(drain));
|
||||
|
||||
// Must run before app_manager_stop() reaps the child: only app_stream_unsubscribe() guarantees
|
||||
// the fd binding is gone and no AppFileOps call is still in flight, which is what makes these
|
||||
// stack-local AppStreams safe to let go out of scope below.
|
||||
app_stream_unsubscribe(&stdinStream);
|
||||
app_stream_unsubscribe(&stdoutStream);
|
||||
app_stream_unsubscribe(&stderrStream);
|
||||
|
||||
app_manager_stop(childId);
|
||||
|
||||
@ -242,6 +253,7 @@ int runApp(AppStartContext& context) {
|
||||
|
||||
memory_free(stdinBuffer);
|
||||
memory_free(stdoutBuffer);
|
||||
memory_free(stderrBuffer);
|
||||
|
||||
return childResult;
|
||||
}
|
||||
@ -260,7 +272,14 @@ int runElf(const char* resolvedPath, int argc, char** argv) {
|
||||
// per-process cwd for fopen() to resolve against (no chdir() at all), and the shell's own cwd
|
||||
// lives in ShellFs, meaning nothing to libc. shouldMakeAbsolute() decides which arguments qualify.
|
||||
char* rewritten[32];
|
||||
char storage[8][FILE_MAX_PATH_STRING_LENGTH];
|
||||
constexpr int STORAGE_COUNT = 8;
|
||||
// On the heap: the loader runs on this task's stack right after, and needs the room
|
||||
constexpr MemoryPolicy policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||
auto* storage = static_cast<char*>(memory_alloc_with_policy(STORAGE_COUNT * FILE_MAX_PATH_STRING_LENGTH, &policy));
|
||||
if (storage == nullptr) {
|
||||
printf("%s: out of memory\n", argv[0]);
|
||||
return 126;
|
||||
}
|
||||
int stored = 0;
|
||||
|
||||
const int passedArgc = (argc < static_cast<int>(sizeof(rewritten) / sizeof(rewritten[0])))
|
||||
@ -271,12 +290,13 @@ int runElf(const char* resolvedPath, int argc, char** argv) {
|
||||
rewritten[i] = argv[i];
|
||||
|
||||
// argv[0] is the binary itself, already resolved by the caller.
|
||||
if (i == 0 || stored >= 8) {
|
||||
if (i == 0 || stored >= STORAGE_COUNT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldMakeAbsolute(argv[i], storage[stored], FILE_MAX_PATH_STRING_LENGTH)) {
|
||||
rewritten[i] = storage[stored];
|
||||
char* slot = storage + stored * FILE_MAX_PATH_STRING_LENGTH;
|
||||
if (shouldMakeAbsolute(argv[i], slot, FILE_MAX_PATH_STRING_LENGTH)) {
|
||||
rewritten[i] = slot;
|
||||
stored++;
|
||||
}
|
||||
}
|
||||
@ -298,6 +318,7 @@ int runElf(const char* resolvedPath, int argc, char** argv) {
|
||||
app_start_context_set_arguments_ext(&context, passedArgc, rewritten);
|
||||
|
||||
const int result = runApp(context);
|
||||
memory_free(storage);
|
||||
|
||||
int exitCode;
|
||||
if (result == RUN_APP_START_FAILED) {
|
||||
|
||||
@ -91,6 +91,9 @@ struct Candidates {
|
||||
int count;
|
||||
// Printed lazily, so a unique match completes silently without disturbing the prompt.
|
||||
char first[FILE_MAX_PATH_STRING_LENGTH];
|
||||
// A bare first word only runs registered commands, so a plain file there would complete to
|
||||
// something that can't run. Directories are still offered, to continue typing a path into.
|
||||
bool directoriesOnly;
|
||||
};
|
||||
|
||||
void offerCandidate(Candidates& candidates, const char* name) {
|
||||
@ -120,7 +123,11 @@ void offerCommand(const Shell::Command& command, void* context) {
|
||||
}
|
||||
|
||||
void offerEntry(const DirectoryEntry* entry, void* context) {
|
||||
offerCandidate(*static_cast<Candidates*>(context), entry->name);
|
||||
auto* candidates = static_cast<Candidates*>(context);
|
||||
if (candidates->directoriesOnly && !entry->is_directory) {
|
||||
return;
|
||||
}
|
||||
offerCandidate(*candidates, entry->name);
|
||||
}
|
||||
|
||||
void printCandidateCommand(const Shell::Command& command, void* context) {
|
||||
@ -132,6 +139,9 @@ void printCandidateCommand(const Shell::Command& command, void* context) {
|
||||
|
||||
void printCandidateEntry(const DirectoryEntry* entry, void* context) {
|
||||
auto* candidates = static_cast<Candidates*>(context);
|
||||
if (candidates->directoriesOnly && !entry->is_directory) {
|
||||
return;
|
||||
}
|
||||
if (strncmp(entry->name, candidates->word, candidates->wordLength) == 0) {
|
||||
printf("%s%s ", entry->name, entry->is_directory ? "/" : "");
|
||||
}
|
||||
@ -148,44 +158,43 @@ bool complete(const char* line, char* outSuffix, size_t suffixSize, bool* outLis
|
||||
const bool isFirstWord = (wordStart == nullptr);
|
||||
wordStart = isFirstWord ? line : wordStart + 1;
|
||||
|
||||
// A first word names a command or, like "./tool" or "app/tool", a file to run.
|
||||
const bool includeCommands = isFirstWord && strchr(wordStart, '/') == nullptr;
|
||||
|
||||
Candidates candidates {};
|
||||
candidates.word = wordStart;
|
||||
candidates.wordLength = strlen(wordStart);
|
||||
candidates.directoriesOnly = includeCommands;
|
||||
|
||||
// Paths complete against a directory, which may be named in the word itself ("ls /data/fo").
|
||||
char directory[FILE_MAX_PATH_STRING_LENGTH] = {};
|
||||
const char* namePart = wordStart;
|
||||
|
||||
if (!isFirstWord) {
|
||||
const char* slash = strrchr(wordStart, '/');
|
||||
if (slash != nullptr) {
|
||||
char prefix[FILE_MAX_PATH_STRING_LENGTH];
|
||||
const size_t prefixLength = static_cast<size_t>(slash - wordStart);
|
||||
if (prefixLength >= sizeof(prefix)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(prefix, wordStart, prefixLength);
|
||||
prefix[prefixLength] = '\0';
|
||||
|
||||
// A leading "/foo" leaves an empty prefix, which means the root itself.
|
||||
if (!ShellFs::resolvePath(prefixLength == 0 ? "/" : prefix, directory, sizeof(directory))) {
|
||||
return false;
|
||||
}
|
||||
namePart = slash + 1;
|
||||
} else {
|
||||
snprintf(directory, sizeof(directory), "%s", ShellFs::cwd());
|
||||
const char* slash = strrchr(wordStart, '/');
|
||||
if (slash != nullptr) {
|
||||
char prefix[FILE_MAX_PATH_STRING_LENGTH];
|
||||
const size_t prefixLength = static_cast<size_t>(slash - wordStart);
|
||||
if (prefixLength >= sizeof(prefix)) {
|
||||
return false;
|
||||
}
|
||||
memcpy(prefix, wordStart, prefixLength);
|
||||
prefix[prefixLength] = '\0';
|
||||
|
||||
candidates.word = namePart;
|
||||
candidates.wordLength = strlen(namePart);
|
||||
}
|
||||
|
||||
if (isFirstWord) {
|
||||
forEachCommand(&candidates, offerCommand);
|
||||
// A leading "/foo" leaves an empty prefix, which means the root itself.
|
||||
if (!ShellFs::resolvePath(prefixLength == 0 ? "/" : prefix, directory, sizeof(directory))) {
|
||||
return false;
|
||||
}
|
||||
namePart = slash + 1;
|
||||
} else {
|
||||
directory_list(directory, &candidates, offerEntry);
|
||||
snprintf(directory, sizeof(directory), "%s", ShellFs::cwd());
|
||||
}
|
||||
|
||||
candidates.word = namePart;
|
||||
candidates.wordLength = strlen(namePart);
|
||||
|
||||
if (includeCommands) {
|
||||
forEachCommand(&candidates, offerCommand);
|
||||
}
|
||||
directory_list(directory, &candidates, offerEntry);
|
||||
|
||||
if (candidates.count == 0) {
|
||||
return false;
|
||||
}
|
||||
@ -200,11 +209,9 @@ bool complete(const char* line, char* outSuffix, size_t suffixSize, bool* outLis
|
||||
if (candidates.count == 1) {
|
||||
const size_t used = strlen(outSuffix);
|
||||
if (used + 1 < suffixSize) {
|
||||
const bool directoryMatch = !isFirstWord && [&] {
|
||||
char full[FILE_MAX_PATH_STRING_LENGTH];
|
||||
const int written = snprintf(full, sizeof(full), "%s/%s", directory, candidates.first);
|
||||
return written > 0 && static_cast<size_t>(written) < sizeof(full) && directory_exists(full);
|
||||
}();
|
||||
char full[FILE_MAX_PATH_STRING_LENGTH];
|
||||
const int written = snprintf(full, sizeof(full), "%s/%s", directory, candidates.first);
|
||||
const bool directoryMatch = written > 0 && static_cast<size_t>(written) < sizeof(full) && directory_exists(full);
|
||||
outSuffix[used] = directoryMatch ? '/' : ' ';
|
||||
outSuffix[used + 1] = '\0';
|
||||
}
|
||||
@ -214,11 +221,10 @@ bool complete(const char* line, char* outSuffix, size_t suffixSize, bool* outLis
|
||||
// Several matches and nothing more to add: show what they are.
|
||||
if (outSuffix[0] == '\0') {
|
||||
printf("\n");
|
||||
if (isFirstWord) {
|
||||
if (includeCommands) {
|
||||
forEachCommand(&candidates, printCandidateCommand);
|
||||
} else {
|
||||
directory_list(directory, &candidates, printCandidateEntry);
|
||||
}
|
||||
directory_list(directory, &candidates, printCandidateEntry);
|
||||
printf("\n");
|
||||
*outListed = true;
|
||||
}
|
||||
@ -269,9 +275,17 @@ int runCommand(int argc, char** argv, int* found) {
|
||||
*found = 1;
|
||||
// ELF binaries and shell scripts are told apart by content rather than by extension,
|
||||
// since the filesystem is FAT and carries no execute bit to consult.
|
||||
return app_is_executable_path(resolved)
|
||||
? runElf(resolved, argc, argv)
|
||||
: runScript(resolved, argc, argv);
|
||||
if (app_is_executable_path(resolved)) {
|
||||
return runElf(resolved, argc, argv);
|
||||
}
|
||||
// A script runs in its own `sh` app instance, so it gets its own task and stack rather
|
||||
// than nesting another interpreter on this one's.
|
||||
std::vector<char*> shArgv;
|
||||
shArgv.push_back(const_cast<char*>("sh"));
|
||||
for (int i = 0; i < argc; i++) {
|
||||
shArgv.push_back(argv[i]);
|
||||
}
|
||||
return runFromMemory("sh", static_cast<int>(shArgv.size()), shArgv.data());
|
||||
}
|
||||
|
||||
*found = 0;
|
||||
@ -279,9 +293,8 @@ int runCommand(int argc, char** argv, int* found) {
|
||||
}
|
||||
|
||||
int runScriptSource(const char* source, int argc, char** argv) {
|
||||
// Scripts run on their own interpreter state rather than the session's. A script is reached
|
||||
// from inside sh_run_string(): the interpreter calls runCommand() for the `sh foo` line while
|
||||
// the outer parse is still in progress, and a fresh state keeps the two runs independent.
|
||||
// Scripts run on their own interpreter state rather than the session's: each runs in its own
|
||||
// `sh` app instance, and a fresh state keeps it independent of the interactive session.
|
||||
//
|
||||
// The consequence is that a script cannot see or modify the session's variables. `.` and
|
||||
// `source` remain the way to run something in the current shell, which is what they are for.
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace tt::app::shell {
|
||||
@ -49,8 +50,16 @@ int main(int, char*[]) {
|
||||
|
||||
puts("Type 'exit' to quit shell.");
|
||||
|
||||
LineEditor editor;
|
||||
editor.begin(PROMPT);
|
||||
// Heap-allocated: its history buffer is several KB, which the interpreter needs on the stack
|
||||
constexpr MemoryPolicy policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
|
||||
void* editorMemory = memory_alloc_with_policy(sizeof(LineEditor), &policy);
|
||||
if (editorMemory == nullptr) {
|
||||
puts("shell: out of memory");
|
||||
Shell::shutdown();
|
||||
return 1;
|
||||
}
|
||||
auto* editor = new (editorMemory) LineEditor();
|
||||
editor->begin(PROMPT);
|
||||
|
||||
// Blocks until a byte arrives or the terminal app running this one closes its end (touch to
|
||||
// exit), which read() reports the same way any closed pipe does: 0, ending this loop. Typing
|
||||
@ -59,17 +68,20 @@ int main(int, char*[]) {
|
||||
char c;
|
||||
while (read(STDIN_FILENO, &c, 1) == 1) {
|
||||
const char* line = nullptr;
|
||||
if (editor.feed(c, &line)) {
|
||||
if (editor->feed(c, &line)) {
|
||||
Shell::execute(line);
|
||||
// Print \n if output didn't end with it, to make the shell more readable.
|
||||
endLineIfNeeded();
|
||||
if (Shell::shouldExit()) {
|
||||
break;
|
||||
}
|
||||
editor.begin(PROMPT);
|
||||
editor->begin(PROMPT);
|
||||
}
|
||||
}
|
||||
|
||||
editor->~LineEditor();
|
||||
memory_free(editorMemory);
|
||||
|
||||
const int exitCode = Shell::exitCode();
|
||||
Shell::shutdown();
|
||||
return exitCode;
|
||||
@ -81,7 +93,7 @@ extern const ::AppManifest manifest = {
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(main) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN | APP_MANIFEST_FLAG_HEADLESS,
|
||||
.stack = { .depth = 11 * 1024, .desired_memory_capability = 0 },
|
||||
.stack = { .depth = 6144, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
static int32_t shMain(int argc, char* argv[]) {
|
||||
@ -111,7 +123,7 @@ extern const ::AppManifest sh_manifest = {
|
||||
.category = APP_CATEGORY_SYSTEM,
|
||||
.location = { .type = APP_LOCATION_MEMORY, .location = reinterpret_cast<void*>(shMain) },
|
||||
.flags = APP_MANIFEST_FLAG_HIDDEN | APP_MANIFEST_FLAG_HEADLESS,
|
||||
.stack = { .depth = 0, .desired_memory_capability = 0 },
|
||||
.stack = { .depth = 8192, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@ -1,309 +0,0 @@
|
||||
// POSIX arithmetic expansion `$(( ))` evaluator.
|
||||
//
|
||||
// Recursive-descent / precedence-climbing parser over a `const char *`,
|
||||
// operating on signed `long` only. No floats, no arrays, no `**`/`++`/`--`
|
||||
// (dash rejects those; so do we), and -- by scope decision -- no ternary,
|
||||
// bitwise, or shift operators. Bare variable names auto-deref;
|
||||
// unset/empty is 0; assignment operators write back to shell variables.
|
||||
#include <Tactility/app/shell/shell/sh_arith.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ARITH_MAX_DEPTH 32 // recursion guard for variable-value re-eval
|
||||
|
||||
typedef struct {
|
||||
sh_state *st;
|
||||
const char *p; // current position
|
||||
int err; // nonzero once an error occurs
|
||||
const char *msg; // error message (static string)
|
||||
int depth; // variable-deref recursion depth
|
||||
} actx;
|
||||
|
||||
static long parse_comma(actx *c);
|
||||
static long parse_assign(actx *c);
|
||||
|
||||
static void fail(actx *c, const char *m)
|
||||
{
|
||||
if (!c->err) { c->err = 1; c->msg = m; }
|
||||
}
|
||||
|
||||
static void skip_ws(actx *c)
|
||||
{
|
||||
while (*c->p == ' ' || *c->p == '\t' || *c->p == '\n') c->p++;
|
||||
}
|
||||
|
||||
// ---- token peeking ----------------------------------------------------------
|
||||
// We tokenize on the fly. `peek` matches a fixed operator string at the current
|
||||
// position (after skipping whitespace) without consuming.
|
||||
static int peek(actx *c, const char *op)
|
||||
{
|
||||
skip_ws(c);
|
||||
return strncmp(c->p, op, strlen(op)) == 0;
|
||||
}
|
||||
|
||||
// Consume operator `op` if present; returns 1 if consumed.
|
||||
static int eat(actx *c, const char *op)
|
||||
{
|
||||
if (peek(c, op)) { c->p += strlen(op); return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- variable deref ---------------------------------------------------------
|
||||
// Evaluate the arithmetic value of a variable's string value (POSIX: variable
|
||||
// values are themselves arithmetic expressions). Empty/unset -> 0.
|
||||
static long deref(actx *c, const char *name)
|
||||
{
|
||||
const char *v = sh_get(c->st, name);
|
||||
if (!v) return 0;
|
||||
while (*v == ' ' || *v == '\t' || *v == '\n') v++; // empty/blank value -> 0
|
||||
if (!*v) return 0;
|
||||
if (c->depth >= ARITH_MAX_DEPTH) { fail(c, "arithmetic recursion too deep"); return 0; }
|
||||
// Parse the value as a fresh sub-expression.
|
||||
actx sub = *c;
|
||||
sub.p = v;
|
||||
sub.depth = c->depth + 1;
|
||||
long r = parse_comma(&sub);
|
||||
skip_ws(&sub);
|
||||
if (!sub.err && *sub.p) fail(&sub, "invalid arithmetic value");
|
||||
if (sub.err) { fail(c, sub.msg); return 0; }
|
||||
return r;
|
||||
}
|
||||
|
||||
// Read a bare identifier at c->p into `name` (caller-sized buf). Returns len.
|
||||
static int read_ident(actx *c, char *name, int cap)
|
||||
{
|
||||
int n = 0;
|
||||
while ((isalnum((unsigned char)*c->p) || *c->p == '_') && n < cap - 1)
|
||||
name[n++] = *c->p++;
|
||||
name[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
// ---- integer literal --------------------------------------------------------
|
||||
static long parse_number(actx *c)
|
||||
{
|
||||
const char *s = c->p;
|
||||
long val = 0;
|
||||
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
|
||||
s += 2;
|
||||
if (!isxdigit((unsigned char)*s)) { fail(c, "bad hex constant"); return 0; }
|
||||
while (isxdigit((unsigned char)*s)) {
|
||||
int d = isdigit((unsigned char)*s) ? *s - '0'
|
||||
: (tolower((unsigned char)*s) - 'a' + 10);
|
||||
val = val * 16 + d;
|
||||
s++;
|
||||
}
|
||||
} else if (s[0] == '0' && isdigit((unsigned char)s[1])) {
|
||||
s++; // octal
|
||||
while (*s >= '0' && *s <= '7') { val = val * 8 + (*s - '0'); s++; }
|
||||
if (*s == '8' || *s == '9') { fail(c, "bad octal constant"); return 0; }
|
||||
} else {
|
||||
while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; }
|
||||
}
|
||||
c->p = s;
|
||||
return val;
|
||||
}
|
||||
|
||||
// ---- primary ----------------------------------------------------------------
|
||||
static long parse_primary(actx *c)
|
||||
{
|
||||
skip_ws(c);
|
||||
if (*c->p == '(') {
|
||||
c->p++;
|
||||
long r = parse_comma(c);
|
||||
skip_ws(c);
|
||||
if (*c->p == ')') c->p++;
|
||||
else fail(c, "missing ) in arithmetic");
|
||||
return r;
|
||||
}
|
||||
// A leading '$' before an identifier: normal expansion would have handled
|
||||
// $var already, but if it reaches here, skip the '$' and deref the name.
|
||||
if (*c->p == '$') c->p++;
|
||||
|
||||
if (isdigit((unsigned char)*c->p)) return parse_number(c);
|
||||
|
||||
if (isalpha((unsigned char)*c->p) || *c->p == '_') {
|
||||
char name[128];
|
||||
read_ident(c, name, sizeof(name));
|
||||
skip_ws(c);
|
||||
// Assignment forms are handled at parse_assign level; here just deref.
|
||||
return deref(c, name);
|
||||
}
|
||||
fail(c, "unexpected token in arithmetic");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- unary ------------------------------------------------------------------
|
||||
static long parse_unary(actx *c)
|
||||
{
|
||||
skip_ws(c);
|
||||
if (eat(c, "+")) return parse_unary(c);
|
||||
if (eat(c, "-")) return -parse_unary(c);
|
||||
if (eat(c, "!")) return !parse_unary(c);
|
||||
return parse_primary(c);
|
||||
}
|
||||
|
||||
// ---- binary precedence levels ----------------------------------------------
|
||||
static long parse_mul(actx *c)
|
||||
{
|
||||
long l = parse_unary(c);
|
||||
for (;;) {
|
||||
skip_ws(c);
|
||||
// Reject `**` explicitly (not POSIX; dash errors).
|
||||
if (c->p[0] == '*' && c->p[1] == '*') { fail(c, "** not supported"); return 0; }
|
||||
if (eat(c, "*")) { l = l * parse_unary(c); }
|
||||
else if (eat(c, "/")) {
|
||||
long r = parse_unary(c);
|
||||
if (r == 0) { fail(c, "division by zero"); return 0; }
|
||||
l = l / r;
|
||||
} else if (eat(c, "%")) {
|
||||
long r = parse_unary(c);
|
||||
if (r == 0) { fail(c, "division by zero"); return 0; }
|
||||
l = l % r;
|
||||
} else break;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static long parse_add(actx *c)
|
||||
{
|
||||
long l = parse_mul(c);
|
||||
for (;;) {
|
||||
skip_ws(c);
|
||||
// Guard against ++/-- (not POSIX).
|
||||
if ((c->p[0] == '+' && c->p[1] == '+') || (c->p[0] == '-' && c->p[1] == '-')) {
|
||||
fail(c, "++/-- not supported"); return 0;
|
||||
}
|
||||
if (eat(c, "+")) l = l + parse_mul(c);
|
||||
else if (eat(c, "-")) l = l - parse_mul(c);
|
||||
else break;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static long parse_rel(actx *c)
|
||||
{
|
||||
long l = parse_add(c);
|
||||
for (;;) {
|
||||
skip_ws(c);
|
||||
if (eat(c, "<=")) l = (l <= parse_add(c));
|
||||
else if (eat(c, ">=")) l = (l >= parse_add(c));
|
||||
else if (eat(c, "<")) l = (l < parse_add(c));
|
||||
else if (eat(c, ">")) l = (l > parse_add(c));
|
||||
else break;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static long parse_eq(actx *c)
|
||||
{
|
||||
long l = parse_rel(c);
|
||||
for (;;) {
|
||||
skip_ws(c);
|
||||
if (eat(c, "==")) l = (l == parse_rel(c));
|
||||
else if (eat(c, "!=")) l = (l != parse_rel(c));
|
||||
else break;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
static long parse_land(actx *c)
|
||||
{
|
||||
long l = parse_eq(c);
|
||||
while (eat(c, "&&")) { long r = parse_eq(c); l = (l && r); }
|
||||
return l;
|
||||
}
|
||||
|
||||
static long parse_lor(actx *c)
|
||||
{
|
||||
long l = parse_land(c);
|
||||
while (eat(c, "||")) { long r = parse_land(c); l = (l || r); }
|
||||
return l;
|
||||
}
|
||||
|
||||
// ---- assignment (right-assoc) ----------------------------------------------
|
||||
// Detect `<ident> <assign-op>` at the current position. On match, records the
|
||||
// name and the compound operator char (0 for plain `=`), consumes both, and
|
||||
// returns 1. Otherwise leaves position untouched and returns 0.
|
||||
static int try_assign_lhs(actx *c, char *name, int cap, char *op)
|
||||
{
|
||||
const char *save = c->p;
|
||||
skip_ws(c);
|
||||
if (*c->p == '$') c->p++;
|
||||
if (!(isalpha((unsigned char)*c->p) || *c->p == '_')) { c->p = save; return 0; }
|
||||
read_ident(c, name, cap);
|
||||
skip_ws(c);
|
||||
const char *ops[] = { "+=", "-=", "*=", "/=", "%=", NULL };
|
||||
for (int i = 0; ops[i]; i++) {
|
||||
size_t len = strlen(ops[i]);
|
||||
if (strncmp(c->p, ops[i], len) == 0) {
|
||||
*op = ops[i][len - 2]; // the operation char before '='
|
||||
c->p += len;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
// plain '=' but not '=='
|
||||
if (c->p[0] == '=' && c->p[1] != '=') { *op = 0; c->p++; return 1; }
|
||||
c->p = save;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static long apply_compound(actx *c, char op, long cur, long rhs)
|
||||
{
|
||||
switch (op) {
|
||||
case 0: return rhs;
|
||||
case '+': return cur + rhs;
|
||||
case '-': return cur - rhs;
|
||||
case '*': return cur * rhs;
|
||||
case '/': if (rhs == 0) { fail(c, "division by zero"); return 0; } return cur / rhs;
|
||||
case '%': if (rhs == 0) { fail(c, "division by zero"); return 0; } return cur % rhs;
|
||||
default: return rhs;
|
||||
}
|
||||
}
|
||||
|
||||
static long parse_assign(actx *c)
|
||||
{
|
||||
char name[128], op;
|
||||
if (try_assign_lhs(c, name, sizeof(name), &op)) {
|
||||
long rhs = parse_assign(c); // right-associative
|
||||
if (c->err) return 0;
|
||||
long cur = 0;
|
||||
if (op) cur = deref(c, name);
|
||||
long res = apply_compound(c, op, cur, rhs);
|
||||
if (c->err) return 0;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%ld", res);
|
||||
sh_set(c->st, name, buf);
|
||||
return res;
|
||||
}
|
||||
return parse_lor(c);
|
||||
}
|
||||
|
||||
static long parse_comma(actx *c)
|
||||
{
|
||||
long l = parse_assign(c);
|
||||
while (eat(c, ",")) l = parse_assign(c);
|
||||
return l;
|
||||
}
|
||||
|
||||
int sh_arith_eval(sh_state *st, const char *expr, long *out, const char **errmsg)
|
||||
{
|
||||
actx c = { .st = st, .p = expr, .err = 0, .msg = NULL, .depth = 0 };
|
||||
// An empty or all-whitespace expression evaluates to 0 (dash: `$(( ))`).
|
||||
skip_ws(&c);
|
||||
if (!*c.p) { if (errmsg) *errmsg = NULL; *out = 0; return 0; }
|
||||
long r = parse_comma(&c);
|
||||
skip_ws(&c);
|
||||
if (!c.err && *c.p) fail(&c, "unexpected trailing characters in arithmetic");
|
||||
if (c.err) {
|
||||
if (errmsg) *errmsg = c.msg;
|
||||
return 1;
|
||||
}
|
||||
if (errmsg) *errmsg = NULL;
|
||||
*out = r;
|
||||
return 0;
|
||||
}
|
||||
404
Tactility/Source/app/shell/shell/sh_arith.cpp
Normal file
404
Tactility/Source/app/shell/shell/sh_arith.cpp
Normal file
@ -0,0 +1,404 @@
|
||||
// POSIX arithmetic expansion `$(( ))` evaluator.
|
||||
//
|
||||
// Operator-precedence evaluation over explicit operator/value/input stacks,
|
||||
// operating on signed `long` only. No floats, no arrays, no `**`/`++`/`--`
|
||||
// (dash rejects those; so do we), and -- by scope decision -- no ternary,
|
||||
// bitwise, or shift operators. Bare variable names auto-deref;
|
||||
// unset/empty is 0; assignment operators write back to shell variables.
|
||||
//
|
||||
// A variable's value is itself an arithmetic expression. It is evaluated by
|
||||
// pushing it as a nested input rather than by recursing, so neither deeply
|
||||
// parenthesized expressions nor chains of variables grow the native stack.
|
||||
#include <Tactility/app/shell/shell/sh_arith.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int ARITH_MAX_DEPTH = 32; // guard for variable-value re-evaluation
|
||||
|
||||
enum class Op {
|
||||
Comma, Assign, Or, And, Equal, NotEqual, Less, LessEqual, Greater, GreaterEqual,
|
||||
Add, Subtract, Multiply, Divide, Modulo, Plus, Negate, Not,
|
||||
// Markers: not operators, never reduced
|
||||
LeftParen, // '('
|
||||
Deref, // boundary of a variable value being evaluated
|
||||
AssignDone // boundary of a compound assignment's current value being evaluated
|
||||
};
|
||||
|
||||
struct Operator {
|
||||
Op op;
|
||||
char assign = 0; // Assign/AssignDone: compound operation char, 0 for plain `=`
|
||||
std::string name; // Assign/AssignDone: target variable
|
||||
long rhs = 0; // AssignDone: the already-evaluated right-hand side
|
||||
};
|
||||
|
||||
struct Input {
|
||||
std::string text;
|
||||
size_t pos = 0;
|
||||
int depth = 0; // 0 for the expression itself, >0 for variable values
|
||||
|
||||
char at(size_t offset = 0) const { return pos + offset < text.size() ? text[pos + offset] : '\0'; }
|
||||
bool startsWith(const char* token) const { return text.compare(pos, std::char_traits<char>::length(token), token) == 0; }
|
||||
void skipWhitespace() { while (at() == ' ' || at() == '\t' || at() == '\n') pos++; }
|
||||
};
|
||||
|
||||
bool isMarker(Op op) {
|
||||
return op == Op::LeftParen || op == Op::Deref || op == Op::AssignDone;
|
||||
}
|
||||
|
||||
bool isUnary(Op op) {
|
||||
return op == Op::Plus || op == Op::Negate || op == Op::Not;
|
||||
}
|
||||
|
||||
int precedence(Op op) {
|
||||
switch (op) {
|
||||
case Op::Comma: return 1;
|
||||
case Op::Assign: return 2;
|
||||
case Op::Or: return 3;
|
||||
case Op::And: return 4;
|
||||
case Op::Equal: case Op::NotEqual: return 5;
|
||||
case Op::Less: case Op::LessEqual: case Op::Greater: case Op::GreaterEqual: return 6;
|
||||
case Op::Add: case Op::Subtract: return 7;
|
||||
case Op::Multiply: case Op::Divide: case Op::Modulo: return 8;
|
||||
default: return 9; // unary
|
||||
}
|
||||
}
|
||||
|
||||
class Evaluator {
|
||||
public:
|
||||
Evaluator(sh_state* state, const char* expr) : st(state) {
|
||||
inputs.push_back(Input { expr, 0, 0 });
|
||||
}
|
||||
|
||||
int run(long* out, const char** errmsg) {
|
||||
// An empty or all-whitespace expression evaluates to 0 (dash: `$(( ))`).
|
||||
inputs.back().skipWhitespace();
|
||||
if (inputs.back().at() == '\0') {
|
||||
if (errmsg) *errmsg = nullptr;
|
||||
*out = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (error == nullptr && step()) {}
|
||||
|
||||
if (error != nullptr) {
|
||||
if (errmsg) *errmsg = error;
|
||||
return 1;
|
||||
}
|
||||
if (errmsg) *errmsg = nullptr;
|
||||
*out = values.back();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
sh_state* st;
|
||||
std::vector<Input> inputs;
|
||||
std::vector<Operator> ops;
|
||||
std::vector<long> values;
|
||||
const char* error = nullptr;
|
||||
bool expectOperand = true;
|
||||
// An assignment may only start an assignment-expression: at the very start, or after '(', ',' or another assignment.
|
||||
bool assignAllowed = true;
|
||||
|
||||
void fail(const char* message) {
|
||||
if (error == nullptr) error = message;
|
||||
}
|
||||
|
||||
long popValue() {
|
||||
long value = values.back();
|
||||
values.pop_back();
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Performs one unit of work. Returns false when evaluation is complete. */
|
||||
bool step() {
|
||||
Input& in = inputs.back();
|
||||
in.skipWhitespace();
|
||||
if (expectOperand) {
|
||||
readOperand(in);
|
||||
return true;
|
||||
}
|
||||
if (in.at() == '\0') {
|
||||
return endOfInput();
|
||||
}
|
||||
readOperator(in);
|
||||
return true;
|
||||
}
|
||||
|
||||
void readOperand(Input& in) {
|
||||
char c = in.at();
|
||||
if (c == '(') { in.pos++; ops.push_back({ Op::LeftParen }); assignAllowed = true; return; }
|
||||
if (c == '+') { in.pos++; ops.push_back({ Op::Plus }); assignAllowed = false; return; }
|
||||
if (c == '-') { in.pos++; ops.push_back({ Op::Negate }); assignAllowed = false; return; }
|
||||
if (c == '!') { in.pos++; ops.push_back({ Op::Not }); assignAllowed = false; return; }
|
||||
|
||||
if (assignAllowed && tryAssignment(in)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A leading '$' before an identifier: normal expansion would have handled
|
||||
// $var already, but if it reaches here, skip the '$' and deref the name.
|
||||
if (in.at() == '$') in.pos++;
|
||||
|
||||
if (isdigit(static_cast<unsigned char>(in.at()))) {
|
||||
long value = readNumber(in);
|
||||
if (error == nullptr) pushOperand(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isalpha(static_cast<unsigned char>(in.at())) || in.at() == '_') {
|
||||
std::string name = readIdentifier(in);
|
||||
deref(name, [this](long value) { pushOperand(value); }, Operator { Op::Deref });
|
||||
return;
|
||||
}
|
||||
|
||||
fail("unexpected token in arithmetic");
|
||||
}
|
||||
|
||||
void pushOperand(long value) {
|
||||
values.push_back(value);
|
||||
expectOperand = false;
|
||||
assignAllowed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a variable's arithmetic value. An unset or blank value is available immediately and
|
||||
* passed to onImmediate. Otherwise its text is pushed as a nested input behind `marker`, and the
|
||||
* result arrives through endOfInput().
|
||||
*/
|
||||
template <typename OnImmediate>
|
||||
void deref(const std::string& name, OnImmediate&& onImmediate, Operator marker) {
|
||||
const char* value = sh_get(st, name.c_str());
|
||||
if (value != nullptr) {
|
||||
while (*value == ' ' || *value == '\t' || *value == '\n') value++; // empty/blank value -> 0
|
||||
}
|
||||
if (value == nullptr || *value == '\0') {
|
||||
onImmediate(0);
|
||||
return;
|
||||
}
|
||||
int depth = inputs.back().depth + 1;
|
||||
if (depth > ARITH_MAX_DEPTH) {
|
||||
fail("arithmetic recursion too deep");
|
||||
return;
|
||||
}
|
||||
ops.push_back(std::move(marker));
|
||||
// Copied: evaluating the value may reassign the variable it came from.
|
||||
inputs.push_back(Input { value, 0, depth });
|
||||
expectOperand = true;
|
||||
assignAllowed = true;
|
||||
}
|
||||
|
||||
// Detect `<ident> <assign-op>` at the current position. On match, pushes the
|
||||
// assignment and returns true. Otherwise leaves the position untouched.
|
||||
bool tryAssignment(Input& in) {
|
||||
size_t save = in.pos;
|
||||
if (in.at() == '$') in.pos++;
|
||||
if (!(isalpha(static_cast<unsigned char>(in.at())) || in.at() == '_')) { in.pos = save; return false; }
|
||||
std::string name = readIdentifier(in);
|
||||
in.skipWhitespace();
|
||||
static const char* compound[] = { "+=", "-=", "*=", "/=", "%=" };
|
||||
for (const char* op : compound) {
|
||||
if (in.startsWith(op)) {
|
||||
in.pos += 2;
|
||||
ops.push_back({ Op::Assign, op[0], std::move(name) });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// plain '=' but not '=='
|
||||
if (in.at() == '=' && in.at(1) != '=') {
|
||||
in.pos++;
|
||||
ops.push_back({ Op::Assign, 0, std::move(name) });
|
||||
return true;
|
||||
}
|
||||
in.pos = save;
|
||||
return false;
|
||||
}
|
||||
|
||||
static std::string readIdentifier(Input& in) {
|
||||
size_t start = in.pos;
|
||||
while (isalnum(static_cast<unsigned char>(in.at())) || in.at() == '_') in.pos++;
|
||||
return in.text.substr(start, in.pos - start);
|
||||
}
|
||||
|
||||
long readNumber(Input& in) {
|
||||
long value = 0;
|
||||
if (in.at() == '0' && (in.at(1) == 'x' || in.at(1) == 'X')) {
|
||||
in.pos += 2;
|
||||
if (!isxdigit(static_cast<unsigned char>(in.at()))) { fail("bad hex constant"); return 0; }
|
||||
while (isxdigit(static_cast<unsigned char>(in.at()))) {
|
||||
char c = in.at();
|
||||
int digit = isdigit(static_cast<unsigned char>(c)) ? c - '0' : (tolower(static_cast<unsigned char>(c)) - 'a' + 10);
|
||||
value = value * 16 + digit;
|
||||
in.pos++;
|
||||
}
|
||||
} else if (in.at() == '0' && isdigit(static_cast<unsigned char>(in.at(1)))) {
|
||||
in.pos++; // octal
|
||||
while (in.at() >= '0' && in.at() <= '7') { value = value * 8 + (in.at() - '0'); in.pos++; }
|
||||
if (in.at() == '8' || in.at() == '9') { fail("bad octal constant"); return 0; }
|
||||
} else {
|
||||
while (isdigit(static_cast<unsigned char>(in.at()))) { value = value * 10 + (in.at() - '0'); in.pos++; }
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const char* trailingError() const {
|
||||
return inputs.back().depth > 0 ? "invalid arithmetic value" : "unexpected trailing characters in arithmetic";
|
||||
}
|
||||
|
||||
void readOperator(Input& in) {
|
||||
Op op;
|
||||
size_t length = 1;
|
||||
char c = in.at();
|
||||
if (c == ')') {
|
||||
if (!ops.empty() && !isMarker(ops.back().op)) { reduce(); return; }
|
||||
if (ops.empty() || ops.back().op != Op::LeftParen) { fail(trailingError()); return; }
|
||||
ops.pop_back();
|
||||
in.pos++;
|
||||
return;
|
||||
}
|
||||
if (c == ',') op = Op::Comma;
|
||||
else if (in.startsWith("||")) { op = Op::Or; length = 2; }
|
||||
else if (in.startsWith("&&")) { op = Op::And; length = 2; }
|
||||
else if (in.startsWith("==")) { op = Op::Equal; length = 2; }
|
||||
else if (in.startsWith("!=")) { op = Op::NotEqual; length = 2; }
|
||||
else if (in.startsWith("<=")) { op = Op::LessEqual; length = 2; }
|
||||
else if (in.startsWith(">=")) { op = Op::GreaterEqual; length = 2; }
|
||||
else if (c == '<') op = Op::Less;
|
||||
else if (c == '>') op = Op::Greater;
|
||||
else if (in.startsWith("**")) { fail("** not supported"); return; }
|
||||
else if (c == '*') op = Op::Multiply;
|
||||
else if (c == '/') op = Op::Divide;
|
||||
else if (c == '%') op = Op::Modulo;
|
||||
else if (in.startsWith("++") || in.startsWith("--")) { fail("++/-- not supported"); return; }
|
||||
else if (c == '+') op = Op::Add;
|
||||
else if (c == '-') op = Op::Subtract;
|
||||
else { fail(trailingError()); return; }
|
||||
|
||||
// Left-associative: finish everything of equal or higher precedence first.
|
||||
if (!ops.empty() && !isMarker(ops.back().op) && precedence(ops.back().op) >= precedence(op)) {
|
||||
reduce();
|
||||
return;
|
||||
}
|
||||
in.pos += length;
|
||||
ops.push_back({ op });
|
||||
expectOperand = true;
|
||||
assignAllowed = op == Op::Comma;
|
||||
}
|
||||
|
||||
/** Called at the end of the current input. Returns false once the whole expression is done. */
|
||||
bool endOfInput() {
|
||||
if (!ops.empty() && !isMarker(ops.back().op)) {
|
||||
reduce();
|
||||
return true;
|
||||
}
|
||||
if (!ops.empty() && ops.back().op == Op::LeftParen) {
|
||||
fail("missing ) in arithmetic");
|
||||
return true;
|
||||
}
|
||||
if (inputs.size() == 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Operator marker = std::move(ops.back());
|
||||
ops.pop_back();
|
||||
inputs.pop_back();
|
||||
if (marker.op == Op::AssignDone) {
|
||||
long current = popValue();
|
||||
finishAssignment(marker.name, marker.assign, current, marker.rhs);
|
||||
} else {
|
||||
// Op::Deref: the value is already on the value stack
|
||||
expectOperand = false;
|
||||
assignAllowed = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
long apply(char op, long current, long rhs) {
|
||||
switch (op) {
|
||||
case 0: return rhs;
|
||||
case '+': return current + rhs;
|
||||
case '-': return current - rhs;
|
||||
case '*': return current * rhs;
|
||||
case '/': if (rhs == 0) { fail("division by zero"); return 0; } return current / rhs;
|
||||
case '%': if (rhs == 0) { fail("division by zero"); return 0; } return current % rhs;
|
||||
default: return rhs;
|
||||
}
|
||||
}
|
||||
|
||||
void finishAssignment(const std::string& name, char op, long current, long rhs) {
|
||||
long result = apply(op, current, rhs);
|
||||
if (error != nullptr) return;
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "%ld", result);
|
||||
sh_set(st, name.c_str(), buffer);
|
||||
pushOperand(result);
|
||||
}
|
||||
|
||||
/** Applies the operator on top of the stack. */
|
||||
void reduce() {
|
||||
Operator top = std::move(ops.back());
|
||||
ops.pop_back();
|
||||
|
||||
if (isUnary(top.op)) {
|
||||
long value = popValue();
|
||||
values.push_back(top.op == Op::Negate ? -value : top.op == Op::Not ? !value : value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (top.op == Op::Assign) {
|
||||
long rhs = popValue();
|
||||
if (top.assign == 0) {
|
||||
finishAssignment(top.name, 0, 0, rhs);
|
||||
return;
|
||||
}
|
||||
// Compound: the variable's current value is read after the right-hand side, like dash.
|
||||
char assign = top.assign;
|
||||
std::string name = top.name;
|
||||
deref(
|
||||
name,
|
||||
[&](long current) { finishAssignment(name, assign, current, rhs); },
|
||||
Operator { Op::AssignDone, assign, name, rhs }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
long rhs = popValue();
|
||||
long lhs = popValue();
|
||||
long result = 0;
|
||||
switch (top.op) {
|
||||
case Op::Comma: result = rhs; break;
|
||||
case Op::Or: result = (lhs || rhs); break;
|
||||
case Op::And: result = (lhs && rhs); break;
|
||||
case Op::Equal: result = (lhs == rhs); break;
|
||||
case Op::NotEqual: result = (lhs != rhs); break;
|
||||
case Op::Less: result = (lhs < rhs); break;
|
||||
case Op::LessEqual: result = (lhs <= rhs); break;
|
||||
case Op::Greater: result = (lhs > rhs); break;
|
||||
case Op::GreaterEqual: result = (lhs >= rhs); break;
|
||||
case Op::Add: result = lhs + rhs; break;
|
||||
case Op::Subtract: result = lhs - rhs; break;
|
||||
case Op::Multiply: result = lhs * rhs; break;
|
||||
case Op::Divide:
|
||||
if (rhs == 0) { fail("division by zero"); return; }
|
||||
result = lhs / rhs;
|
||||
break;
|
||||
case Op::Modulo:
|
||||
if (rhs == 0) { fail("division by zero"); return; }
|
||||
result = lhs % rhs;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
values.push_back(result);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int sh_arith_eval(sh_state* st, const char* expr, long* out, const char** errmsg)
|
||||
{
|
||||
return Evaluator(st, expr).run(out, errmsg);
|
||||
}
|
||||
@ -112,66 +112,98 @@ static int t_binop(const char *a, const char *op, const char *b, int *err)
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct { char **a; int n; int i; int err; } tparse;
|
||||
// Operator-precedence evaluation with explicit value/operator stacks, so a
|
||||
// long run of `!` or `(` arguments can't exhaust the native stack. Operators:
|
||||
// '!' (prefix, binds to the following primary or group), 'a' (-a) above 'o'
|
||||
// (-o), and '(' as a group marker. Both sides of -a/-o are always evaluated,
|
||||
// matching dash.
|
||||
typedef struct {
|
||||
char *ops;
|
||||
int *vals;
|
||||
int nops, nvals, cap;
|
||||
} tstack;
|
||||
|
||||
static int t_oexpr(tparse *t);
|
||||
static void t_push_op(tstack *t, char op) { t->ops[t->nops++] = op; }
|
||||
static void t_push_val(tstack *t, int v) { t->vals[t->nvals++] = v; }
|
||||
|
||||
static int t_primary(tparse *t)
|
||||
// Apply the binary operator on top of the op stack.
|
||||
static void t_reduce(tstack *t)
|
||||
{
|
||||
if (t->i >= t->n) return 0; // missing expression -> false
|
||||
char *s = t->a[t->i];
|
||||
if (strcmp(s, "(") == 0) {
|
||||
t->i++;
|
||||
int r = t_oexpr(t);
|
||||
if (t->i >= t->n || strcmp(t->a[t->i], ")") != 0) { t->err = 1; return r; }
|
||||
t->i++;
|
||||
return r;
|
||||
}
|
||||
if (t_is_unop(s) && t->i + 1 < t->n) {
|
||||
int r = t_unop(s[1], t->a[t->i + 1]);
|
||||
t->i += 2;
|
||||
return r;
|
||||
}
|
||||
if (t->i + 1 < t->n && t_is_binop(t->a[t->i + 1])) {
|
||||
if (t->i + 2 >= t->n) { t->err = 1; return 0; }
|
||||
int r = t_binop(t->a[t->i], t->a[t->i + 1], t->a[t->i + 2], &t->err);
|
||||
t->i += 3;
|
||||
return r;
|
||||
}
|
||||
int r = s[0] != 0;
|
||||
t->i++;
|
||||
return r;
|
||||
char op = t->ops[--t->nops];
|
||||
int rhs = t->vals[--t->nvals];
|
||||
int lhs = t->vals[--t->nvals];
|
||||
t_push_val(t, op == 'a' ? (rhs && lhs) : (rhs || lhs));
|
||||
}
|
||||
|
||||
static int t_nexpr(tparse *t)
|
||||
// Push a finished operand, applying any '!' waiting directly in front of it.
|
||||
static void t_push_operand(tstack *t, int v)
|
||||
{
|
||||
if (t->i < t->n && strcmp(t->a[t->i], "!") == 0) {
|
||||
t->i++;
|
||||
return !t_nexpr(t);
|
||||
}
|
||||
return t_primary(t);
|
||||
while (t->nops > 0 && t->ops[t->nops - 1] == '!') { t->nops--; v = !v; }
|
||||
t_push_val(t, v);
|
||||
}
|
||||
|
||||
static int t_aexpr(tparse *t)
|
||||
// Evaluate argv[0..n). Returns 1/0 (true/false); sets *err on a syntax error;
|
||||
// *consumed receives how many arguments were used.
|
||||
static int t_evaluate(char **a, int n, int *err, int *consumed)
|
||||
{
|
||||
int r = t_nexpr(t);
|
||||
while (t->i < t->n && strcmp(t->a[t->i], "-a") == 0) {
|
||||
t->i++;
|
||||
int rhs = t_nexpr(t);
|
||||
r = rhs && r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
tstack t;
|
||||
t.cap = n + 1;
|
||||
t.ops = malloc(t.cap);
|
||||
t.vals = malloc(t.cap * sizeof(int));
|
||||
t.nops = t.nvals = 0;
|
||||
|
||||
static int t_oexpr(tparse *t)
|
||||
{
|
||||
int r = t_aexpr(t);
|
||||
while (t->i < t->n && strcmp(t->a[t->i], "-o") == 0) {
|
||||
t->i++;
|
||||
int rhs = t_aexpr(t);
|
||||
r = rhs || r;
|
||||
int i = 0;
|
||||
int expect_operand = 1;
|
||||
for (;;) {
|
||||
if (expect_operand) {
|
||||
if (i >= n) { t_push_operand(&t, 0); expect_operand = 0; break; } // missing expression -> false
|
||||
char *s = a[i];
|
||||
if (strcmp(s, "!") == 0) { t_push_op(&t, '!'); i++; continue; }
|
||||
if (strcmp(s, "(") == 0) { t_push_op(&t, '('); i++; continue; }
|
||||
int r;
|
||||
if (t_is_unop(s) && i + 1 < n) {
|
||||
r = t_unop(s[1], a[i + 1]);
|
||||
i += 2;
|
||||
} else if (i + 1 < n && t_is_binop(a[i + 1])) {
|
||||
if (i + 2 >= n) { *err = 1; i = n; r = 0; }
|
||||
else { r = t_binop(a[i], a[i + 1], a[i + 2], err); i += 3; }
|
||||
} else {
|
||||
r = s[0] != 0;
|
||||
i++;
|
||||
}
|
||||
t_push_operand(&t, r);
|
||||
expect_operand = 0;
|
||||
continue;
|
||||
}
|
||||
if (i < n && (strcmp(a[i], "-a") == 0 || strcmp(a[i], "-o") == 0)) {
|
||||
char op = a[i][1];
|
||||
while (t.nops > 0 && t.ops[t.nops - 1] != '(' && (op == 'o' || t.ops[t.nops - 1] == 'a')) t_reduce(&t);
|
||||
t_push_op(&t, op);
|
||||
i++;
|
||||
expect_operand = 1;
|
||||
continue;
|
||||
}
|
||||
if (i < n && strcmp(a[i], ")") == 0) {
|
||||
while (t.nops > 0 && t.ops[t.nops - 1] != '(') t_reduce(&t);
|
||||
if (t.nops == 0) break; // unmatched ')': left for the caller to reject
|
||||
t.nops--; // the '('
|
||||
i++;
|
||||
int v = t.vals[--t.nvals];
|
||||
t_push_operand(&t, v);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return r;
|
||||
while (t.nops > 0) {
|
||||
if (t.ops[t.nops - 1] == '(') { *err = 1; t.nops--; continue; } // missing ')'
|
||||
if (t.ops[t.nops - 1] == '!') { t.nops--; continue; }
|
||||
t_reduce(&t);
|
||||
}
|
||||
int result = t.nvals > 0 ? t.vals[t.nvals - 1] : 0;
|
||||
free(t.ops);
|
||||
free(t.vals);
|
||||
*consumed = i;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Evaluate a test expression (argv/argc already stripped of the program name
|
||||
@ -189,9 +221,9 @@ static int eval_test(int argc, char **argv)
|
||||
return err ? 2 : (r ? 0 : 1);
|
||||
}
|
||||
|
||||
tparse t = { argv, argc, 0, 0 };
|
||||
int r = t_oexpr(&t);
|
||||
if (t.err || t.i != t.n) return 2;
|
||||
int err = 0, consumed = 0;
|
||||
int r = t_evaluate(argv, argc, &err, &consumed);
|
||||
if (err || consumed != argc) return 2;
|
||||
return r ? 0 : 1;
|
||||
}
|
||||
|
||||
@ -249,60 +281,6 @@ static int builtin_echo(int argc, char **argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- eval / source ---------------------------------------------------------
|
||||
|
||||
static int builtin_eval(sh_state *st, int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) return 0;
|
||||
int len = 0;
|
||||
for (int i = 1; i < argc; i++) len += (int)strlen(argv[i]) + 1;
|
||||
char *joined = malloc(len + 1);
|
||||
joined[0] = 0;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (i > 1) strcat(joined, " ");
|
||||
strcat(joined, argv[i]);
|
||||
}
|
||||
int rc = sh_run_string(st, joined);
|
||||
free(joined);
|
||||
// A syntax error in eval'd text is fatal in a non-interactive shell
|
||||
// (dash aborts with status 2); a runtime failure inside is not.
|
||||
if (st->parse_error) { st->exiting = 1; st->exit_code = 2; }
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int builtin_source(sh_state *st, int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) { fprintf(stderr, "%s: filename argument required\n", argv[0]); return 2; }
|
||||
/* Tactility: read through the bridge rather than fopen() directly. The shell's working
|
||||
* directory is tracked in ShellFs and is unrelated to the C library's, so a relative path
|
||||
* would otherwise resolve against the wrong place - and file access has to take the mount
|
||||
* lock, since the display and SD card can share a bus. */
|
||||
size_t len = 0;
|
||||
char *src = shell_bridge_read_file(argv[1], &len);
|
||||
if (!src) { fprintf(stderr, "%s: %s: cannot open\n", argv[0], argv[1]); return 1; }
|
||||
|
||||
// Extra args set the positional params for the duration (dash behavior).
|
||||
char **sp = NULL; int snp = 0, replaced = 0;
|
||||
if (argc > 2) {
|
||||
sp = st->pos; snp = st->npos;
|
||||
st->pos = NULL; st->npos = 0;
|
||||
sh_set_positional(st, NULL, argv + 2, argc - 2);
|
||||
replaced = 1;
|
||||
}
|
||||
int rc = sh_run_string(st, src);
|
||||
if (replaced) {
|
||||
for (int i = 0; i < st->npos; i++) free(st->pos[i]);
|
||||
free(st->pos);
|
||||
st->pos = sp; st->npos = snp;
|
||||
}
|
||||
free(src);
|
||||
// `return` inside a sourced file stops the file, not the whole shell.
|
||||
if (st->returning) { rc = st->return_code; st->returning = 0; }
|
||||
// A syntax error in the sourced file is fatal (dash aborts with status 2).
|
||||
if (st->parse_error) { st->exiting = 1; st->exit_code = 2; }
|
||||
return rc;
|
||||
}
|
||||
|
||||
// ---- read ------------------------------------------------------------------
|
||||
|
||||
// Field-split the raw line (buf[0..len), with a parallel `esc` flag marking
|
||||
@ -514,11 +492,6 @@ int sh_run_builtin(sh_state *st, int argc, char **argv, int *status)
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "eval") == 0) { *status = builtin_eval(st, argc, argv); return 1; }
|
||||
if (strcmp(cmd, ".") == 0 || strcmp(cmd, "source") == 0) {
|
||||
*status = builtin_source(st, argc, argv);
|
||||
return 1;
|
||||
}
|
||||
if (strcmp(cmd, "read") == 0) { *status = builtin_read(st, argc, argv); return 1; }
|
||||
|
||||
if (strcmp(cmd, "break") == 0 || strcmp(cmd, "continue") == 0) {
|
||||
|
||||
@ -1,559 +0,0 @@
|
||||
#include <Tactility/app/shell/shell/sh.h>
|
||||
#include <Tactility/app/shell/shell/sh_lex.h>
|
||||
#include <Tactility/app/shell/shell/sh_parse.h>
|
||||
#include <Tactility/app/shell/shell/sh_builtins.h>
|
||||
#include <Tactility/app/shell/shell/sh_glob.h>
|
||||
#include <Tactility/app/shell/shell/sh_port.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int exec_node(sh_state *st, node *n, int exempt);
|
||||
static int call_function(sh_state *st, sh_func *fn, int argc, char **argv, int exempt);
|
||||
|
||||
// Run one already-expanded argv (function, builtin, or external), no redirection.
|
||||
// `exempt` marks an errexit-suppressed context (propagated into functions).
|
||||
static int run_argv(sh_state *st, int argc, char **argv, int exempt)
|
||||
{
|
||||
int status = 0;
|
||||
// Functions override non-special builtins and externals (dash order).
|
||||
sh_func *fn = sh_func_find(st, argv[0]);
|
||||
if (fn) return call_function(st, fn, argc, argv, exempt);
|
||||
if (sh_run_builtin(st, argc, argv, &status)) return status;
|
||||
int found = 0;
|
||||
status = sh_port_run_external(argc, argv, &found);
|
||||
if (!found) {
|
||||
fprintf(stderr, "\x1B[91m%s: not found\x1B[0m\n", argv[0]);
|
||||
return 127;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// Apply a "NAME=raw" assignment (value is expanded). A leading `~` in the value
|
||||
// tilde-expands via the normal word-start rule (so a=~/src works); the full
|
||||
// POSIX colon rule (PATH=~/bin:~/sbin) is deliberately left out.
|
||||
static void apply_assign(sh_state *st, const char *raw)
|
||||
{
|
||||
const char *eq = strchr(raw, '=');
|
||||
if (!eq) return;
|
||||
int nlen = (int)(eq - raw);
|
||||
char name[128];
|
||||
if (nlen >= (int)sizeof(name)) nlen = sizeof(name) - 1;
|
||||
memcpy(name, raw, nlen);
|
||||
name[nlen] = 0;
|
||||
char *val = sh_expand_single(st, eq + 1);
|
||||
sh_set(st, name, val);
|
||||
free(val);
|
||||
}
|
||||
|
||||
// Build argv by expanding all words. Returns argc; argv is malloc'd and
|
||||
// NULL-terminated (caller frees each item and the array).
|
||||
static int build_argv(sh_state *st, node *n, char ***out_argv)
|
||||
{
|
||||
sh_fields f;
|
||||
sh_fields_init(&f);
|
||||
for (int i = 0; i < n->nword; i++) sh_expand_word(st, n->words[i], &f);
|
||||
char **argv = malloc((f.count + 1) * sizeof(char *));
|
||||
for (int i = 0; i < f.count; i++) argv[i] = f.items[i]; // take ownership
|
||||
argv[f.count] = NULL;
|
||||
free(f.items); // items themselves transferred
|
||||
*out_argv = argv;
|
||||
return f.count;
|
||||
}
|
||||
|
||||
static void free_argv(char **argv, int argc)
|
||||
{
|
||||
for (int i = 0; i < argc; i++) free(argv[i]);
|
||||
free(argv);
|
||||
}
|
||||
|
||||
// Applied redirection context: the saved streams plus resources to release when
|
||||
// the command finishes (expanded target strings and here-doc temp files).
|
||||
typedef struct {
|
||||
sh_redir_saved io;
|
||||
int active;
|
||||
char *paths[16]; int npath; // expanded filename strings to free
|
||||
char *tmps[8]; int ntmp; // here-doc temp files to remove + free
|
||||
} redir_state;
|
||||
|
||||
// Build the ordered runtime redirect list from a node and apply it. Returns 0
|
||||
// on success (rs holds state for redir_end), -1 on failure (rs already cleaned;
|
||||
// do not call redir_end).
|
||||
static int redir_begin(sh_state *st, node *n, redir_state *rs)
|
||||
{
|
||||
rs->active = 0; rs->npath = 0; rs->ntmp = 0;
|
||||
sh_redir_rt items[16];
|
||||
int ni = 0;
|
||||
for (int i = 0; i < n->nredir && ni < 16; i++) {
|
||||
sh_redir *r = &n->redirs[i];
|
||||
sh_redir_rt *it = &items[ni];
|
||||
it->fd = r->fd; it->dupfd = r->dupfd; it->path = NULL;
|
||||
if (r->kind == R_HEREDOC) {
|
||||
char *body = r->heredoc_quoted ? strdup(r->word)
|
||||
: sh_expand_heredoc(st, r->word);
|
||||
char buf[512];
|
||||
sh_port_tmpfile(2, buf, sizeof(buf));
|
||||
FILE *hf = fopen(buf, "w");
|
||||
if (hf) { fwrite(body, 1, strlen(body), hf); fclose(hf); }
|
||||
free(body);
|
||||
char *tmp = strdup(buf);
|
||||
if (rs->ntmp < 8) rs->tmps[rs->ntmp++] = tmp;
|
||||
it->op = SH_RD_IN; it->fd = 0; it->path = tmp;
|
||||
} else if (r->kind == R_DUP) {
|
||||
it->op = SH_RD_DUP;
|
||||
} else if (r->kind == R_CLOSE) {
|
||||
it->op = SH_RD_CLOSE;
|
||||
} else {
|
||||
char *path = sh_expand_single(st, r->word);
|
||||
if (rs->npath < 16) rs->paths[rs->npath++] = path;
|
||||
it->op = (r->kind == R_IN) ? SH_RD_IN
|
||||
: (r->kind == R_APPEND) ? SH_RD_APPEND : SH_RD_OUT;
|
||||
it->path = path;
|
||||
}
|
||||
ni++;
|
||||
}
|
||||
if (sh_redir_apply(items, ni, &rs->io) != 0) {
|
||||
for (int i = 0; i < rs->npath; i++) free(rs->paths[i]);
|
||||
for (int i = 0; i < rs->ntmp; i++) { remove(rs->tmps[i]); free(rs->tmps[i]); }
|
||||
return -1;
|
||||
}
|
||||
rs->active = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void redir_end(redir_state *rs)
|
||||
{
|
||||
if (rs->active) sh_redir_restore(&rs->io);
|
||||
for (int i = 0; i < rs->npath; i++) free(rs->paths[i]);
|
||||
for (int i = 0; i < rs->ntmp; i++) { remove(rs->tmps[i]); free(rs->tmps[i]); }
|
||||
}
|
||||
|
||||
static int exec_simple(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
st->cmdsub_ran = 0;
|
||||
|
||||
// Pure assignment (no command words): persist to shell state. If a value
|
||||
// contained command substitution, $? is that command sub's status; else 0.
|
||||
if (n->nword == 0) {
|
||||
for (int i = 0; i < n->nassign; i++) apply_assign(st, n->assigns[i]);
|
||||
if (st->exiting) return st->exit_code; // ${a?msg} in a value is fatal
|
||||
if (n->nredir > 0) {
|
||||
// Bare redirect (`>file`): open/close the targets, run nothing.
|
||||
redir_state rs;
|
||||
if (redir_begin(st, n, &rs) != 0) return 2;
|
||||
redir_end(&rs);
|
||||
}
|
||||
return st->cmdsub_ran ? st->cmdsub_status : 0;
|
||||
}
|
||||
|
||||
char **argv;
|
||||
int argc = build_argv(st, n, &argv);
|
||||
// A ${v:?} expansion may have aborted mid-word; don't run the command.
|
||||
if (st->exiting) {
|
||||
free_argv(argv, argc);
|
||||
return st->exit_code;
|
||||
}
|
||||
if (argc == 0) {
|
||||
// Command expanded to nothing (e.g. `$(exit 42)`); still apply pure
|
||||
// assignments. $? is the last command sub's status, or 0 if none ran.
|
||||
int cs = st->cmdsub_ran ? st->cmdsub_status : 0;
|
||||
for (int i = 0; i < n->nassign; i++) apply_assign(st, n->assigns[i]);
|
||||
free_argv(argv, argc);
|
||||
return cs;
|
||||
}
|
||||
|
||||
// Temporary bindings: apply assignments, remember old values to restore.
|
||||
int nsaved = n->nassign;
|
||||
char *saved_names[32];
|
||||
char *saved_vals[32];
|
||||
char *saved_env[32];
|
||||
if (nsaved > 32) nsaved = 32;
|
||||
for (int i = 0; i < nsaved; i++) {
|
||||
const char *eq = strchr(n->assigns[i], '=');
|
||||
int nlen = (int)(eq - n->assigns[i]);
|
||||
char *nm = malloc(nlen + 1);
|
||||
memcpy(nm, n->assigns[i], nlen); nm[nlen] = 0;
|
||||
saved_names[i] = nm;
|
||||
const char *ov = sh_get(st, nm);
|
||||
saved_vals[i] = ov ? strdup(ov) : NULL;
|
||||
const char *oe = getenv(nm);
|
||||
saved_env[i] = oe ? strdup(oe) : NULL;
|
||||
apply_assign(st, n->assigns[i]);
|
||||
// A prefix binding is exported to the command's environment (POSIX);
|
||||
// setenv so the forked child inherits it. Restored below.
|
||||
const char *nv = sh_get(st, nm);
|
||||
setenv(nm, nv ? nv : "", 1);
|
||||
}
|
||||
|
||||
// Redirections (ordered, fd-level).
|
||||
int status;
|
||||
redir_state rs;
|
||||
if (st->exiting) { // fatal ${a?msg} while expanding a prefix binding
|
||||
free_argv(argv, argc);
|
||||
for (int i = 0; i < nsaved; i++) { free(saved_names[i]); free(saved_vals[i]); free(saved_env[i]); }
|
||||
return st->exit_code;
|
||||
}
|
||||
if (redir_begin(st, n, &rs) != 0) {
|
||||
fprintf(stderr, "%s: redirection failed\n", argv[0]);
|
||||
status = 2; // dash exits a failed redirection with status 2
|
||||
} else {
|
||||
status = run_argv(st, argc, argv, exempt);
|
||||
redir_end(&rs);
|
||||
}
|
||||
|
||||
// Restore temporary bindings.
|
||||
for (int i = 0; i < nsaved; i++) {
|
||||
if (saved_vals[i]) sh_set(st, saved_names[i], saved_vals[i]);
|
||||
else sh_unset(st, saved_names[i]);
|
||||
if (saved_env[i]) setenv(saved_names[i], saved_env[i], 1);
|
||||
else unsetenv(saved_names[i]);
|
||||
free(saved_names[i]);
|
||||
free(saved_vals[i]);
|
||||
free(saved_env[i]);
|
||||
}
|
||||
|
||||
free_argv(argv, argc);
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_pipe(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
if (n->nchild == 1) return exec_node(st, n->children[0], exempt);
|
||||
|
||||
// Pipelines are emulated with temp files. A stage may itself be a compound
|
||||
// command containing another pipeline (`{ a | b; } | c`, a pipe inside a
|
||||
// loop body, `eval` of a pipeline, ...), so each nesting level must use its
|
||||
// own temp files -- otherwise the inner pipeline clobbers and removes the
|
||||
// files backing the outer stage's redirection. Track depth and derive
|
||||
// distinct file ids from it.
|
||||
static int pipe_depth = 0;
|
||||
int d = pipe_depth++;
|
||||
|
||||
char tmpa[512], tmpb[512];
|
||||
sh_port_tmpfile(2 * d, tmpa, sizeof(tmpa));
|
||||
sh_port_tmpfile(2 * d + 1, tmpb, sizeof(tmpb));
|
||||
|
||||
int status = 0;
|
||||
const char *prev_in = NULL;
|
||||
for (int i = 0; i < n->nchild; i++) {
|
||||
const char *out = (i < n->nchild - 1) ? ((i % 2) ? tmpa : tmpb) : NULL;
|
||||
node *c = n->children[i];
|
||||
|
||||
sh_redir_rt items[2];
|
||||
int ni = 0;
|
||||
if (prev_in) items[ni++] = (sh_redir_rt){ 0, SH_RD_IN, prev_in, 0 };
|
||||
if (out) items[ni++] = (sh_redir_rt){ 1, SH_RD_OUT, out, 0 };
|
||||
sh_redir_saved io;
|
||||
if (sh_redir_apply(items, ni, &io) != 0) {
|
||||
status = 1;
|
||||
break;
|
||||
}
|
||||
// Each stage runs like a subshell for control flow: an inner `exit`,
|
||||
// `return`, or errexit abort stays local to the stage (real shells fork
|
||||
// each stage). Only the final stage's status becomes the pipeline's, and
|
||||
// errexit is decided at the pipeline level (see exec_node), so the stage
|
||||
// itself runs with errexit active (exempt inherited from the pipeline).
|
||||
int s_exit = st->exiting, s_code = st->exit_code;
|
||||
int s_brk = st->brk, s_cont = st->cont;
|
||||
int s_ret = st->returning, s_rcode = st->return_code;
|
||||
status = exec_node(st, c, exempt);
|
||||
st->exiting = s_exit; st->exit_code = s_code;
|
||||
st->brk = s_brk; st->cont = s_cont;
|
||||
st->returning = s_ret; st->return_code = s_rcode;
|
||||
sh_redir_restore(&io);
|
||||
|
||||
prev_in = out;
|
||||
}
|
||||
remove(tmpa);
|
||||
remove(tmpb);
|
||||
pipe_depth--;
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_list(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
int status = 0;
|
||||
for (int i = 0; i < n->nchild; i++) {
|
||||
status = exec_node(st, n->children[i], exempt);
|
||||
if (st->exiting || st->returning || st->brk || st->cont) break;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_andor(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
// The left operand is always errexit-exempt (its failure is tested by the
|
||||
// &&/||). The right (tail) operand inherits the surrounding context.
|
||||
int status = exec_node(st, n->left, 1);
|
||||
if (st->exiting || st->returning) return status;
|
||||
if (n->andor_op == T_AMPAMP) {
|
||||
if (status == 0) status = exec_node(st, n->right, exempt);
|
||||
} else { // T_BARBAR
|
||||
if (status != 0) status = exec_node(st, n->right, exempt);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_if(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
for (int i = 0; i < n->nclause; i++) {
|
||||
int c = exec_node(st, n->conds[i], 1); // condition: errexit-exempt
|
||||
if (st->exiting || st->returning) return c;
|
||||
if (c == 0) return exec_node(st, n->bodies[i], exempt);
|
||||
}
|
||||
if (n->else_body) return exec_node(st, n->else_body, exempt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle break/continue after running a loop body. Returns 1 if the loop
|
||||
// should stop, 0 if it should continue to the next iteration.
|
||||
static int loop_control(sh_state *st)
|
||||
{
|
||||
if (st->exiting || st->returning) return 1;
|
||||
if (st->brk) { st->brk--; return 1; } // break; outer loops see remaining
|
||||
if (st->cont) { st->cont--; return st->cont ? 1 : 0; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int exec_while(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
int status = 0;
|
||||
st->loop_depth++;
|
||||
while (!st->exiting) {
|
||||
int c = exec_node(st, n->cond, 1); // condition: errexit-exempt
|
||||
// A break/continue evaluated inside the condition applies to this loop.
|
||||
if (st->brk || st->cont) { loop_control(st); break; }
|
||||
if (n->until) c = (c == 0); // `until`: loop while cond fails
|
||||
if (st->exiting || st->returning || c != 0) break;
|
||||
status = exec_node(st, n->body, exempt);
|
||||
if (loop_control(st)) break;
|
||||
}
|
||||
st->loop_depth--;
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_for(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
int status = 0;
|
||||
sh_fields f;
|
||||
sh_fields_init(&f);
|
||||
if (n->for_implicit) {
|
||||
// `for x; do` iterates over "$@" (each positional param, unsplit).
|
||||
for (int i = 0; i < st->npos; i++) sh_fields_push(&f, st->pos[i]);
|
||||
} else {
|
||||
for (int i = 0; i < n->for_nword; i++) sh_expand_word(st, n->for_words[i], &f);
|
||||
}
|
||||
st->loop_depth++;
|
||||
for (int i = 0; i < f.count && !st->exiting; i++) {
|
||||
sh_set(st, n->for_name, f.items[i]);
|
||||
status = exec_node(st, n->body, exempt);
|
||||
if (loop_control(st)) break;
|
||||
}
|
||||
st->loop_depth--;
|
||||
sh_fields_free(&f);
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_case(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
char *subj = sh_expand_single(st, n->case_word);
|
||||
int status = 0;
|
||||
for (int i = 0; i < n->nclause_case; i++) {
|
||||
sh_case_clause *cl = &n->clauses[i];
|
||||
for (int j = 0; j < cl->npat; j++) {
|
||||
char *pat = sh_expand_single(st, cl->pats[j]);
|
||||
int hit = sh_pattern_match(pat, subj);
|
||||
free(pat);
|
||||
if (hit) {
|
||||
status = exec_node(st, cl->body, exempt);
|
||||
free(subj);
|
||||
return status; // first match wins, no fallthrough
|
||||
}
|
||||
}
|
||||
}
|
||||
free(subj);
|
||||
return status;
|
||||
}
|
||||
|
||||
// Deep-copy / free the variable list for subshell isolation.
|
||||
static sh_var *clone_vars(sh_var *v)
|
||||
{
|
||||
sh_var *head = NULL, **tail = &head;
|
||||
for (; v; v = v->next) {
|
||||
sh_var *c = malloc(sizeof(*c));
|
||||
c->name = strdup(v->name);
|
||||
c->value = strdup(v->value);
|
||||
c->exported = v->exported;
|
||||
c->next = NULL;
|
||||
*tail = c;
|
||||
tail = &c->next;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
static void free_vars(sh_var *v)
|
||||
{
|
||||
while (v) { sh_var *nx = v->next; free(v->name); free(v->value); free(v); v = nx; }
|
||||
}
|
||||
|
||||
static int exec_group(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
if (!n->subshell) return exec_node(st, n->body, exempt);
|
||||
|
||||
// Subshell ( list ): snapshot vars, positional params, cwd, and control
|
||||
// flags; run isolated; restore everything but the resulting $?.
|
||||
sh_var *saved_vars = clone_vars(st->vars);
|
||||
char **saved_pos = st->pos;
|
||||
int saved_npos = st->npos;
|
||||
char *saved_arg0 = st->arg0 ? strdup(st->arg0) : NULL;
|
||||
st->pos = NULL; st->npos = 0;
|
||||
if (saved_pos) {
|
||||
st->pos = malloc(saved_npos * sizeof(char *));
|
||||
for (int i = 0; i < saved_npos; i++) st->pos[i] = strdup(saved_pos[i]);
|
||||
st->npos = saved_npos;
|
||||
}
|
||||
char cwd[512];
|
||||
sh_port_getcwd(cwd, sizeof(cwd));
|
||||
int s_exit = st->exiting, s_code = st->exit_code;
|
||||
int s_brk = st->brk, s_cont = st->cont;
|
||||
int s_ret = st->returning, s_rcode = st->return_code;
|
||||
int s_ee = st->opt_errexit, s_nu = st->opt_nounset;
|
||||
|
||||
int status = exec_node(st, n->body, exempt);
|
||||
|
||||
// Restore. Option changes (set -e/-u) inside a subshell must not leak out.
|
||||
free_vars(st->vars);
|
||||
st->vars = saved_vars;
|
||||
for (int i = 0; i < st->npos; i++) free(st->pos[i]);
|
||||
free(st->pos);
|
||||
st->pos = saved_pos; st->npos = saved_npos;
|
||||
free(st->arg0); st->arg0 = saved_arg0;
|
||||
sh_port_chdir(cwd);
|
||||
st->exiting = s_exit; st->exit_code = s_code;
|
||||
st->brk = s_brk; st->cont = s_cont;
|
||||
st->returning = s_ret; st->return_code = s_rcode;
|
||||
st->opt_errexit = s_ee; st->opt_nounset = s_nu;
|
||||
return status;
|
||||
}
|
||||
|
||||
static int exec_funcdef(sh_state *st, node *n)
|
||||
{
|
||||
sh_func_define(st, n->func_name, n->body);
|
||||
n->body = NULL; // ownership transferred; keep sh_free_node(root) from freeing it
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Call a function: swap in the call args as $1..$#, run the body under a fresh
|
||||
// `local` scope, honor `return`, and restore the caller's positional params.
|
||||
static int call_function(sh_state *st, sh_func *fn, int argc, char **argv, int exempt)
|
||||
{
|
||||
if (st->call_depth >= SH_MAX_CALL_DEPTH) {
|
||||
fprintf(stderr, "%s: recursion too deep\n", fn->name);
|
||||
return 1;
|
||||
}
|
||||
char **saved_pos = st->pos;
|
||||
int saved_npos = st->npos;
|
||||
st->pos = NULL; st->npos = 0;
|
||||
sh_set_positional(st, NULL, argv + 1, argc - 1); // $0 unchanged (dash)
|
||||
|
||||
sh_scope_push(st);
|
||||
st->call_depth++;
|
||||
int prev_ret = st->returning;
|
||||
st->returning = 0;
|
||||
|
||||
exec_node(st, fn->body, exempt);
|
||||
|
||||
int code = st->returning ? st->return_code : st->last_status;
|
||||
st->returning = prev_ret;
|
||||
st->call_depth--;
|
||||
sh_scope_pop(st);
|
||||
|
||||
for (int i = 0; i < st->npos; i++) free(st->pos[i]);
|
||||
free(st->pos);
|
||||
st->pos = saved_pos; st->npos = saved_npos;
|
||||
return code;
|
||||
}
|
||||
|
||||
static int exec_node(sh_state *st, node *n, int exempt)
|
||||
{
|
||||
// Compound commands may carry a trailing redirect list; apply it around the
|
||||
// whole command. Simple commands handle their own redirects internally.
|
||||
redir_state rs;
|
||||
int has_redir = (n->kind != N_SIMPLE) && n->nredir > 0;
|
||||
if (has_redir && redir_begin(st, n, &rs) != 0) {
|
||||
st->last_status = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// A negated pipeline (`! cmd`) is an errexit-exempt context throughout, and
|
||||
// its own non-zero result never triggers errexit.
|
||||
int inner_exempt = exempt || n->negated;
|
||||
|
||||
int status = 0;
|
||||
switch (n->kind) {
|
||||
case N_LIST: status = exec_list(st, n, inner_exempt); break;
|
||||
case N_ANDOR: status = exec_andor(st, n, inner_exempt); break;
|
||||
case N_PIPE: status = exec_pipe(st, n, inner_exempt); break;
|
||||
case N_SIMPLE: status = exec_simple(st, n, inner_exempt); break;
|
||||
case N_IF: status = exec_if(st, n, inner_exempt); break;
|
||||
case N_WHILE: status = exec_while(st, n, inner_exempt); break;
|
||||
case N_FOR: status = exec_for(st, n, inner_exempt); break;
|
||||
case N_CASE: status = exec_case(st, n, inner_exempt); break;
|
||||
case N_GROUP: status = exec_group(st, n, inner_exempt); break;
|
||||
case N_FUNCDEF: status = exec_funcdef(st, n); break;
|
||||
}
|
||||
if (has_redir) redir_end(&rs);
|
||||
|
||||
if (n->negated) status = (status == 0);
|
||||
st->last_status = status;
|
||||
|
||||
// errexit: a command that returns non-zero in a non-exempt context aborts.
|
||||
// Structural nodes (list / and-or / brace group) don't trigger — their inner
|
||||
// commands already decide — but a subshell `( )` is a command, so it does.
|
||||
// Setting `exiting` stops the current command list; a subshell/$() confines
|
||||
// it, the top level exits.
|
||||
int structural = (n->kind == N_LIST || n->kind == N_ANDOR ||
|
||||
(n->kind == N_GROUP && !n->subshell));
|
||||
if (st->opt_errexit && !exempt && !n->negated && status != 0 && !structural &&
|
||||
!st->exiting && !st->returning && !st->brk && !st->cont) {
|
||||
st->exiting = 1;
|
||||
st->exit_code = status;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
int sh_run_string_args(sh_state *st, const char *src, int argc, char **argv)
|
||||
{
|
||||
if (argv) {
|
||||
const char *arg0 = argc > 0 ? argv[0] : NULL;
|
||||
sh_set_positional(st, arg0, argv + 1, argc > 1 ? argc - 1 : 0);
|
||||
}
|
||||
return sh_run_string(st, src);
|
||||
}
|
||||
|
||||
int sh_run_string(sh_state *st, const char *src)
|
||||
{
|
||||
st->parse_error = 0;
|
||||
sh_toklist tl;
|
||||
if (sh_lex(src, &tl) != 0) {
|
||||
fprintf(stderr, "sh: syntax error (unterminated quote)\n");
|
||||
st->parse_error = 1;
|
||||
st->last_status = 2;
|
||||
return 2;
|
||||
}
|
||||
const char *err = NULL;
|
||||
node *root = sh_parse(&tl, &err);
|
||||
if (!root) {
|
||||
fprintf(stderr, "sh: syntax error: %s\n", err ? err : "parse error");
|
||||
st->parse_error = 1;
|
||||
sh_toklist_free(&tl);
|
||||
st->last_status = 2;
|
||||
return 2;
|
||||
}
|
||||
int status = exec_node(st, root, 0);
|
||||
sh_free_node(root);
|
||||
sh_toklist_free(&tl);
|
||||
if (st->exiting) return st->exit_code;
|
||||
return status;
|
||||
}
|
||||
1071
Tactility/Source/app/shell/shell/sh_exec.cpp
Normal file
1071
Tactility/Source/app/shell/shell/sh_exec.cpp
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,691 +0,0 @@
|
||||
#include <Tactility/app/shell/shell/sh.h>
|
||||
#include <Tactility/app/shell/shell/sh_port.h>
|
||||
#include <Tactility/app/shell/shell/sh_glob.h>
|
||||
#include <Tactility/app/shell/shell/sh_arith.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
|
||||
void sh_fields_init(sh_fields *f) { f->items = NULL; f->count = 0; f->cap = 0; }
|
||||
|
||||
void sh_fields_push(sh_fields *f, const char *s)
|
||||
{
|
||||
if (f->count == f->cap) {
|
||||
f->cap = f->cap ? f->cap * 2 : 8;
|
||||
f->items = realloc(f->items, f->cap * sizeof(char *));
|
||||
}
|
||||
f->items[f->count++] = strdup(s);
|
||||
}
|
||||
|
||||
void sh_fields_free(sh_fields *f)
|
||||
{
|
||||
for (int i = 0; i < f->count; i++) free(f->items[i]);
|
||||
free(f->items);
|
||||
f->items = NULL;
|
||||
f->count = f->cap = 0;
|
||||
}
|
||||
|
||||
// A growable string used to build the current field.
|
||||
typedef struct { char *buf; int len; int cap; int started; } fld;
|
||||
|
||||
static void fld_init(fld *b) { b->buf = malloc(16); b->cap = 16; b->len = 0; b->started = 0; b->buf[0] = 0; }
|
||||
static void fld_put(fld *b, char c)
|
||||
{
|
||||
if (b->len + 1 >= b->cap) { b->cap *= 2; b->buf = realloc(b->buf, b->cap); }
|
||||
b->buf[b->len++] = c;
|
||||
b->buf[b->len] = 0;
|
||||
b->started = 1;
|
||||
}
|
||||
static void fld_emit(fld *b, sh_fields *out)
|
||||
{
|
||||
if (b->started) { sh_fields_push(out, b->buf); }
|
||||
b->len = 0; b->started = 0; b->buf[0] = 0;
|
||||
}
|
||||
// Like fld_emit, but always pushes a field even when empty/unstarted. Used at an
|
||||
// IFS delimiter boundary, where a field ends regardless of whether it had any
|
||||
// content (so `a__b` -> a,'',b rather than a,b).
|
||||
static void fld_emit_force(fld *b, sh_fields *out)
|
||||
{
|
||||
sh_fields_push(out, b->buf);
|
||||
b->len = 0; b->started = 0; b->buf[0] = 0;
|
||||
}
|
||||
|
||||
// Run `cmd` as command substitution: execute it with stdout captured to a temp
|
||||
// file, read the output back, and strip trailing newlines (POSIX). Control-flow
|
||||
// flags are saved/restored so an inner `exit`/`break`/`continue` stays local to
|
||||
// the substitution. Returns an owned string (never NULL).
|
||||
static char *command_subst(sh_state *st, const char *cmd)
|
||||
{
|
||||
static int depth = 0; // nested $() get distinct temp names (avoid pipe 0/1)
|
||||
char tmp[512];
|
||||
sh_port_tmpfile(2 + depth, tmp, sizeof(tmp));
|
||||
depth++;
|
||||
|
||||
int s_exit = st->exiting, s_code = st->exit_code;
|
||||
int s_brk = st->brk, s_cont = st->cont;
|
||||
int s_ret = st->returning, s_rcode = st->return_code;
|
||||
|
||||
char *buf = NULL;
|
||||
int len = 0;
|
||||
sh_redir_rt item = { 1, SH_RD_OUT, tmp, 0 };
|
||||
sh_redir_saved io;
|
||||
if (sh_redir_apply(&item, 1, &io) == 0) {
|
||||
sh_run_string(st, cmd);
|
||||
sh_redir_restore(&io);
|
||||
FILE *f = fopen(tmp, "rb");
|
||||
if (f) {
|
||||
int cap = 0, c;
|
||||
while ((c = fgetc(f)) != EOF) {
|
||||
if (len + 1 >= cap) { cap = cap ? cap * 2 : 128; buf = realloc(buf, cap); }
|
||||
buf[len++] = (char)c;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
remove(tmp);
|
||||
}
|
||||
|
||||
depth--;
|
||||
// Remember the substitution's own exit status: a command or assignment
|
||||
// whose only "command" is command substitution(s) reports the status of
|
||||
// the last one (POSIX). sh_run_string left it in last_status.
|
||||
st->cmdsub_ran = 1;
|
||||
st->cmdsub_status = st->last_status;
|
||||
st->exiting = s_exit; st->exit_code = s_code;
|
||||
st->brk = s_brk; st->cont = s_cont;
|
||||
st->returning = s_ret; st->return_code = s_rcode;
|
||||
|
||||
if (!buf) return strdup("");
|
||||
while (len > 0 && buf[len - 1] == '\n') len--;
|
||||
char *res = realloc(buf, len + 1);
|
||||
if (res) buf = res;
|
||||
buf[len] = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
// ---- parameter helpers -----------------------------------------------------
|
||||
|
||||
static const char *get_ifs(sh_state *st)
|
||||
{
|
||||
const char *v = sh_get(st, "IFS");
|
||||
return v ? v : " \t\n";
|
||||
}
|
||||
static int is_ifs(char c, const char *ifs) { return c && strchr(ifs, c) != NULL; }
|
||||
static int is_ifs_ws(char c) { return c == ' ' || c == '\t' || c == '\n'; }
|
||||
|
||||
// Append `v` to the field `b`. If `split`, break fields on $IFS following POSIX
|
||||
// 2.6.5: leading IFS whitespace at the start of a field is elided; a run of IFS
|
||||
// whitespace is one delimiter; each non-whitespace IFS char (with adjacent IFS
|
||||
// whitespace) is also a delimiter and so can create empty fields between/at the
|
||||
// front, but a lone trailing delimiter does not create a trailing empty field.
|
||||
// The final (possibly partial or empty) field is left in `b` so it can continue
|
||||
// across adjacent word parts; the caller flushes it with fld_emit.
|
||||
static void append_val(fld *b, sh_fields *out, const char *v, int split, const char *ifs)
|
||||
{
|
||||
if (!split) { for (const char *q = v; *q; q++) fld_put(b, *q); return; }
|
||||
const char *q = v;
|
||||
// Strip leading IFS whitespace only when no earlier part has begun this field.
|
||||
if (!b->started)
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
for (;;) {
|
||||
// Accumulate the field's content up to the next IFS char.
|
||||
while (*q && !is_ifs(*q, ifs)) { fld_put(b, *q); q++; }
|
||||
if (!*q) return; // field continues into next part / flush
|
||||
fld_emit_force(b, out); // delimiter -> field ends (even if empty)
|
||||
// Consume one delimiter: IFS whitespace, at most one non-ws IFS char,
|
||||
// then trailing IFS whitespace.
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
if (*q && is_ifs(*q, ifs) && !is_ifs_ws(*q)) {
|
||||
q++;
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
}
|
||||
if (!*q) return; // trailing delimiter: no empty field
|
||||
}
|
||||
}
|
||||
|
||||
// Is `name` a plain variable name (assignable, not a positional/special param)?
|
||||
static int is_var_name(const char *name)
|
||||
{
|
||||
if (!(isalpha((unsigned char)name[0]) || name[0] == '_')) return 0;
|
||||
for (const char *q = name + 1; *q; q++)
|
||||
if (!(isalnum((unsigned char)*q) || *q == '_')) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolve a scalar parameter's raw value. Returns NULL if the parameter is
|
||||
// unset (needed to distinguish unset from set-but-empty). *owned receives a
|
||||
// malloc'd buffer the caller must free (may stay NULL).
|
||||
static const char *param_raw(sh_state *st, const char *name, char **owned)
|
||||
{
|
||||
*owned = NULL;
|
||||
if (!name[0]) return NULL;
|
||||
if (name[1] == 0) {
|
||||
char c = name[0];
|
||||
if (c == '?') { char *b = malloc(16); snprintf(b, 16, "%d", st->last_status); *owned = b; return b; }
|
||||
if (c == '#') { char *b = malloc(16); snprintf(b, 16, "%d", st->npos); *owned = b; return b; }
|
||||
if (c == '$') return "1"; // stub: fake constant pid
|
||||
if (c == '!') return ""; // stub: no last bg pid, but "set"
|
||||
if (c == '-') return ""; // stub: shell flags
|
||||
if (c == '0') return st->arg0 ? st->arg0 : "";
|
||||
}
|
||||
int alldig = 1;
|
||||
for (const char *q = name; *q; q++)
|
||||
if (!isdigit((unsigned char)*q)) { alldig = 0; break; }
|
||||
if (alldig) {
|
||||
long idx = strtol(name, NULL, 10);
|
||||
if (idx == 0) return st->arg0 ? st->arg0 : "";
|
||||
if (idx >= 1 && idx <= st->npos) return st->pos[idx - 1];
|
||||
return NULL; // out of range -> unset
|
||||
}
|
||||
return sh_get(st, name);
|
||||
}
|
||||
|
||||
// nounset (set -u): if `val` is unset (NULL) and nounset is on, report the error
|
||||
// and abort the shell (dash exits 2). Returns 1 if it fired (caller emits nothing).
|
||||
// Specials ($?, $#, $0, ...) resolve non-NULL, so only genuine unset vars and
|
||||
// out-of-range positionals reach here; a bare $@/$* is handled separately.
|
||||
static int nounset_fire(sh_state *st, const char *name, const char *val)
|
||||
{
|
||||
if (val || !st->opt_nounset) return 0;
|
||||
fprintf(stderr, "%s: parameter not set\n", name);
|
||||
st->exiting = 1; st->exit_code = 2; st->last_status = 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Expand $@ / $* into fields. `star` selects '*'. `quoted` is set inside "...".
|
||||
static void expand_at_star(sh_state *st, int star, int quoted, fld *b,
|
||||
sh_fields *out, int split, const char *ifs)
|
||||
{
|
||||
int n = st->npos;
|
||||
char sep = ' ';
|
||||
const char *ifsv = sh_get(st, "IFS");
|
||||
if (ifsv) sep = ifsv[0]; // may be 0 if IFS=""
|
||||
|
||||
if (star && quoted) {
|
||||
// "$*": join all params by the first char of $IFS into a single field.
|
||||
int total = 1;
|
||||
for (int i = 0; i < n; i++) total += (int)strlen(st->pos[i]) + 1;
|
||||
char *tmp = malloc(total);
|
||||
tmp[0] = 0;
|
||||
int len = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int l = (int)strlen(st->pos[i]);
|
||||
memcpy(tmp + len, st->pos[i], l); len += l;
|
||||
if (i < n - 1 && sep) tmp[len++] = sep;
|
||||
}
|
||||
tmp[len] = 0;
|
||||
append_val(b, out, tmp, quoted ? 0 : split, ifs);
|
||||
free(tmp);
|
||||
return;
|
||||
}
|
||||
|
||||
// $@ : one field per positional param.
|
||||
if (n == 0) return; // "$@" with no params vanishes (prefix field kept)
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 0) fld_emit(b, out);
|
||||
if (quoted) {
|
||||
for (char *q = st->pos[i]; *q; q++) fld_put(b, *q);
|
||||
b->started = 1; // keep even empty params as fields
|
||||
} else {
|
||||
append_val(b, out, st->pos[i], split, ifs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void do_brace(sh_state *st, const char *content, int quoted, fld *b,
|
||||
sh_fields *out, int split, const char *ifs);
|
||||
static char *sh_expand_single_t(sh_state *st, const char *raw, int allow_tilde,
|
||||
int quoted_ctx);
|
||||
|
||||
// Parse a $-expansion at raw[*i] (raw[*i]=='$'). Append the value(s).
|
||||
// `split` enables IFS word-splitting; `quoted` marks double-quote context.
|
||||
static void do_dollar(sh_state *st, const char *raw, int *i, fld *b,
|
||||
sh_fields *out, int split, int quoted)
|
||||
{
|
||||
int p = *i + 1; // skip '$'
|
||||
char name[128];
|
||||
int n = 0;
|
||||
const char *ifs = get_ifs(st);
|
||||
|
||||
if (raw[p] == '(' && raw[p + 1] == '(') {
|
||||
// $((expr)) arithmetic expansion. Scan to the matching `))`, tracking
|
||||
// nested `((`/`))` (so `$(( $((a)) + 1 ))` works).
|
||||
p += 2;
|
||||
int start = p, depth = 1;
|
||||
while (raw[p] && depth > 0) {
|
||||
if (raw[p] == '(') depth++;
|
||||
else if (raw[p] == ')') depth--;
|
||||
if (depth == 0) break;
|
||||
p++;
|
||||
}
|
||||
int inner_len = p - start;
|
||||
char *inner = malloc(inner_len + 1);
|
||||
memcpy(inner, raw + start, inner_len);
|
||||
inner[inner_len] = 0;
|
||||
// Consume the closing `))`.
|
||||
if (raw[p] == ')') p++;
|
||||
if (raw[p] == ')') p++;
|
||||
// Command substitutions and `$var` forms inside the expression are
|
||||
// expanded first (parameter/command expansion only, no glob/split);
|
||||
// bare identifiers are left for the evaluator to dereference.
|
||||
char *expr = sh_expand_heredoc(st, inner);
|
||||
free(inner);
|
||||
inner = expr;
|
||||
long result = 0;
|
||||
const char *aerr = NULL;
|
||||
char num[32];
|
||||
if (sh_arith_eval(st, inner, &result, &aerr) == 0) {
|
||||
snprintf(num, sizeof(num), "%ld", result);
|
||||
append_val(b, out, num, split, ifs);
|
||||
} else {
|
||||
fprintf(stderr, "sh: arithmetic: %s\n", aerr ? aerr : "error");
|
||||
// dash treats an arithmetic evaluation error as a fatal error in a
|
||||
// non-interactive shell: abort the current command and exit 2.
|
||||
st->exiting = 1; st->exit_code = 2; st->last_status = 2;
|
||||
}
|
||||
free(inner);
|
||||
*i = p;
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw[p] == '(') {
|
||||
// $(...) command substitution.
|
||||
p++;
|
||||
int start = p, depth = 1;
|
||||
while (raw[p] && depth > 0) {
|
||||
char d = raw[p];
|
||||
if (d == '\'') { p++; while (raw[p] && raw[p] != '\'') p++; if (raw[p]) p++; continue; }
|
||||
if (d == '"') {
|
||||
p++;
|
||||
while (raw[p] && raw[p] != '"') { if (raw[p] == '\\' && raw[p + 1]) p += 2; else p++; }
|
||||
if (raw[p]) p++;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\' && raw[p + 1]) { p += 2; continue; }
|
||||
if (d == '(') depth++;
|
||||
else if (d == ')') { if (--depth == 0) break; }
|
||||
p++;
|
||||
}
|
||||
int inner_len = p - start;
|
||||
char *inner = malloc(inner_len + 1);
|
||||
memcpy(inner, raw + start, inner_len);
|
||||
inner[inner_len] = 0;
|
||||
if (raw[p] == ')') p++;
|
||||
char *val = command_subst(st, inner);
|
||||
free(inner);
|
||||
append_val(b, out, val, quoted ? 0 : split, ifs);
|
||||
free(val);
|
||||
*i = p;
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw[p] == '{') {
|
||||
// ${...} : find the matching '}' (respect nesting, $(), quotes).
|
||||
p++;
|
||||
int start = p, depth = 1;
|
||||
while (raw[p] && depth > 0) {
|
||||
char d = raw[p];
|
||||
if (d == '\\' && raw[p + 1]) { p += 2; continue; }
|
||||
if (d == '\'') { p++; while (raw[p] && raw[p] != '\'') p++; if (raw[p]) p++; continue; }
|
||||
if (d == '"') {
|
||||
p++;
|
||||
while (raw[p] && raw[p] != '"') { if (raw[p] == '\\' && raw[p + 1]) p += 2; else p++; }
|
||||
if (raw[p]) p++;
|
||||
continue;
|
||||
}
|
||||
if (d == '$' && raw[p + 1] == '{') { depth++; p += 2; continue; }
|
||||
if (d == '$' && raw[p + 1] == '(') {
|
||||
p += 2; int pd = 1;
|
||||
while (raw[p] && pd) { if (raw[p] == '(') pd++; else if (raw[p] == ')') { if (--pd == 0) { p++; break; } } p++; }
|
||||
continue;
|
||||
}
|
||||
if (d == '{') depth++;
|
||||
else if (d == '}') { if (--depth == 0) break; }
|
||||
p++;
|
||||
}
|
||||
int clen = p - start;
|
||||
char *content = malloc(clen + 1);
|
||||
memcpy(content, raw + start, clen);
|
||||
content[clen] = 0;
|
||||
if (raw[p] == '}') p++;
|
||||
do_brace(st, content, quoted, b, out, split, ifs);
|
||||
free(content);
|
||||
*i = p;
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw[p] == '@' || raw[p] == '*') {
|
||||
expand_at_star(st, raw[p] == '*', quoted, b, out, split, ifs);
|
||||
*i = p + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (raw[p] == '?' || raw[p] == '#' || raw[p] == '$' || raw[p] == '!' || raw[p] == '-') {
|
||||
name[0] = raw[p]; name[1] = 0; p++;
|
||||
} else if (isdigit((unsigned char)raw[p])) {
|
||||
name[0] = raw[p]; name[1] = 0; p++; // bare $N is a single digit
|
||||
} else if (isalpha((unsigned char)raw[p]) || raw[p] == '_') {
|
||||
while ((isalnum((unsigned char)raw[p]) || raw[p] == '_') && n < (int)sizeof(name) - 1)
|
||||
name[n++] = raw[p++];
|
||||
name[n] = 0;
|
||||
} else {
|
||||
fld_put(b, '$'); // lone '$' - literal
|
||||
*i = *i + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
char *owned = NULL;
|
||||
const char *val = param_raw(st, name, &owned);
|
||||
if (nounset_fire(st, name, val)) { free(owned); *i = p; return; }
|
||||
append_val(b, out, val ? val : "", split, ifs);
|
||||
free(owned);
|
||||
*i = p;
|
||||
}
|
||||
|
||||
// Handle the interior of a ${...} expression (operators, length, plain).
|
||||
static void do_brace(sh_state *st, const char *content, int quoted, fld *b,
|
||||
sh_fields *out, int split, const char *ifs)
|
||||
{
|
||||
// Length form: ${#name}
|
||||
if (content[0] == '#') {
|
||||
char d = content[1];
|
||||
if (isalnum((unsigned char)d) || d == '_' || d == '@' || d == '*' ||
|
||||
(d == '#' && content[2] == 0)) { // ${##}: length of "$#"
|
||||
const char *nm = content + 1;
|
||||
char num[16];
|
||||
if (strcmp(nm, "@") == 0 || strcmp(nm, "*") == 0) {
|
||||
snprintf(num, sizeof(num), "%d", st->npos);
|
||||
} else {
|
||||
char *ow = NULL;
|
||||
const char *v = param_raw(st, nm, &ow);
|
||||
if (nounset_fire(st, nm, v)) { free(ow); return; }
|
||||
snprintf(num, sizeof(num), "%d", v ? (int)strlen(v) : 0);
|
||||
free(ow);
|
||||
}
|
||||
append_val(b, out, num, split, ifs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the parameter name.
|
||||
char name[128];
|
||||
int nl = 0;
|
||||
const char *r = content;
|
||||
if (*r == '@' || *r == '*' || *r == '?' || *r == '!' || *r == '$' ||
|
||||
*r == '-' || *r == '#') {
|
||||
name[0] = *r; nl = 1; r++;
|
||||
} else if (isdigit((unsigned char)*r)) {
|
||||
while (isdigit((unsigned char)*r) && nl < 127) name[nl++] = *r++;
|
||||
} else if (isalpha((unsigned char)*r) || *r == '_') {
|
||||
while ((isalnum((unsigned char)*r) || *r == '_') && nl < 127) name[nl++] = *r++;
|
||||
}
|
||||
name[nl] = 0;
|
||||
|
||||
// No operator: plain ${name}.
|
||||
if (*r == 0) {
|
||||
if (strcmp(name, "@") == 0 || strcmp(name, "*") == 0) {
|
||||
expand_at_star(st, name[0] == '*', quoted, b, out, split, ifs);
|
||||
} else {
|
||||
char *ow = NULL;
|
||||
const char *v = param_raw(st, name, &ow);
|
||||
if (nounset_fire(st, name, v)) { free(ow); return; }
|
||||
append_val(b, out, v ? v : "", split, ifs);
|
||||
free(ow);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefix/suffix strip: ${v#p} ${v##p} ${v%p} ${v%%p}
|
||||
if (*r == '#' || *r == '%') {
|
||||
char kind = *r; r++;
|
||||
int longest = 0;
|
||||
if (*r == kind) { longest = 1; r++; }
|
||||
char *pat = sh_expand_single(st, r);
|
||||
char *ow = NULL;
|
||||
const char *v = param_raw(st, name, &ow);
|
||||
const char *sv = v ? v : "";
|
||||
char *res = (kind == '#') ? sh_strip_prefix(sv, pat, longest)
|
||||
: sh_strip_suffix(sv, pat, longest);
|
||||
append_val(b, out, res, split, ifs);
|
||||
free(res); free(pat); free(ow);
|
||||
return;
|
||||
}
|
||||
|
||||
// Value operators: :- - :+ + := = :? ?
|
||||
int colon = 0;
|
||||
if (*r == ':' && (r[1] == '-' || r[1] == '+' || r[1] == '=' || r[1] == '?')) {
|
||||
colon = 1; r++;
|
||||
}
|
||||
char op = *r;
|
||||
if (op == '-' || op == '+' || op == '=' || op == '?') {
|
||||
const char *word = r + 1;
|
||||
char *ow = NULL;
|
||||
const char *val = param_raw(st, name, &ow);
|
||||
int set = (val != NULL);
|
||||
int active = colon ? (!set || val[0] == 0) : !set; // "empty or unset" test
|
||||
|
||||
if (op == '+') {
|
||||
// Use word if the parameter IS set (non-empty for colon form).
|
||||
if (!active) {
|
||||
char *w = sh_expand_single_t(st, word, !quoted, quoted);
|
||||
append_val(b, out, w, quoted ? 0 : split, ifs);
|
||||
free(w);
|
||||
}
|
||||
} else if (op == '-') {
|
||||
if (active) {
|
||||
char *w = sh_expand_single_t(st, word, !quoted, quoted);
|
||||
append_val(b, out, w, quoted ? 0 : split, ifs);
|
||||
free(w);
|
||||
} else {
|
||||
append_val(b, out, val, quoted ? 0 : split, ifs);
|
||||
}
|
||||
} else if (op == '=') {
|
||||
if (active) {
|
||||
char *w = sh_expand_single_t(st, word, !quoted, quoted);
|
||||
if (is_var_name(name)) sh_set(st, name, w);
|
||||
else fprintf(stderr, "%s: cannot assign in this way\n", name);
|
||||
append_val(b, out, w, quoted ? 0 : split, ifs);
|
||||
free(w);
|
||||
} else {
|
||||
append_val(b, out, val, quoted ? 0 : split, ifs);
|
||||
}
|
||||
} else { // '?'
|
||||
if (active) {
|
||||
char *w = sh_expand_single_t(st, word, !quoted, quoted);
|
||||
if (w[0]) fprintf(stderr, "%s: %s\n", name, w);
|
||||
else fprintf(stderr, "%s: parameter not set\n", name);
|
||||
free(w);
|
||||
st->exiting = 1;
|
||||
st->exit_code = 2;
|
||||
st->last_status = 2;
|
||||
} else {
|
||||
append_val(b, out, val, quoted ? 0 : split, ifs);
|
||||
}
|
||||
}
|
||||
free(ow);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown operator: fall back to plain value.
|
||||
char *ow = NULL;
|
||||
const char *v = param_raw(st, name, &ow);
|
||||
append_val(b, out, v ? v : "", split, ifs);
|
||||
free(ow);
|
||||
}
|
||||
|
||||
// Core expansion of a raw word. Emits one or more fields into `out`.
|
||||
static void expand(sh_state *st, const char *raw, sh_fields *out,
|
||||
int allow_split, int allow_tilde, int quoted_ctx)
|
||||
{
|
||||
fld b;
|
||||
fld_init(&b);
|
||||
int i = 0;
|
||||
|
||||
// Tilde expansion: only unquoted and only at word start. Since the tokenizer
|
||||
// keeps quotes in the raw word ("~" arrives as "\"~\""), a literal raw[0]=='~'
|
||||
// check gives the "unquoted, word-start" rule for free.
|
||||
if (raw[0] == '~') {
|
||||
int j = 1;
|
||||
while (raw[j] && raw[j] != '/') j++;
|
||||
if (allow_tilde && j == 1) { // bare "~" or "~/..." -> $HOME
|
||||
const char *home = sh_get(st, "HOME");
|
||||
if (home) {
|
||||
for (const char *p = home; *p; p++) fld_put(&b, *p);
|
||||
b.started = 1;
|
||||
i = j; // resume at '/' or end; leaves ~name literal
|
||||
}
|
||||
}
|
||||
// ~user: no passwd db here, leave literal (handled by normal loop).
|
||||
}
|
||||
|
||||
while (raw[i]) {
|
||||
char c = raw[i];
|
||||
if (c == '\'') {
|
||||
b.started = 1;
|
||||
i++;
|
||||
while (raw[i] && raw[i] != '\'') fld_put(&b, raw[i++]);
|
||||
if (raw[i] == '\'') i++;
|
||||
} else if (c == '"') {
|
||||
b.started = 1;
|
||||
i++;
|
||||
while (raw[i] && raw[i] != '"') {
|
||||
if (raw[i] == '\\' && (raw[i+1] == '$' || raw[i+1] == '"' ||
|
||||
raw[i+1] == '\\' || raw[i+1] == '`')) {
|
||||
fld_put(&b, raw[i+1]); i += 2;
|
||||
} else if (raw[i] == '$') {
|
||||
do_dollar(st, raw, &i, &b, out, 0, 1); // quoted: no split
|
||||
} else {
|
||||
fld_put(&b, raw[i++]);
|
||||
}
|
||||
}
|
||||
if (raw[i] == '"') i++;
|
||||
} else if (c == '\\') {
|
||||
// In a double-quoted context (e.g. the word of `${x-...}` inside
|
||||
// "..."), a backslash only escapes $ " ` and itself; before any
|
||||
// other char it stays literal. Unquoted, it escapes the next char.
|
||||
if (quoted_ctx) {
|
||||
char nx = raw[i+1];
|
||||
if (nx == '$' || nx == '"' || nx == '`' || nx == '\\') {
|
||||
fld_put(&b, nx); i += 2;
|
||||
} else {
|
||||
fld_put(&b, '\\'); i++;
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
if (raw[i]) fld_put(&b, raw[i++]);
|
||||
}
|
||||
} else if (c == '$') {
|
||||
do_dollar(st, raw, &i, &b, out, allow_split, 0);
|
||||
} else {
|
||||
fld_put(&b, c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
fld_emit(&b, out);
|
||||
free(b.buf);
|
||||
}
|
||||
|
||||
// Does the raw (still-quoted) word contain a glob metachar outside quotes and
|
||||
// not backslash-escaped? Only such words are candidates for pathname expansion:
|
||||
// quoted metachars never glob, and (scope decision) metachars
|
||||
// that arrive via $var/$() expansion don't either -- so the expanded fields can
|
||||
// be used as patterns directly, with no quoted-mask tracking.
|
||||
static int raw_has_glob(const char *raw)
|
||||
{
|
||||
for (int i = 0; raw[i]; i++) {
|
||||
char c = raw[i];
|
||||
if (c == '\'') { i++; while (raw[i] && raw[i] != '\'') i++; if (!raw[i]) break; }
|
||||
else if (c == '"') { i++; while (raw[i] && raw[i] != '"') { if (raw[i] == '\\' && raw[i+1]) i++; i++; } if (!raw[i]) break; }
|
||||
else if (c == '\\') { if (raw[i+1]) i++; }
|
||||
else if (c == '*' || c == '?' || c == '[') return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void sh_expand_word(sh_state *st, const char *raw, sh_fields *out)
|
||||
{
|
||||
if (!raw_has_glob(raw)) {
|
||||
expand(st, raw, out, 1, 1, 0);
|
||||
return;
|
||||
}
|
||||
sh_fields tmp;
|
||||
sh_fields_init(&tmp);
|
||||
expand(st, raw, &tmp, 1, 1, 0);
|
||||
for (int i = 0; i < tmp.count; i++) {
|
||||
if (sh_glob_pathnames(tmp.items[i], out) == 0)
|
||||
sh_fields_push(out, tmp.items[i]); // no match: keep literal
|
||||
}
|
||||
sh_fields_free(&tmp);
|
||||
}
|
||||
|
||||
char *sh_expand_heredoc(sh_state *st, const char *raw)
|
||||
{
|
||||
sh_fields f;
|
||||
sh_fields_init(&f); // do_dollar needs an out sink; splitting is disabled
|
||||
fld b;
|
||||
fld_init(&b);
|
||||
b.started = 1; // an empty body is still a (zero-length) result
|
||||
int i = 0;
|
||||
while (raw[i]) {
|
||||
char c = raw[i];
|
||||
if (c == '\\' && (raw[i+1] == '$' || raw[i+1] == '`' ||
|
||||
raw[i+1] == '\\' || raw[i+1] == '\n')) {
|
||||
if (raw[i+1] == '\n') i += 2; // line continuation
|
||||
else { fld_put(&b, raw[i+1]); i += 2; }
|
||||
} else if (c == '$') {
|
||||
do_dollar(st, raw, &i, &b, &f, 0, 1); // quoted style, no split
|
||||
} else if (c == '`') {
|
||||
// Backtick command substitution reached without the lexer's
|
||||
// `->$() rewrite (e.g. inside a $((...)) operand). Capture to the
|
||||
// matching backtick and run it, honoring \` \\ \$ escapes.
|
||||
int j = i + 1;
|
||||
fld cmd; fld_init(&cmd); cmd.started = 1;
|
||||
while (raw[j] && raw[j] != '`') {
|
||||
if (raw[j] == '\\' && (raw[j+1] == '`' || raw[j+1] == '\\' ||
|
||||
raw[j+1] == '$')) {
|
||||
fld_put(&cmd, raw[j+1]); j += 2;
|
||||
} else {
|
||||
fld_put(&cmd, raw[j]); j++;
|
||||
}
|
||||
}
|
||||
if (raw[j] == '`') j++;
|
||||
char *val = command_subst(st, cmd.buf);
|
||||
for (const char *p = val; *p; p++) fld_put(&b, *p);
|
||||
free(val);
|
||||
free(cmd.buf);
|
||||
i = j;
|
||||
} else {
|
||||
fld_put(&b, c); i++;
|
||||
}
|
||||
}
|
||||
char *res = strdup(b.buf);
|
||||
free(b.buf);
|
||||
sh_fields_free(&f);
|
||||
return res;
|
||||
}
|
||||
|
||||
char *sh_expand_single(sh_state *st, const char *raw)
|
||||
{
|
||||
return sh_expand_single_t(st, raw, 1, 0);
|
||||
}
|
||||
|
||||
static char *sh_expand_single_t(sh_state *st, const char *raw, int allow_tilde,
|
||||
int quoted_ctx)
|
||||
{
|
||||
sh_fields f;
|
||||
sh_fields_init(&f);
|
||||
expand(st, raw, &f, 0, allow_tilde, quoted_ctx);
|
||||
char *res;
|
||||
if (f.count == 0) res = strdup("");
|
||||
else if (f.count == 1) res = strdup(f.items[0]);
|
||||
else {
|
||||
// join with spaces (shouldn't happen with split disabled)
|
||||
int len = 0;
|
||||
for (int i = 0; i < f.count; i++) len += strlen(f.items[i]) + 1;
|
||||
res = malloc(len + 1);
|
||||
res[0] = 0;
|
||||
for (int i = 0; i < f.count; i++) { if (i) strcat(res, " "); strcat(res, f.items[i]); }
|
||||
}
|
||||
sh_fields_free(&f);
|
||||
return res;
|
||||
}
|
||||
686
Tactility/Source/app/shell/shell/sh_expand.cpp
Normal file
686
Tactility/Source/app/shell/shell/sh_expand.cpp
Normal file
@ -0,0 +1,686 @@
|
||||
// Word expansion: tilde, parameter, arithmetic and command substitution,
|
||||
// quote removal, IFS field splitting and pathname globbing.
|
||||
//
|
||||
// Expansion is a pair of frames (see sh_machine.h). A word is scanned left to
|
||||
// right by Expand, which suspends at `$(...)`, `${...}` and `$((...))` to let a
|
||||
// child frame produce the value, then resumes scanning where it left off.
|
||||
#include <Tactility/app/shell/shell/sh_machine.h>
|
||||
|
||||
#include <Tactility/app/shell/shell/sh_arith.h>
|
||||
#include <Tactility/app/shell/shell/sh_glob.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
|
||||
void sh_fields_init(sh_fields* f) { f->items = nullptr; f->count = 0; f->cap = 0; }
|
||||
|
||||
void sh_fields_push(sh_fields* f, const char* s)
|
||||
{
|
||||
if (f->count == f->cap) {
|
||||
f->cap = f->cap ? f->cap * 2 : 8;
|
||||
f->items = static_cast<char**>(realloc(f->items, f->cap * sizeof(char*)));
|
||||
}
|
||||
f->items[f->count++] = strdup(s);
|
||||
}
|
||||
|
||||
void sh_fields_free(sh_fields* f)
|
||||
{
|
||||
for (int i = 0; i < f->count; i++) free(f->items[i]);
|
||||
free(f->items);
|
||||
f->items = nullptr;
|
||||
f->count = f->cap = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace sh {
|
||||
|
||||
namespace {
|
||||
|
||||
bool is_ifs(char c, const char* ifs) { return c && strchr(ifs, c) != nullptr; }
|
||||
bool is_ifs_ws(char c) { return c == ' ' || c == '\t' || c == '\n'; }
|
||||
|
||||
// Does the raw (still-quoted) word contain a glob metachar outside quotes and
|
||||
// not backslash-escaped? Only such words are candidates for pathname expansion:
|
||||
// quoted metachars never glob, and (scope decision) metachars
|
||||
// that arrive via $var/$() expansion don't either -- so the expanded fields can
|
||||
// be used as patterns directly, with no quoted-mask tracking.
|
||||
bool raw_has_glob(const std::string& raw)
|
||||
{
|
||||
size_t n = raw.size();
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = raw[i];
|
||||
if (c == '\'') { i++; while (i < n && raw[i] != '\'') i++; if (i >= n) break; }
|
||||
else if (c == '"') { i++; while (i < n && raw[i] != '"') { if (raw[i] == '\\' && i + 1 < n) i++; i++; } if (i >= n) break; }
|
||||
else if (c == '\\') { if (i + 1 < n) i++; }
|
||||
else if (c == '*' || c == '?' || c == '[') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// region Parameter helpers
|
||||
|
||||
const char* get_ifs(sh_state* st)
|
||||
{
|
||||
const char* v = sh_get(st, "IFS");
|
||||
return v ? v : " \t\n";
|
||||
}
|
||||
|
||||
// Append `v` to the field `b`. If `split`, break fields on $IFS following POSIX
|
||||
// 2.6.5: leading IFS whitespace at the start of a field is elided; a run of IFS
|
||||
// whitespace is one delimiter; each non-whitespace IFS char (with adjacent IFS
|
||||
// whitespace) is also a delimiter and so can create empty fields between/at the
|
||||
// front, but a lone trailing delimiter does not create a trailing empty field.
|
||||
// The final (possibly partial or empty) field is left in `b` so it can continue
|
||||
// across adjacent word parts; the caller flushes it with emit().
|
||||
void append_val(FieldBuilder& b, Fields& out, const char* v, bool split, const char* ifs)
|
||||
{
|
||||
if (!split) { for (const char* q = v; *q; q++) b.put(*q); return; }
|
||||
const char* q = v;
|
||||
// Strip leading IFS whitespace only when no earlier part has begun this field.
|
||||
if (!b.started)
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
for (;;) {
|
||||
// Accumulate the field's content up to the next IFS char.
|
||||
while (*q && !is_ifs(*q, ifs)) { b.put(*q); q++; }
|
||||
if (!*q) return; // field continues into next part / flush
|
||||
b.emitForced(out); // delimiter -> field ends (even if empty)
|
||||
// Consume one delimiter: IFS whitespace, at most one non-ws IFS char,
|
||||
// then trailing IFS whitespace.
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
if (*q && is_ifs(*q, ifs) && !is_ifs_ws(*q)) {
|
||||
q++;
|
||||
while (*q && is_ifs(*q, ifs) && is_ifs_ws(*q)) q++;
|
||||
}
|
||||
if (!*q) return; // trailing delimiter: no empty field
|
||||
}
|
||||
}
|
||||
|
||||
// Is `name` a plain variable name (assignable, not a positional/special param)?
|
||||
bool is_var_name(const char* name)
|
||||
{
|
||||
if (!(isalpha(static_cast<unsigned char>(name[0])) || name[0] == '_')) return false;
|
||||
for (const char* q = name + 1; *q; q++)
|
||||
if (!(isalnum(static_cast<unsigned char>(*q)) || *q == '_')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve a scalar parameter's raw value. Returns NULL if the parameter is
|
||||
// unset (needed to distinguish unset from set-but-empty). *owned receives a
|
||||
// malloc'd buffer the caller must free (may stay NULL).
|
||||
const char* param_raw(sh_state* st, const char* name, char** owned)
|
||||
{
|
||||
*owned = nullptr;
|
||||
if (!name[0]) return nullptr;
|
||||
if (name[1] == 0) {
|
||||
char c = name[0];
|
||||
if (c == '?') { auto* b = static_cast<char*>(malloc(16)); snprintf(b, 16, "%d", st->last_status); *owned = b; return b; }
|
||||
if (c == '#') { auto* b = static_cast<char*>(malloc(16)); snprintf(b, 16, "%d", st->npos); *owned = b; return b; }
|
||||
if (c == '$') return "1"; // stub: fake constant pid
|
||||
if (c == '!') return ""; // stub: no last bg pid, but "set"
|
||||
if (c == '-') return ""; // stub: shell flags
|
||||
if (c == '0') return st->arg0 ? st->arg0 : "";
|
||||
}
|
||||
bool alldig = true;
|
||||
for (const char* q = name; *q; q++)
|
||||
if (!isdigit(static_cast<unsigned char>(*q))) { alldig = false; break; }
|
||||
if (alldig) {
|
||||
long idx = strtol(name, nullptr, 10);
|
||||
if (idx == 0) return st->arg0 ? st->arg0 : "";
|
||||
if (idx >= 1 && idx <= st->npos) return st->pos[idx - 1];
|
||||
return nullptr; // out of range -> unset
|
||||
}
|
||||
return sh_get(st, name);
|
||||
}
|
||||
|
||||
// nounset (set -u): if `val` is unset (NULL) and nounset is on, report the error
|
||||
// and abort the shell (dash exits 2). Returns true if it fired (caller emits nothing).
|
||||
// Specials ($?, $#, $0, ...) resolve non-NULL, so only genuine unset vars and
|
||||
// out-of-range positionals reach here; a bare $@/$* is handled separately.
|
||||
bool nounset_fire(sh_state* st, const char* name, const char* val)
|
||||
{
|
||||
if (val || !st->opt_nounset) return false;
|
||||
fprintf(stderr, "%s: parameter not set\n", name);
|
||||
st->exiting = 1; st->exit_code = 2; st->last_status = 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Expand $@ / $* into fields. `star` selects '*'. `quoted` is set inside "...".
|
||||
void expand_at_star(sh_state* st, bool star, bool quoted, FieldBuilder& b, Fields& out, bool split, const char* ifs)
|
||||
{
|
||||
int n = st->npos;
|
||||
char sep = ' ';
|
||||
const char* ifsv = sh_get(st, "IFS");
|
||||
if (ifsv) sep = ifsv[0]; // may be 0 if IFS=""
|
||||
|
||||
if (star && quoted) {
|
||||
// "$*": join all params by the first char of $IFS into a single field.
|
||||
std::string joined;
|
||||
for (int i = 0; i < n; i++) {
|
||||
joined += st->pos[i];
|
||||
if (i < n - 1 && sep) joined.push_back(sep);
|
||||
}
|
||||
append_val(b, out, joined.c_str(), false, ifs);
|
||||
return;
|
||||
}
|
||||
|
||||
// $@ : one field per positional param.
|
||||
if (n == 0) {
|
||||
// "$@" with no params vanishes; a non-empty prefix ("x$@") keeps the field.
|
||||
if (quoted && !star && b.text.empty()) b.started = false;
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i > 0) b.emit(out);
|
||||
if (quoted) {
|
||||
for (char* q = st->pos[i]; *q; q++) b.put(*q);
|
||||
b.started = true; // keep even empty params as fields
|
||||
} else {
|
||||
append_val(b, out, st->pos[i], split, ifs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Expand
|
||||
|
||||
Expand Expand::word(std::string raw)
|
||||
{
|
||||
Expand e {};
|
||||
e.raw = std::move(raw);
|
||||
e.mode = Mode::Word;
|
||||
e.allowSplit = true;
|
||||
e.allowTilde = true;
|
||||
return e;
|
||||
}
|
||||
|
||||
Expand Expand::single(std::string raw, bool allowTilde, bool quotedContext)
|
||||
{
|
||||
Expand e {};
|
||||
e.raw = std::move(raw);
|
||||
e.mode = Mode::Single;
|
||||
e.allowTilde = allowTilde;
|
||||
e.quotedContext = quotedContext;
|
||||
return e;
|
||||
}
|
||||
|
||||
// A here-doc body: parameter/command expansion and backslash escaping of
|
||||
// $ ` \ (double-quote semantics), preserving newlines and literal quotes.
|
||||
Expand Expand::heredoc(std::string raw)
|
||||
{
|
||||
Expand e {};
|
||||
e.raw = std::move(raw);
|
||||
e.mode = Mode::Heredoc;
|
||||
return e;
|
||||
}
|
||||
|
||||
Step Expand::step(Machine& m)
|
||||
{
|
||||
if (pending != Pending::None) resume(m);
|
||||
|
||||
if (!started) {
|
||||
started = true;
|
||||
if (mode == Mode::Heredoc) {
|
||||
field.started = true; // an empty body is still a (zero-length) result
|
||||
} else if (at(0) == '~') {
|
||||
// Tilde expansion: only unquoted and only at word start. Since the tokenizer
|
||||
// keeps quotes in the raw word ("~" arrives as "\"~\""), a literal raw[0]=='~'
|
||||
// check gives the "unquoted, word-start" rule for free.
|
||||
size_t j = 1;
|
||||
while (at(j) && at(j) != '/') j++;
|
||||
if (allowTilde && j == 1) { // bare "~" or "~/..." -> $HOME
|
||||
if (const char* home = sh_get(&m.st, "HOME")) {
|
||||
for (const char* p = home; *p; p++) field.put(*p);
|
||||
field.started = true;
|
||||
i = j; // resume at '/' or end; leaves ~name literal
|
||||
}
|
||||
}
|
||||
// ~user: no passwd db here, leave literal (handled by normal loop).
|
||||
}
|
||||
}
|
||||
|
||||
bool suspended = (mode == Mode::Heredoc) ? scanHeredoc(m) : scanWord(m);
|
||||
return suspended ? Step::Call : finish(m);
|
||||
}
|
||||
|
||||
// Handles the value a child frame produced for the expansion that suspended the scan.
|
||||
void Expand::resume(Machine& m)
|
||||
{
|
||||
Pending was = pending;
|
||||
pending = Pending::None;
|
||||
switch (was) {
|
||||
case Pending::Arithmetic: {
|
||||
// Command substitutions and `$var` forms inside the expression were
|
||||
// expanded first (parameter/command expansion only, no glob/split);
|
||||
// bare identifiers are left for the evaluator to dereference.
|
||||
long result = 0;
|
||||
const char* error = nullptr;
|
||||
if (sh_arith_eval(&m.st, m.result.text.c_str(), &result, &error) == 0) {
|
||||
char num[32];
|
||||
snprintf(num, sizeof(num), "%ld", result);
|
||||
append_val(field, out, num, pendingSplit, get_ifs(&m.st));
|
||||
} else {
|
||||
fprintf(stderr, "sh: arithmetic: %s\n", error ? error : "error");
|
||||
// dash treats an arithmetic evaluation error as a fatal error in a
|
||||
// non-interactive shell: abort the current command and exit 2.
|
||||
m.st.exiting = 1; m.st.exit_code = 2; m.st.last_status = 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Pending::CommandSubstitution:
|
||||
append_val(field, out, m.result.text.c_str(), pendingSplit, get_ifs(&m.st));
|
||||
break;
|
||||
case Pending::Backtick:
|
||||
for (char c : m.result.text) field.put(c);
|
||||
break;
|
||||
case Pending::Brace:
|
||||
case Pending::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Scans an ordinary word. Returns true when it suspended for a child frame.
|
||||
bool Expand::scanWord(Machine& m)
|
||||
{
|
||||
for (;;) {
|
||||
char c = at(i);
|
||||
if (inDoubleQuote) {
|
||||
if (c == '\0') { inDoubleQuote = false; continue; }
|
||||
if (c == '"') { i++; inDoubleQuote = false; continue; }
|
||||
char next = at(i + 1);
|
||||
if (c == '\\' && (next == '$' || next == '"' || next == '\\' || next == '`')) {
|
||||
field.put(next);
|
||||
i += 2;
|
||||
} else if (c == '$') {
|
||||
if (dollar(m, false, true)) return true; // quoted: no split
|
||||
} else {
|
||||
field.put(c);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '\0') return false;
|
||||
if (c == '\'') {
|
||||
field.started = true;
|
||||
i++;
|
||||
while (at(i) && at(i) != '\'') field.put(raw[i++]);
|
||||
if (at(i) == '\'') i++;
|
||||
} else if (c == '"') {
|
||||
field.started = true;
|
||||
i++;
|
||||
inDoubleQuote = true;
|
||||
} else if (c == '\\') {
|
||||
// In a double-quoted context (e.g. the word of `${x-...}` inside
|
||||
// "..."), a backslash only escapes $ " ` and itself; before any
|
||||
// other char it stays literal. Unquoted, it escapes the next char.
|
||||
if (quotedContext) {
|
||||
char next = at(i + 1);
|
||||
if (next == '$' || next == '"' || next == '`' || next == '\\') {
|
||||
field.put(next);
|
||||
i += 2;
|
||||
} else {
|
||||
field.put('\\');
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
if (at(i)) field.put(raw[i++]);
|
||||
}
|
||||
} else if (c == '$') {
|
||||
if (dollar(m, allowSplit, false)) return true;
|
||||
} else {
|
||||
field.put(c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scans a here-doc body. Returns true when it suspended for a child frame.
|
||||
bool Expand::scanHeredoc(Machine& m)
|
||||
{
|
||||
while (at(i)) {
|
||||
char c = at(i);
|
||||
char next = at(i + 1);
|
||||
if (c == '\\' && (next == '$' || next == '`' || next == '\\' || next == '\n')) {
|
||||
if (next != '\n') field.put(next); // a backslash-newline is a line continuation
|
||||
i += 2;
|
||||
} else if (c == '$') {
|
||||
if (dollar(m, false, true)) return true; // quoted style, no split
|
||||
} else if (c == '`') {
|
||||
// Backtick command substitution reached without the lexer's
|
||||
// `->$() rewrite (e.g. inside a $((...)) operand). Capture to the
|
||||
// matching backtick and run it, honoring \` \\ \$ escapes.
|
||||
size_t j = i + 1;
|
||||
std::string command;
|
||||
while (at(j) && at(j) != '`') {
|
||||
if (at(j) == '\\' && (at(j + 1) == '`' || at(j + 1) == '\\' || at(j + 1) == '$')) {
|
||||
command.push_back(at(j + 1));
|
||||
j += 2;
|
||||
} else {
|
||||
command.push_back(at(j));
|
||||
j++;
|
||||
}
|
||||
}
|
||||
if (at(j) == '`') j++;
|
||||
i = j;
|
||||
pending = Pending::Backtick;
|
||||
m.call(CommandSubst(std::move(command)));
|
||||
return true;
|
||||
} else {
|
||||
field.put(c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse a $-expansion at raw[i] (raw[i]=='$') and append the value(s). `split`
|
||||
// enables IFS word-splitting; `quoted` marks double-quote context. Returns true
|
||||
// when the value comes from a child frame, which resume() then appends.
|
||||
bool Expand::dollar(Machine& m, bool split, bool quoted)
|
||||
{
|
||||
sh_state* st = &m.st;
|
||||
size_t p = i + 1; // skip '$'
|
||||
|
||||
if (at(p) == '(' && at(p + 1) == '(') {
|
||||
// $((expr)) arithmetic expansion. Scan to the matching `))`, tracking
|
||||
// nested `((`/`))` (so `$(( $((a)) + 1 ))` works).
|
||||
p += 2;
|
||||
size_t start = p;
|
||||
int depth = 1;
|
||||
while (at(p) && depth > 0) {
|
||||
if (at(p) == '(') depth++;
|
||||
else if (at(p) == ')') depth--;
|
||||
if (depth == 0) break;
|
||||
p++;
|
||||
}
|
||||
std::string inner = raw.substr(start, p - start);
|
||||
// Consume the closing `))`.
|
||||
if (at(p) == ')') p++;
|
||||
if (at(p) == ')') p++;
|
||||
i = p;
|
||||
pending = Pending::Arithmetic;
|
||||
pendingSplit = split;
|
||||
m.call(Expand::heredoc(std::move(inner)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (at(p) == '(') {
|
||||
// $(...) command substitution.
|
||||
p++;
|
||||
size_t start = p;
|
||||
int depth = 1;
|
||||
while (at(p) && depth > 0) {
|
||||
char d = at(p);
|
||||
if (d == '\'') { p++; while (at(p) && at(p) != '\'') p++; if (at(p)) p++; continue; }
|
||||
if (d == '"') {
|
||||
p++;
|
||||
while (at(p) && at(p) != '"') { if (at(p) == '\\' && at(p + 1)) p += 2; else p++; }
|
||||
if (at(p)) p++;
|
||||
continue;
|
||||
}
|
||||
if (d == '\\' && at(p + 1)) { p += 2; continue; }
|
||||
if (d == '(') depth++;
|
||||
else if (d == ')') { if (--depth == 0) break; }
|
||||
p++;
|
||||
}
|
||||
std::string inner = raw.substr(start, p - start);
|
||||
if (at(p) == ')') p++;
|
||||
i = p;
|
||||
pending = Pending::CommandSubstitution;
|
||||
pendingSplit = quoted ? false : split;
|
||||
m.call(CommandSubst(std::move(inner)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (at(p) == '{') {
|
||||
// ${...} : find the matching '}' (respect nesting, $(), quotes).
|
||||
p++;
|
||||
size_t start = p;
|
||||
int depth = 1;
|
||||
while (at(p) && depth > 0) {
|
||||
char d = at(p);
|
||||
if (d == '\\' && at(p + 1)) { p += 2; continue; }
|
||||
if (d == '\'') { p++; while (at(p) && at(p) != '\'') p++; if (at(p)) p++; continue; }
|
||||
if (d == '"') {
|
||||
p++;
|
||||
while (at(p) && at(p) != '"') { if (at(p) == '\\' && at(p + 1)) p += 2; else p++; }
|
||||
if (at(p)) p++;
|
||||
continue;
|
||||
}
|
||||
if (d == '$' && at(p + 1) == '{') { depth++; p += 2; continue; }
|
||||
if (d == '$' && at(p + 1) == '(') {
|
||||
p += 2;
|
||||
int pd = 1;
|
||||
while (at(p) && pd) { if (at(p) == '(') pd++; else if (at(p) == ')') { if (--pd == 0) { p++; break; } } p++; }
|
||||
continue;
|
||||
}
|
||||
if (d == '{') depth++;
|
||||
else if (d == '}') { if (--depth == 0) break; }
|
||||
p++;
|
||||
}
|
||||
std::string content = raw.substr(start, p - start);
|
||||
if (at(p) == '}') p++;
|
||||
i = p;
|
||||
pending = Pending::Brace;
|
||||
m.call(ExpandBrace { .content = std::move(content), .quoted = quoted, .split = split, .field = &field, .out = &out });
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* ifs = get_ifs(st);
|
||||
|
||||
if (at(p) == '@' || at(p) == '*') {
|
||||
expand_at_star(st, at(p) == '*', quoted, field, out, split, ifs);
|
||||
i = p + 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
char c = at(p);
|
||||
if (c == '?' || c == '#' || c == '$' || c == '!' || c == '-') {
|
||||
name.push_back(c);
|
||||
p++;
|
||||
} else if (isdigit(static_cast<unsigned char>(c))) {
|
||||
name.push_back(c); // bare $N is a single digit
|
||||
p++;
|
||||
} else if (isalpha(static_cast<unsigned char>(c)) || c == '_') {
|
||||
while ((isalnum(static_cast<unsigned char>(at(p))) || at(p) == '_') && name.size() < 127) name.push_back(at(p++));
|
||||
} else {
|
||||
field.put('$'); // lone '$' - literal
|
||||
i++;
|
||||
return false;
|
||||
}
|
||||
|
||||
char* owned = nullptr;
|
||||
const char* val = param_raw(st, name.c_str(), &owned);
|
||||
if (!nounset_fire(st, name.c_str(), val)) {
|
||||
append_val(field, out, val ? val : "", split, ifs);
|
||||
}
|
||||
free(owned);
|
||||
i = p;
|
||||
return false;
|
||||
}
|
||||
|
||||
Step Expand::finish(Machine& m)
|
||||
{
|
||||
if (mode == Mode::Heredoc) return m.done(std::move(field.text));
|
||||
|
||||
field.emit(out);
|
||||
|
||||
if (mode == Mode::Single) {
|
||||
if (out.empty()) return m.done(std::string());
|
||||
if (out.size() == 1) return m.done(std::move(out[0]));
|
||||
// join with spaces (shouldn't happen with split disabled)
|
||||
std::string joined;
|
||||
for (size_t k = 0; k < out.size(); k++) {
|
||||
if (k) joined.push_back(' ');
|
||||
joined += out[k];
|
||||
}
|
||||
return m.done(std::move(joined));
|
||||
}
|
||||
|
||||
if (!raw_has_glob(raw)) return m.done(std::move(out));
|
||||
Fields globbed;
|
||||
for (auto& item : out) {
|
||||
sh_fields matches;
|
||||
sh_fields_init(&matches);
|
||||
if (sh_glob_pathnames(item.c_str(), &matches) == 0) {
|
||||
globbed.push_back(std::move(item)); // no match: keep literal
|
||||
} else {
|
||||
for (int k = 0; k < matches.count; k++) globbed.emplace_back(matches.items[k]);
|
||||
}
|
||||
sh_fields_free(&matches);
|
||||
}
|
||||
return m.done(std::move(globbed));
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region ExpandBrace
|
||||
|
||||
Step ExpandBrace::step(Machine& m)
|
||||
{
|
||||
sh_state* st = &m.st;
|
||||
const char* ifs = get_ifs(st);
|
||||
switch (phase) {
|
||||
case Phase::Start:
|
||||
return start(m);
|
||||
case Phase::Strip: {
|
||||
char* owned = nullptr;
|
||||
const char* v = param_raw(st, name.c_str(), &owned);
|
||||
const char* subject = v ? v : "";
|
||||
char* stripped = (stripKind == '#')
|
||||
? sh_strip_prefix(subject, m.result.text.c_str(), longest)
|
||||
: sh_strip_suffix(subject, m.result.text.c_str(), longest);
|
||||
append_val(*field, *out, stripped, split, ifs);
|
||||
free(stripped);
|
||||
free(owned);
|
||||
break;
|
||||
}
|
||||
case Phase::Plus:
|
||||
case Phase::Minus:
|
||||
append_val(*field, *out, m.result.text.c_str(), quoted ? false : split, ifs);
|
||||
break;
|
||||
case Phase::Assign:
|
||||
if (is_var_name(name.c_str())) sh_set(st, name.c_str(), m.result.text.c_str());
|
||||
else fprintf(stderr, "%s: cannot assign in this way\n", name.c_str());
|
||||
append_val(*field, *out, m.result.text.c_str(), quoted ? false : split, get_ifs(st));
|
||||
break;
|
||||
case Phase::Error:
|
||||
if (!m.result.text.empty()) fprintf(stderr, "%s: %s\n", name.c_str(), m.result.text.c_str());
|
||||
else fprintf(stderr, "%s: parameter not set\n", name.c_str());
|
||||
st->exiting = 1;
|
||||
st->exit_code = 2;
|
||||
st->last_status = 2;
|
||||
break;
|
||||
}
|
||||
return m.done(0);
|
||||
}
|
||||
|
||||
// Handle the interior of a ${...} expression (operators, length, plain).
|
||||
Step ExpandBrace::start(Machine& m)
|
||||
{
|
||||
sh_state* st = &m.st;
|
||||
const char* ifs = get_ifs(st);
|
||||
const char* c = content.c_str();
|
||||
|
||||
// Length form: ${#name}
|
||||
if (c[0] == '#') {
|
||||
char d = c[1];
|
||||
if (isalnum(static_cast<unsigned char>(d)) || d == '_' || d == '@' || d == '*' ||
|
||||
(d == '#' && c[2] == 0)) { // ${##}: length of "$#"
|
||||
const char* nm = c + 1;
|
||||
char num[16];
|
||||
if (strcmp(nm, "@") == 0 || strcmp(nm, "*") == 0) {
|
||||
snprintf(num, sizeof(num), "%d", st->npos);
|
||||
} else {
|
||||
char* owned = nullptr;
|
||||
const char* v = param_raw(st, nm, &owned);
|
||||
if (nounset_fire(st, nm, v)) { free(owned); return m.done(0); }
|
||||
snprintf(num, sizeof(num), "%d", v ? static_cast<int>(strlen(v)) : 0);
|
||||
free(owned);
|
||||
}
|
||||
append_val(*field, *out, num, split, ifs);
|
||||
return m.done(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the parameter name.
|
||||
const char* r = c;
|
||||
if (*r == '@' || *r == '*' || *r == '?' || *r == '!' || *r == '$' || *r == '-' || *r == '#') {
|
||||
name.push_back(*r++);
|
||||
} else if (isdigit(static_cast<unsigned char>(*r))) {
|
||||
while (isdigit(static_cast<unsigned char>(*r)) && name.size() < 127) name.push_back(*r++);
|
||||
} else if (isalpha(static_cast<unsigned char>(*r)) || *r == '_') {
|
||||
while ((isalnum(static_cast<unsigned char>(*r)) || *r == '_') && name.size() < 127) name.push_back(*r++);
|
||||
}
|
||||
|
||||
// No operator: plain ${name}.
|
||||
if (*r == 0) {
|
||||
if (name == "@" || name == "*") {
|
||||
expand_at_star(st, name[0] == '*', quoted, *field, *out, split, ifs);
|
||||
} else {
|
||||
char* owned = nullptr;
|
||||
const char* v = param_raw(st, name.c_str(), &owned);
|
||||
if (!nounset_fire(st, name.c_str(), v)) append_val(*field, *out, v ? v : "", split, ifs);
|
||||
free(owned);
|
||||
}
|
||||
return m.done(0);
|
||||
}
|
||||
|
||||
// Prefix/suffix strip: ${v#p} ${v##p} ${v%p} ${v%%p}
|
||||
if (*r == '#' || *r == '%') {
|
||||
stripKind = *r++;
|
||||
if (*r == stripKind) { longest = true; r++; }
|
||||
phase = Phase::Strip;
|
||||
return m.call(Expand::single(r));
|
||||
}
|
||||
|
||||
// Value operators: :- - :+ + := = :? ?
|
||||
bool colon = false;
|
||||
if (*r == ':' && (r[1] == '-' || r[1] == '+' || r[1] == '=' || r[1] == '?')) {
|
||||
colon = true;
|
||||
r++;
|
||||
}
|
||||
char op = *r;
|
||||
if (op == '-' || op == '+' || op == '=' || op == '?') {
|
||||
const char* word = r + 1;
|
||||
char* owned = nullptr;
|
||||
const char* val = param_raw(st, name.c_str(), &owned);
|
||||
bool set = val != nullptr;
|
||||
bool active = colon ? (!set || val[0] == 0) : !set; // "empty or unset" test
|
||||
value = val ? val : "";
|
||||
free(owned);
|
||||
|
||||
if (op == '+') {
|
||||
// Use word if the parameter IS set (non-empty for colon form).
|
||||
if (active) return m.done(0);
|
||||
phase = Phase::Plus;
|
||||
} else if (!active) {
|
||||
append_val(*field, *out, value.c_str(), quoted ? false : split, ifs);
|
||||
return m.done(0);
|
||||
} else {
|
||||
phase = (op == '-') ? Phase::Minus : (op == '=') ? Phase::Assign : Phase::Error;
|
||||
}
|
||||
return m.call(Expand::single(word, !quoted, quoted));
|
||||
}
|
||||
|
||||
// Unknown operator: fall back to plain value.
|
||||
char* owned = nullptr;
|
||||
const char* v = param_raw(st, name.c_str(), &owned);
|
||||
append_val(*field, *out, v ? v : "", split, ifs);
|
||||
free(owned);
|
||||
return m.done(0);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace sh
|
||||
@ -61,38 +61,48 @@ static int bracket_match(const char **pp, unsigned char c)
|
||||
return negate ? !matched : matched;
|
||||
}
|
||||
|
||||
// Iterative with a single backtrack point: on a mismatch, the most recent '*'
|
||||
// absorbs one more character. That is sufficient for '*' semantics and keeps
|
||||
// the native stack flat regardless of how many stars the pattern has.
|
||||
int sh_pattern_match(const char *p, const char *s)
|
||||
{
|
||||
while (*p) {
|
||||
if (*p == '[') {
|
||||
const char *pp = p + 1;
|
||||
int r = bracket_match(&pp, (unsigned char)*s);
|
||||
if (r >= 0) {
|
||||
if (!*s || !r) return 0;
|
||||
p = pp; s++;
|
||||
const char *star_p = NULL; // pattern position just after the last '*'
|
||||
const char *star_s = NULL; // subject position that '*' currently ends at
|
||||
for (;;) {
|
||||
if (*p) {
|
||||
if (*p == '[') {
|
||||
const char *pp = p + 1;
|
||||
int r = bracket_match(&pp, (unsigned char)*s);
|
||||
if (r >= 0) {
|
||||
if (*s && r) { p = pp; s++; continue; }
|
||||
goto mismatch;
|
||||
}
|
||||
// fall through: unterminated bracket, '[' is literal
|
||||
}
|
||||
if (*p == '*') {
|
||||
p++;
|
||||
if (!*p) return 1; // trailing '*' matches the rest
|
||||
star_p = p;
|
||||
star_s = s;
|
||||
continue;
|
||||
}
|
||||
// fall through: unterminated bracket, '[' is literal
|
||||
}
|
||||
if (*p == '*') {
|
||||
p++;
|
||||
if (!*p) return 1; // trailing '*' matches the rest
|
||||
for (const char *t = s; ; t++) {
|
||||
if (sh_pattern_match(p, t)) return 1;
|
||||
if (!*t) return 0;
|
||||
if (*p == '?') {
|
||||
if (*s) { p++; s++; continue; }
|
||||
goto mismatch;
|
||||
}
|
||||
} else if (*p == '?') {
|
||||
if (!*s) return 0;
|
||||
p++; s++;
|
||||
} else if (*p == '\\' && p[1]) {
|
||||
if (*s != p[1]) return 0;
|
||||
p += 2; s++;
|
||||
} else {
|
||||
if (*s != *p) return 0;
|
||||
p++; s++;
|
||||
if (*p == '\\' && p[1]) {
|
||||
if (*s == p[1]) { p += 2; s++; continue; }
|
||||
goto mismatch;
|
||||
}
|
||||
if (*s == *p) { p++; s++; continue; }
|
||||
} else if (!*s) {
|
||||
return 1;
|
||||
}
|
||||
mismatch:
|
||||
if (!star_p || !*star_s) return 0;
|
||||
p = star_p;
|
||||
s = ++star_s;
|
||||
}
|
||||
return *s == 0;
|
||||
}
|
||||
|
||||
char *sh_strip_prefix(const char *s, const char *p, int longest)
|
||||
|
||||
@ -39,33 +39,75 @@ typedef struct { char *buf; int len; int cap; } wbuf;
|
||||
// src[j+1]=='('), copying the whole balanced expression verbatim into `w`.
|
||||
// Parens inside single/double quotes don't affect nesting; nested $() do.
|
||||
// sh_expand later runs the inner command. Returns the index past the ')'.
|
||||
//
|
||||
// Nesting ($( inside "..." inside $(...)) is tracked with an explicit context
|
||||
// stack, so arbitrarily deep input can't exhaust the native stack. Each $(
|
||||
// context carries its own paren depth.
|
||||
typedef struct { char kind; int depth; } scan_ctx; // kind: '(' command, '"' double quote
|
||||
|
||||
typedef struct {
|
||||
scan_ctx fixed[16];
|
||||
scan_ctx *items;
|
||||
int cap;
|
||||
int top;
|
||||
} scan_ctx_stack;
|
||||
|
||||
static void push_ctx(scan_ctx_stack *cs, char kind, int depth)
|
||||
{
|
||||
if (cs->top == cs->cap) {
|
||||
scan_ctx *grown = malloc(cs->cap * 2 * sizeof(scan_ctx));
|
||||
memcpy(grown, cs->items, cs->cap * sizeof(scan_ctx));
|
||||
if (cs->items != cs->fixed) free(cs->items);
|
||||
cs->items = grown;
|
||||
cs->cap *= 2;
|
||||
}
|
||||
cs->items[cs->top].kind = kind;
|
||||
cs->items[cs->top].depth = depth;
|
||||
cs->top++;
|
||||
}
|
||||
|
||||
static int scan_cmdsub(const char *src, int j, wbuf *w)
|
||||
{
|
||||
scan_ctx_stack cs;
|
||||
cs.items = cs.fixed;
|
||||
cs.cap = 16;
|
||||
cs.top = 0;
|
||||
push_ctx(&cs, '(', 1);
|
||||
WPUT(w, src[j]); j++; // '$'
|
||||
WPUT(w, src[j]); j++; // '('
|
||||
int depth = 1;
|
||||
while (src[j] && depth > 0) {
|
||||
|
||||
while (src[j] && cs.top > 0) {
|
||||
char d = src[j];
|
||||
scan_ctx *c = &cs.items[cs.top - 1];
|
||||
if (c->kind == '"') {
|
||||
if (d == '$' && src[j + 1] == '(') {
|
||||
WPUT(w, d); j++; WPUT(w, src[j]); j++;
|
||||
push_ctx(&cs, '(', 1);
|
||||
} else if (d == '\\' && src[j + 1]) {
|
||||
WPUT(w, d); j++; WPUT(w, src[j]); j++;
|
||||
} else {
|
||||
WPUT(w, d); j++;
|
||||
if (d == '"') cs.top--;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (d == '\'') {
|
||||
WPUT(w, d); j++;
|
||||
while (src[j] && src[j] != '\'') { WPUT(w, src[j]); j++; }
|
||||
if (src[j] == '\'') { WPUT(w, src[j]); j++; }
|
||||
} else if (d == '"') {
|
||||
WPUT(w, d); j++;
|
||||
while (src[j] && src[j] != '"') {
|
||||
if (src[j] == '$' && src[j + 1] == '(') { j = scan_cmdsub(src, j, w); }
|
||||
else if (src[j] == '\\' && src[j + 1]) { WPUT(w, src[j]); j++; WPUT(w, src[j]); j++; }
|
||||
else { WPUT(w, src[j]); j++; }
|
||||
}
|
||||
if (src[j] == '"') { WPUT(w, src[j]); j++; }
|
||||
push_ctx(&cs, '"', 0);
|
||||
} else if (d == '\\' && src[j + 1]) {
|
||||
WPUT(w, d); j++; WPUT(w, src[j]); j++;
|
||||
} else {
|
||||
if (d == '(') depth++;
|
||||
else if (d == ')') depth--;
|
||||
if (d == '(') c->depth++;
|
||||
else if (d == ')') c->depth--;
|
||||
WPUT(w, d); j++;
|
||||
if (c->depth == 0) cs.top--;
|
||||
}
|
||||
}
|
||||
if (cs.items != cs.fixed) free(cs.items);
|
||||
return j;
|
||||
}
|
||||
|
||||
|
||||
@ -1,504 +0,0 @@
|
||||
#include <Tactility/app/shell/shell/sh_parse.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
sh_toklist *tl;
|
||||
int pos;
|
||||
const char *err;
|
||||
} P;
|
||||
|
||||
static sh_tok *cur(P *p) { return &p->tl->toks[p->pos]; }
|
||||
static tok_type curt(P *p){ return p->tl->toks[p->pos].type; }
|
||||
static tok_type peekt(P *p){ return p->tl->toks[p->pos].type == T_EOF ? T_EOF : p->tl->toks[p->pos + 1].type; }
|
||||
static void adv(P *p) { if (p->tl->toks[p->pos].type != T_EOF) p->pos++; }
|
||||
|
||||
// Skip newlines/semicolons acting as blank separators.
|
||||
static void skip_seps(P *p)
|
||||
{
|
||||
while (curt(p) == T_NEWLINE || curt(p) == T_SEMI) adv(p);
|
||||
}
|
||||
static void skip_newlines(P *p)
|
||||
{
|
||||
while (curt(p) == T_NEWLINE) adv(p);
|
||||
}
|
||||
|
||||
static node *new_node(node_kind k)
|
||||
{
|
||||
node *n = calloc(1, sizeof(node));
|
||||
n->kind = k;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Is the current token a bare word equal to `kw`?
|
||||
static int is_word(P *p, const char *kw)
|
||||
{
|
||||
sh_tok *t = cur(p);
|
||||
return t->type == T_WORD && t->text && strcmp(t->text, kw) == 0;
|
||||
}
|
||||
|
||||
static int is_reserved(P *p)
|
||||
{
|
||||
static const char *kw[] = { "then","else","elif","fi","do","done","esac","}", NULL };
|
||||
if (curt(p) != T_WORD) return 0;
|
||||
for (int i = 0; kw[i]; i++) if (is_word(p, kw[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static node *parse_list(P *p);
|
||||
static node *parse_and_or(P *p);
|
||||
static node *parse_pipeline(P *p);
|
||||
static node *parse_command(P *p);
|
||||
|
||||
static void push_child(node *n, node *c)
|
||||
{
|
||||
n->children = realloc(n->children, (n->nchild + 1) * sizeof(node *));
|
||||
n->children[n->nchild++] = c;
|
||||
}
|
||||
|
||||
// --- redirections -----------------------------------------------------------
|
||||
|
||||
static int is_redir_tok(tok_type t)
|
||||
{
|
||||
return t == T_LT || t == T_GT || t == T_GTGT || t == T_GTAMP ||
|
||||
t == T_LTAMP || t == T_CLOBBER || t == T_DLESS || t == T_DLESSDASH;
|
||||
}
|
||||
|
||||
static sh_redir *push_redir(node *n)
|
||||
{
|
||||
n->redirs = realloc(n->redirs, (n->nredir + 1) * sizeof(sh_redir));
|
||||
sh_redir *r = &n->redirs[n->nredir++];
|
||||
r->kind = R_OUT; r->fd = 1; r->dupfd = 0; r->word = NULL; r->heredoc_quoted = 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
static int all_digits(const char *s)
|
||||
{
|
||||
if (!*s) return 0;
|
||||
for (; *s; s++) if (*s < '0' || *s > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse the redirection at the current token onto `n`. Returns 1 on success,
|
||||
// 0 on error (p->err set).
|
||||
static int parse_redir(P *p, node *n)
|
||||
{
|
||||
tok_type t = curt(p);
|
||||
int fd = cur(p)->rfd;
|
||||
// No explicit fd digit (rfd == -1): default to stdin for input operators,
|
||||
// stdout for output operators.
|
||||
if (fd < 0) {
|
||||
fd = (t == T_LT || t == T_LTAMP || t == T_DLESS || t == T_DLESSDASH)
|
||||
? 0 : 1;
|
||||
}
|
||||
|
||||
if (t == T_DLESS || t == T_DLESSDASH) {
|
||||
char *body = cur(p)->text ? strdup(cur(p)->text) : strdup("");
|
||||
int quoted = cur(p)->hd_quoted;
|
||||
adv(p);
|
||||
if (curt(p) != T_WORD) { free(body); p->err = "expected here-doc delimiter"; return 0; }
|
||||
sh_redir *r = push_redir(n);
|
||||
r->kind = R_HEREDOC; r->fd = 0; r->word = body; r->heredoc_quoted = quoted;
|
||||
adv(p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (t == T_GTAMP || t == T_LTAMP) {
|
||||
adv(p);
|
||||
if (curt(p) != T_WORD) { p->err = "expected fd or filename after >&"; return 0; }
|
||||
const char *tgt = cur(p)->text;
|
||||
sh_redir *r = push_redir(n);
|
||||
r->fd = fd;
|
||||
if (all_digits(tgt)) { r->kind = R_DUP; r->dupfd = (int)strtol(tgt, NULL, 10); }
|
||||
else if (strcmp(tgt, "-") == 0) { r->kind = R_CLOSE; }
|
||||
else { r->kind = (t == T_GTAMP) ? R_OUT : R_IN; r->word = strdup(tgt); }
|
||||
adv(p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// T_LT / T_GT / T_GTGT / T_CLOBBER: filename target.
|
||||
redir_kind rk = (t == T_LT) ? R_IN : (t == T_GTGT) ? R_APPEND : R_OUT;
|
||||
adv(p);
|
||||
if (curt(p) != T_WORD) { p->err = "expected filename after redirection"; return 0; }
|
||||
sh_redir *r = push_redir(n);
|
||||
r->kind = rk; r->fd = fd; r->word = strdup(cur(p)->text);
|
||||
adv(p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Consume any trailing redirections onto a compound command. Returns 1/0.
|
||||
static int parse_redir_list(P *p, node *n)
|
||||
{
|
||||
while (is_redir_tok(curt(p))) {
|
||||
if (!parse_redir(p, n)) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- simple command ---------------------------------------------------------
|
||||
|
||||
static int looks_like_assign(const char *w)
|
||||
{
|
||||
if (!(*w == '_' || (*w >= 'A' && *w <= 'Z') || (*w >= 'a' && *w <= 'z'))) return 0;
|
||||
const char *q = w + 1;
|
||||
while (*q && *q != '=') {
|
||||
if (!(*q == '_' || (*q >= 'A' && *q <= 'Z') || (*q >= 'a' && *q <= 'z') ||
|
||||
(*q >= '0' && *q <= '9'))) return 0;
|
||||
q++;
|
||||
}
|
||||
return *q == '=';
|
||||
}
|
||||
|
||||
static node *parse_simple(P *p)
|
||||
{
|
||||
node *n = new_node(N_SIMPLE);
|
||||
int seen_word = 0;
|
||||
|
||||
for (;;) {
|
||||
tok_type t = curt(p);
|
||||
if (t == T_WORD) {
|
||||
if (!seen_word && looks_like_assign(cur(p)->text)) {
|
||||
n->assigns = realloc(n->assigns, (n->nassign + 1) * sizeof(char *));
|
||||
n->assigns[n->nassign++] = strdup(cur(p)->text);
|
||||
adv(p);
|
||||
continue;
|
||||
}
|
||||
// Reserved words are only special in command position (first word);
|
||||
// as arguments they are ordinary words (e.g. `echo done`).
|
||||
if (!seen_word && is_reserved(p)) break;
|
||||
n->words = realloc(n->words, (n->nword + 1) * sizeof(char *));
|
||||
n->words[n->nword++] = strdup(cur(p)->text);
|
||||
seen_word = 1;
|
||||
adv(p);
|
||||
} else if (is_redir_tok(t)) {
|
||||
// Redirects may appear before, between, or after words and do not
|
||||
// count as the command word (so `FOO=x >f BAR=y cmd` still assigns).
|
||||
if (!parse_redir(p, n)) { sh_free_node(n); return NULL; }
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n->nword == 0 && n->nassign == 0 && n->nredir == 0) {
|
||||
p->err = "expected command";
|
||||
sh_free_node(n);
|
||||
return NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// --- control flow -----------------------------------------------------------
|
||||
|
||||
// Collect the raw words after `for NAME in` up to a separator.
|
||||
static node *parse_for(P *p)
|
||||
{
|
||||
adv(p); // 'for'
|
||||
if (curt(p) != T_WORD) { p->err = "expected name after for"; return NULL; }
|
||||
// The loop variable must be a valid name (dash rejects e.g. `for -`).
|
||||
const char *nm = cur(p)->text;
|
||||
if (!(nm[0] == '_' || (nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z'))) {
|
||||
p->err = "bad for loop variable"; return NULL;
|
||||
}
|
||||
for (const char *q = nm + 1; *q; q++)
|
||||
if (!(*q == '_' || (*q >= '0' && *q <= '9') ||
|
||||
(*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z'))) {
|
||||
p->err = "bad for loop variable"; return NULL;
|
||||
}
|
||||
node *n = new_node(N_FOR);
|
||||
n->for_name = strdup(nm);
|
||||
adv(p);
|
||||
skip_newlines(p); // `for i <newline> in ...` is legal (`for i; in` is not)
|
||||
if (is_word(p, "in")) {
|
||||
adv(p);
|
||||
while (curt(p) == T_WORD && !is_reserved(p)) {
|
||||
n->for_words = realloc(n->for_words, (n->for_nword + 1) * sizeof(char *));
|
||||
n->for_words[n->for_nword++] = strdup(cur(p)->text);
|
||||
adv(p);
|
||||
}
|
||||
} else {
|
||||
// `for x; do ...` with no `in`: iterate over the positional params.
|
||||
n->for_implicit = 1;
|
||||
}
|
||||
skip_seps(p);
|
||||
if (!is_word(p, "do")) { p->err = "expected 'do'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
n->body = parse_list(p);
|
||||
if (!n->body) { sh_free_node(n); return NULL; }
|
||||
if (n->body->nchild == 0) { p->err = "empty do/done body"; sh_free_node(n); return NULL; }
|
||||
if (!is_word(p, "done")) { p->err = "expected 'done'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_while(P *p)
|
||||
{
|
||||
int until = is_word(p, "until");
|
||||
adv(p); // 'while' / 'until'
|
||||
node *n = new_node(N_WHILE);
|
||||
n->until = until;
|
||||
n->cond = parse_list(p);
|
||||
if (!n->cond) { sh_free_node(n); return NULL; }
|
||||
if (!is_word(p, "do")) { p->err = "expected 'do'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
n->body = parse_list(p);
|
||||
if (!n->body) { sh_free_node(n); return NULL; }
|
||||
if (n->body->nchild == 0) { p->err = "empty do/done body"; sh_free_node(n); return NULL; }
|
||||
if (!is_word(p, "done")) { p->err = "expected 'done'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
static void push_clause(node *n, node *cond, node *body)
|
||||
{
|
||||
n->conds = realloc(n->conds, (n->nclause + 1) * sizeof(node *));
|
||||
n->bodies = realloc(n->bodies, (n->nclause + 1) * sizeof(node *));
|
||||
n->conds[n->nclause] = cond;
|
||||
n->bodies[n->nclause] = body;
|
||||
n->nclause++;
|
||||
}
|
||||
|
||||
static node *parse_if(P *p)
|
||||
{
|
||||
adv(p); // 'if'
|
||||
node *n = new_node(N_IF);
|
||||
for (;;) {
|
||||
node *cond = parse_list(p);
|
||||
if (!cond) { sh_free_node(n); return NULL; }
|
||||
if (!is_word(p, "then")) { p->err = "expected 'then'"; sh_free_node(cond); sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
node *body = parse_list(p);
|
||||
if (!body) { sh_free_node(cond); sh_free_node(n); return NULL; }
|
||||
if (body->nchild == 0) { p->err = "empty then body"; sh_free_node(cond); sh_free_node(body); sh_free_node(n); return NULL; }
|
||||
push_clause(n, cond, body);
|
||||
if (is_word(p, "elif")) { adv(p); continue; }
|
||||
break;
|
||||
}
|
||||
if (is_word(p, "else")) {
|
||||
adv(p);
|
||||
n->else_body = parse_list(p);
|
||||
if (!n->else_body) { sh_free_node(n); return NULL; }
|
||||
if (n->else_body->nchild == 0) { p->err = "empty else body"; sh_free_node(n); return NULL; }
|
||||
}
|
||||
if (!is_word(p, "fi")) { p->err = "expected 'fi'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
// case WORD in [(] pat [| pat]... ) list ;; ... esac
|
||||
static node *parse_case(P *p)
|
||||
{
|
||||
adv(p); // 'case'
|
||||
if (curt(p) != T_WORD) { p->err = "expected word after case"; return NULL; }
|
||||
node *n = new_node(N_CASE);
|
||||
n->case_word = strdup(cur(p)->text);
|
||||
adv(p);
|
||||
if (!is_word(p, "in")) { p->err = "expected 'in' after case word"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
skip_seps(p);
|
||||
while (!is_word(p, "esac") && curt(p) != T_EOF) {
|
||||
if (curt(p) == T_LPAREN) adv(p); // optional leading '('
|
||||
sh_case_clause cl;
|
||||
cl.pats = NULL; cl.npat = 0; cl.body = NULL;
|
||||
for (;;) {
|
||||
if (curt(p) != T_WORD) {
|
||||
p->err = "expected case pattern";
|
||||
for (int i = 0; i < cl.npat; i++) free(cl.pats[i]);
|
||||
free(cl.pats); sh_free_node(n); return NULL;
|
||||
}
|
||||
cl.pats = realloc(cl.pats, (cl.npat + 1) * sizeof(char *));
|
||||
cl.pats[cl.npat++] = strdup(cur(p)->text);
|
||||
adv(p);
|
||||
if (curt(p) == T_BAR) { adv(p); continue; }
|
||||
break;
|
||||
}
|
||||
if (curt(p) != T_RPAREN) {
|
||||
p->err = "expected ')' in case";
|
||||
for (int i = 0; i < cl.npat; i++) free(cl.pats[i]);
|
||||
free(cl.pats); sh_free_node(n); return NULL;
|
||||
}
|
||||
adv(p);
|
||||
cl.body = parse_list(p);
|
||||
if (!cl.body) {
|
||||
for (int i = 0; i < cl.npat; i++) free(cl.pats[i]);
|
||||
free(cl.pats); sh_free_node(n); return NULL;
|
||||
}
|
||||
n->clauses = realloc(n->clauses, (n->nclause_case + 1) * sizeof(sh_case_clause));
|
||||
n->clauses[n->nclause_case++] = cl;
|
||||
if (curt(p) == T_DSEMI) { adv(p); skip_seps(p); }
|
||||
else break; // last clause may omit ';;'
|
||||
}
|
||||
if (!is_word(p, "esac")) { p->err = "expected 'esac'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_group(P *p)
|
||||
{
|
||||
adv(p); // '{'
|
||||
node *n = new_node(N_GROUP);
|
||||
n->subshell = 0;
|
||||
n->body = parse_list(p);
|
||||
if (!n->body) { sh_free_node(n); return NULL; }
|
||||
if (!is_word(p, "}")) { p->err = "expected '}'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_subshell(P *p)
|
||||
{
|
||||
adv(p); // '('
|
||||
node *n = new_node(N_GROUP);
|
||||
n->subshell = 1;
|
||||
n->body = parse_list(p);
|
||||
if (!n->body) { sh_free_node(n); return NULL; }
|
||||
if (curt(p) != T_RPAREN) { p->err = "expected ')'"; sh_free_node(n); return NULL; }
|
||||
adv(p);
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_funcdef(P *p)
|
||||
{
|
||||
node *n = new_node(N_FUNCDEF);
|
||||
n->func_name = strdup(cur(p)->text);
|
||||
adv(p); // name
|
||||
adv(p); // '('
|
||||
if (curt(p) != T_RPAREN) { p->err = "expected ')' in function definition"; sh_free_node(n); return NULL; }
|
||||
adv(p); // ')'
|
||||
skip_newlines(p);
|
||||
n->body = parse_command(p);
|
||||
if (!n->body) { sh_free_node(n); return NULL; }
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_command(P *p)
|
||||
{
|
||||
// Simple commands handle their own (possibly interleaved) redirects.
|
||||
if (curt(p) != T_LPAREN && !is_word(p, "{") &&
|
||||
!(curt(p) == T_WORD && !is_reserved(p) && peekt(p) == T_LPAREN) &&
|
||||
!is_word(p, "if") && !is_word(p, "while") && !is_word(p, "until") &&
|
||||
!is_word(p, "for") &&
|
||||
!is_word(p, "case"))
|
||||
return parse_simple(p);
|
||||
|
||||
// Compound command: parse it, then attach any trailing redirect list.
|
||||
node *n;
|
||||
if (curt(p) == T_LPAREN) n = parse_subshell(p);
|
||||
else if (is_word(p, "{")) n = parse_group(p);
|
||||
else if (is_word(p, "if")) n = parse_if(p);
|
||||
else if (is_word(p, "while") || is_word(p, "until")) n = parse_while(p);
|
||||
else if (is_word(p, "for")) n = parse_for(p);
|
||||
else if (is_word(p, "case")) n = parse_case(p);
|
||||
else n = parse_funcdef(p); // WORD '(' ')' body
|
||||
if (!n) return NULL;
|
||||
if (!parse_redir_list(p, n)) { sh_free_node(n); return NULL; }
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_pipeline(P *p)
|
||||
{
|
||||
// A leading `!` (as its own word, at command-word position) negates the
|
||||
// pipeline's exit status. `! ! x` toggles, matching bash/dash.
|
||||
int negated = 0;
|
||||
while (is_word(p, "!")) { negated = !negated; adv(p); skip_newlines(p); }
|
||||
|
||||
node *first = parse_command(p);
|
||||
if (!first) return NULL;
|
||||
if (curt(p) != T_BAR) { first->negated = negated; return first; }
|
||||
|
||||
node *n = new_node(N_PIPE);
|
||||
push_child(n, first);
|
||||
while (curt(p) == T_BAR) {
|
||||
adv(p);
|
||||
skip_newlines(p);
|
||||
node *c = parse_command(p);
|
||||
if (!c) { sh_free_node(n); return NULL; }
|
||||
push_child(n, c);
|
||||
}
|
||||
n->negated = negated;
|
||||
return n;
|
||||
}
|
||||
|
||||
static node *parse_and_or(P *p)
|
||||
{
|
||||
node *left = parse_pipeline(p);
|
||||
if (!left) return NULL;
|
||||
while (curt(p) == T_AMPAMP || curt(p) == T_BARBAR) {
|
||||
int op = curt(p);
|
||||
adv(p);
|
||||
skip_newlines(p);
|
||||
node *right = parse_pipeline(p);
|
||||
if (!right) { sh_free_node(left); return NULL; }
|
||||
node *n = new_node(N_ANDOR);
|
||||
n->left = left;
|
||||
n->right = right;
|
||||
n->andor_op = op;
|
||||
left = n;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
// A list runs until EOF or a reserved terminator word (then/else/elif/fi/do/done).
|
||||
static node *parse_list(P *p)
|
||||
{
|
||||
node *n = new_node(N_LIST);
|
||||
skip_seps(p);
|
||||
while (curt(p) != T_EOF && curt(p) != T_RPAREN && curt(p) != T_DSEMI &&
|
||||
!is_reserved(p)) {
|
||||
node *ao = parse_and_or(p);
|
||||
if (!ao) { sh_free_node(n); return NULL; }
|
||||
push_child(n, ao);
|
||||
// separators between and_or units
|
||||
if (curt(p) == T_SEMI || curt(p) == T_NEWLINE) {
|
||||
skip_seps(p);
|
||||
} else {
|
||||
break; // next token is EOF or a reserved word
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
node *sh_parse(sh_toklist *tl, const char **errmsg)
|
||||
{
|
||||
P p = { tl, 0, NULL };
|
||||
node *n = parse_list(&p);
|
||||
if (!n) { if (errmsg) *errmsg = p.err ? p.err : "parse error"; return NULL; }
|
||||
if (curt(&p) != T_EOF) {
|
||||
if (errmsg) *errmsg = "unexpected token";
|
||||
sh_free_node(n);
|
||||
return NULL;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void sh_free_node(node *n)
|
||||
{
|
||||
if (!n) return;
|
||||
for (int i = 0; i < n->nchild; i++) sh_free_node(n->children[i]);
|
||||
free(n->children);
|
||||
sh_free_node(n->left);
|
||||
sh_free_node(n->right);
|
||||
for (int i = 0; i < n->nassign; i++) free(n->assigns[i]);
|
||||
free(n->assigns);
|
||||
for (int i = 0; i < n->nword; i++) free(n->words[i]);
|
||||
free(n->words);
|
||||
for (int i = 0; i < n->nredir; i++) free(n->redirs[i].word);
|
||||
free(n->redirs);
|
||||
for (int i = 0; i < n->nclause; i++) { sh_free_node(n->conds[i]); sh_free_node(n->bodies[i]); }
|
||||
free(n->conds);
|
||||
free(n->bodies);
|
||||
sh_free_node(n->else_body);
|
||||
sh_free_node(n->cond);
|
||||
sh_free_node(n->body);
|
||||
free(n->for_name);
|
||||
for (int i = 0; i < n->for_nword; i++) free(n->for_words[i]);
|
||||
free(n->for_words);
|
||||
free(n->case_word);
|
||||
for (int i = 0; i < n->nclause_case; i++) {
|
||||
for (int j = 0; j < n->clauses[i].npat; j++) free(n->clauses[i].pats[j]);
|
||||
free(n->clauses[i].pats);
|
||||
sh_free_node(n->clauses[i].body);
|
||||
}
|
||||
free(n->clauses);
|
||||
free(n->func_name);
|
||||
free(n);
|
||||
}
|
||||
656
Tactility/Source/app/shell/shell/sh_parse.cpp
Normal file
656
Tactility/Source/app/shell/shell/sh_parse.cpp
Normal file
@ -0,0 +1,656 @@
|
||||
// Shell grammar parser: token list -> AST.
|
||||
//
|
||||
// A push-down parser: each grammar rule is a frame on an explicit stack, and a
|
||||
// rule that needs a nested rule (a compound command's body, a pipeline's next
|
||||
// command, ...) pushes it and resumes when it completes. Nesting depth is
|
||||
// bounded by SH_MAX_PARSE_FRAMES instead of the native stack.
|
||||
#include <Tactility/app/shell/shell/sh_parse.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#define SH_MAX_PARSE_FRAMES 1024
|
||||
|
||||
namespace {
|
||||
|
||||
struct NodeDeleter {
|
||||
void operator()(node* n) const { sh_free_node(n); }
|
||||
};
|
||||
using NodePtr = std::unique_ptr<node, NodeDeleter>;
|
||||
|
||||
node* new_node(node_kind k)
|
||||
{
|
||||
auto* n = static_cast<node*>(calloc(1, sizeof(node)));
|
||||
n->kind = k;
|
||||
return n;
|
||||
}
|
||||
|
||||
void push_child(node* n, node* c)
|
||||
{
|
||||
n->children = static_cast<node**>(realloc(n->children, (n->nchild + 1) * sizeof(node*)));
|
||||
n->children[n->nchild++] = c;
|
||||
}
|
||||
|
||||
void push_clause(node* n, node* cond, node* body)
|
||||
{
|
||||
n->conds = static_cast<node**>(realloc(n->conds, (n->nclause + 1) * sizeof(node*)));
|
||||
n->bodies = static_cast<node**>(realloc(n->bodies, (n->nclause + 1) * sizeof(node*)));
|
||||
n->conds[n->nclause] = cond;
|
||||
n->bodies[n->nclause] = body;
|
||||
n->nclause++;
|
||||
}
|
||||
|
||||
int is_redir_tok(tok_type t)
|
||||
{
|
||||
return t == T_LT || t == T_GT || t == T_GTGT || t == T_GTAMP ||
|
||||
t == T_LTAMP || t == T_CLOBBER || t == T_DLESS || t == T_DLESSDASH;
|
||||
}
|
||||
|
||||
sh_redir* push_redir(node* n)
|
||||
{
|
||||
n->redirs = static_cast<sh_redir*>(realloc(n->redirs, (n->nredir + 1) * sizeof(sh_redir)));
|
||||
sh_redir* r = &n->redirs[n->nredir++];
|
||||
r->kind = R_OUT; r->fd = 1; r->dupfd = 0; r->word = nullptr; r->heredoc_quoted = 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
int all_digits(const char* s)
|
||||
{
|
||||
if (!*s) return 0;
|
||||
for (; *s; s++) if (*s < '0' || *s > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int looks_like_assign(const char* w)
|
||||
{
|
||||
if (!(*w == '_' || (*w >= 'A' && *w <= 'Z') || (*w >= 'a' && *w <= 'z'))) return 0;
|
||||
const char* q = w + 1;
|
||||
while (*q && *q != '=') {
|
||||
if (!(*q == '_' || (*q >= 'A' && *q <= 'Z') || (*q >= 'a' && *q <= 'z') ||
|
||||
(*q >= '0' && *q <= '9'))) return 0;
|
||||
q++;
|
||||
}
|
||||
return *q == '=';
|
||||
}
|
||||
|
||||
class Parser;
|
||||
|
||||
enum class Step { Call, Return, Fail };
|
||||
|
||||
// One frame per grammar rule. step() is called on first entry with a null
|
||||
// `child`, and again after each nested rule it pushed has completed, with that
|
||||
// rule's result.
|
||||
struct ParseList { NodePtr n; bool started = false; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseAndOr { NodePtr left; int op = 0; enum { Start, Left, Right } phase = Start; Step step(Parser& p, NodePtr child); };
|
||||
struct ParsePipeline { NodePtr pipe; int negated = 0; enum { Start, First, Next } phase = Start; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseCommand { bool started = false; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseGroup { NodePtr n; bool subshell; bool started = false; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseFuncdef { NodePtr n; bool started = false; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseIf { NodePtr n; NodePtr cond; enum { Start, Cond, Body, Else } phase = Start; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseWhile { NodePtr n; enum { Start, Cond, Body } phase = Start; Step step(Parser& p, NodePtr child); };
|
||||
struct ParseFor { NodePtr n; bool started = false; Step step(Parser& p, NodePtr child); };
|
||||
|
||||
struct ParseCase {
|
||||
NodePtr n;
|
||||
std::vector<char*> pats; // patterns of the clause whose body is being parsed
|
||||
bool started = false;
|
||||
|
||||
ParseCase() = default;
|
||||
ParseCase(ParseCase&&) = default;
|
||||
~ParseCase() { for (char* pat : pats) free(pat); }
|
||||
|
||||
Step step(Parser& p, NodePtr child);
|
||||
};
|
||||
|
||||
using ParseFrame = std::variant<ParseList, ParseAndOr, ParsePipeline, ParseCommand, ParseGroup,
|
||||
ParseFuncdef, ParseIf, ParseWhile, ParseFor, ParseCase>;
|
||||
|
||||
class Parser {
|
||||
public:
|
||||
explicit Parser(sh_toklist* tl) : tl(tl) {}
|
||||
|
||||
sh_toklist* tl;
|
||||
int pos = 0;
|
||||
const char* err = nullptr;
|
||||
|
||||
sh_tok* cur() { return &tl->toks[pos]; }
|
||||
tok_type curt() { return tl->toks[pos].type; }
|
||||
tok_type peekt() { return tl->toks[pos].type == T_EOF ? T_EOF : tl->toks[pos + 1].type; }
|
||||
void adv() { if (tl->toks[pos].type != T_EOF) pos++; }
|
||||
|
||||
// Skip newlines/semicolons acting as blank separators.
|
||||
void skip_seps() { while (curt() == T_NEWLINE || curt() == T_SEMI) adv(); }
|
||||
void skip_newlines() { while (curt() == T_NEWLINE) adv(); }
|
||||
|
||||
// Is the current token a bare word equal to `kw`?
|
||||
bool is_word(const char* kw)
|
||||
{
|
||||
sh_tok* t = cur();
|
||||
return t->type == T_WORD && t->text && strcmp(t->text, kw) == 0;
|
||||
}
|
||||
|
||||
bool is_reserved()
|
||||
{
|
||||
static const char* kw[] = { "then", "else", "elif", "fi", "do", "done", "esac", "}", nullptr };
|
||||
if (curt() != T_WORD) return false;
|
||||
for (int i = 0; kw[i]; i++) if (is_word(kw[i])) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
Step fail(const char* message)
|
||||
{
|
||||
err = message;
|
||||
return Step::Fail;
|
||||
}
|
||||
|
||||
Step call(ParseFrame frame)
|
||||
{
|
||||
pending.emplace(std::move(frame));
|
||||
return Step::Call;
|
||||
}
|
||||
|
||||
Step finish(NodePtr n)
|
||||
{
|
||||
result = std::move(n);
|
||||
return Step::Return;
|
||||
}
|
||||
|
||||
node* run()
|
||||
{
|
||||
std::vector<ParseFrame> frames;
|
||||
frames.emplace_back(ParseList {});
|
||||
NodePtr child;
|
||||
while (!frames.empty()) {
|
||||
Step step = std::visit([&](auto& frame) { return frame.step(*this, std::move(child)); }, frames.back());
|
||||
switch (step) {
|
||||
case Step::Call:
|
||||
if (frames.size() >= SH_MAX_PARSE_FRAMES) {
|
||||
err = "nesting too deep";
|
||||
return nullptr;
|
||||
}
|
||||
frames.push_back(std::move(*pending));
|
||||
pending.reset();
|
||||
break;
|
||||
case Step::Return:
|
||||
frames.pop_back();
|
||||
child = std::move(result);
|
||||
break;
|
||||
case Step::Fail:
|
||||
return nullptr; // frames' partial nodes are freed as the stack unwinds
|
||||
}
|
||||
}
|
||||
return child.release();
|
||||
}
|
||||
|
||||
// Parse the redirection at the current token onto `n`. Returns 1 on success,
|
||||
// 0 on error (err set).
|
||||
int parse_redir(node* n)
|
||||
{
|
||||
tok_type t = curt();
|
||||
int fd = cur()->rfd;
|
||||
// No explicit fd digit (rfd == -1): default to stdin for input operators,
|
||||
// stdout for output operators.
|
||||
if (fd < 0) {
|
||||
fd = (t == T_LT || t == T_LTAMP || t == T_DLESS || t == T_DLESSDASH) ? 0 : 1;
|
||||
}
|
||||
|
||||
if (t == T_DLESS || t == T_DLESSDASH) {
|
||||
char* body = cur()->text ? strdup(cur()->text) : strdup("");
|
||||
int quoted = cur()->hd_quoted;
|
||||
adv();
|
||||
if (curt() != T_WORD) { free(body); err = "expected here-doc delimiter"; return 0; }
|
||||
sh_redir* r = push_redir(n);
|
||||
r->kind = R_HEREDOC; r->fd = 0; r->word = body; r->heredoc_quoted = quoted;
|
||||
adv();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (t == T_GTAMP || t == T_LTAMP) {
|
||||
adv();
|
||||
if (curt() != T_WORD) { err = "expected fd or filename after >&"; return 0; }
|
||||
const char* tgt = cur()->text;
|
||||
sh_redir* r = push_redir(n);
|
||||
r->fd = fd;
|
||||
if (all_digits(tgt)) { r->kind = R_DUP; r->dupfd = static_cast<int>(strtol(tgt, nullptr, 10)); }
|
||||
else if (strcmp(tgt, "-") == 0) { r->kind = R_CLOSE; }
|
||||
else { r->kind = (t == T_GTAMP) ? R_OUT : R_IN; r->word = strdup(tgt); }
|
||||
adv();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// T_LT / T_GT / T_GTGT / T_CLOBBER: filename target.
|
||||
redir_kind rk = (t == T_LT) ? R_IN : (t == T_GTGT) ? R_APPEND : R_OUT;
|
||||
adv();
|
||||
if (curt() != T_WORD) { err = "expected filename after redirection"; return 0; }
|
||||
sh_redir* r = push_redir(n);
|
||||
r->kind = rk; r->fd = fd; r->word = strdup(cur()->text);
|
||||
adv();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Consume any trailing redirections onto a compound command. Returns 1/0.
|
||||
int parse_redir_list(node* n)
|
||||
{
|
||||
while (is_redir_tok(curt())) {
|
||||
if (!parse_redir(n)) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
NodePtr parse_simple()
|
||||
{
|
||||
NodePtr n(new_node(N_SIMPLE));
|
||||
int seen_word = 0;
|
||||
|
||||
for (;;) {
|
||||
tok_type t = curt();
|
||||
if (t == T_WORD) {
|
||||
if (!seen_word && looks_like_assign(cur()->text)) {
|
||||
n->assigns = static_cast<char**>(realloc(n->assigns, (n->nassign + 1) * sizeof(char*)));
|
||||
n->assigns[n->nassign++] = strdup(cur()->text);
|
||||
adv();
|
||||
continue;
|
||||
}
|
||||
// Reserved words are only special in command position (first word);
|
||||
// as arguments they are ordinary words (e.g. `echo done`).
|
||||
if (!seen_word && is_reserved()) break;
|
||||
n->words = static_cast<char**>(realloc(n->words, (n->nword + 1) * sizeof(char*)));
|
||||
n->words[n->nword++] = strdup(cur()->text);
|
||||
seen_word = 1;
|
||||
adv();
|
||||
} else if (is_redir_tok(t)) {
|
||||
// Redirects may appear before, between, or after words and do not
|
||||
// count as the command word (so `FOO=x >f BAR=y cmd` still assigns).
|
||||
if (!parse_redir(n.get())) return nullptr;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n->nword == 0 && n->nassign == 0 && n->nredir == 0) {
|
||||
err = "expected command";
|
||||
return nullptr;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<ParseFrame> pending;
|
||||
NodePtr result;
|
||||
};
|
||||
|
||||
// A list runs until EOF or a reserved terminator word (then/else/elif/fi/do/done).
|
||||
Step ParseList::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (!started) {
|
||||
started = true;
|
||||
n.reset(new_node(N_LIST));
|
||||
p.skip_seps();
|
||||
} else {
|
||||
push_child(n.get(), child.release());
|
||||
// separators between and_or units
|
||||
if (p.curt() == T_SEMI || p.curt() == T_NEWLINE) {
|
||||
p.skip_seps();
|
||||
} else {
|
||||
return p.finish(std::move(n)); // next token is EOF or a reserved word
|
||||
}
|
||||
}
|
||||
if (p.curt() != T_EOF && p.curt() != T_RPAREN && p.curt() != T_DSEMI && !p.is_reserved()) {
|
||||
return p.call(ParseAndOr {});
|
||||
}
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
Step ParseAndOr::step(Parser& p, NodePtr child)
|
||||
{
|
||||
switch (phase) {
|
||||
case Start:
|
||||
phase = Left;
|
||||
return p.call(ParsePipeline {});
|
||||
case Left:
|
||||
left = std::move(child);
|
||||
break;
|
||||
case Right: {
|
||||
node* n = new_node(N_ANDOR);
|
||||
n->left = left.release();
|
||||
n->right = child.release();
|
||||
n->andor_op = op;
|
||||
left.reset(n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (p.curt() == T_AMPAMP || p.curt() == T_BARBAR) {
|
||||
op = p.curt();
|
||||
p.adv();
|
||||
p.skip_newlines();
|
||||
phase = Right;
|
||||
return p.call(ParsePipeline {});
|
||||
}
|
||||
return p.finish(std::move(left));
|
||||
}
|
||||
|
||||
Step ParsePipeline::step(Parser& p, NodePtr child)
|
||||
{
|
||||
switch (phase) {
|
||||
case Start:
|
||||
// A leading `!` (as its own word, at command-word position) negates the
|
||||
// pipeline's exit status. `! ! x` toggles, matching bash/dash.
|
||||
while (p.is_word("!")) { negated = !negated; p.adv(); p.skip_newlines(); }
|
||||
phase = First;
|
||||
return p.call(ParseCommand {});
|
||||
case First:
|
||||
if (p.curt() != T_BAR) {
|
||||
child->negated = negated;
|
||||
return p.finish(std::move(child));
|
||||
}
|
||||
pipe.reset(new_node(N_PIPE));
|
||||
push_child(pipe.get(), child.release());
|
||||
break;
|
||||
case Next:
|
||||
push_child(pipe.get(), child.release());
|
||||
break;
|
||||
}
|
||||
if (p.curt() == T_BAR) {
|
||||
p.adv();
|
||||
p.skip_newlines();
|
||||
phase = Next;
|
||||
return p.call(ParseCommand {});
|
||||
}
|
||||
pipe->negated = negated;
|
||||
return p.finish(std::move(pipe));
|
||||
}
|
||||
|
||||
Step ParseCommand::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (started) {
|
||||
// Compound command parsed: attach any trailing redirect list.
|
||||
if (!p.parse_redir_list(child.get())) return Step::Fail;
|
||||
return p.finish(std::move(child));
|
||||
}
|
||||
started = true;
|
||||
|
||||
// Simple commands handle their own (possibly interleaved) redirects.
|
||||
if (p.curt() != T_LPAREN && !p.is_word("{") &&
|
||||
!(p.curt() == T_WORD && !p.is_reserved() && p.peekt() == T_LPAREN) &&
|
||||
!p.is_word("if") && !p.is_word("while") && !p.is_word("until") &&
|
||||
!p.is_word("for") &&
|
||||
!p.is_word("case")) {
|
||||
NodePtr n = p.parse_simple();
|
||||
if (!n) return Step::Fail;
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
if (p.curt() == T_LPAREN) return p.call(ParseGroup { .subshell = true });
|
||||
if (p.is_word("{")) return p.call(ParseGroup { .subshell = false });
|
||||
if (p.is_word("if")) return p.call(ParseIf {});
|
||||
if (p.is_word("while") || p.is_word("until")) return p.call(ParseWhile {});
|
||||
if (p.is_word("for")) return p.call(ParseFor {});
|
||||
if (p.is_word("case")) return p.call(ParseCase {});
|
||||
return p.call(ParseFuncdef {}); // WORD '(' ')' body
|
||||
}
|
||||
|
||||
// { list; } or ( list )
|
||||
Step ParseGroup::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (!started) {
|
||||
started = true;
|
||||
p.adv(); // '{' or '('
|
||||
n.reset(new_node(N_GROUP));
|
||||
n->subshell = subshell ? 1 : 0;
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
n->body = child.release();
|
||||
if (subshell) {
|
||||
if (p.curt() != T_RPAREN) return p.fail("expected ')'");
|
||||
} else {
|
||||
if (!p.is_word("}")) return p.fail("expected '}'");
|
||||
}
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
Step ParseFuncdef::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (!started) {
|
||||
started = true;
|
||||
n.reset(new_node(N_FUNCDEF));
|
||||
n->func_name = strdup(p.cur()->text);
|
||||
p.adv(); // name
|
||||
p.adv(); // '('
|
||||
if (p.curt() != T_RPAREN) return p.fail("expected ')' in function definition");
|
||||
p.adv(); // ')'
|
||||
p.skip_newlines();
|
||||
return p.call(ParseCommand {});
|
||||
}
|
||||
n->body = child.release();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
Step ParseIf::step(Parser& p, NodePtr child)
|
||||
{
|
||||
switch (phase) {
|
||||
case Start:
|
||||
p.adv(); // 'if'
|
||||
n.reset(new_node(N_IF));
|
||||
phase = Cond;
|
||||
return p.call(ParseList {});
|
||||
case Cond:
|
||||
cond = std::move(child);
|
||||
if (!p.is_word("then")) return p.fail("expected 'then'");
|
||||
p.adv();
|
||||
phase = Body;
|
||||
return p.call(ParseList {});
|
||||
case Body:
|
||||
if (child->nchild == 0) return p.fail("empty then body");
|
||||
push_clause(n.get(), cond.release(), child.release());
|
||||
if (p.is_word("elif")) {
|
||||
p.adv();
|
||||
phase = Cond;
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
if (p.is_word("else")) {
|
||||
p.adv();
|
||||
phase = Else;
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
break;
|
||||
case Else:
|
||||
n->else_body = child.release();
|
||||
if (n->else_body->nchild == 0) return p.fail("empty else body");
|
||||
break;
|
||||
}
|
||||
if (!p.is_word("fi")) return p.fail("expected 'fi'");
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
Step ParseWhile::step(Parser& p, NodePtr child)
|
||||
{
|
||||
switch (phase) {
|
||||
case Start: {
|
||||
int until = p.is_word("until");
|
||||
p.adv(); // 'while' / 'until'
|
||||
n.reset(new_node(N_WHILE));
|
||||
n->until = until;
|
||||
phase = Cond;
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
case Cond:
|
||||
n->cond = child.release();
|
||||
if (!p.is_word("do")) return p.fail("expected 'do'");
|
||||
p.adv();
|
||||
phase = Body;
|
||||
return p.call(ParseList {});
|
||||
case Body:
|
||||
break;
|
||||
}
|
||||
n->body = child.release();
|
||||
if (n->body->nchild == 0) return p.fail("empty do/done body");
|
||||
if (!p.is_word("done")) return p.fail("expected 'done'");
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
// Collect the raw words after `for NAME in` up to a separator.
|
||||
Step ParseFor::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (started) {
|
||||
n->body = child.release();
|
||||
if (n->body->nchild == 0) return p.fail("empty do/done body");
|
||||
if (!p.is_word("done")) return p.fail("expected 'done'");
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
started = true;
|
||||
|
||||
p.adv(); // 'for'
|
||||
if (p.curt() != T_WORD) return p.fail("expected name after for");
|
||||
// The loop variable must be a valid name (dash rejects e.g. `for -`).
|
||||
const char* nm = p.cur()->text;
|
||||
if (!(nm[0] == '_' || (nm[0] >= 'a' && nm[0] <= 'z') || (nm[0] >= 'A' && nm[0] <= 'Z'))) {
|
||||
return p.fail("bad for loop variable");
|
||||
}
|
||||
for (const char* q = nm + 1; *q; q++) {
|
||||
if (!(*q == '_' || (*q >= '0' && *q <= '9') ||
|
||||
(*q >= 'a' && *q <= 'z') || (*q >= 'A' && *q <= 'Z'))) {
|
||||
return p.fail("bad for loop variable");
|
||||
}
|
||||
}
|
||||
n.reset(new_node(N_FOR));
|
||||
n->for_name = strdup(nm);
|
||||
p.adv();
|
||||
p.skip_newlines(); // `for i <newline> in ...` is legal (`for i; in` is not)
|
||||
if (p.is_word("in")) {
|
||||
p.adv();
|
||||
while (p.curt() == T_WORD && !p.is_reserved()) {
|
||||
n->for_words = static_cast<char**>(realloc(n->for_words, (n->for_nword + 1) * sizeof(char*)));
|
||||
n->for_words[n->for_nword++] = strdup(p.cur()->text);
|
||||
p.adv();
|
||||
}
|
||||
} else {
|
||||
// `for x; do ...` with no `in`: iterate over the positional params.
|
||||
n->for_implicit = 1;
|
||||
}
|
||||
p.skip_seps();
|
||||
if (!p.is_word("do")) return p.fail("expected 'do'");
|
||||
p.adv();
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
|
||||
// case WORD in [(] pat [| pat]... ) list ;; ... esac
|
||||
Step ParseCase::step(Parser& p, NodePtr child)
|
||||
{
|
||||
if (!started) {
|
||||
started = true;
|
||||
p.adv(); // 'case'
|
||||
if (p.curt() != T_WORD) return p.fail("expected word after case");
|
||||
n.reset(new_node(N_CASE));
|
||||
n->case_word = strdup(p.cur()->text);
|
||||
p.adv();
|
||||
if (!p.is_word("in")) return p.fail("expected 'in' after case word");
|
||||
p.adv();
|
||||
p.skip_seps();
|
||||
} else {
|
||||
sh_case_clause cl;
|
||||
cl.npat = static_cast<int>(pats.size());
|
||||
cl.pats = static_cast<char**>(malloc(pats.size() * sizeof(char*)));
|
||||
memcpy(cl.pats, pats.data(), pats.size() * sizeof(char*));
|
||||
pats.clear();
|
||||
cl.body = child.release();
|
||||
n->clauses = static_cast<sh_case_clause*>(realloc(n->clauses, (n->nclause_case + 1) * sizeof(sh_case_clause)));
|
||||
n->clauses[n->nclause_case++] = cl;
|
||||
if (p.curt() == T_DSEMI) {
|
||||
p.adv();
|
||||
p.skip_seps();
|
||||
} else {
|
||||
// last clause may omit ';;'
|
||||
if (!p.is_word("esac")) return p.fail("expected 'esac'");
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
}
|
||||
|
||||
if (p.is_word("esac") || p.curt() == T_EOF) {
|
||||
if (!p.is_word("esac")) return p.fail("expected 'esac'");
|
||||
p.adv();
|
||||
return p.finish(std::move(n));
|
||||
}
|
||||
|
||||
if (p.curt() == T_LPAREN) p.adv(); // optional leading '('
|
||||
for (;;) {
|
||||
if (p.curt() != T_WORD) return p.fail("expected case pattern");
|
||||
pats.push_back(strdup(p.cur()->text));
|
||||
p.adv();
|
||||
if (p.curt() == T_BAR) { p.adv(); continue; }
|
||||
break;
|
||||
}
|
||||
if (p.curt() != T_RPAREN) return p.fail("expected ')' in case");
|
||||
p.adv();
|
||||
return p.call(ParseList {});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
node* sh_parse(sh_toklist* tl, const char** errmsg)
|
||||
{
|
||||
Parser p(tl);
|
||||
node* n = p.run();
|
||||
if (!n) {
|
||||
if (errmsg) *errmsg = p.err ? p.err : "parse error";
|
||||
return nullptr;
|
||||
}
|
||||
if (p.curt() != T_EOF) {
|
||||
if (errmsg) *errmsg = "unexpected token";
|
||||
sh_free_node(n);
|
||||
return nullptr;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void sh_free_node(node* root)
|
||||
{
|
||||
// A worklist keeps freeing deeply nested trees off the native stack.
|
||||
std::vector<node*> pending;
|
||||
if (root) pending.push_back(root);
|
||||
while (!pending.empty()) {
|
||||
node* n = pending.back();
|
||||
pending.pop_back();
|
||||
auto later = [&pending](node* c) { if (c) pending.push_back(c); };
|
||||
|
||||
for (int i = 0; i < n->nchild; i++) later(n->children[i]);
|
||||
free(n->children);
|
||||
later(n->left);
|
||||
later(n->right);
|
||||
for (int i = 0; i < n->nassign; i++) free(n->assigns[i]);
|
||||
free(n->assigns);
|
||||
for (int i = 0; i < n->nword; i++) free(n->words[i]);
|
||||
free(n->words);
|
||||
for (int i = 0; i < n->nredir; i++) free(n->redirs[i].word);
|
||||
free(n->redirs);
|
||||
for (int i = 0; i < n->nclause; i++) { later(n->conds[i]); later(n->bodies[i]); }
|
||||
free(n->conds);
|
||||
free(n->bodies);
|
||||
later(n->else_body);
|
||||
later(n->cond);
|
||||
later(n->body);
|
||||
free(n->for_name);
|
||||
for (int i = 0; i < n->for_nword; i++) free(n->for_words[i]);
|
||||
free(n->for_words);
|
||||
free(n->case_word);
|
||||
for (int i = 0; i < n->nclause_case; i++) {
|
||||
for (int j = 0; j < n->clauses[i].npat; j++) free(n->clauses[i].pats[j]);
|
||||
free(n->clauses[i].pats);
|
||||
later(n->clauses[i].body);
|
||||
}
|
||||
free(n->clauses);
|
||||
free(n->func_name);
|
||||
free(n);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -22,12 +22,23 @@ namespace {
|
||||
|
||||
constexpr auto* TAG = "terminal";
|
||||
|
||||
// How often the pump loop below checks for new keyboard input while draining the shell app.
|
||||
// The pump loop wakes on keyboard input and shell output. This bound only matters for noticing a
|
||||
// stop request, or for polling input if no event bit could be claimed for it.
|
||||
constexpr uint32_t SHELL_PUMP_INTERVAL_MS = 50;
|
||||
|
||||
struct InputSignal {
|
||||
TaskEventGroup* group;
|
||||
uint32_t bit;
|
||||
};
|
||||
|
||||
void signalInput(void* context) {
|
||||
auto* signal = static_cast<InputSignal*>(context);
|
||||
task_event_group_signal(signal->group, signal->bit);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void runShell(int columns, int rows, volatile bool* stopRequested) {
|
||||
void runShell(int columns, int rows, volatile bool* stopRequested, TaskHandle_t renderTask) {
|
||||
static uint8_t stdinBuffer[256];
|
||||
static uint8_t stdoutBuffer[1024];
|
||||
AppStream stdinStream {};
|
||||
@ -65,20 +76,36 @@ void runShell(int columns, int rows, volatile bool* stopRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keyboard input wakes the loop below through the same event group as the shell's output.
|
||||
InputSignal inputSignal { &eventGroup, 0 };
|
||||
const bool inputSignalled = task_event_group_claim_bit(&eventGroup, &inputSignal.bit) == ERROR_NONE;
|
||||
if (inputSignalled) {
|
||||
vterm_set_input_callback(signalInput, &inputSignal);
|
||||
}
|
||||
|
||||
bool shellStdinClosed = false;
|
||||
bool shellDone = false;
|
||||
uint8_t drain[256];
|
||||
|
||||
while (!shellDone) {
|
||||
const int c = vterm_getchar(vterm_get_active(), pdMS_TO_TICKS(SHELL_PUMP_INTERVAL_MS));
|
||||
if (c >= 0 && !shellStdinClosed) {
|
||||
const char ch = static_cast<char>(c);
|
||||
app_stream_write(&stdinStream, &ch, 1);
|
||||
task_event_group_wait_any(&eventGroup, nullptr, pdMS_TO_TICKS(SHELL_PUMP_INTERVAL_MS));
|
||||
|
||||
int c;
|
||||
while ((c = vterm_getchar(vterm_get_active(), 0)) >= 0) {
|
||||
if (!shellStdinClosed) {
|
||||
const char ch = static_cast<char>(c);
|
||||
app_stream_write(&stdinStream, &ch, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool wroteOutput = false;
|
||||
size_t n;
|
||||
while ((n = app_stream_read(&stdoutStream, drain, sizeof(drain))) > 0) {
|
||||
vterm_write_translated(reinterpret_cast<const char*>(drain), n);
|
||||
wroteOutput = true;
|
||||
}
|
||||
if (wroteOutput) {
|
||||
xTaskNotifyGive(renderTask);
|
||||
}
|
||||
|
||||
// The shell app never sees stopRequested directly: its stdin is closed instead, which
|
||||
@ -101,6 +128,12 @@ void runShell(int columns, int rows, volatile bool* stopRequested) {
|
||||
while ((n = app_stream_read(&stdoutStream, drain, sizeof(drain))) > 0) {
|
||||
vterm_write_translated(reinterpret_cast<const char*>(drain), n);
|
||||
}
|
||||
xTaskNotifyGive(renderTask);
|
||||
|
||||
if (inputSignalled) {
|
||||
vterm_set_input_callback(nullptr, nullptr);
|
||||
task_event_group_release_bit(&eventGroup, inputSignal.bit);
|
||||
}
|
||||
|
||||
// Only app_stream_unsubscribe() guarantees the fd-table binding is gone and no AppFileOps call
|
||||
// is still in flight, which is what makes these stack-local AppStreams safe to let go out of
|
||||
|
||||
@ -16,6 +16,8 @@
|
||||
#include <tactility/freertos/semphr.h>
|
||||
#include <tactility/freertos/task.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <esp_log.h>
|
||||
#endif
|
||||
@ -26,9 +28,9 @@ extern "C" {
|
||||
|
||||
constexpr auto* TAG = "terminal";
|
||||
|
||||
// Redraw cadence. A text grid only changes when something is written, so this is a polling
|
||||
// interval rather than a frame rate.
|
||||
constexpr uint32_t RENDER_INTERVAL_MS = 33;
|
||||
// Keyboard polling and redraw cadence. Shell output wakes the I/O task early, so this mostly bounds
|
||||
// how long a keypress waits to be picked up.
|
||||
constexpr uint32_t RENDER_INTERVAL_MS = 16;
|
||||
|
||||
// The I/O task polls the keyboards, paints the screen, and watches for the touch-to-exit gesture.
|
||||
// It runs a step above this app's own task: input, drawing and the exit gesture must keep working
|
||||
@ -48,6 +50,10 @@ namespace {
|
||||
* pump loop, which closes the shell app's stdin to unstick it (see that loop's own comment). */
|
||||
volatile bool stopRequested = false;
|
||||
|
||||
/** Set once runShell() has returned. The I/O task keeps running until then, because runShell()
|
||||
* notifies it about output until the very end. */
|
||||
std::atomic<bool> shellFinished = false;
|
||||
|
||||
/**
|
||||
* Translates a kernel keyboard event into the byte a terminal expects.
|
||||
* Returns 0 for keys with no terminal representation.
|
||||
@ -153,20 +159,24 @@ struct IoTaskParams {
|
||||
void ioTask(void* arg) {
|
||||
auto* params = static_cast<IoTaskParams*>(arg);
|
||||
|
||||
while (!stopRequested) {
|
||||
while (!shellFinished) {
|
||||
// A scroll replaces every row at once, so the renderer is told to repaint rather than rely
|
||||
// on its per-cell comparison.
|
||||
const bool viewMoved = params->keyboards->pump(handleKey);
|
||||
params->renderer->render(viewMoved);
|
||||
|
||||
if (params->touch->touched(!params->keyboards->empty())) {
|
||||
if (!stopRequested && params->touch->touched(!params->keyboards->empty())) {
|
||||
LOG_I(TAG, "Touch detected - stopping");
|
||||
stopRequested = true;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(RENDER_INTERVAL_MS));
|
||||
// runShell() notifies this task when output arrives, so it is drawn without waiting out the interval
|
||||
ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(RENDER_INTERVAL_MS));
|
||||
}
|
||||
|
||||
// The shell's last output can arrive after the final pass above, so draw once more
|
||||
params->renderer->render(false);
|
||||
|
||||
xSemaphoreGive(params->doneSem);
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
@ -175,6 +185,7 @@ void ioTask(void* arg) {
|
||||
|
||||
void runTerminal(Device* display) {
|
||||
stopRequested = false;
|
||||
shellFinished = false;
|
||||
|
||||
KeyboardInput keyboards;
|
||||
TouchInput touch;
|
||||
@ -227,7 +238,9 @@ void runTerminal(Device* display) {
|
||||
#ifdef ESP_PLATFORM
|
||||
esp_log_level_set("ELF", ESP_LOG_WARN);
|
||||
#endif
|
||||
runShell(renderer.columns(), renderer.rows(), &stopRequested);
|
||||
runShell(renderer.columns(), renderer.rows(), &stopRequested, ioHandle);
|
||||
shellFinished = true;
|
||||
xTaskNotifyGive(ioHandle);
|
||||
#ifdef ESP_PLATFORM
|
||||
esp_log_level_set("ELF", ESP_LOG_INFO);
|
||||
#endif
|
||||
|
||||
@ -81,6 +81,10 @@ void vterm_set_scroll_callback(void (*callback)(void)) {
|
||||
}
|
||||
|
||||
static void (*s_on_switch_cb)(int new_vt) = NULL;
|
||||
static void (*s_on_input_cb)(void *context) = NULL;
|
||||
static void *s_on_input_context = NULL;
|
||||
// Held while the input callback is replaced or invoked, so replacing it waits for a call in progress.
|
||||
static struct Mutex s_input_cb_mux;
|
||||
|
||||
/*
|
||||
* How many rows the writing task should treat as the bottom of the screen.
|
||||
@ -594,6 +598,7 @@ static struct Mutex s_input_mux;
|
||||
error_t vterm_init(void)
|
||||
{
|
||||
mutex_construct(&s_input_mux);
|
||||
mutex_construct(&s_input_cb_mux);
|
||||
|
||||
const struct MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
|
||||
|
||||
@ -683,9 +688,12 @@ void vterm_deinit(void)
|
||||
s_dirty = true;
|
||||
s_scroll_callback = NULL;
|
||||
s_on_switch_cb = NULL;
|
||||
s_on_input_cb = NULL;
|
||||
s_on_input_context = NULL;
|
||||
|
||||
vterm_clear_size_override();
|
||||
mutex_destruct(&s_input_mux);
|
||||
mutex_destruct(&s_input_cb_mux);
|
||||
}
|
||||
|
||||
vterm_cell_t *vterm_get_direct_buffer(void)
|
||||
@ -847,6 +855,12 @@ void vterm_write_translated(const char *data, size_t size)
|
||||
|
||||
// Helpers
|
||||
void vterm_set_switch_callback(void (*cb)(int)) { s_on_switch_cb = cb; }
|
||||
void vterm_set_input_callback(void (*cb)(void *), void *context) {
|
||||
mutex_lock(&s_input_cb_mux);
|
||||
s_on_input_cb = cb;
|
||||
s_on_input_context = context;
|
||||
mutex_unlock(&s_input_cb_mux);
|
||||
}
|
||||
int vterm_get_active(void) { return s_active_vt; }
|
||||
// Per-task size override (for SSH sessions with different terminal dimensions)
|
||||
static TaskHandle_t s_size_override_task = NULL;
|
||||
@ -898,7 +912,11 @@ int vterm_getchar(int vt_id, int timeout_ms) {
|
||||
}
|
||||
|
||||
void vterm_send_input(int vt_id, char c) {
|
||||
if (vt_id >= 0 && vt_id < VTERM_COUNT) xQueueSend(s_vterms[vt_id].input_queue, &c, 0);
|
||||
if (vt_id < 0 || vt_id >= VTERM_COUNT) return;
|
||||
xQueueSend(s_vterms[vt_id].input_queue, &c, 0);
|
||||
mutex_lock(&s_input_cb_mux);
|
||||
if (s_on_input_cb) s_on_input_cb(s_on_input_context);
|
||||
mutex_unlock(&s_input_cb_mux);
|
||||
}
|
||||
|
||||
void vterm_input_flush(int vt_id) {
|
||||
|
||||
256
Tactility/Tests/Scripts/shell_test.sh
Normal file
256
Tactility/Tests/Scripts/shell_test.sh
Normal file
@ -0,0 +1,256 @@
|
||||
# Shell interpreter self-test.
|
||||
#
|
||||
# Copy it to the device (e.g. the SD card) and run it from the Terminal:
|
||||
# sh /sdcard/shell_test.sh
|
||||
# It prints a FAIL line per failed check and a summary, and exits with 1 when anything failed.
|
||||
# Temp files are written next to the script.
|
||||
#
|
||||
# Command substitution nests only 3 deep here: each level keeps a temp file open, and ESP32
|
||||
# filesystems allow 4 open files at a time.
|
||||
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
# check <description> <expected> <actual>
|
||||
check() {
|
||||
if [ "$2" = "$3" ]; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
echo "FAIL: $1 (expected '$2', got '$3')"
|
||||
fi
|
||||
}
|
||||
|
||||
case $0 in
|
||||
*/*) dir=${0%/*} ;;
|
||||
*) dir=. ;;
|
||||
esac
|
||||
|
||||
echo "Shell test: writing temp files to $dir"
|
||||
|
||||
# --- Variables and quoting ---
|
||||
|
||||
x='a b'
|
||||
check "unquoted splitting" "a b" "$(echo $x)"
|
||||
check "double quotes keep spaces" "a b" "$(echo "$x")"
|
||||
check "single quotes are literal" '$x' "$(echo '$x')"
|
||||
check "backslash escape" '$x' "$(echo \$x)"
|
||||
false
|
||||
check "exit status" 1 "$?"
|
||||
|
||||
set -- one "two words" three
|
||||
check "positional count" 3 "$#"
|
||||
check "positional parameter" "two words" "$2"
|
||||
n=0
|
||||
for p in "$@"; do n=$((n + 1)); done
|
||||
check "quoted \$@" 3 "$n"
|
||||
|
||||
IFS=:
|
||||
set -- $(echo a:b:c)
|
||||
IFS=' '
|
||||
check "custom IFS" 3 "$#"
|
||||
|
||||
# --- Parameter expansion ---
|
||||
|
||||
path=a/b/c.tar.gz
|
||||
check "default" def "${unset_var:-def}"
|
||||
check "assign default" set "${assigned:=set}"
|
||||
check "assigned variable" set "$assigned"
|
||||
check "alternate" alt "${path:+alt}"
|
||||
check "length" 12 "${#path}"
|
||||
check "shortest prefix" b/c.tar.gz "${path#*/}"
|
||||
check "longest prefix" c.tar.gz "${path##*/}"
|
||||
check "shortest suffix" a/b/c.tar "${path%.*}"
|
||||
check "longest suffix" a/b/c "${path%%.*}"
|
||||
check "nested default" deep "${unset1:-${unset2:-deep}}"
|
||||
lazy=set
|
||||
check "lazy default word" set "${lazy:-$(ran=yes; echo no)}"
|
||||
check "lazy word not run" "" "$ran"
|
||||
|
||||
# --- Arithmetic ---
|
||||
|
||||
a=5
|
||||
expr='a+1'
|
||||
check "precedence" 7 "$((1 + 2 * 3))"
|
||||
check "parentheses" 9 "$(( (1 + 2) * 3 ))"
|
||||
check "division and modulo" 3/1 "$((10 / 3))/$((10 % 3))"
|
||||
check "unary minus" -5 "$((-3 + -2))"
|
||||
check "variables" 10 "$((a * 2))"
|
||||
check "compound assignment" 6 "$((a += 1))"
|
||||
check "expression-valued variable" 14 "$((expr * 2))"
|
||||
check "chained assignment" 4 "$((p = q = 4))"
|
||||
check "hex and octal" 16/8 "$((0x10))/$((010))"
|
||||
check "comparison and logic" 1 "$((1 < 2 && 3 != 4))"
|
||||
|
||||
# --- Control flow ---
|
||||
|
||||
r=
|
||||
for v in 1 2 3; do
|
||||
if [ $v = 1 ]; then r="${r}one"; elif [ $v = 2 ]; then r="${r}two"; else r="${r}other"; fi
|
||||
done
|
||||
check "if/elif/else" onetwoother "$r"
|
||||
|
||||
i=0
|
||||
while [ $i -lt 1000 ]; do i=$((i + 1)); done
|
||||
check "1000-iteration while loop" 1000 "$i"
|
||||
|
||||
j=0
|
||||
until [ $j -ge 3 ]; do j=$((j + 1)); done
|
||||
check "until loop" 3 "$j"
|
||||
|
||||
s=
|
||||
for i in 1 2 3 4; do
|
||||
if [ $i = 2 ]; then continue; fi
|
||||
if [ $i = 4 ]; then break; fi
|
||||
s="$s$i"
|
||||
done
|
||||
check "break and continue" 13 "$s"
|
||||
|
||||
t=
|
||||
for a in 1 2; do for b in 1 2; do if [ $b = 2 ]; then break 2; fi; t="$t$a$b"; done; done
|
||||
check "break 2" 11 "$t"
|
||||
|
||||
r=
|
||||
for f in a.txt b.c x; do
|
||||
case $f in
|
||||
*.txt) r="${r}T" ;;
|
||||
a|b.c) r="${r}C" ;;
|
||||
[xy]) r="${r}X" ;;
|
||||
*) r="${r}?" ;;
|
||||
esac
|
||||
done
|
||||
check "case patterns" TCX "$r"
|
||||
|
||||
false && x1=1 || x2=2
|
||||
check "and-or lists" "/2" "$x1/$x2"
|
||||
|
||||
# --- Functions ---
|
||||
|
||||
greet() {
|
||||
g="hello $1"
|
||||
return 4
|
||||
}
|
||||
greet world
|
||||
check "function return status" 4 "$?"
|
||||
check "function arguments" "hello world" "$g"
|
||||
|
||||
outer=outer
|
||||
scoped() {
|
||||
local outer=inner
|
||||
seen=$outer
|
||||
}
|
||||
scoped
|
||||
check "local inside function" inner "$seen"
|
||||
check "local restored" outer "$outer"
|
||||
|
||||
fact() {
|
||||
if [ $1 -le 1 ]; then f=1; else fact $(($1 - 1)); f=$((f * $1)); fi
|
||||
}
|
||||
fact 6
|
||||
check "recursive factorial" 720 "$f"
|
||||
|
||||
# --- Subshells, command substitution, eval ---
|
||||
|
||||
y=1
|
||||
(y=2)
|
||||
check "subshell isolation" 1 "$y"
|
||||
(exit 3)
|
||||
check "subshell exit status" 3 "$?"
|
||||
{ y=4; }
|
||||
check "brace group" 4 "$y"
|
||||
|
||||
check "nested command substitution" deep "$(echo $(echo $(echo deep)))"
|
||||
check "trailing newlines stripped" a "$(echo a; echo; echo)"
|
||||
check "backticks" tick "`echo tick`"
|
||||
eval 'e=5'
|
||||
check "eval" 5 "$e"
|
||||
|
||||
# --- Redirection, here-documents, pipelines ---
|
||||
|
||||
tmp="$dir/shell_test.tmp"
|
||||
echo one > "$tmp"
|
||||
echo two >> "$tmp"
|
||||
{ read l1; read l2; } < "$tmp"
|
||||
check "redirect and append" one/two "$l1/$l2"
|
||||
check "stderr to stdout" "out err" "$( { echo out; echo err >&2; } 2>&1 | { read o; read e; echo $o $e; } )"
|
||||
|
||||
hd=w
|
||||
read h1 <<EOF
|
||||
hello $hd
|
||||
EOF
|
||||
read h2 <<'EOF'
|
||||
hello $hd
|
||||
EOF
|
||||
check "here-document" "hello w" "$h1"
|
||||
check "quoted here-document" 'hello $hd' "$h2"
|
||||
|
||||
lines=
|
||||
while read l; do lines="$lines[$l]"; done <<EOF
|
||||
a
|
||||
b
|
||||
EOF
|
||||
check "while-read from here-document" "[a][b]" "$lines"
|
||||
check "pipeline of builtins" "b c" "$(echo a b c | { read p q; echo $q; })"
|
||||
|
||||
echo 'sourced=9; first=$1' > "$tmp"
|
||||
. "$tmp" arg
|
||||
check "source with arguments" 9/arg "$sourced/$first"
|
||||
|
||||
# --- test builtin ---
|
||||
|
||||
[ ! -z x ] && [ \( a = a \) -a \( b = c -o d = d \) ]
|
||||
check "test grouping and negation" 0 "$?"
|
||||
test -f "$tmp"
|
||||
check "test -f" 0 "$?"
|
||||
test -d "$tmp"
|
||||
check "test -d" 1 "$?"
|
||||
|
||||
# --- Deep nesting (the interpreter must not run out of stack) ---
|
||||
|
||||
depth() {
|
||||
if [ $1 -lt 50 ]; then depth $(($1 + 1)); else deepest=$1; fi
|
||||
}
|
||||
depth 0
|
||||
check "50 nested function calls" 50 "$deepest"
|
||||
|
||||
open=; close=; i=0
|
||||
while [ $i -lt 100 ]; do open="$open{ "; close="$close} "; i=$((i + 1)); done
|
||||
eval "${open}r=groups; ${close}"
|
||||
check "100 nested brace groups" groups "$r"
|
||||
|
||||
open=; close=; i=0
|
||||
while [ $i -lt 100 ]; do open="$open\${u:-"; close="$close}"; i=$((i + 1)); done
|
||||
eval "r=${open}braces${close}"
|
||||
check "100 nested \${...}" braces "$r"
|
||||
|
||||
open=; close=; i=0
|
||||
while [ $i -lt 100 ]; do open="$open("; close="$close)"; i=$((i + 1)); done
|
||||
eval "r=\$(( ${open}1 + 1${close} ))"
|
||||
check "100 nested arithmetic parentheses" 2 "$r"
|
||||
|
||||
# --- Scripts run as their own sh instance ---
|
||||
|
||||
echo 'exit 7' > "$tmp"
|
||||
sh "$tmp"
|
||||
check "sh script exit status" 7 "$?"
|
||||
|
||||
open=; close=; i=0
|
||||
while [ $i -lt 300 ]; do open="$open{ "; close="$close} "; i=$((i + 1)); done
|
||||
echo "${open}x=1; ${close}" > "$tmp"
|
||||
sh "$tmp"
|
||||
check "excessive nesting is reported, not a crash" 2 "$?"
|
||||
|
||||
echo 'x=$((1 / 0)); echo unreachable' > "$tmp"
|
||||
check "arithmetic error aborts the script" "" "$(sh "$tmp")"
|
||||
|
||||
echo 'set -e; false; echo unreachable' > "$tmp"
|
||||
check "set -e" "" "$(sh "$tmp")"
|
||||
|
||||
rm "$tmp"
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
echo "passed: $pass, failed: $fail"
|
||||
if [ $fail -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@ -4,6 +4,14 @@
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
||||
// app-module overrides chdir() with an app-aware version, which needs the kernel to be running
|
||||
extern "C" int __real_chdir(const char* path);
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/dts.h>
|
||||
#include <tactility/kernel_init.h>
|
||||
@ -39,6 +47,15 @@ void test_task(void* parameter) {
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// The POSIX platform registers its "data" filesystem relative to the working directory, and
|
||||
// tests that need temp files (e.g. the shell interpreter) require it. Running in a scratch
|
||||
// directory makes that independent of where the binary is started from.
|
||||
char scratch[] = "/tmp/tactility-tests-XXXXXX";
|
||||
if (mkdtemp(scratch) == nullptr || __real_chdir(scratch) != 0 || mkdir("data", 0755) != 0) {
|
||||
perror("Failed to set up the test working directory");
|
||||
return 1;
|
||||
}
|
||||
|
||||
TestTaskData data = {
|
||||
.argc = argc,
|
||||
.argv = argv,
|
||||
@ -60,5 +77,8 @@ int main(int argc, char** argv) {
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(scratch, ignored);
|
||||
|
||||
return data.result;
|
||||
}
|
||||
|
||||
52
Tactility/Tests/Source/ShellCompletionTest.cpp
Normal file
52
Tactility/Tests/Source/ShellCompletionTest.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <Tactility/app/shell/Shell.h>
|
||||
|
||||
#include <tactility/filesystem/fs.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::string complete(const std::string& line) {
|
||||
char suffix[FILE_MAX_PATH_STRING_LENGTH];
|
||||
bool listed = false;
|
||||
if (!Shell::complete(line.c_str(), suffix, sizeof(suffix), &listed)) {
|
||||
return "<none>";
|
||||
}
|
||||
return suffix;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("shell completion: first word completes directories as well as commands") {
|
||||
char temp[FILE_MAX_PATH_STRING_LENGTH];
|
||||
REQUIRE_EQ(paths_get_temp_path(temp, sizeof(temp)), ERROR_NONE);
|
||||
const std::string base = std::string(temp) + "/completiontest";
|
||||
REQUIRE_EQ(directory_make((base + "/appfolder").c_str(), true), ERROR_NONE);
|
||||
|
||||
// A path typed as the command completes like any other path
|
||||
CHECK_EQ(complete(base + "/app"), "folder/");
|
||||
CHECK_EQ(complete("ls " + base + "/app"), "folder/");
|
||||
// A relative first word completes against the working directory, which is "/" without an app instance
|
||||
CHECK_EQ(complete("./tm"), "p/");
|
||||
CHECK_EQ(complete("tm"), "p/");
|
||||
CHECK_EQ(complete("./nothing"), "<none>");
|
||||
}
|
||||
|
||||
TEST_CASE("shell completion: plain files complete when the word names a path") {
|
||||
char temp[FILE_MAX_PATH_STRING_LENGTH];
|
||||
REQUIRE_EQ(paths_get_temp_path(temp, sizeof(temp)), ERROR_NONE);
|
||||
const std::string base = std::string(temp) + "/completiontest";
|
||||
REQUIRE_EQ(directory_make(base.c_str(), true), ERROR_NONE);
|
||||
FILE* file = fopen((base + "/runme.sh").c_str(), "w");
|
||||
REQUIRE_NE(file, nullptr);
|
||||
fclose(file);
|
||||
|
||||
// With a '/', the file can be run, so it completes
|
||||
CHECK_EQ(complete(base + "/run"), "me.sh ");
|
||||
// As an argument it completes too
|
||||
CHECK_EQ(complete("cat " + base + "/run"), "me.sh ");
|
||||
}
|
||||
602
Tactility/Tests/Source/ShellInterpreterTest.cpp
Normal file
602
Tactility/Tests/Source/ShellInterpreterTest.cpp
Normal file
@ -0,0 +1,602 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <tactility/paths.h>
|
||||
|
||||
extern "C" {
|
||||
#include <Tactility/app/shell/shell/sh.h>
|
||||
}
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
|
||||
namespace {
|
||||
|
||||
void makeDirectories(const std::string& path) {
|
||||
for (size_t i = 1; i <= path.size(); i++) {
|
||||
if (i == path.size() || path[i] == '/') {
|
||||
mkdir(path.substr(0, i).c_str(), 0755);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A fresh interpreter state per test, with $TMP pointing at a writable scratch directory. */
|
||||
class Shell {
|
||||
public:
|
||||
sh_state state {};
|
||||
std::string tmp;
|
||||
|
||||
Shell() {
|
||||
char path[FILE_MAX_PATH_STRING_LENGTH];
|
||||
REQUIRE_EQ(paths_get_temp_path(path, sizeof(path)), ERROR_NONE);
|
||||
tmp = std::string(path) + "/shelltest";
|
||||
makeDirectories(tmp);
|
||||
sh_state_init(&state);
|
||||
sh_set(&state, "TMP", tmp.c_str());
|
||||
}
|
||||
|
||||
~Shell() {
|
||||
sh_state_free(&state);
|
||||
}
|
||||
|
||||
int run(const char* source) {
|
||||
return sh_run_string(&state, source);
|
||||
}
|
||||
|
||||
std::string var(const char* name) {
|
||||
const char* value = sh_get(&state, name);
|
||||
return value != nullptr ? value : "<unset>";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// region Variables and quoting
|
||||
|
||||
TEST_CASE("shell: assignment and command substitution") {
|
||||
Shell sh;
|
||||
CHECK_EQ(sh.run("x=1; y=$(echo hi)"), 0);
|
||||
CHECK_EQ(sh.var("x"), "1");
|
||||
CHECK_EQ(sh.var("y"), "hi");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: exit status in $?") {
|
||||
Shell sh;
|
||||
sh.run("false; a=$?; true; b=$?");
|
||||
CHECK_EQ(sh.var("a"), "1");
|
||||
CHECK_EQ(sh.var("b"), "0");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: quoting and word splitting") {
|
||||
Shell sh;
|
||||
sh.run("x='a b'; unquoted=$(echo $x); quoted=$(echo \"$x\"); single='$x'; escaped=\\$x");
|
||||
CHECK_EQ(sh.var("unquoted"), "a b");
|
||||
CHECK_EQ(sh.var("quoted"), "a b");
|
||||
CHECK_EQ(sh.var("single"), "$x");
|
||||
CHECK_EQ(sh.var("escaped"), "$x");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: quoted $@ without parameters") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"set --\n"
|
||||
"n=0; for a in \"$@\"; do n=$((n+1)); done\n"
|
||||
"p=0; for a in x\"$@\"; do p=$((p+1)); v=$a; done\n"
|
||||
"s=0; for a in \"$*\"; do s=$((s+1)); done"
|
||||
);
|
||||
CHECK_EQ(sh.var("n"), "0");
|
||||
CHECK_EQ(sh.var("p"), "1");
|
||||
CHECK_EQ(sh.var("v"), "x");
|
||||
CHECK_EQ(sh.var("s"), "1");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: IFS splitting") {
|
||||
Shell sh;
|
||||
sh.run("IFS=:; set -- $(echo a:b:c); n=$#; second=$2");
|
||||
CHECK_EQ(sh.var("n"), "3");
|
||||
CHECK_EQ(sh.var("second"), "b");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: positional parameters") {
|
||||
Shell sh;
|
||||
sh.run("set -- a 'b c' d; n=$#; second=$2; all=\"$*\"; count=0; for p in \"$@\"; do count=$((count+1)); done");
|
||||
CHECK_EQ(sh.var("n"), "3");
|
||||
CHECK_EQ(sh.var("second"), "b c");
|
||||
CHECK_EQ(sh.var("all"), "a b c d");
|
||||
CHECK_EQ(sh.var("count"), "3");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: parameter expansion operators") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"p=a/b/c; empty=\n"
|
||||
"d1=${unset:-def}; d2=${unset-def}; d3=${empty:-def}; d4=${empty-def}\n"
|
||||
": ${assigned:=set}\n"
|
||||
"alt=${p:+alt}; noalt=${unset:+alt}; len=${#p}\n"
|
||||
"s1=${p#*/}; s2=${p##*/}; s3=${p%/*}; s4=${p%%/*}\n"
|
||||
"nested=${unset:-${unset2:-deep}}"
|
||||
);
|
||||
CHECK_EQ(sh.var("d1"), "def");
|
||||
CHECK_EQ(sh.var("d2"), "def");
|
||||
CHECK_EQ(sh.var("d3"), "def");
|
||||
CHECK_EQ(sh.var("d4"), "");
|
||||
CHECK_EQ(sh.var("assigned"), "set");
|
||||
CHECK_EQ(sh.var("alt"), "alt");
|
||||
CHECK_EQ(sh.var("noalt"), "");
|
||||
CHECK_EQ(sh.var("len"), "5");
|
||||
CHECK_EQ(sh.var("s1"), "b/c");
|
||||
CHECK_EQ(sh.var("s2"), "c");
|
||||
CHECK_EQ(sh.var("s3"), "a/b");
|
||||
CHECK_EQ(sh.var("s4"), "a");
|
||||
CHECK_EQ(sh.var("nested"), "deep");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: default word is only expanded when needed") {
|
||||
Shell sh;
|
||||
sh.run("x=set; y=${x:-$(ran=yes; echo v)}");
|
||||
CHECK_EQ(sh.var("y"), "set");
|
||||
CHECK_EQ(sh.var("ran"), "<unset>");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: unset and set -u") {
|
||||
Shell sh;
|
||||
sh.run("x=1; unset x");
|
||||
CHECK_EQ(sh.var("x"), "<unset>");
|
||||
|
||||
Shell strict;
|
||||
CHECK_EQ(strict.run("set -u; y=$undefined_variable; after=1"), 2);
|
||||
CHECK_EQ(strict.var("after"), "<unset>");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Arithmetic
|
||||
|
||||
TEST_CASE("shell: arithmetic") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"a=5; c='a+1'\n"
|
||||
"r1=$((1+2*3)); r2=$(( (1+2)*3 )); r3=$((10/3)); r4=$((10%3)); r5=$((-3 + -2))\n"
|
||||
"r6=$((a*2)); r7=$((a+=1)); r8=$((1<2)); r9=$((1==2 || 3)); r10=$((0x10)); r11=$((010))\n"
|
||||
"r12=$((!0)); r13=$((1,2)); r14=$((c*2)); r15=$(( ((((2)))) ))"
|
||||
);
|
||||
CHECK_EQ(sh.var("r1"), "7");
|
||||
CHECK_EQ(sh.var("r2"), "9");
|
||||
CHECK_EQ(sh.var("r3"), "3");
|
||||
CHECK_EQ(sh.var("r4"), "1");
|
||||
CHECK_EQ(sh.var("r5"), "-5");
|
||||
CHECK_EQ(sh.var("r6"), "10");
|
||||
CHECK_EQ(sh.var("r7"), "6");
|
||||
CHECK_EQ(sh.var("a"), "6");
|
||||
CHECK_EQ(sh.var("r8"), "1");
|
||||
CHECK_EQ(sh.var("r9"), "1");
|
||||
CHECK_EQ(sh.var("r10"), "16");
|
||||
CHECK_EQ(sh.var("r11"), "8");
|
||||
CHECK_EQ(sh.var("r12"), "1");
|
||||
CHECK_EQ(sh.var("r13"), "2");
|
||||
CHECK_EQ(sh.var("r14"), "14");
|
||||
CHECK_EQ(sh.var("r15"), "2");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: arithmetic variables, assignment and precedence") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"b='2*3'; m='1+1'\n"
|
||||
"r1=$((b+1)); r2=$((p=q=4)); r3=$((m*=3)); r4=$((- -3)); r5=$((!!5))\n"
|
||||
"r6=$((1+2*3-4/2)); r7=$((2<3==1)); r8=$(( ((((((1+1)))))) )); r9=$((-b))"
|
||||
);
|
||||
CHECK_EQ(sh.var("r1"), "7");
|
||||
CHECK_EQ(sh.var("r2"), "4");
|
||||
CHECK_EQ(sh.var("p"), "4");
|
||||
CHECK_EQ(sh.var("q"), "4");
|
||||
CHECK_EQ(sh.var("r3"), "6");
|
||||
CHECK_EQ(sh.var("m"), "6");
|
||||
CHECK_EQ(sh.var("r4"), "3");
|
||||
CHECK_EQ(sh.var("r5"), "1");
|
||||
CHECK_EQ(sh.var("r6"), "5");
|
||||
CHECK_EQ(sh.var("r7"), "1");
|
||||
CHECK_EQ(sh.var("r8"), "2");
|
||||
CHECK_EQ(sh.var("r9"), "-6");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: arithmetic syntax errors") {
|
||||
const std::string scripts[] = {
|
||||
"x=$((1+)); after=1", "x=$((1 2)); after=1", "v='(1'; x=$((v)); after=1", "x=$((1)+1)); after=1",
|
||||
"x=$((2**3)); after=1", "v='1)'; x=$((v)); after=1", "r=r; x=$((r)); after=1"
|
||||
};
|
||||
for (const std::string& script : scripts) {
|
||||
Shell sh;
|
||||
CAPTURE(script);
|
||||
CHECK_EQ(sh.run(script.c_str()), 2);
|
||||
CHECK_EQ(sh.var("after"), "<unset>");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("shell: arithmetic errors are fatal") {
|
||||
Shell sh;
|
||||
CHECK_EQ(sh.run("x=$((1/0)); after=1"), 2);
|
||||
CHECK_EQ(sh.var("after"), "<unset>");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Control flow
|
||||
|
||||
TEST_CASE("shell: if, elif and else") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"for v in 1 2 3; do\n"
|
||||
" if [ $v = 1 ]; then r=\"${r}one\"; elif [ $v = 2 ]; then r=\"${r}two\"; else r=\"${r}other\"; fi\n"
|
||||
"done"
|
||||
);
|
||||
CHECK_EQ(sh.var("r"), "onetwoother");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: while and until loops") {
|
||||
Shell sh;
|
||||
sh.run("i=0; while [ $i -lt 5 ]; do i=$((i+1)); done; j=0; until [ $j -ge 3 ]; do j=$((j+1)); done");
|
||||
CHECK_EQ(sh.var("i"), "5");
|
||||
CHECK_EQ(sh.var("j"), "3");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: break and continue") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"for i in 1 2 3 4; do if [ $i = 2 ]; then continue; fi; if [ $i = 4 ]; then break; fi; s=\"$s$i\"; done\n"
|
||||
"for a in 1 2; do for b in 1 2; do if [ $b = 2 ]; then break 2; fi; t=\"$t$a$b\"; done; done"
|
||||
);
|
||||
CHECK_EQ(sh.var("s"), "13");
|
||||
CHECK_EQ(sh.var("t"), "11");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: case patterns") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"for f in a.txt b.c x; do\n"
|
||||
" case $f in *.txt) r=\"${r}T\";; a|b.c) r=\"${r}C\";; [xy]) r=\"${r}X\";; *) r=\"${r}?\";; esac\n"
|
||||
"done"
|
||||
);
|
||||
CHECK_EQ(sh.var("r"), "TCX");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: and-or lists and negation") {
|
||||
Shell sh;
|
||||
sh.run("false && x=1 || y=2; ! false; neg=$?");
|
||||
CHECK_EQ(sh.var("x"), "<unset>");
|
||||
CHECK_EQ(sh.var("y"), "2");
|
||||
CHECK_EQ(sh.var("neg"), "0");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: exit") {
|
||||
Shell sh;
|
||||
CHECK_EQ(sh.run("exit 3; after=1"), 3);
|
||||
CHECK_EQ(sh.var("after"), "<unset>");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Functions
|
||||
|
||||
TEST_CASE("shell: functions, arguments, return and local") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"greet() { g=\"hello $1\"; return 4; }\n"
|
||||
"greet world; status=$?\n"
|
||||
"x=outer; scoped() { local x=inner; seen=$x; }; scoped\n"
|
||||
"fact() { if [ $1 -le 1 ]; then r=1; else fact $(($1-1)); r=$((r*$1)); fi; }; fact 5"
|
||||
);
|
||||
CHECK_EQ(sh.var("g"), "hello world");
|
||||
CHECK_EQ(sh.var("status"), "4");
|
||||
CHECK_EQ(sh.var("seen"), "inner");
|
||||
CHECK_EQ(sh.var("x"), "outer");
|
||||
CHECK_EQ(sh.var("r"), "120");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: return from inside a loop") {
|
||||
Shell sh;
|
||||
sh.run("f() { for i in 1 2 3; do if [ $i = 2 ]; then return 7; fi; last=$i; done; }; f; s=$?");
|
||||
CHECK_EQ(sh.var("s"), "7");
|
||||
CHECK_EQ(sh.var("last"), "1");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: function recursion limit") {
|
||||
Shell sh;
|
||||
sh.run("f() { f; }; f; s=$?");
|
||||
CHECK_EQ(sh.var("s"), "1");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Subshells, groups and command substitution
|
||||
|
||||
TEST_CASE("shell: subshell isolation and groups") {
|
||||
Shell sh;
|
||||
sh.run("x=1; (x=2); a=$x; (exit 3); s=$?; { x=4; }; b=$x");
|
||||
CHECK_EQ(sh.var("a"), "1");
|
||||
CHECK_EQ(sh.var("s"), "3");
|
||||
CHECK_EQ(sh.var("b"), "4");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: nested command substitution") {
|
||||
Shell sh;
|
||||
sh.run("x=$(echo $(echo $(echo deep))); y=$(echo a; echo; echo); z=`echo tick`");
|
||||
CHECK_EQ(sh.var("x"), "deep");
|
||||
CHECK_EQ(sh.var("y"), "a");
|
||||
CHECK_EQ(sh.var("z"), "tick");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: command substitution drops NUL bytes") {
|
||||
Shell sh;
|
||||
sh.run("x=$(echo 'a\\0b')");
|
||||
CHECK_EQ(sh.var("x"), "ab");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: eval") {
|
||||
Shell sh;
|
||||
sh.run("eval 'x=5'; eval \"y=\\$x\"");
|
||||
CHECK_EQ(sh.var("x"), "5");
|
||||
CHECK_EQ(sh.var("y"), "5");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: source") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"echo 'z=9; a1=$1' > $TMP/sourced.sh\n"
|
||||
". $TMP/sourced.sh first\n"
|
||||
"echo 'return 5; never=1' > $TMP/returning.sh\n"
|
||||
"source $TMP/returning.sh; s=$?"
|
||||
);
|
||||
CHECK_EQ(sh.var("z"), "9");
|
||||
CHECK_EQ(sh.var("a1"), "first");
|
||||
CHECK_EQ(sh.var("s"), "5");
|
||||
CHECK_EQ(sh.var("never"), "<unset>");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Redirection and pipelines
|
||||
|
||||
TEST_CASE("shell: output and input redirection") {
|
||||
Shell sh;
|
||||
sh.run("echo one > $TMP/f; echo two >> $TMP/f; { read a; read b; } < $TMP/f");
|
||||
CHECK_EQ(sh.var("a"), "one");
|
||||
CHECK_EQ(sh.var("b"), "two");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: stderr duplication") {
|
||||
Shell sh;
|
||||
sh.run("x=$( { echo out; echo err >&2; } 2>&1 )");
|
||||
CHECK_EQ(sh.var("x"), "out\nerr");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: here-documents") {
|
||||
Shell sh;
|
||||
sh.run("x=w\nread a <<EOF\nhello $x\nEOF\nread b <<'EOF'\nhello $x\nEOF\n");
|
||||
CHECK_EQ(sh.var("a"), "hello w");
|
||||
CHECK_EQ(sh.var("b"), "hello $x");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: while-read loop fed by a here-document") {
|
||||
Shell sh;
|
||||
sh.run("while read l; do s=\"$s[$l]\"; done <<EOF\na\nb\nEOF\n");
|
||||
CHECK_EQ(sh.var("s"), "[a][b]");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: pipelines") {
|
||||
Shell sh;
|
||||
sh.run("x=$(echo a b c | { read p q; echo $q; }); echo hello | read v; r=$v");
|
||||
CHECK_EQ(sh.var("x"), "b c");
|
||||
CHECK_EQ(sh.var("r"), "hello");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Builtins and globbing
|
||||
|
||||
TEST_CASE("shell: test builtin") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"[ 1 -lt 2 ]; a=$?; [ ! -z x ]; b=$?; [ \\( a = a \\) -a b = c ]; c=$?\n"
|
||||
"echo x > $TMP/exists; test -f $TMP/exists; d=$?; test -d $TMP/exists; e=$?"
|
||||
);
|
||||
CHECK_EQ(sh.var("a"), "0");
|
||||
CHECK_EQ(sh.var("b"), "0");
|
||||
CHECK_EQ(sh.var("c"), "1");
|
||||
CHECK_EQ(sh.var("d"), "0");
|
||||
CHECK_EQ(sh.var("e"), "1");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: test builtin negation and grouping") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"[ ! ! x ]; a=$?; [ ! \\( a = b \\) ]; b=$?; [ \\( \\( x \\) \\) -o '' ]; c=$?\n"
|
||||
"[ a = a -a \\( b = c -o d = d \\) ]; d=$?; [ \\( a ]; e=$?; [ a -a ]; f=$?"
|
||||
);
|
||||
CHECK_EQ(sh.var("a"), "0");
|
||||
CHECK_EQ(sh.var("b"), "0");
|
||||
CHECK_EQ(sh.var("c"), "0");
|
||||
CHECK_EQ(sh.var("d"), "0");
|
||||
CHECK_EQ(sh.var("e"), "2");
|
||||
CHECK_EQ(sh.var("f"), "1");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: pattern matching with multiple stars") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"p=a/b/c.tar.gz\n"
|
||||
"case abcabc in *b*c*c) m1=y;; esac; case abc in *x*) m2=y;; *) m2=n;; esac\n"
|
||||
"case ab in a*b*) m3=y;; esac; case a\\*b in 'a*b') m4=y;; esac\n"
|
||||
"s1=${p%.*}; s2=${p%%.*}; s3=${p#*.}; s4=${p##*/*.}"
|
||||
);
|
||||
CHECK_EQ(sh.var("m1"), "y");
|
||||
CHECK_EQ(sh.var("m2"), "n");
|
||||
CHECK_EQ(sh.var("m3"), "y");
|
||||
CHECK_EQ(sh.var("s1"), "a/b/c.tar");
|
||||
CHECK_EQ(sh.var("s2"), "a/b/c");
|
||||
CHECK_EQ(sh.var("s3"), "tar.gz");
|
||||
CHECK_EQ(sh.var("s4"), "gz");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: echo options and escapes") {
|
||||
Shell sh;
|
||||
sh.run("a=$(echo -n x; echo y); b=$(echo 'p\\tq')");
|
||||
CHECK_EQ(sh.var("a"), "xy");
|
||||
CHECK_EQ(sh.var("b"), "p\tq");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: pathname globbing") {
|
||||
Shell sh;
|
||||
sh.run(
|
||||
"echo > $TMP/g1.txt; echo > $TMP/g2.txt; echo > $TMP/g3.log\n"
|
||||
"n=0; for f in $TMP/g*.txt; do n=$((n+1)); done; none=$(echo $TMP/nomatch*.zzz)"
|
||||
);
|
||||
CHECK_EQ(sh.var("n"), "2");
|
||||
CHECK_EQ(sh.var("none"), sh.tmp + "/nomatch*.zzz");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region errexit
|
||||
|
||||
TEST_CASE("shell: set -e") {
|
||||
Shell sh;
|
||||
CHECK_EQ(sh.run("set -e; false; after=1"), 1);
|
||||
CHECK_EQ(sh.var("after"), "<unset>");
|
||||
|
||||
Shell exempt;
|
||||
exempt.run("set -e; if false; then :; fi; false || true; ! true; x=1");
|
||||
CHECK_EQ(exempt.var("x"), "1");
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Syntax errors
|
||||
|
||||
TEST_CASE("shell: syntax errors") {
|
||||
Shell sh;
|
||||
CHECK_EQ(sh.run("if true; then"), 2);
|
||||
CHECK_EQ(sh.run("echo 'unterminated"), 2);
|
||||
CHECK_EQ(sh.run("case x in"), 2);
|
||||
CHECK_EQ(sh.run("x=1"), 0);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Deep nesting
|
||||
|
||||
namespace {
|
||||
|
||||
std::string repeat(const std::string& text, int count) {
|
||||
std::string result;
|
||||
for (int i = 0; i < count; i++) result += text;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("shell: deep command substitution nesting") {
|
||||
Shell sh;
|
||||
sh.run(("x=" + repeat("$(echo ", 80) + "deep" + repeat(")", 80)).c_str());
|
||||
CHECK_EQ(sh.var("x"), "deep");
|
||||
}
|
||||
|
||||
TEST_CASE("shell: excessive nesting fails cleanly and the shell keeps working") {
|
||||
Shell sh;
|
||||
// Parse nesting
|
||||
CHECK_EQ(sh.run((repeat("{ ", 300) + "x=1; " + repeat("} ", 300)).c_str()), 2);
|
||||
CHECK_EQ(sh.var("x"), "<unset>");
|
||||
// Execution nesting
|
||||
CHECK_EQ(sh.run(("for i in 1; do y=" + repeat("$(echo ", 200) + "deep" + repeat(")", 200) + "; done").c_str()), 2);
|
||||
CHECK_EQ(sh.var("y"), "<unset>");
|
||||
CHECK_EQ(sh.state.loop_depth, 0);
|
||||
CHECK_EQ(sh.state.call_depth, 0);
|
||||
CHECK_EQ(sh.run("for i in 1 2; do z=$i; done; w=$(echo ok)"), 0);
|
||||
CHECK_EQ(sh.var("z"), "2");
|
||||
CHECK_EQ(sh.var("w"), "ok");
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
// platform-posix wraps pthread_attr_setstack() into a no-op, the linker still provides the real one.
|
||||
extern "C" int __real_pthread_attr_setstack(pthread_attr_t* attr, void* stackaddr, size_t stacksize);
|
||||
|
||||
namespace {
|
||||
|
||||
struct StackRun {
|
||||
sh_state* state;
|
||||
const char* script;
|
||||
int status;
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a script on a thread with a pattern-filled stack and returns how many bytes of that stack it touched.
|
||||
* Only for scripts that stay inside the interpreter: this thread is not a FreeRTOS task.
|
||||
*/
|
||||
size_t runMeasuringStack(sh_state* state, const std::string& script, int* status) {
|
||||
constexpr size_t STACK_SIZE = 512 * 1024;
|
||||
constexpr unsigned char PATTERN = 0xA5;
|
||||
auto* stack = static_cast<unsigned char*>(aligned_alloc(4096, STACK_SIZE));
|
||||
memset(stack, PATTERN, STACK_SIZE);
|
||||
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
__real_pthread_attr_setstack(&attr, stack, STACK_SIZE);
|
||||
StackRun run { state, script.c_str(), -1 };
|
||||
pthread_t thread;
|
||||
pthread_create(&thread, &attr, [](void* parameter) -> void* {
|
||||
auto* run = static_cast<StackRun*>(parameter);
|
||||
run->status = sh_run_string(run->state, run->script);
|
||||
return nullptr;
|
||||
}, &run);
|
||||
pthread_join(thread, nullptr);
|
||||
pthread_attr_destroy(&attr);
|
||||
|
||||
// The stack grows down: everything above the lowest overwritten byte was used.
|
||||
size_t untouched = 0;
|
||||
while (untouched < STACK_SIZE && stack[untouched] == PATTERN) untouched++;
|
||||
free(stack);
|
||||
*status = run.status;
|
||||
return STACK_SIZE - untouched;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("shell: native stack use does not grow with nesting") {
|
||||
Shell sh;
|
||||
int status = -1;
|
||||
size_t baseline = runMeasuringStack(&sh.state, "f() { r=$1; }; if [ 1 = 1 ]; then f ${u:-v}; fi", &status);
|
||||
CHECK_EQ(status, 0);
|
||||
|
||||
const std::string scripts[] = {
|
||||
// 60 nested function calls
|
||||
"f() { if [ $1 -lt 60 ]; then f $(($1+1)); else r=$1; fi; }; f 0",
|
||||
// 150 nested brace groups
|
||||
repeat("{ ", 150) + "r=groups; " + repeat("} ", 150),
|
||||
// 200 nested ${...:-...} defaults
|
||||
"r=" + repeat("${u:-", 200) + "braces" + repeat("}", 200),
|
||||
// 60 nested loops, each iterating once
|
||||
repeat("for i in 1; do ", 60) + "r=loops; " + repeat("done; ", 60),
|
||||
// 200 nested arithmetic parentheses
|
||||
"r=$((" + repeat("(", 200) + "1" + repeat(")", 200) + "))",
|
||||
};
|
||||
const char* expected[] = { "60", "groups", "braces", "loops", "1" };
|
||||
|
||||
for (size_t i = 0; i < std::size(scripts); i++) {
|
||||
CAPTURE(i);
|
||||
Shell deep;
|
||||
size_t used = runMeasuringStack(&deep.state, scripts[i], &status);
|
||||
CHECK_EQ(status, 0);
|
||||
CHECK_EQ(deep.var("r"), expected[i]);
|
||||
CHECK_LE(used, baseline + 2048);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// endregion
|
||||
Loading…
x
Reference in New Issue
Block a user