Sort-of ... lousy description on my part. I can see the command interpreter
as a state machine consisting of a current object + the available commands
on the current object. I'll start with the latter.
The available commands on the current object could change as you do things
to it. Suppose each of its commands has some declared pre-condition. A
command is disabled if its precondition is false. Here is one of many ways
to do this.
class PhoneBook < Cmd
# ... define commands & docs
pre :add { |pb| pb.condition_on_phonebook_add }
pre :remove { |pb| pb.condition_on_phonebook_remove }
end
All commands whose pre is true are 'enabled', all others are 'greyed-out'.
Preconditions could have a descriptive string for help purposes.
Perhaps the already existent precmd and postcmd methods could be extracted
into something that was a bit more aware and versatile. Like above where
rather than having precmd be a hook that is called before all commands, the
pre macro here would set up a pre condition for the specified command. But
the example above is about what commands you are then afforded, which sort of
translates to what commands the completion proc will display you and not much
else that I can think of. I imagine the notion of setting pre condition hooks
on a per command basis could be generalized to offer more than just what
subcommands you are then afforded, like 'input actions' in FSM. Sort of seems
to get into the realm of Design by Contract.
Secondly the current object itself could be switched. e.g If you add some
special commands to "cd", "pushd", and "popd" to any command-able object, to
start working in a new command context.
PBook> pushd `find Sam`
- execute command "find Sam" in current context
- perhaps check return (e.g. a Person) is command-able
- make that the new command context, so ...
Sam> first Sammy
last Brown
popd
PBook>
This idea seems quite similar to irb's job management where you can create a
subirb which lets you essentially "enter" the object that you invoke the
subirb session onto. Perhaps rather than having to fall back on some exiting
convention like popd, something sort of like what Luke Graham was suggesting
could happen. A thread with a specialized Cmd. Or perhaps there could be come
simple protocol where by Cmd instances could interact with one another. It
all gets a bit sophisticated after a certain point. I don't know whether this
kind of stuff is better suited built into Cmd or on top of it. But I do
welcome the ideas and look forward to see what bubbles up as I take the time
to think about them.
And, lastly, every command has arguments. Each argument has a range of valid
values that can depend on current state. If you store an array of blocks
that computes the possible values for each argument given the current
context object (when applicable), you can even provide completion for
arguments to commands.
class PhoneBook < Cmd
# can only delete X if X currently in phonebook
command :delete do
args [ {|pb| pb.people.collect { |p| p.name }}
# even better if returned objects, not strings
# using to_s or some protocol to get strings for completion
# command intereter keeps map of string->object
# refuse to execute if no valid completion
# inner code just deals with objects
]
end
end
PBook> delete <TAB>
#--> Sammy Mary
PBook> delete S<TAB>
#--> delete Sammy
More DbC sort of stuff kind of. More simply, this functionality is actually
sort of already implemented. Or at least the blue print is there and a I've
commented on the hypothetical API in the TODO. Customized completion per
command should be as easy as providing a proc or block or method reference or
hard-coded array or whatever that provides a collection of things to complete
against. This can be set for commands, subcommands, etc.
btw: have you considered adding a Command class to keep all info about one
command?
Yes actually. Here is the branch at the point where I abandoned it for the
time being:
http://svn.vernix.org/main/library/cmd/branches/custom-classes-for-commands/lib/cmd.rb
I set out to make cmd way more compositional. There was a Command class, a
Commands container, Shortcut, SubCommand and I was thinking of Documentation.
It would be really useful to be able to have a command object and just ask it
if it has subcommands and get a list of them or ask it for its documentation.
That's what I was aiming for. It ended up just adding way more complexity
(which I was aiming to diminish by taking that approach). It was kludgy and
sucked. This was largely just because I did a bad job of it. I have the
feeling there could be a nice balance, where the composition of objects
approach is succinct and elegant and simple. I just had to back out and
approach it again from a different angle at the time. So after hanging out on
that branch for a few days I just moved back to the original approach. Now
the plan, enumerated in the TODO file, is to have Cmd be a module that you
mix in. When you mix it in you define methods with whatever name you want and
then you a 'command' class method to designate something a command. For
example:
command :foo
def foo
# ...
end
This makes it easy to take code that was developed previously or without Cmd
in mind and stick a Cmd type interface onto it.
Alternatively, there would be some Cmd::Base class that has the Cmd module
mixed in. You could instead inherit from this and rather than use the
'command' class method or have some naming system, work off the convention
that all public methods of the subclass are implicitly commands. After doing
that I think I would tackle wrapping command and subcommand discovery into
specialized instances of stuff like a Command class.
Thanks for the suggestions! Glad you're interested,
marcel