hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit df8e15c305a0bd6456cef3b39b709b70fdd18787
parent fdb93e96bbf15259966be1adc72d464cd6fa44e6
Author: Drew DeVault <sir@cmpwn.com>
Date:   Fri, 24 Nov 2023 09:26:09 +0100

wordexp: new module

Implements: https://todo.sr.ht/~sircmpwn/hare/274
Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Awordexp/+test.ha | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Awordexp/README | 9+++++++++
Awordexp/error.ha | 26++++++++++++++++++++++++++
Awordexp/wordexp.ha | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 156 insertions(+), 0 deletions(-)

diff --git a/wordexp/+test.ha b/wordexp/+test.ha @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// (c) Hare authors <https://harelang.org> + +use os; +use strings; + +fn streq(a: []str, b: []str) bool = { + if (len(a) != len(b)) { + return false; + }; + for (let i = 0z; i < len(a); i += 1) { + if (a[i] != b[i]) { + return false; + }; + }; + return true; +}; + +@test fn wordexp() void = { + os::setenv("TESTVAR", "test value")!; + os::unsetenv("UNSET")!; + static const cases: [_](str, []str) = [ + (``, []), + (`hello world`, ["hello", "world"]), + (`hello $TESTVAR`, ["hello", "test", "value"]), + (`hello "$TESTVAR"`, ["hello", "test value"]), + (`hello $(echo world)`, ["hello", "world"]), + (`hello $((2+2))`, ["hello", "4"]), + (`hello $UNSET`, ["hello"]), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const (in, out) = cases[i]; + const words = wordexp(in, flag::NONE)!; + defer strings::freeall(words); + assert(streq(words, out)); + }; +}; + +@test fn wordexp_error() void = { + os::unsetenv("UNSET")!; + static const cases: [_](str, flag) = [ + (`hello $UNSET`, flag::UNDEF), + (`hello $(`, 0), + ]; + + for (let i = 0z; i < len(cases); i += 1) { + const (in, flag) = cases[i]; + const result = wordexp(in, flag); + assert(result is sh_error); + }; +}; diff --git a/wordexp/README b/wordexp/README @@ -0,0 +1,9 @@ +The wordexp module implements word expansion using shell semantics, similar to +POSIX wordexp(3). Word expansion is performed with the platform-specific system +shell, which is generally POSIX sh(1) compatible on Unix-like systems. + +When used with a POSIX shell, the IFS variable is unconditionally unset in the +environment, causing the shell to assume the default value of " \t\n". + +Note that, by design, this module runs arbitrary shell commands from +user-supplied inputs. It must only be used in a trusted environment. diff --git a/wordexp/error.ha b/wordexp/error.ha @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// (c) Hare authors <https://harelang.org> + +use encoding::utf8; +use io; +use os::exec; + +// Tagged union of possible wordexp error conditions. +export type error = !(io::error | exec::error | utf8::invalid | sh_error); + +// An error occured during shell expansion. +export type sh_error = !void; + +// Converts an [[error]] to a human-friendly string. +export fn strerror(err: error) const str = { + match (err) { + case let err: io::error => + return io::strerror(err); + case let err: exec::error => + return exec::strerror(err); + case utf8::invalid => + return "Word expansion returned invalid UTF-8 data"; + case sh_error => + return "An error occured during shell expansion"; + }; +}; diff --git a/wordexp/wordexp.ha b/wordexp/wordexp.ha @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +// (c) Hare authors <https://harelang.org> +// (c) 2005-2020 Rich Felker, et al +// Based on the musl libc implementation + +use bufio; +use io; +use os; +use os::exec; +use strings; +use types; + +// Flags applicable to a [[wordexp]] operation. +export type flag = enum uint { + NONE = 0, + // DOOFFS = (1 << 0), // not implemented + // APPEND = (1 << 1), // not implemented + // REUSE = (1 << 3), // not implemented + // NOCMD = (1 << 2), // not implemented + SHOWERR = (1 << 4), + UNDEF = (1 << 5), +}; + +// Performs shell expansion and word splitting on the provided string, returning +// a list of expanded words, similar to POSIX wordexp(3). Note that this +// function, by design, will execute arbitrary commands from the input string. +// +// Pass the return value to [[strings::freeall]] to free resources associated +// with the return value. +export fn wordexp(s: str, flags: flag) ([]str | error) = { + if (s == "") { + // Special case + return []; + }; + + const (rd, wr) = exec::pipe(); + + const cmd = exec::cmd("/bin/sh", + if (flags & flag::UNDEF != 0) "-uc" else "-c", + `eval "printf %s\\\\0 $1"`, "sh", s)!; + exec::unsetenv(&cmd, "IFS")!; + exec::addfile(&cmd, os::stdout_file, wr); + if (flags & flag::SHOWERR == 0) { + exec::addfile(&cmd, os::stderr_file, exec::nullfd); + }; + const child = exec::start(&cmd)!; + io::close(wr)!; + + const scan = bufio::newscanner(rd, types::SIZE_MAX); + defer bufio::finish(&scan); + + let words: []str = []; + for (true) { + match (bufio::scan_string(&scan, "\0")?) { + case io::EOF => break; + case let word: const str => + append(words, strings::dup(word)); + }; + }; + + io::close(rd)!; + const st = exec::wait(&child)!; + match (exec::check(&st)) { + case !exec::exit_status => + return sh_error; + case void => + return words; + }; +};