From 9a62f8d6a6f8148deaea89e73d866439b588babb Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Tue, 25 Feb 2025 00:59:31 -0500 Subject: [PATCH] Add $Shell.execute() --- docs/shell.md | 117 +++++++++++++++++++++++++++++++++++++++++++++++++ environment.c | 4 +- stdlib/shell.c | 10 +++-- stdlib/shell.h | 2 +- 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 docs/shell.md diff --git a/docs/shell.md b/docs/shell.md new file mode 100644 index 0000000..f453a1d --- /dev/null +++ b/docs/shell.md @@ -0,0 +1,117 @@ +# Shell Scripting + +Tomo comes with a built-in [lang](langs.md) called `Shell` for shell commands. +This lets you write and invoke shell commands in a more type-safe way. + +```tomo +user_name := ask("What's your name? ") +_ := $Shell"echo Hello $user_name" +``` + +In the above example, there is no risk of code injection, because the +user-controlled string is automatically escaped when performing interpolation. + +## Shell Methods + +### `by_line` + +**Description:** +Run a shell command and return an iterator over its output, line-by-line. + +**Signature:** +```tomo +func by_line(command: Shell -> Void) +``` + +**Parameters:** + +- `command`: The command to run. + +**Returns:** +An optional iterator over the lines of the command's output. If the command fails +to run, `none` will be returned. + +**Example:** +```tomo +i := 1 +for line in $Shell"ping www.example.com":by_line()!: + stop if i > 5 + i += 1 +``` + +### `execute` + +**Description:** +Execute a shell command without capturing its output and return its exit status. + +**Signature:** +```tomo +func execute(command: Shell -> Int32?) +``` + +**Parameters:** + +- `command`: The command to execute. + +**Returns:** +If the command exits normally, return its exit status. Otherwise return `none`. + +**Example:** +```tomo +>> $Shell"touch file.txt":execute() += 0? +``` + +--- + +### `run` + +**Description:** +Run a shell command and return the output text from `stdout`. + +**Signature:** +```tomo +func run(command: Shell -> Text?) +``` + +**Parameters:** + +- `command`: The command to run. + +**Returns:** +If the program fails to run (e.g. a non-existent command), return `none`, +otherwise return the entire standard output of the command as text. **Note:** +if there is a trailing newline, it will be stripped. + +**Example:** +```tomo +>> $Shell"seq 5":run() += "1$\n2$\n3$\n4$\n5" +``` + +--- + +### `run_bytes` + +**Description:** +Run a shell command and return the output in raw bytes from `stdout`. + +**Signature:** +```tomo +func run(command: Shell -> [Byte]?) +``` + +**Parameters:** + +- `command`: The command to run. + +**Returns:** +If the program fails to run (e.g. a non-existent command), return `none`, +otherwise return the entire standard output of the command as an array of +bytes. + +**Example:** +```tomo +>> $Shell"seq 5":run_bytes() += [0x31, 0x0A, 0x32, 0x0A, 0x33, 0x0A, 0x34, 0x0A, 0x35, 0x0A] +``` diff --git a/environment.c b/environment.c index b785a8e..2e1c4ad 100644 --- a/environment.c +++ b/environment.c @@ -380,8 +380,8 @@ env_t *new_compilation_unit(CORD libname) {"escape_int", "Int$value_as_text", "func(i:Int -> Shell)"}, {"escape_text", "Shell$escape_text", "func(text:Text -> Shell)"}, {"escape_text_array", "Shell$escape_text_array", "func(texts:[Text] -> Shell)"}, - {"execute", "Shell$execute", "func(command:Shell -> Int32)"}, - {"run_bytes", "Shell$run", "func(command:Shell -> [Byte]?)"}, + {"execute", "Shell$execute", "func(command:Shell -> Int32?)"}, + {"run_bytes", "Shell$run_bytes", "func(command:Shell -> [Byte]?)"}, {"run", "Shell$run", "func(command:Shell -> Text?)"}, )}, {"Text", TEXT_TYPE, "Text_t", "Text$info", TypedArray(ns_entry_t, diff --git a/stdlib/shell.c b/stdlib/shell.c index 08898ac..7c54950 100644 --- a/stdlib/shell.c +++ b/stdlib/shell.c @@ -50,7 +50,7 @@ public OptionalArray_t Shell$run_bytes(Shell_t command) if (len + (size_t)just_read >= capacity) content = GC_REALLOC(content, (capacity *= 2)); - memcpy(&content[len], chunk, (size_t)just_read); + memcpy(content + len, chunk, (size_t)just_read); len += (size_t)just_read; } while (just_read == sizeof(chunk)); @@ -75,10 +75,14 @@ public OptionalText_t Shell$run(Shell_t command) return Text$from_bytes(bytes); } -public int32_t Shell$execute(Shell_t command) +public OptionalInt32_t Shell$execute(Shell_t command) { const char *cmd_str = Text$as_c_string(command); - return system(cmd_str); + int status = system(cmd_str); + if (WIFEXITED(status)) + return (OptionalInt32_t){.i=WEXITSTATUS(status)}; + else + return (OptionalInt32_t){.is_none=true}; } static void _line_reader_cleanup(FILE **f) diff --git a/stdlib/shell.h b/stdlib/shell.h index a8f35f6..500bb04 100644 --- a/stdlib/shell.h +++ b/stdlib/shell.h @@ -21,7 +21,7 @@ Shell_t Shell$escape_text(Text_t text); Shell_t Shell$escape_text_array(Array_t texts); OptionalArray_t Shell$run_bytes(Shell_t command); OptionalText_t Shell$run(Shell_t command); -int32_t Shell$execute(Shell_t command); +OptionalInt32_t Shell$execute(Shell_t command); #define Shell$hash Text$hash #define Shell$compare Text$compare