mirror of https://github.com/adamdruppe/arsd.git
initial arsd.shell commit
This commit is contained in:
parent
f7c2cbc0d2
commit
b3d88cafa8
173
core.d
173
core.d
|
|
@ -525,7 +525,7 @@ struct Union(T...) {
|
||||||
|
|
||||||
total: 25 bits + 17 bits = 42 bits
|
total: 25 bits + 17 bits = 42 bits
|
||||||
|
|
||||||
fractional seconds: 10 bits
|
fractional seconds: 10 bits (about milliseconds)
|
||||||
|
|
||||||
accuracy flags: date_valid | time_valid = 2 bits
|
accuracy flags: date_valid | time_valid = 2 bits
|
||||||
|
|
||||||
|
|
@ -2546,11 +2546,13 @@ class ArsdExceptionBase : object.Exception {
|
||||||
sink(value);
|
sink(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// full stack trace
|
// full stack trace, if available
|
||||||
sink("\n----------------\n");
|
if(info) {
|
||||||
foreach(str; info) {
|
sink("\n----------------\n");
|
||||||
sink(str);
|
foreach(str; info) {
|
||||||
sink("\n");
|
sink(str);
|
||||||
|
sink("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// ditto
|
/// ditto
|
||||||
|
|
@ -8166,6 +8168,13 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
else throw new NotYetImplementedException();
|
else throw new NotYetImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
package {
|
||||||
|
int overrideStdinFd = -2;
|
||||||
|
int overrideStdoutFd = -2;
|
||||||
|
int overrideStderrFd = -2;
|
||||||
|
int pgid = -2;
|
||||||
|
}
|
||||||
|
|
||||||
/++
|
/++
|
||||||
|
|
||||||
+/
|
+/
|
||||||
|
|
@ -8176,40 +8185,52 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
int[2] stdinPipes;
|
int[2] stdinPipes;
|
||||||
ret = pipe(stdinPipes);
|
if(overrideStdinFd == -2) {
|
||||||
if(ret == -1)
|
ret = pipe(stdinPipes);
|
||||||
throw new ErrnoApiException("stdin pipe", errno);
|
if(ret == -1)
|
||||||
|
throw new ErrnoApiException("stdin pipe", errno);
|
||||||
scope(failure) {
|
|
||||||
close(stdinPipes[0]);
|
|
||||||
close(stdinPipes[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stdinFd = stdinPipes[1];
|
scope(failure) {
|
||||||
|
if(overrideStdinFd == -2) {
|
||||||
|
close(stdinPipes[0]);
|
||||||
|
close(stdinPipes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stdinFd = overrideStdinFd == -2 ? stdinPipes[1] : -1;
|
||||||
|
|
||||||
int[2] stdoutPipes;
|
int[2] stdoutPipes;
|
||||||
ret = pipe(stdoutPipes);
|
if(overrideStdoutFd == -2) {
|
||||||
if(ret == -1)
|
ret = pipe(stdoutPipes);
|
||||||
throw new ErrnoApiException("stdout pipe", errno);
|
if(ret == -1)
|
||||||
|
throw new ErrnoApiException("stdout pipe", errno);
|
||||||
scope(failure) {
|
|
||||||
close(stdoutPipes[0]);
|
|
||||||
close(stdoutPipes[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stdoutFd = stdoutPipes[0];
|
scope(failure) {
|
||||||
|
if(overrideStdoutFd == -2) {
|
||||||
|
close(stdoutPipes[0]);
|
||||||
|
close(stdoutPipes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stdoutFd = overrideStdoutFd == -2 ? stdoutPipes[0] : -1;
|
||||||
|
|
||||||
int[2] stderrPipes;
|
int[2] stderrPipes;
|
||||||
ret = pipe(stderrPipes);
|
if(overrideStderrFd == -2) {
|
||||||
if(ret == -1)
|
ret = pipe(stderrPipes);
|
||||||
throw new ErrnoApiException("stderr pipe", errno);
|
if(ret == -1)
|
||||||
|
throw new ErrnoApiException("stderr pipe", errno);
|
||||||
scope(failure) {
|
|
||||||
close(stderrPipes[0]);
|
|
||||||
close(stderrPipes[1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stderrFd = stderrPipes[0];
|
scope(failure) {
|
||||||
|
if(overrideStderrFd == -2) {
|
||||||
|
close(stderrPipes[0]);
|
||||||
|
close(stderrPipes[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto stderrFd = overrideStderrFd == -2 ? stderrPipes[0] : -1;
|
||||||
|
|
||||||
|
|
||||||
int[2] errorReportPipes;
|
int[2] errorReportPipes;
|
||||||
|
|
@ -8252,18 +8273,39 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dup2 closes the fd it is replacing automatically
|
// both parent and child are supposed to try to set it
|
||||||
dup2(stdinPipes[0], 0);
|
if(pgid != -2) {
|
||||||
dup2(stdoutPipes[1], 1);
|
setpgid(0, pgid == 0 ? getpid() : pgid);
|
||||||
dup2(stderrPipes[1], 2);
|
}
|
||||||
|
|
||||||
// don't need either of the original pipe fds anymore
|
// dup2 closes the fd it is replacing automatically
|
||||||
close(stdinPipes[0]);
|
// then don't need either of the original pipe fds anymore
|
||||||
close(stdinPipes[1]);
|
if(overrideStdinFd == -2) {
|
||||||
close(stdoutPipes[0]);
|
dup2(stdinPipes[0], 0);
|
||||||
close(stdoutPipes[1]);
|
close(stdinPipes[0]);
|
||||||
close(stderrPipes[0]);
|
close(stdinPipes[1]);
|
||||||
close(stderrPipes[1]);
|
} else if(overrideStdinFd != 0) {
|
||||||
|
dup2(overrideStdinFd, 0);
|
||||||
|
close(overrideStdinFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(overrideStdoutFd == -2) {
|
||||||
|
dup2(stdoutPipes[1], 1);
|
||||||
|
close(stdoutPipes[0]);
|
||||||
|
close(stdoutPipes[1]);
|
||||||
|
} else if(overrideStdoutFd != 1) {
|
||||||
|
dup2(overrideStdoutFd, 1);
|
||||||
|
close(overrideStdoutFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(overrideStderrFd == -2) {
|
||||||
|
dup2(stderrPipes[1], 2);
|
||||||
|
close(stderrPipes[0]);
|
||||||
|
close(stderrPipes[1]);
|
||||||
|
} else if(overrideStderrFd != 2) {
|
||||||
|
dup2(overrideStderrFd, 2);
|
||||||
|
close(overrideStderrFd);
|
||||||
|
}
|
||||||
|
|
||||||
// the error reporting pipe will be closed upon exec since we set cloexec before fork
|
// the error reporting pipe will be closed upon exec since we set cloexec before fork
|
||||||
// and everything else should have cloexec set too hopefully.
|
// and everything else should have cloexec set too hopefully.
|
||||||
|
|
@ -8297,6 +8339,11 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
} else {
|
} else {
|
||||||
pid = forkRet;
|
pid = forkRet;
|
||||||
|
|
||||||
|
// both parent and child are supposed to try to set it
|
||||||
|
if(pgid != -2) {
|
||||||
|
setpgid(pid, pgid == 0 ? pid : pgid);
|
||||||
|
}
|
||||||
|
|
||||||
recordChildCreated(pid, this);
|
recordChildCreated(pid, this);
|
||||||
|
|
||||||
// close our copy of the write side of the error reporting pipe
|
// close our copy of the write side of the error reporting pipe
|
||||||
|
|
@ -8305,10 +8352,14 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
|
|
||||||
int[2] msg;
|
int[2] msg;
|
||||||
// this will block to wait for it to actually either start up or fail to exec (which should be near instant)
|
// this will block to wait for it to actually either start up or fail to exec (which should be near instant)
|
||||||
|
try_again:
|
||||||
auto val = read(errorReportPipes[0], msg.ptr, msg.sizeof);
|
auto val = read(errorReportPipes[0], msg.ptr, msg.sizeof);
|
||||||
|
|
||||||
if(val == -1)
|
if(val == -1) {
|
||||||
|
if(errno == EINTR)
|
||||||
|
goto try_again;
|
||||||
throw new ErrnoApiException("read error report", errno);
|
throw new ErrnoApiException("read error report", errno);
|
||||||
|
}
|
||||||
|
|
||||||
if(val == msg.sizeof) {
|
if(val == msg.sizeof) {
|
||||||
// error happened
|
// error happened
|
||||||
|
|
@ -8321,25 +8372,29 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
// set the ones we keep to close upon future execs
|
// set the ones we keep to close upon future execs
|
||||||
// FIXME should i set NOBLOCK at this time too? prolly should
|
|
||||||
setCloExec(stdinPipes[1]);
|
|
||||||
setCloExec(stdoutPipes[0]);
|
|
||||||
setCloExec(stderrPipes[0]);
|
|
||||||
|
|
||||||
// and close the others
|
// and close the others
|
||||||
ErrnoEnforce!close(stdinPipes[0]);
|
if(overrideStdinFd == -2) {
|
||||||
ErrnoEnforce!close(stdoutPipes[1]);
|
setCloExec(stdinPipes[1]);
|
||||||
ErrnoEnforce!close(stderrPipes[1]);
|
ErrnoEnforce!close(stdinPipes[0]);
|
||||||
|
makeNonBlocking(stdinFd);
|
||||||
|
_stdin = new AsyncFile(stdinFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(overrideStdoutFd == -2) {
|
||||||
|
setCloExec(stdoutPipes[0]);
|
||||||
|
ErrnoEnforce!close(stdoutPipes[1]);
|
||||||
|
makeNonBlocking(stdoutFd);
|
||||||
|
_stdout = new AsyncFile(stdoutFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(overrideStderrFd == -2) {
|
||||||
|
setCloExec(stderrPipes[0]);
|
||||||
|
ErrnoEnforce!close(stderrPipes[1]);
|
||||||
|
makeNonBlocking(stderrFd);
|
||||||
|
_stderr = new AsyncFile(stderrFd);
|
||||||
|
}
|
||||||
|
|
||||||
ErrnoEnforce!close(errorReportPipes[0]);
|
ErrnoEnforce!close(errorReportPipes[0]);
|
||||||
|
|
||||||
makeNonBlocking(stdinFd);
|
|
||||||
makeNonBlocking(stdoutFd);
|
|
||||||
makeNonBlocking(stderrFd);
|
|
||||||
|
|
||||||
_stdin = new AsyncFile(stdinFd);
|
|
||||||
_stdout = new AsyncFile(stdoutFd);
|
|
||||||
_stderr = new AsyncFile(stderrFd);
|
|
||||||
}
|
}
|
||||||
} else version(Windows) {
|
} else version(Windows) {
|
||||||
WCharzBuffer program = this.program.path;
|
WCharzBuffer program = this.program.path;
|
||||||
|
|
@ -8432,7 +8487,7 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
|
||||||
import core.sys.posix.unistd;
|
import core.sys.posix.unistd;
|
||||||
import core.sys.posix.fcntl;
|
import core.sys.posix.fcntl;
|
||||||
|
|
||||||
private pid_t pid = -1;
|
package pid_t pid = -1;
|
||||||
|
|
||||||
public void delegate() beforeExec;
|
public void delegate() beforeExec;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,796 @@
|
||||||
|
/++
|
||||||
|
Support functions to build a custom unix-style shell.
|
||||||
|
|
||||||
|
|
||||||
|
$(PITFALL
|
||||||
|
Do NOT use this to try to sanitize, escape, or otherwise parse what another shell would do with a string! Every shell is different and this implements my rules which may differ in subtle ways from any other common shell.
|
||||||
|
|
||||||
|
If you want to use this to understand a command, also use it to execute that command so you get what you expect.
|
||||||
|
)
|
||||||
|
|
||||||
|
History:
|
||||||
|
Added October 18, 2025
|
||||||
|
+/
|
||||||
|
module arsd.shell;
|
||||||
|
|
||||||
|
import arsd.core;
|
||||||
|
|
||||||
|
/++
|
||||||
|
Holds some context needed for shell expansions.
|
||||||
|
+/
|
||||||
|
struct ShellContext {
|
||||||
|
string[string] vars;
|
||||||
|
string cwd;
|
||||||
|
string[string] userHomes;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum QuoteStyle {
|
||||||
|
none,
|
||||||
|
nonExpanding, // 'thing'
|
||||||
|
expanding, // "thing"
|
||||||
|
subcommand, // `thing`
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
|
||||||
|
+/
|
||||||
|
alias Globber = string[] delegate(string str, ShellContext context);
|
||||||
|
|
||||||
|
/++
|
||||||
|
Represents one component of a shell command line as a precursor to parsing.
|
||||||
|
+/
|
||||||
|
struct ShellLexeme {
|
||||||
|
string l;
|
||||||
|
QuoteStyle quoteStyle;
|
||||||
|
|
||||||
|
string toEscapedFormat() {
|
||||||
|
if(quoteStyle == QuoteStyle.nonExpanding) {
|
||||||
|
string ret;
|
||||||
|
ret.reserve(l.length);
|
||||||
|
foreach(ch; l) {
|
||||||
|
if(ch == '*')
|
||||||
|
ret ~= "\\*";
|
||||||
|
else
|
||||||
|
ret ~= ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
} else {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
/++
|
||||||
|
The second thing should be have toSingleArg called on it
|
||||||
|
+/
|
||||||
|
EnvironmentPair toEnvironmentPair(ShellLexeme context) {
|
||||||
|
assert(quoteStyle == QuoteStyle.none);
|
||||||
|
|
||||||
|
size_t splitPoint = l.length;
|
||||||
|
foreach(size_t idx, char ch; l) {
|
||||||
|
if(ch == '=') {
|
||||||
|
splitPoint = idx;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(splitPoint != l.length) {
|
||||||
|
return EnvironmentPair(l[0 .. splitPoint], ShellLexeme(l[splitPoint + 1 .. $]));
|
||||||
|
} else {
|
||||||
|
return EnvironmentPair(null, ShellLexeme.init);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Expands variables but not globs while replacing quotes and such. Note it is NOT safe to pass an expanded single arg to another shell
|
||||||
|
+/
|
||||||
|
string toExpandedSingleArg(ShellContext context) {
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Returns the value as an argv array, after shell expansion of variables, tildes, and globs
|
||||||
|
|
||||||
|
Does NOT attempt to execute `subcommands`.
|
||||||
|
+/
|
||||||
|
string[] toExpandedArgs(ShellContext context, Globber globber) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
|
/++
|
||||||
|
This function in pure in all but formal annotation; it does not interact with the outside world.
|
||||||
|
+/
|
||||||
|
ShellLexeme[] lexShellCommandLine(string commandLine) {
|
||||||
|
ShellLexeme[] ret;
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
consumingWhitespace,
|
||||||
|
readingWord,
|
||||||
|
readingSingleQuoted,
|
||||||
|
readingEscaped,
|
||||||
|
readingExpandingContextEscaped,
|
||||||
|
readingDoubleQuoted,
|
||||||
|
// FIXME: readingSubcommand for `thing`
|
||||||
|
readingComment,
|
||||||
|
}
|
||||||
|
|
||||||
|
State state = State.consumingWhitespace;
|
||||||
|
size_t first = commandLine.length;
|
||||||
|
|
||||||
|
void endWord() {
|
||||||
|
state = State.consumingWhitespace;
|
||||||
|
first = commandLine.length; // we'll rewind upon encountering the next word, if there is one
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(size_t idx, char ch; commandLine) {
|
||||||
|
again:
|
||||||
|
final switch(state) {
|
||||||
|
case State.consumingWhitespace:
|
||||||
|
switch(ch) {
|
||||||
|
case ' ':
|
||||||
|
// the arg separators should all be collapsed to exactly one
|
||||||
|
if(ret.length && !(ret[$-1].quoteStyle == QuoteStyle.none && ret[$-1].l == " "))
|
||||||
|
ret ~= ShellLexeme(" ");
|
||||||
|
continue;
|
||||||
|
case '#':
|
||||||
|
state = State.readingComment;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
first = idx;
|
||||||
|
state = State.readingWord;
|
||||||
|
goto again;
|
||||||
|
}
|
||||||
|
case State.readingWord:
|
||||||
|
switch(ch) {
|
||||||
|
case '\'':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingSingleQuoted;
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
// a \ch can be treated as just a single quoted single char...
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingEscaped;
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
first = idx + 1;
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
break;
|
||||||
|
case ' ':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
ret ~= ShellLexeme(" "); // an argument separator
|
||||||
|
endWord();
|
||||||
|
continue;
|
||||||
|
case '|', '<', '>', '&':
|
||||||
|
if(first != idx)
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx]);
|
||||||
|
ret ~= ShellLexeme(commandLine[idx .. idx + 1]); // shell special symbol
|
||||||
|
endWord();
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
// keep searching
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingComment:
|
||||||
|
if(ch == '\n') {
|
||||||
|
endWord();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingSingleQuoted:
|
||||||
|
switch(ch) {
|
||||||
|
case '\'':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||||
|
endWord();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingDoubleQuoted:
|
||||||
|
switch(ch) {
|
||||||
|
case '"':
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.expanding);
|
||||||
|
endWord();
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
state = State.readingExpandingContextEscaped;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State.readingEscaped:
|
||||||
|
if(ch >= 0x80 && ch <= 0xBF) {
|
||||||
|
// continuation byte
|
||||||
|
continue;
|
||||||
|
} else if(first == idx) {
|
||||||
|
// first byte, keep searching for continuations
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// same as if the user wrote the escaped character in single quotes
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx], QuoteStyle.nonExpanding);
|
||||||
|
|
||||||
|
if(state == State.readingExpandingContextEscaped) {
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
first = idx;
|
||||||
|
} else {
|
||||||
|
endWord();
|
||||||
|
}
|
||||||
|
goto again;
|
||||||
|
}
|
||||||
|
case State.readingExpandingContextEscaped:
|
||||||
|
if(ch == '"') {
|
||||||
|
// the -1 trims out the \
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. idx - 1], QuoteStyle.expanding);
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
first = idx; // we need to INCLUDE the " itself
|
||||||
|
} else {
|
||||||
|
// this was actually nothing special, the backslash is kept in the double quotes
|
||||||
|
state = State.readingDoubleQuoted;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(first != commandLine.length) {
|
||||||
|
if(state != State.readingWord && state != State.readingComment)
|
||||||
|
throw new Exception("ran out of data in inappropriate state");
|
||||||
|
ret ~= ShellLexeme(commandLine[first .. $]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
ShellLexeme[] got;
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar");
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
|
||||||
|
// comments can only happen at whitespace contexts, not at the end of a single word
|
||||||
|
got = lexShellCommandLine("FOO=bar#commentspam");
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "FOO=bar#commentspam");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar #commentspam");
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // arg separator still there even tho there is no arg cuz of the comment, but that's semantic
|
||||||
|
|
||||||
|
got = lexShellCommandLine("#commentspam");
|
||||||
|
assert(got.length == 0, got[0].l);
|
||||||
|
|
||||||
|
got = lexShellCommandLine("FOO=bar ./prog");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // argument separator
|
||||||
|
assert(got[2].l == "./prog");
|
||||||
|
|
||||||
|
// all whitespace should be collapsed to a single argument separator
|
||||||
|
got = lexShellCommandLine("FOO=bar ./prog");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " "); // argument separator
|
||||||
|
assert(got[2].l == "./prog");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("'foo'bar");
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "foo");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.nonExpanding);
|
||||||
|
assert(got[1].l == "bar");
|
||||||
|
assert(got[1].quoteStyle == QuoteStyle.none);
|
||||||
|
|
||||||
|
// escaped single char works as if you wrote it in single quotes
|
||||||
|
got = lexShellCommandLine("test\\'bar");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "test");
|
||||||
|
assert(got[1].l == "'");
|
||||||
|
assert(got[2].l == "bar");
|
||||||
|
|
||||||
|
// checking for utf-8 decode of escaped char
|
||||||
|
got = lexShellCommandLine("test\\\»bar");
|
||||||
|
assert(got.length == 3);
|
||||||
|
assert(got[0].l == "test");
|
||||||
|
assert(got[1].l == "\»");
|
||||||
|
assert(got[2].l == "bar");
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`"ok"`);
|
||||||
|
assert(got.length == 1);
|
||||||
|
assert(got[0].l == "ok");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`"ok\"after"`);
|
||||||
|
assert(got.length == 2);
|
||||||
|
assert(got[0].l == "ok");
|
||||||
|
assert(got[0].quoteStyle == QuoteStyle.expanding);
|
||||||
|
assert(got[1].l == "\"after");
|
||||||
|
assert(got[1].quoteStyle == QuoteStyle.expanding);
|
||||||
|
|
||||||
|
got = lexShellCommandLine(`FOO=bar ./thing 'my ard' second_arg "quoted\"thing"`);
|
||||||
|
assert(got.length == 10); // because quoted\"thing is two in this weird system
|
||||||
|
assert(got[0].l == "FOO=bar");
|
||||||
|
assert(got[1].l == " ");
|
||||||
|
assert(got[2].l == "./thing");
|
||||||
|
assert(got[3].l == " ");
|
||||||
|
assert(got[4].l == "my ard");
|
||||||
|
assert(got[5].l == " ");
|
||||||
|
assert(got[6].l == "second_arg");
|
||||||
|
assert(got[7].l == " ");
|
||||||
|
assert(got[8].l == "quoted");
|
||||||
|
assert(got[9].l == "\"thing");
|
||||||
|
|
||||||
|
got = lexShellCommandLine("a | b c");
|
||||||
|
assert(got.length == 7);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ShellIo {
|
||||||
|
enum Kind {
|
||||||
|
inherit,
|
||||||
|
fd,
|
||||||
|
filename,
|
||||||
|
pipedCommand,
|
||||||
|
memoryBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
Kind kind;
|
||||||
|
int fd;
|
||||||
|
string filename;
|
||||||
|
ShellCommand pipedCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShellCommand {
|
||||||
|
ShellIo stdin;
|
||||||
|
ShellIo stdout;
|
||||||
|
ShellIo stderr;
|
||||||
|
// yes i know in unix you can do other fds too. do i care?
|
||||||
|
|
||||||
|
private ExternalProcess externalProcess;
|
||||||
|
|
||||||
|
string exePath;
|
||||||
|
string cwd;
|
||||||
|
string[] argv;
|
||||||
|
EnvironmentPair[] environmentPairs;
|
||||||
|
|
||||||
|
/++
|
||||||
|
The return value may be null! Some things can be executed without external processes.
|
||||||
|
|
||||||
|
This function is absolutely NOT pure. It may modify your shell context, run external processes, and generally carry out operations outside the shell.
|
||||||
|
+/
|
||||||
|
ExternalProcess execute(ref ShellContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
A shell component - which is likely an argument, but that is a semantic distinction we can't make until parsing - may be made up of several lexemes. Think `foo'bar'`. This will extract them from the given array up to and including the next unquoted space or newline char.
|
||||||
|
+/
|
||||||
|
ShellLexeme[] nextComponent(ref ShellLexeme[] lexemes) {
|
||||||
|
if(lexemes.length == 0)
|
||||||
|
return lexemes[$ .. $];
|
||||||
|
|
||||||
|
int pos;
|
||||||
|
while(
|
||||||
|
pos < lexemes.length &&
|
||||||
|
!(
|
||||||
|
// identify an arg or command separator
|
||||||
|
lexemes[pos].quoteStyle == QuoteStyle.none &&
|
||||||
|
(
|
||||||
|
lexemes[pos].l == " " ||
|
||||||
|
lexemes[pos].l == "\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pos == 0)
|
||||||
|
pos++; // include the termination condition as its own component
|
||||||
|
|
||||||
|
auto ret = lexemes[0 .. pos];
|
||||||
|
lexemes = lexemes[pos .. $];
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnvironmentPair {
|
||||||
|
string environmentVariableName;
|
||||||
|
string assignedValue;
|
||||||
|
|
||||||
|
string toString() {
|
||||||
|
return environmentVariableName ~ "=" ~ assignedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string expandSingleArg(string escapedArg, ShellContext context) {
|
||||||
|
return escapedArg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Parses a set of lexemes into set of command objects.
|
||||||
|
|
||||||
|
This function in pure in all but formal annotation; it does not interact with the outside world, except through the globber delegate you provide (which should not make any changes to the outside world!).
|
||||||
|
+/
|
||||||
|
ShellCommand[] parseShellCommand(ShellLexeme[] lexemes, ShellContext context, Globber globber) {
|
||||||
|
ShellCommand[] ret;
|
||||||
|
|
||||||
|
ShellCommand currentCommand;
|
||||||
|
ShellCommand firstCommand;
|
||||||
|
|
||||||
|
while(lexemes.length) {
|
||||||
|
auto component = nextComponent(lexemes);
|
||||||
|
if(component.length) {
|
||||||
|
if(currentCommand is null)
|
||||||
|
currentCommand = new ShellCommand();
|
||||||
|
if(firstCommand is null)
|
||||||
|
firstCommand = currentCommand;
|
||||||
|
|
||||||
|
/+
|
||||||
|
Command syntax in bash is basically:
|
||||||
|
|
||||||
|
Zero or more `ENV=value` sets, separated by whitespace, followed by zero or more arg things.
|
||||||
|
OR
|
||||||
|
a shell builtin which does special things to the rest of the command, and may even require subsequent commands
|
||||||
|
|
||||||
|
Argv[0] can be a shell built in which reads the rest of argv separately. It may even require subsequent commands!
|
||||||
|
|
||||||
|
For some shell built in keywords, you should not actually do expansion:
|
||||||
|
$ for $i in one two; do ls $i; done
|
||||||
|
bash: `$i': not a valid identifier
|
||||||
|
|
||||||
|
So there must be some kind of intermediate representation of possible expansions.
|
||||||
|
|
||||||
|
|
||||||
|
BUT THIS IS MY SHELL I CAN DO WHAT I WANT!!!!!!!!!!!!
|
||||||
|
+/
|
||||||
|
|
||||||
|
enum ParseState {
|
||||||
|
lookingForVarAssignment,
|
||||||
|
lookingForArg,
|
||||||
|
}
|
||||||
|
ParseState parseState = ParseState.lookingForVarAssignment;
|
||||||
|
|
||||||
|
bool thisWasEnvironmentPair = false;
|
||||||
|
EnvironmentPair environmentPair;
|
||||||
|
bool thisWasRedirection = false;
|
||||||
|
bool thisWasPipe = false;
|
||||||
|
string arg;
|
||||||
|
|
||||||
|
if(component.length == 0) {
|
||||||
|
// nothing left, should never happen
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(component.length == 1) {
|
||||||
|
if(component[0].quoteStyle == QuoteStyle.none && component[0].l == " ") {
|
||||||
|
// just an arg separator
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(lexeme; component) {
|
||||||
|
again:
|
||||||
|
final switch(parseState) {
|
||||||
|
case ParseState.lookingForVarAssignment:
|
||||||
|
if(thisWasEnvironmentPair) {
|
||||||
|
environmentPair.assignedValue ~= lexeme.toEscapedFormat();
|
||||||
|
} else {
|
||||||
|
// assume there is no var until we prove otherwise
|
||||||
|
parseState = ParseState.lookingForArg;
|
||||||
|
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||||
|
foreach(idx, ch; lexeme.l) {
|
||||||
|
if(ch == '=') {
|
||||||
|
// actually found one!
|
||||||
|
thisWasEnvironmentPair = true;
|
||||||
|
environmentPair.environmentVariableName = lexeme.l[0 .. idx];
|
||||||
|
environmentPair.assignedValue = lexeme.l[idx + 1 .. $];
|
||||||
|
parseState = ParseState.lookingForVarAssignment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(parseState == ParseState.lookingForArg)
|
||||||
|
goto case;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ParseState.lookingForArg:
|
||||||
|
if(lexeme.quoteStyle == QuoteStyle.none) {
|
||||||
|
if(lexeme.l == "<" || lexeme.l == ">")
|
||||||
|
thisWasRedirection = true;
|
||||||
|
if(lexeme.l == "|")
|
||||||
|
thisWasPipe = true;
|
||||||
|
}
|
||||||
|
arg ~= lexeme.toEscapedFormat();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(thisWasEnvironmentPair) {
|
||||||
|
environmentPair.assignedValue = expandSingleArg(environmentPair.assignedValue, context);
|
||||||
|
currentCommand.environmentPairs ~= environmentPair;
|
||||||
|
} else if(thisWasRedirection) {
|
||||||
|
// FIXME: read the fd off this arg
|
||||||
|
// FIXME: read the filename off the next arg, new parse state
|
||||||
|
assert(0);
|
||||||
|
} else if(thisWasPipe) {
|
||||||
|
// FIXME: read the fd? i kinda wanna support 2| and such
|
||||||
|
auto newCommand = new ShellCommand();
|
||||||
|
currentCommand.stdout.kind = ShellIo.Kind.pipedCommand;
|
||||||
|
currentCommand.stdout.pipedCommand = newCommand;
|
||||||
|
newCommand.stdin.kind = ShellIo.Kind.pipedCommand;
|
||||||
|
newCommand.stdin.pipedCommand = currentCommand;
|
||||||
|
|
||||||
|
currentCommand = newCommand;
|
||||||
|
} else {
|
||||||
|
currentCommand.argv ~= globber(arg, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(firstCommand)
|
||||||
|
ret ~= firstCommand;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
string[] globber(string s, ShellContext context) {
|
||||||
|
return [s];
|
||||||
|
}
|
||||||
|
ShellContext context;
|
||||||
|
ShellCommand[] commands;
|
||||||
|
|
||||||
|
commands = parseShellCommand(lexShellCommandLine("foo bar"), context, &globber);
|
||||||
|
assert(commands.length == 1);
|
||||||
|
assert(commands[0].argv.length == 2);
|
||||||
|
assert(commands[0].argv[0] == "foo");
|
||||||
|
assert(commands[0].argv[1] == "bar");
|
||||||
|
|
||||||
|
commands = parseShellCommand(lexShellCommandLine("foo bar'baz'"), context, &globber);
|
||||||
|
assert(commands.length == 1);
|
||||||
|
assert(commands[0].argv.length == 2);
|
||||||
|
assert(commands[0].argv[0] == "foo");
|
||||||
|
assert(commands[0].argv[1] == "barbaz");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/+
|
||||||
|
interface OSInterface {
|
||||||
|
setEnv
|
||||||
|
getEnv
|
||||||
|
getAllEnv
|
||||||
|
|
||||||
|
runCommand
|
||||||
|
waitForCommand
|
||||||
|
}
|
||||||
|
+/
|
||||||
|
|
||||||
|
class Shell {
|
||||||
|
protected ShellContext context;
|
||||||
|
|
||||||
|
this() {
|
||||||
|
context.cwd = getCurrentWorkingDirectory().toString;
|
||||||
|
prompt = "[deesh]" ~ context.cwd ~ "$ ";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string prompt;
|
||||||
|
|
||||||
|
protected string[] glob(string s) {
|
||||||
|
string[] ret;
|
||||||
|
getFiles(context.cwd, (string name, bool isDirectory) {
|
||||||
|
if(name.length && name[0] == '.' && (s.length == 0 || s[0] != '.'))
|
||||||
|
return; // skip hidden unless specifically requested
|
||||||
|
if(name.matchesFilePattern(s))
|
||||||
|
ret ~= name;
|
||||||
|
});
|
||||||
|
if(ret.length == 0)
|
||||||
|
return [s];
|
||||||
|
else
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final string[] globberForwarder(string s, ShellContext context) {
|
||||||
|
return glob(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
void dumpCommand(ShellCommand command) {
|
||||||
|
foreach(ep; command.environmentPairs)
|
||||||
|
writeln(ep.toString());
|
||||||
|
writeln(command.argv);
|
||||||
|
if(command.stdout.kind == ShellIo.Kind.pipedCommand) {
|
||||||
|
writeln(" | ");
|
||||||
|
dumpCommand(command.stdout.pipedCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilePath searchPathForCommand(string arg0) {
|
||||||
|
if(arg0.indexOf("/") != -1)
|
||||||
|
return FilePath(arg0);
|
||||||
|
// could also be built-ins and cmdlets...
|
||||||
|
// and on Windows we should check .exe, .com, .bat, or ask the OS maybe
|
||||||
|
|
||||||
|
version(Posix) { // FIXME
|
||||||
|
immutable searchPaths = ["/usr/bin", "/bin", "/usr/local/bin", "/home/me/bin"]; // FIXME
|
||||||
|
foreach(path; searchPaths) {
|
||||||
|
auto t = FilePath(arg0).makeAbsolute(FilePath(path));
|
||||||
|
import core.sys.posix.sys.stat;
|
||||||
|
stat_t sbuf;
|
||||||
|
|
||||||
|
CharzBuffer buf = t.toString();
|
||||||
|
auto ret = stat(buf.ptr, &sbuf);
|
||||||
|
if(ret != -1)
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FilePath(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Windows)
|
||||||
|
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||||
|
string windowsCommandLine;
|
||||||
|
foreach(arg; command.argv) {
|
||||||
|
if(windowsCommandLine.length)
|
||||||
|
windowsCommandLine ~= " ";
|
||||||
|
windowsCommandLine ~= arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fp = searchPathForCommand(command.argv[0]);
|
||||||
|
|
||||||
|
auto proc = new ExternalProcess(fp, windowsCommandLine);
|
||||||
|
command.externalProcess = proc;
|
||||||
|
proc.start;
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Posix)
|
||||||
|
private ExternalProcess startCommand(ShellCommand command, int inheritedPipe, int pgid) {
|
||||||
|
auto fp = searchPathForCommand(command.argv[0]);
|
||||||
|
if(fp.isNull()) {
|
||||||
|
throw new Exception("Command not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
import core.sys.posix.unistd;
|
||||||
|
|
||||||
|
int[2] pipes;
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
auto ret = pipe(pipes);
|
||||||
|
|
||||||
|
setCloExec(pipes[0]);
|
||||||
|
setCloExec(pipes[1]);
|
||||||
|
|
||||||
|
import core.stdc.errno;
|
||||||
|
|
||||||
|
if(ret == -1)
|
||||||
|
throw new ErrnoApiException("stdin pipe", errno);
|
||||||
|
} else {
|
||||||
|
pipes[0] = inheritedPipe;
|
||||||
|
pipes[1] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto proc = new ExternalProcess(fp, command.argv);
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
proc.beforeExec = () {
|
||||||
|
// reset ignored signals to default behavior
|
||||||
|
import core.sys.posix.signal;
|
||||||
|
signal (SIGINT, SIG_DFL);
|
||||||
|
signal (SIGQUIT, SIG_DFL);
|
||||||
|
signal (SIGTSTP, SIG_DFL);
|
||||||
|
signal (SIGTTIN, SIG_DFL);
|
||||||
|
signal (SIGTTOU, SIG_DFL);
|
||||||
|
signal (SIGCHLD, SIG_DFL);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
proc.pgid = pgid; // 0 here means to lead the group, all subsequent pipe programs should inherit the leader
|
||||||
|
// and inherit the standard handles
|
||||||
|
proc.overrideStdinFd = inheritedPipe;
|
||||||
|
proc.overrideStdoutFd = pipes[1];
|
||||||
|
proc.overrideStderrFd = 2;
|
||||||
|
command.externalProcess = proc;
|
||||||
|
proc.start;
|
||||||
|
|
||||||
|
if(command.stdout.pipedCommand) {
|
||||||
|
startCommand(command.stdout.pipedCommand, pipes[0], pgid ? pgid : proc.pid);
|
||||||
|
|
||||||
|
// we're done with them now
|
||||||
|
close(pipes[0]);
|
||||||
|
close(pipes[1]);
|
||||||
|
pipes[] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int waitForCommand(ShellCommand command) {
|
||||||
|
command.externalProcess.waitForCompletion();
|
||||||
|
writeln(command.externalProcess.status);
|
||||||
|
if(auto cmd = command.stdout.pipedCommand)
|
||||||
|
waitForCommand(cmd);
|
||||||
|
return command.externalProcess.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void execute(string commandLine) {
|
||||||
|
auto commands = parseShellCommand(lexShellCommandLine(commandLine), context, &globberForwarder);
|
||||||
|
foreach(command; commands)
|
||||||
|
try {
|
||||||
|
//dumpCommand(command);
|
||||||
|
|
||||||
|
version(Posix) {
|
||||||
|
import core.sys.posix.unistd;
|
||||||
|
import core.sys.posix.signal;
|
||||||
|
|
||||||
|
auto proc = startCommand(command, 0, 0);
|
||||||
|
|
||||||
|
// put the child group in control of the tty
|
||||||
|
ErrnoEnforce!tcsetpgrp(1, proc.pid);
|
||||||
|
kill(-proc.pid, SIGCONT); // and if it beat us to the punch and is waiting, go ahead and wake it up (this is harmless if it is already running
|
||||||
|
waitForCommand(command);
|
||||||
|
// reassert control of the tty to the shell
|
||||||
|
ErrnoEnforce!tcsetpgrp(1, getpid());
|
||||||
|
}
|
||||||
|
|
||||||
|
version(Windows) {
|
||||||
|
auto proc = startCommand(command, 0, 0);
|
||||||
|
waitForCommand(command);
|
||||||
|
}
|
||||||
|
} catch(ArsdExceptionBase e) {
|
||||||
|
string more;
|
||||||
|
e.getAdditionalPrintableInformation((string name, in char[] value) {
|
||||||
|
more ~= ", ";
|
||||||
|
more ~= name ~ ": " ~ value;
|
||||||
|
});
|
||||||
|
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.message, more);
|
||||||
|
} catch(Exception e) {
|
||||||
|
writeln("deesh: ", command.argv.length ? command.argv[0] : "", ": ", e.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
Constructs an instance of [arsd.terminal.LineGetter] appropriate for use in a repl for this shell.
|
||||||
|
+/
|
||||||
|
auto constructLineGetter()() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/+
|
||||||
|
Parts of bash I like:
|
||||||
|
|
||||||
|
glob expansion
|
||||||
|
! command recall
|
||||||
|
redirection
|
||||||
|
f{1..3} expand to f1 f2 f3. can add ..incr btw
|
||||||
|
f{a,b,c} expand to fa fb fc
|
||||||
|
for i in *; do cmd; done
|
||||||
|
`command expansion`. also $( cmd ) is a thing
|
||||||
|
~ expansion
|
||||||
|
|
||||||
|
foo && bar
|
||||||
|
foo || bar
|
||||||
|
|
||||||
|
$(( maybe arithmetic but idk ))
|
||||||
|
|
||||||
|
ENV=whatever cmd.
|
||||||
|
$ENV ...?
|
||||||
|
|
||||||
|
tab complete!
|
||||||
|
|
||||||
|
PATH lookup. if requested.
|
||||||
|
|
||||||
|
Globbing could insert -- before if there's any - in there.
|
||||||
|
|
||||||
|
Or better yet all the commands must either start with ./
|
||||||
|
or be found internally. Internal can be expanded by definition
|
||||||
|
files that tell how to expand the real thing.
|
||||||
|
|
||||||
|
* flags
|
||||||
|
* arguments
|
||||||
|
* command line
|
||||||
|
* i/o
|
||||||
|
+/
|
||||||
Loading…
Reference in New Issue