-
Notifications
You must be signed in to change notification settings - Fork 278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Alternative command implementation #95
Comments
Thanks and received! I'll have to spend some more time thinking this over, but this is possibly something we can implement in the future. |
Further thoughts: Instead of using the streams of a FRP library, this should instead use simple node streams. The implementation could actually mirror that of UNIX pipes, with a stdout, a stderr and an exit code. Then, a bridge could be built to Node's Child Process, which could enable running normal commands from vorpal. This implementation of commands with std-streams and exit code should probably go into its own module, which Vorpal then could use. The module would also do the command parsing, handling the piping logic itself, and delegating options to Commander/Minimist/whatever proven library exists. Still lacking the time to implement this (those pesky real life obligations...), but I'll definitely get around to doing this some time. Even if it might be overkill for Vorpal, I find this much too fascinating not to do it. |
Yeah, those darn real-life grumble grumble grumble... :) I'm totally down for streaming. It's kind of embarrassing that Vorpal piping doesn't use streams. Whenever you get around to it, throw together some concepts and then we'll go over it. |
Basic API sketch (without asynchronous support): The functions defined here would be passed to some constructor. // A fizzbuzz command.
var init = function () {
this.fizzCount = 0;
};
var onInput = function (input) {
var number = parseInt(input);
if (Number.isNaN(number)) {
// method provided by library
this.stdErr("invalid input, expected a number");
} else {
var fb = "";
if (number % 3 === 0) {
fb += "Fizz";
this.fizzCount++;
}
if (number % 5 === 0) {
fb += "Buzz";
}
// method provided by library
this.stdOut(fb === "" ? number : fb);
}
};
var onInputExit = function (code) {
this.stdOut("fizzCount: " + this.fizzCount);
// method provided by library
this.exit(0);
}; Expected outputs:
// A counter command.
var init = function () {
var delta = 1;
// options object provided by library
if (this.options.from > this.options.to) {
delta = -1;
}
for (var i = this.options.from; i === this.options.to; i += delta) {
if (i !== 12) {
this.stdOut(i);
} else {
this.stdErr("Nah, no 12.");
}
}
this.exit(0);
}; Expected outputs: $ counter --from 3 --to 6
=> 3
=> 4
=> 5
=> 6
$ counter --from 13 --to 11
=> 13
=> Nah, no 12. # evil red error message
=> 11
$ Combining them: $ counter --from 3 --to 6 | fizzbuzz
=> Fizz
=> 4
=> Buzz
=> Fizz
=> fizzCount: 2
$ counter --from 13 --to 11 | fizzbuzz
=> 13
=> Nah, no 12. # evil red error message
=> 11
=> fizzCount: 0
$ Behind the scenes, each command has three streams, stdin, stdout, and stderr (stdin and stdout can optionally be object streams). stdout is piped to the next command's stdin (or to the higher-level stdout). stdin is listened on to call Stuff to consider: How to handle asynchronous actions.
Substitute "promise" for "callback" as preferred. The code managing the command lifecycles should be able to interrupt commands (analogy to SIGINT). Maybe an event system is needed to notify the commands. Anyways, doing more pesky real life grumbling for now =) |
Any ideas for the SIGINT-equivalent? The framework should be able to stop a running command, but forcing the interruption does not sound like a good approach. Maybe add a Also I'd gladly take better naming suggestions for the functions (or the module itself for that matter) - you are far better with (English) words than I am... |
First of all, I'm not expecting immediate feedback on this, I suppose cash keeps you pretty busy right now. I'm just sharing some of the involved decisions. Because why not? Thoughts on objectMode for the streams:The low-level implementation can do either via options, but perhaps Vorpal should enforce one or the other.
Alternatively, commands could define whether they expect objects or strings. Vorpal could pass the required format (e.g. by using In either case, the developer using vorpal should not have to worry about handling strings as well as objects in the same command. Similiary, should the error stream use strings, Error objects, both? Again the low-level implementation should allow both, but Vorpal might want to restrict this for consistency and a more convenient API. |
This turned out to be more work than I anticipated, but here is a first implementation. Working version of the above example is here. Most of the complexity will be abstracted away, the end user would only pass three lifecycle methods ( As a next step I'll write something which parses strings like The parser will be independently extendable with redirection, command substitution, here documents etc. Also, tee could be done as a vorpal extension/plugin. A realistic command factory would be given command definitions similiar to the ones currently used in vorpal, parsing the string for options and passing the options for the created instances. The factory would probably be implemented by vorpal (but it can utilize commander or yargs or someting similiar), but the parser and the CommandInstance class would be self-contained modules. All in all still a lot of work to do, but the result should be worth it. |
You're overwhelming me with good ideas! Great work, will try to take a thorough look at this as soon as I can. First priority currently is closing existing issues, so we have a clean base for some redesigns. |
Okay, I looked at this a bit, it actually looks really good. The most important thing though, as you said, very simply abstracting it for the user, and making it behave similarly to existing Vorpal commands. For example,
vorpal.command('foo')
.action(action)
.cleanup(cleanup);
vorpal.command('foo')
.action(function () {
this.on('data', function() {
// piped data.
});
}); Can we discuss the end-user API on this? |
Abstraction HierarchyI'll outline a full abstraction hierarchy for implementing all this stuff in this comment. See comments below for API discussion. Command InstanceNot going to summarize this again, just check the readme. Command ChainThe The Stream redirection belongs on this level as well. The implementation might need to not only operate on ParserThe ping -c1 8.8.8.8 || {echo 'no internet connection'; ls /etc/netctl | grep wlp2s0} The command strings in here are Coding all this will take some work, but we can start by just implementing piping - that's all vorpal currently does as well. But by writing a parser which builds a syntax tree, all of this becomes doable. And the implementation is abstracted away in the The parser needs to deal with command strings, because the Vorpal ImplementationVorpal lets the parser build Vorpal's
The information from all This would create a bare-bones factory, but vorpal can enhance it with automatic help etc. |
CommandInstance discussionI'll need to adjust handling initialization and the I could switch the implementation to use callbacks instead of promises. Promise resolution and rejection are handled the same anyways, they just indicate that the lifecycle method is done with its potentially async action. Errors need to be signaled with Handling OperandsStill unsure about how to deal with operands. The current implemention locks in the command options, and then expects all operands to be provided via stdin. So
This is equivalent to The big advantage of this is that a CommandInstance can treat operands given via the command line and piped operands exactly the same. Or more accurately, it doesn't even know that an operand might have come from the command line instead of from another CommandInstance (or really any stream whatsoever). I like this behavior a lot. However: What happens with a command like I'd really appreciate thoughts on or suggestions for this. |
Vorpal Command API DiscussionEverything below is of course just a suggestion. Point of view is that of the To recap what information we need:
Determining which command to instantiate:Strategy:Interpret the first part of the command string as the name. Dispatch which CommandInstance to create based on a map of names. Fail if no command of this name is found. API:
Parse and set optionsStrategy:Define which options the command takes, and what kind of options they are. Pass the parsing of the command string and the actual creation of an options hash to library like commander or yargs. API:Can stay the same. Read in operandsStrategy:Pass the operands of a command to its stdin after its initialization API:None, this happens implicitely. Lifecycle:Strategy:Allow the user to pass a function, or supply a default no-op one (e.g. API:
Example UsageTrivial command: vorpal.command('name'); A simple command: vorpal.command('reverse').action(function (input) {
// don't reverse strings like this at home!
this.log(input.split('').reverse().join(''););
return Promise.resolve();
}); The FizzBuzz command from the CommandInstance example: vorpal.command('fizzbuzz')
.init(function () {
this.fizzCount = 0;
return Promise.resolve();
}).action(function (input) {
// nobody wants to read another FizzBuzz implementation
// returns a promise
return doTheFizzBuzz(input);
}).cleanup(function () {
this.log('fizzCount: ' + this.fizzCount );
return Promise.resolve();
}); Example with options: vorpal.command('repeat')
.option('-a, --amount <int>', 'How often to repeat.')
.action(function (input) {
for (var i = 0; i < this.options.amount; i++) {
this.log(input);
}
return Promise.resolve();
}); Always returning Stream handlingAn interesting question is how vorpal should handle the stream options. Commands could output Buffers, Strings in various encodings, these could be automatically decoded, or object mode could be used. Vorpal coul either restrict this to one, or default to one but allow usage of others. No idea for the API for that though... |
Can we just refer to Okay, I'm really liking all of this. Here's the deal: I want to model Vorpal into a full bash interpreter (as you are saying). This would mean we would match all syntax and functionality of bash (including It is very possible that Cash is going to be packaged into NPM as a cross-platform Unix shell, and this update would make this possible. I think this should release as Vorpal 2.0. If I added you as a collaborator to Vorpal, and created a 2.0 branch, do you think we could start rolling on all of this? Is this something you could dedicate some time to? I would ideally love to do this as soon as possible. |
Okay you have access to the repo now, and I created a |
Sure. Additionally, I would like commands to support Another thing is that I would like actions to return status code ( |
btw, what time zone are you in (I'm PST). We seem to have flipped schedules, which is going to suck. |
Because it would be very elegant to handle operands by piping them into stdin after initialization. Disregarding exceptions like grep, most commands want to handle stdin as if it just contained additional operands, which happen to be supplied asynchronously. That way, our user has to define only one function which handles operands as well als stdin. This is different from the way UNIX commands work. E.g. However, if we actually try to copy bash, this would have to be handled differently. And since commands like grep require an operand as well as stdin, the current CommandInstance implementation will need to be changed anyways. I suppose CommandInstance should just mimic UNIX commands and vorpal has to figure out how to provide a convenient API for command definition.
Sure, sounds reasonable.
A full bash iterpreter is going to be a lot of work. Also a pretty interesting challenge. I'd suggest writing a jison grammar with the most basic features and then gradually adding to it.
The current CommandInstance implementation already has these, exit code and message are given witht the
I'm in CET, but look on the bright side: 24h/day of combined vorpalness... Ok yeah, this sucks.
It is something I want to dedicate some time to. Currently writing my bachelor's thesis tough, so working on open source projects obviously is a lower priority. Then again, I don't spend all my waking hours on the thesis anyways, so I will get some coding done. And I will put aside some other projects/ideas of mine for this. So yes, count me in. |
Also, we can move the CommandInstance specific discussion to its repo, and focus on the vorpal API here. |
As for the bash reimplementation, I found these two projects, which may or may not be helpful. The bash manual Do you think a full reimplementation is necessary and realistically possible? Bash essentially provides a whole programming language. A bash-inspired custom syntax or a strict subset of bash might be the way to go. Just read most of the bash manual, these are my impressions:
A first suggestion for the subset to implement: Section Core features:
These core features still ignore much of what bash can do, but they should suffice for realistic non-scripting usage. Not only for vorpal, but also for advanced cash usage. And while this will definitely take some work, it is completely doable. I'd argue to leave out compound commands (basically control structures) Also worth a look are the built-in commands, since some of these are mirrored in vorpal's api. |
Wow - it is surprisingly readable. How pleasantly rare. I think you nailed it on what's needed, I was about to say the same things. Getting through the manual myself, will update you in a bit. |
As a sequence of actions, I think I'm going to start working on the shell parser / AST. If you continue on the control flow ( |
Ok, my next step after polishing A At a later point, the But before coding another line of javascript I have to take care of some other stuff first, which might take a few days. I'll still read everything and try to respond, though. If you haven't built a parser/ast before, I recommend taking a look at this tutorial for a nice explanation of the manual process in js, and jison for automatic parser generation from a grammar (which is the approach I'd prefer). Or you might know this stuff already =) |
Thanks.
Let me know when your time frees up a bit! |
Sweet! I spent some time tinkering with the babel api, and I'm tempted to try the following for a basic shell implementation:
I'm not sure whether that is a very practical approach, but the idea is cool enough to build a prototype for. For vorpal, building on js-shell-parse should be the better plan. As for useful stuff, I'll soon upload a process class with an API similiar to node's |
Okay, awesome. Maybe we should focus on the plan in line with Vorpal, just because its already a huge amount of work, and then once we have a proof of concept we can turn it into tons of powerful offshoots, like the babel one (which sounds like a cool idea!). My thought is:
|
Just uploading this before going to bed, without finished tests or documentation: pseudo-process. This could represent commands internally, with an API similiar to node's ChildProcess. Instances of PseudoCommand have methods to pipe and to redirect input and output. Also has a function to construct PseudoCommands from actual ChildProcesses. |
Very nice! |
These are just some thoughts about an alternative way to implement commands, no actual implementation done. It might be overkill, result in a weird API, or plain unnecessary, but I might as well post this here. Idea came after reading this introduction to frp. I hope this is not just incoherent mumbling, but actually understandable.
Basic concept: Commands communicate via frp streams. Each instance of a command has an input stream and an output stream. When a user types and activates a command, an instance of the command is created, based on the given options. The actual argument is not used in the creation of the command instance. Then, the argument(s) are passed to the command's input stream. The command does some stuff, then writes the output through it's output stream. The output stream is listened to by vorpal, and by default the output is printed into the console. When the command is done, it ends the stream, which vorpal interprets as if the command's callback in the current implementation was called, returning focus to the user.
Command piping could easily be implemented by piping the output stream of the first command into the input stream of the second one. Unlike the current implementation, commands would run asynchronously. This way, two commands separated by
;
could be started simultaneously.Commands could also emit errors. This would allow implementing
&&
and||
for command chaining. One could even go further and use2>>
to only pass on errors etc.Parsing a command string from the user could be done in two stages: First, separate actual commands and chaining operators via regexes. Second, create command instances and correctly chain the streams together. By separating this, parsing the command options could be delegated to one of the existing node modules.
edit: Parsing using regexes is not a good idea...
Command definition would be done by giving methods handling arguments from the input stream, errors from the input stream, and end of the input stream (vorpal could provide a sensible default for errors and end of stream), instead of
command.action()
.Pros:
Cons:
I currently don't have time to spend on this, but I wanted to get the idea out there. And if this is not just garbage, I could start working on this later - no need to hurry...
The text was updated successfully, but these errors were encountered: