Implements: https://todo.sr.ht/~sircmpwn/hare/274
Signed-off-by: Drew DeVault <sir@cmpwn.com>
---
This version abandons NOCMD, removes any attempt at syntax checking,
defines the shell as the system shell rather than a POSIX shell, and
more adequately explains the trusted user input constraints.
Is this a hack? Well, it's a small hack, but it works fine. I think the
interface is useful and not really replacable by another solution in
many of the use-cases to which it is suited, and I'd rather have a
little hack here in the stdlib where we can be certain that we've done
our little hack correctly rather than allow copy-pasted versions of this
code to proliferate throughout the ecosystem.
Fair enough. I do think this argument works better for making wordexp an
extended library rather than a stdlib module, but of course that's not
an option if we use it for hare.ini, so I guess it's fine.
As an aside: it would be neat if rc had a flag to only perform word
expansion, so the printf hack won't be necessary on gaia at all.
Either this test should os::unsetenv("IFS"), or wordexp should just
always temporarily unset IFS. I'm not sure which makes more sense; I'll
leave that to your judgement.
++@test fn wordexp_error() void = {+ os::unsetenv("UNSET")!;+ static const cases: [_](str, we_error, flag) = [+ (`hello $UNSET`, we_error::BADVAL, flag::UNDEF),+ (`hello $(`, we_error::SYNTAX, 0),+ ];++ for (let i = 0z; i < len(cases); i += 1) {+ const (in, err, flag) = cases[i];+ const result = wordexp(in, flag) as we_error;+ assert(result == err);+ };+};
diff --git a/wordexp/README b/wordexp/README
new file mode 100644
index 00000000..c8a8aead
--- /dev/null+++ b/wordexp/README
@@ -0,0 +1,6 @@
+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.++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
new file mode 100644
index 00000000..223ca0db
--- /dev/null+++ b/wordexp/error.ha
@@ -0,0 +1,31 @@
+use encoding::utf8;+use io;+use os::exec;++// Tagged union of possible wordexp error conditions.+export type error = !(io::error | exec::error | utf8::invalid | we_error);++// wordexp-specific errors+export type we_error = enum uint {+ BADVAL,+ SYNTAX,+};
If we support both badval and syntax, then IMO they should be two
separate types (but I wrote some more comments about this below). Also,
syntax should be changed to a more accurate name, like sh_error or
something like that. Even besides things like OOM, there are other
instances where the shell may fail due to a non-syntax-error, like
${UNSET?error}.
++// 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 let err: we_error =>+ switch (err) {+ case we_error::BADVAL =>+ return "Undefined shell variable in word expansion";+ case we_error::SYNTAX =>+ return "Shell syntax error in word expansion";
This, isn't right. Syntax errors can still occur if flag::UNDEF is set.
Without re-introducing syntax-checking logic, it's not possible afaict
to differentiate between these errors. So I think we may be better off
just combining them into one.
Another idea: sh_error (or whatever it's called) could be defined as
!str, and could hold the contents written to stderr (possibly with some
default error string for when a failure occurs but stderr is empty).
Then we could also get rid of flag::SHOW_ERR.
+ };++ 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));+ };+ };