![]() |
![]() | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
|
Welcome to Vista Forums we are your forum to discuss Windows Vista x64 and x86 systems. Whether you need help or just want to post an idea you have on Vista, this is the forum for you.
br> br> |
| |||||||
![]() |
| | Thread Tools | Display Modes |
| | #1 (permalink) |
| Guest | Tab expansion extensions Hi everyone, I was browsing through some of the tab expansion work that people have been doing, and the big thing I noticed was the lack of composability. There mostly seems to be a focus on adding new features, rather than on organizing the features we already have in such a way that things can be simply and easily added. The result is that if I want to write a new tab-expansion feature, I end up appending it to a switch -regex statement that just keeps getting bigger and bigger as more and more people contribute to it. I spent some time putting together a management infrastructure that I think is pretty interesting, and I was hoping to get some feedback from the community on how useful people think this sort of construct might be. This is going to be pretty long, but I think it's worth it. The version of tabExpansion that's in my profile right now looks like this: param($line, $lastWord) & { # extract the command name from the string # first split the string into statements and pipeline elements # This doesn't handle strings however. $cmdlet = [regex]::Split($line, '[|;]')[-1] # Extract the trailing unclosed block e.g. ls | foreach { cp if ($cmdlet -match '\{([^\{\}]*)$') { $cmdlet = $matches[1] } # Extract the longest unclosed parenthetical expression... if ($cmdlet -match '\(([^()]*)$') { $cmdlet = $matches[1] } $command = $cmdlet; if ($command -eq $lastWord) { $command = ''; } else { if ($lastWord -ne '') { # Store the arguments passed to the cmdlet separately, minus the last word $command = $command.Remove($command.Length - $lastWord.Length - 1).Trim() }} # take the first space separated token of the remaining string # as the command to look up. Trim any leading or trailing spaces # so you don't get leading empty elements. $cmdlet = $cmdlet.Trim().Split()[0] if ($cmdlet -ne $line) { # Remove the name of the cmdlet itself from the argument string $command = $command.Substring($cmdlet.Length).Trim() } else { $command = ''; } # now get the info object for it... $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0] # loop resolving aliases... while ($cmdlet.CommandType -eq 'alias') { $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet.Definition)[0] } $xml = load-xml (join-path (split-path -parent $profile) "expansions") $xml.root.expansion | where { invoke-expression $_.rule } | foreach { invoke-expression $_.script } } | sort | unique The load-xml function is a helper that looks like this: param($path) [xml](dir (join-path $path "*.xml") | foreach-object ` -begin { [string]$result = "" } ` { $result += (gc $_) } ` -end { "<root>$($result)</root>" }) The purpose of load-xml is to walk through all the .xml files in a directory, concatenate their contents together, and wrap them in a global root element -- essentially, merge multiple XML files into one. This model gives us a very convenient way of adding new "modules" -- just drop an XML file that contains them into the appropriate folder. If you look at tabExpansion, you'll see that I'm loading all the XML files in a folder called "expansions" that's in the same folder as the profile file. The location of these files and the loading mechanism are purely place-holder, but they get the point across. Once I've loaded the XML files, I search through them for <expansion> elements whose rule attribute, when invoked as a PSH expression, returns true. Any such elements that I find, I invoke their <script> elements as PSH expressions. Before writing new extensions, we need to make sure that the existing functionality continues to work. One of the existing features in the default tabExpansion function is tab-completion on the names of parameters passed to cmdlets. This can be implemented in this system by adding a .xml file to the "expansions" folder (you can name the XML file anything you want, of course) with the following content: <expansion rule="$lastWord -match '(^.*)(\$(\w|\.)+)\.(\w*)$'"> <script> $method = [Management.Automation.PSMemberTypes] 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'; $base = $matches[1]; $expression = $matches[2]; Invoke-Expression ('$val=' + $expression); $pat = $matches[4] + '*'; Get-Member -inputobject $val $pat | sort membertype,name | where { $_.name -notmatch '^[gs]et_'} | foreach { if ($_.MemberType -band $method) { $base + $expression + '.' + $_.name + '('; } else { $base + $expression + '.' + $_.name; } } </script> </expansion> The rule attribute on the expansion element (all element and attribute names are completely place-holder, by the way) replaces the entry in that big switch -regex in the built-in function, and the <script> element contains the PSH script that will generate the list of alternatives we want. That's pretty much it...now instead of editing one big function that has a bunch of switch statements in it, you can edit a bunch of small, self-contained XML files to add new features. Of course, if loading all those files on pressing tab becomes a performance problem, the XML can easily be pre-loaded and cached at the beginning of the session. I simply prefer to load them dynamically on demand because I'm not seeing a noticeable delay and because it makes it easier to test changes to the files. To me, the coolest feature that this enables is the ability to easily tweak bits and pieces of the algorithm. The thing that started me down this path was that I was manipulating services and I thought, "Man, wouldn't it be great if I could just type start-service, and start tabbing through the list of services installed on my machine?" The answer of course is yes, which brings me to commandParameter.xml, which I've dropped into the expansions folder: <expansion rule="$command -match '-(\w+)$'"> <script> $paramName = $matches[1]; $_xml = load-xml (join-path (split-path -parent $profile) commands); $pattern = $lastWord + '*'; foreach ($cmd in $_xml.root.command | where {$cmdlet -like $_.pattern}) { foreach ($param in $cmd.parameter | where { $paramName -like $_.name}) { invoke-expression $param.script; } } </script> </expansion> The concept is very similar to tabExpansion itself. It loads all the XML files in a folder (this time the "commands" folder instead of "expansions"), walks through the resulting XML looking for applicable nodes -- in this case, <command> elements with a "pattern" attribute that matches the name of the cmdlet we found, then walks through each <parameter> element in that <command> element looking for matching names. For each one that it finds, it runs the corresponding script. Now, all that's left is to implement some custom parameter completion... <command pattern="*-service"> <parameter name="Name"> <script> get-service $pattern | foreach { $_.Name }; </script> </parameter> </command> Drop that file in our "commands" folder, and we've achieved the original goal: typing any cmdlet name that ends with "-service" and asking for tab completion on the "-Name" parameter will allow us to tab through the list of services available on the system. Note that if we wanted, we could specialize this even further. For example, the service lifecycle commands are each only valid for services in a particular state: you can't start a service that's already running. We could easily have separate logic for start-service, stop-service, etc that only returns the services that actually make sense in their current state...this model of breaking logic up by natural category lends itself quite easily to that sort of thing. There are other useful things to do here as well...for example, I have a <command> element that provides tab-completion on any parameter whose type is an enum. I also have the ability to tab through valid help topics by typing "get-help -name [tab]" -- the first option in that list is "about_alias", and the last is "Write-Warning". I can post the rest of the stuff I've done so far later, if there's a favourable response. The biggest real drawback to the current implementation is that it doesn't support sequential parameters -- I suspect that this could be done by breaking the parameters into space-delimited tokens (tricky with embedded parentheses and the like, but probably doable) and then using the CmdletInfo to figure out the name of the property and set $propName to that. Pretty simple in theory, but I think breaking the string into those tokens will probably be more trouble than it's really worth. If anyone wants to tackle it though, please feel free to do so and post the results. Like I said earlier, feedback is more than welcome -- I think this could be the beginnings of a pretty useful way to organize this sort of thing, but I want to hear what others have to say about it. Known caveats if you want to play around with this stuff: 1. invoke-expression does not support comments, and is much more rigid than normal scripts. For example, every statement has to end with a semi-colon. 2. If you want to put multiple <expansion> or <command> elements in one file, the current design has you writing non-well-formed XML files with multiple root elements. This is something that should probably be addressed later, but for now doesn't really bother me. 3. Remember that all these things are sharing the same variable space. I have a variable called "$xml" in the main function, so I have to call a similar variable "$_xml" in the command parameter bit in order to avoid clobbering the original value. Again, something that would want to be addressed in the future, but can be pretty easily worked around for now. That's about it, really. I've been having fun writing these little parameter completion extensions...hopefully some of you will find it fun and interesting, too. Looking back at the length of this post, I'll consider myself lucky if anyone even reads all the way to the end :-) Thanks in advance! -- Ryan Milligan |
My System Specs![]() |
| | #2 (permalink) |
| Guest | RE: Tab expansion extensions I read it to the end! Looks like a good idea - theres a lot in there to digest -- Richard Siddaway Please note that all scripts are supplied "as is" and with no warranty Blog: http://richardsiddaway.spaces.live.com/ PowerShell User Group: http://www.get-psuguk.org.uk "Ryan Milligan" wrote: > Hi everyone, I was browsing through some of the tab expansion work that > people have been doing, and the big thing I noticed was the lack of > composability. There mostly seems to be a focus on adding new features, > rather than on organizing the features we already have in such a way that > things can be simply and easily added. The result is that if I want to write > a new tab-expansion feature, I end up appending it to a switch -regex > statement that just keeps getting bigger and bigger as more and more people > contribute to it. I spent some time putting together a management > infrastructure that I think is pretty interesting, and I was hoping to get > some feedback from the community on how useful people think this sort of > construct might be. This is going to be pretty long, but I think it's worth > it. > > The version of tabExpansion that's in my profile right now looks like this: > > param($line, $lastWord) & { > # extract the command name from the string > # first split the string into statements and pipeline elements > # This doesn't handle strings however. > $cmdlet = [regex]::Split($line, '[|;]')[-1] > > # Extract the trailing unclosed block e.g. ls | foreach { cp > if ($cmdlet -match '\{([^\{\}]*)$') > { > $cmdlet = $matches[1] > } > > # Extract the longest unclosed parenthetical expression... > if ($cmdlet -match '\(([^()]*)$') > { > $cmdlet = $matches[1] > } > > $command = $cmdlet; > if ($command -eq $lastWord) > { > $command = ''; > } > else { if ($lastWord -ne '') > { > # Store the arguments passed to the cmdlet separately, minus the > last word > $command = $command.Remove($command.Length - $lastWord.Length - > 1).Trim() > }} > > # take the first space separated token of the remaining string > # as the command to look up. Trim any leading or trailing spaces > # so you don't get leading empty elements. > $cmdlet = $cmdlet.Trim().Split()[0] > > if ($cmdlet -ne $line) > { > # Remove the name of the cmdlet itself from the argument string > $command = $command.Substring($cmdlet.Length).Trim() > } > else > { > $command = ''; > } > > # now get the info object for it... > $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0] > > # loop resolving aliases... > while ($cmdlet.CommandType -eq 'alias') { > $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet.Definition)[0] > } > > $xml = load-xml (join-path (split-path -parent $profile) "expansions") > $xml.root.expansion | where { invoke-expression $_.rule } | foreach { > invoke-expression $_.script } > } | sort | unique > > The load-xml function is a helper that looks like this: > > param($path) [xml](dir (join-path $path "*.xml") | foreach-object ` > -begin { [string]$result = "" } ` > { $result += (gc $_) } ` > -end { "<root>$($result)</root>" }) > > The purpose of load-xml is to walk through all the .xml files in a > directory, concatenate their contents together, and wrap them in a global > root element -- essentially, merge multiple XML files into one. This model > gives us a very convenient way of adding new "modules" -- just drop an XML > file that contains them into the appropriate folder. If you look at > tabExpansion, you'll see that I'm loading all the XML files in a folder > called "expansions" that's in the same folder as the profile file. The > location of these files and the loading mechanism are purely place-holder, > but they get the point across. Once I've loaded the XML files, I search > through them for <expansion> elements whose rule attribute, when invoked as > a PSH expression, returns true. Any such elements that I find, I invoke > their <script> elements as PSH expressions. > > Before writing new extensions, we need to make sure that the existing > functionality continues to work. One of the existing features in the default > tabExpansion function is tab-completion on the names of parameters passed to > cmdlets. This can be implemented in this system by adding a .xml file to the > "expansions" folder (you can name the XML file anything you want, of course) > with the following content: > > <expansion rule="$lastWord -match '(^.*)(\$(\w|\.)+)\.(\w*)$'"> > <script> > $method = [Management.Automation.PSMemberTypes] > 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'; > $base = $matches[1]; > $expression = $matches[2]; > Invoke-Expression ('$val=' + $expression); > $pat = $matches[4] + '*'; > Get-Member -inputobject $val $pat | sort membertype,name | > where { $_.name -notmatch '^[gs]et_'} | > foreach > { > if ($_.MemberType -band $method) > { > $base + $expression + '.' + $_.name + '('; > } > else > { > $base + $expression + '.' + $_.name; > } > } > </script> > </expansion> > > The rule attribute on the expansion element (all element and attribute names > are completely place-holder, by the way) replaces the entry in that big > switch -regex in the built-in function, and the <script> element contains > the PSH script that will generate the list of alternatives we want. That's > pretty much it...now instead of editing one big function that has a bunch of > switch statements in it, you can edit a bunch of small, self-contained XML > files to add new features. Of course, if loading all those files on pressing > tab becomes a performance problem, the XML can easily be pre-loaded and > cached at the beginning of the session. I simply prefer to load them > dynamically on demand because I'm not seeing a noticeable delay and because > it makes it easier to test changes to the files. > > To me, the coolest feature that this enables is the ability to easily tweak > bits and pieces of the algorithm. The thing that started me down this path > was that I was manipulating services and I thought, "Man, wouldn't it be > great if I could just type start-service, and start tabbing through the list > of services installed on my machine?" The answer of course is yes, which > brings me to commandParameter.xml, which I've dropped into the expansions > folder: > > <expansion rule="$command -match '-(\w+)$'"> > <script> > $paramName = $matches[1]; > $_xml = load-xml (join-path (split-path -parent $profile) commands); > $pattern = $lastWord + '*'; > foreach ($cmd in $_xml.root.command | where {$cmdlet -like > $_.pattern}) > { > foreach ($param in $cmd.parameter | where { $paramName -like > $_.name}) > { > invoke-expression $param.script; > } > } > </script> > </expansion> > > The concept is very similar to tabExpansion itself. It loads all the XML > files in a folder (this time the "commands" folder instead of "expansions"), > walks through the resulting XML looking for applicable nodes -- in this > case, <command> elements with a "pattern" attribute that matches the name of > the cmdlet we found, then walks through each <parameter> element in that > <command> element looking for matching names. For each one that it finds, it > runs the corresponding script. Now, all that's left is to implement some > custom parameter completion... > > <command pattern="*-service"> > <parameter name="Name"> > <script> > get-service $pattern | foreach { $_.Name }; > </script> > </parameter> > </command> > > Drop that file in our "commands" folder, and we've achieved the original > goal: typing any cmdlet name that ends with "-service" and asking for tab > completion on the "-Name" parameter will allow us to tab through the list of > services available on the system. Note that if we wanted, we could > specialize this even further. For example, the service lifecycle commands > are each only valid for services in a particular state: you can't start a > service that's already running. We could easily have separate logic for > start-service, stop-service, etc that only returns the services that > actually make sense in their current state...this model of breaking logic up > by natural category lends itself quite easily to that sort of thing. > > There are other useful things to do here as well...for example, I have a > <command> element that provides tab-completion on any parameter whose type > is an enum. I also have the ability to tab through valid help topics by > typing "get-help -name [tab]" -- the first option in that list is > "about_alias", and the last is "Write-Warning". I can post the rest of the > stuff I've done so far later, if there's a favourable response. The biggest > real drawback to the current implementation is that it doesn't support > sequential parameters -- I suspect that this could be done by breaking the > parameters into space-delimited tokens (tricky with embedded parentheses and > the like, but probably doable) and then using the CmdletInfo to figure out > the name of the property and set $propName to that. Pretty simple in theory, > but I think breaking the string into those tokens will probably be more > trouble than it's really worth. If anyone wants to tackle it though, please > feel free to do so and post the results. > > Like I said earlier, feedback is more than welcome -- I think this could be > the beginnings of a pretty useful way to organize this sort of thing, but I > want to hear what others have to say about it. Known caveats if you want to > play around with this stuff: > > 1. invoke-expression does not support comments, and is much more rigid than > normal scripts. For example, every statement has to end with a semi-colon. > 2. If you want to put multiple <expansion> or <command> elements in one > file, the current design has you writing non-well-formed XML files with > multiple root elements. This is something that should probably be addressed > later, but for now doesn't really bother me. > 3. Remember that all these things are sharing the same variable space. I > have a variable called "$xml" in the main function, so I have to call a > similar variable "$_xml" in the command parameter bit in order to avoid > clobbering the original value. Again, something that would want to be > addressed in the future, but can be pretty easily worked around for now. > > That's about it, really. I've been having fun writing these little parameter > completion extensions...hopefully some of you will find it fun and > interesting, too. Looking back at the length of this post, I'll consider > myself lucky if anyone even reads all the way to the end :-) Thanks in > advance! > > -- Ryan Milligan > > > |
My System Specs![]() |
| | #3 (permalink) |
| Guest | Re: Tab expansion extensions It's true that it's a bit complicated, but once it's set up...well, I was just loading the snapin I've been using to play around with PSH, and I thought to myself, "Wouldn't it be nice if I could get tab completion on snapin names when I'm loading them? And hey, what if the tab completion only showed snapins that are registered but not loaded when you're adding them, and only showed snapins that are actually loaded when you're removing them?" A few minutes later, I have snapins.xml: <command pattern="add-pssnapin"> <parameter name="Name"> <script> get-pssnapin -registered $pattern | foreach {$_.Name}; </script> </parameter> </command> <command pattern="remove-pssnapin"> <parameter name="Name"> <script> get-pssnapin $pattern | foreach {$_.Name}; </script> </parameter> </command> Drop it into the appropriate folder, and boom -- instant, customized tab completion for those cmdlets, with very little danger of interfering with any of the existing functionality. In fact, because of the way Powershell manages the return pipeline, these things chain together very nicely -- if you have multiple expansion or command elements that all match your input, they'll all have an opportunity to respond, and all the results they return will be made available, after being run through sort and unique to keep them organized. -- Ryan Milligan "RichS" <RichS@discussions.microsoft.com> wrote in message news:101A0A6F-C81B-4003-AA9C-2FE327BAB586@microsoft.com... >I read it to the end! > > Looks like a good idea - theres a lot in there to digest > -- > Richard Siddaway > Please note that all scripts are supplied "as is" and with no warranty > Blog: http://richardsiddaway.spaces.live.com/ > PowerShell User Group: http://www.get-psuguk.org.uk > > > "Ryan Milligan" wrote: > >> Hi everyone, I was browsing through some of the tab expansion work that >> people have been doing, and the big thing I noticed was the lack of >> composability. There mostly seems to be a focus on adding new features, >> rather than on organizing the features we already have in such a way that >> things can be simply and easily added. The result is that if I want to >> write >> a new tab-expansion feature, I end up appending it to a switch -regex >> statement that just keeps getting bigger and bigger as more and more >> people >> contribute to it. I spent some time putting together a management >> infrastructure that I think is pretty interesting, and I was hoping to >> get >> some feedback from the community on how useful people think this sort of >> construct might be. This is going to be pretty long, but I think it's >> worth >> it. >> >> The version of tabExpansion that's in my profile right now looks like >> this: >> >> param($line, $lastWord) & { >> # extract the command name from the string >> # first split the string into statements and pipeline elements >> # This doesn't handle strings however. >> $cmdlet = [regex]::Split($line, '[|;]')[-1] >> >> # Extract the trailing unclosed block e.g. ls | foreach { cp >> if ($cmdlet -match '\{([^\{\}]*)$') >> { >> $cmdlet = $matches[1] >> } >> >> # Extract the longest unclosed parenthetical expression... >> if ($cmdlet -match '\(([^()]*)$') >> { >> $cmdlet = $matches[1] >> } >> >> $command = $cmdlet; >> if ($command -eq $lastWord) >> { >> $command = ''; >> } >> else { if ($lastWord -ne '') >> { >> # Store the arguments passed to the cmdlet separately, minus the >> last word >> $command = $command.Remove($command.Length - $lastWord.Length - >> 1).Trim() >> }} >> >> # take the first space separated token of the remaining string >> # as the command to look up. Trim any leading or trailing spaces >> # so you don't get leading empty elements. >> $cmdlet = $cmdlet.Trim().Split()[0] >> >> if ($cmdlet -ne $line) >> { >> # Remove the name of the cmdlet itself from the argument string >> $command = $command.Substring($cmdlet.Length).Trim() >> } >> else >> { >> $command = ''; >> } >> >> # now get the info object for it... >> $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0] >> >> # loop resolving aliases... >> while ($cmdlet.CommandType -eq 'alias') { >> $cmdlet = @(Get-Command -type 'cmdlet,alias' >> $cmdlet.Definition)[0] >> } >> >> $xml = load-xml (join-path (split-path -parent $profile) >> "expansions") >> $xml.root.expansion | where { invoke-expression $_.rule } | foreach { >> invoke-expression $_.script } >> } | sort | unique >> >> The load-xml function is a helper that looks like this: >> >> param($path) [xml](dir (join-path $path "*.xml") | foreach-object ` >> -begin { [string]$result = "" } ` >> { $result += (gc $_) } ` >> -end { "<root>$($result)</root>" }) >> >> The purpose of load-xml is to walk through all the .xml files in a >> directory, concatenate their contents together, and wrap them in a global >> root element -- essentially, merge multiple XML files into one. This >> model >> gives us a very convenient way of adding new "modules" -- just drop an >> XML >> file that contains them into the appropriate folder. If you look at >> tabExpansion, you'll see that I'm loading all the XML files in a folder >> called "expansions" that's in the same folder as the profile file. The >> location of these files and the loading mechanism are purely >> place-holder, >> but they get the point across. Once I've loaded the XML files, I search >> through them for <expansion> elements whose rule attribute, when invoked >> as >> a PSH expression, returns true. Any such elements that I find, I invoke >> their <script> elements as PSH expressions. >> >> Before writing new extensions, we need to make sure that the existing >> functionality continues to work. One of the existing features in the >> default >> tabExpansion function is tab-completion on the names of parameters passed >> to >> cmdlets. This can be implemented in this system by adding a .xml file to >> the >> "expansions" folder (you can name the XML file anything you want, of >> course) >> with the following content: >> >> <expansion rule="$lastWord -match '(^.*)(\$(\w|\.)+)\.(\w*)$'"> >> <script> >> $method = [Management.Automation.PSMemberTypes] >> 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'; >> $base = $matches[1]; >> $expression = $matches[2]; >> Invoke-Expression ('$val=' + $expression); >> $pat = $matches[4] + '*'; >> Get-Member -inputobject $val $pat | sort membertype,name | >> where { $_.name -notmatch '^[gs]et_'} | >> foreach >> { >> if ($_.MemberType -band $method) >> { >> $base + $expression + '.' + $_.name + '('; >> } >> else >> { >> $base + $expression + '.' + $_.name; >> } >> } >> </script> >> </expansion> >> >> The rule attribute on the expansion element (all element and attribute >> names >> are completely place-holder, by the way) replaces the entry in that big >> switch -regex in the built-in function, and the <script> element contains >> the PSH script that will generate the list of alternatives we want. >> That's >> pretty much it...now instead of editing one big function that has a bunch >> of >> switch statements in it, you can edit a bunch of small, self-contained >> XML >> files to add new features. Of course, if loading all those files on >> pressing >> tab becomes a performance problem, the XML can easily be pre-loaded and >> cached at the beginning of the session. I simply prefer to load them >> dynamically on demand because I'm not seeing a noticeable delay and >> because >> it makes it easier to test changes to the files. >> >> To me, the coolest feature that this enables is the ability to easily >> tweak >> bits and pieces of the algorithm. The thing that started me down this >> path >> was that I was manipulating services and I thought, "Man, wouldn't it be >> great if I could just type start-service, and start tabbing through the >> list >> of services installed on my machine?" The answer of course is yes, which >> brings me to commandParameter.xml, which I've dropped into the expansions >> folder: >> >> <expansion rule="$command -match '-(\w+)$'"> >> <script> >> $paramName = $matches[1]; >> $_xml = load-xml (join-path (split-path -parent $profile) >> commands); >> $pattern = $lastWord + '*'; >> foreach ($cmd in $_xml.root.command | where {$cmdlet -like >> $_.pattern}) >> { >> foreach ($param in $cmd.parameter | where { $paramName -like >> $_.name}) >> { >> invoke-expression $param.script; >> } >> } >> </script> >> </expansion> >> >> The concept is very similar to tabExpansion itself. It loads all the XML >> files in a folder (this time the "commands" folder instead of >> "expansions"), >> walks through the resulting XML looking for applicable nodes -- in this >> case, <command> elements with a "pattern" attribute that matches the name >> of >> the cmdlet we found, then walks through each <parameter> element in that >> <command> element looking for matching names. For each one that it finds, >> it >> runs the corresponding script. Now, all that's left is to implement some >> custom parameter completion... >> >> <command pattern="*-service"> >> <parameter name="Name"> >> <script> >> get-service $pattern | foreach { $_.Name }; >> </script> >> </parameter> >> </command> >> >> Drop that file in our "commands" folder, and we've achieved the original >> goal: typing any cmdlet name that ends with "-service" and asking for tab >> completion on the "-Name" parameter will allow us to tab through the list >> of >> services available on the system. Note that if we wanted, we could >> specialize this even further. For example, the service lifecycle commands >> are each only valid for services in a particular state: you can't start a >> service that's already running. We could easily have separate logic for >> start-service, stop-service, etc that only returns the services that >> actually make sense in their current state...this model of breaking logic >> up >> by natural category lends itself quite easily to that sort of thing. >> >> There are other useful things to do here as well...for example, I have a >> <command> element that provides tab-completion on any parameter whose >> type >> is an enum. I also have the ability to tab through valid help topics by >> typing "get-help -name [tab]" -- the first option in that list is >> "about_alias", and the last is "Write-Warning". I can post the rest of >> the >> stuff I've done so far later, if there's a favourable response. The >> biggest >> real drawback to the current implementation is that it doesn't support >> sequential parameters -- I suspect that this could be done by breaking >> the >> parameters into space-delimited tokens (tricky with embedded parentheses >> and >> the like, but probably doable) and then using the CmdletInfo to figure >> out >> the name of the property and set $propName to that. Pretty simple in >> theory, >> but I think breaking the string into those tokens will probably be more >> trouble than it's really worth. If anyone wants to tackle it though, >> please >> feel free to do so and post the results. >> >> Like I said earlier, feedback is more than welcome -- I think this could >> be >> the beginnings of a pretty useful way to organize this sort of thing, but >> I >> want to hear what others have to say about it. Known caveats if you want >> to >> play around with this stuff: >> >> 1. invoke-expression does not support comments, and is much more rigid >> than >> normal scripts. For example, every statement has to end with a >> semi-colon. >> 2. If you want to put multiple <expansion> or <command> elements in one >> file, the current design has you writing non-well-formed XML files with >> multiple root elements. This is something that should probably be >> addressed >> later, but for now doesn't really bother me. >> 3. Remember that all these things are sharing the same variable space. I >> have a variable called "$xml" in the main function, so I have to call a >> similar variable "$_xml" in the command parameter bit in order to avoid >> clobbering the original value. Again, something that would want to be >> addressed in the future, but can be pretty easily worked around for now. >> >> That's about it, really. I've been having fun writing these little >> parameter >> completion extensions...hopefully some of you will find it fun and >> interesting, too. Looking back at the length of this post, I'll consider >> myself lucky if anyone even reads all the way to the end :-) Thanks in >> advance! >> >> -- Ryan Milligan >> >> >> |
My System Specs![]() |
| | #4 (permalink) |
| Guest | Re: Tab expansion extensions That looks great -- easy extensibility is a great thing to have. About the plugin model, XML is one way to go ... but another more Powershelly alternative might be to let people drop a script into a directory. You could make your function iterate through those scripts, supplying the line and lastword to each of them. If they return anything, then add their output to the tabcompletion results. That provides an extremely clean plugin model with a minimal amount of infrastructure overhead. -- Lee Holmes [MSFT] Windows PowerShell Development Microsoft Corporation This posting is provided "AS IS" with no warranties, and confers no rights. "Ryan Milligan" <Ceiled@hotmail.com> wrote in message news:xZqdnfwzveSdelfYnZ2dnUVZ_oytnZ2d@comcast.com... > It's true that it's a bit complicated, but once it's set up...well, I was > just loading the snapin I've been using to play around with PSH, and I > thought to myself, "Wouldn't it be nice if I could get tab completion on > snapin names when I'm loading them? And hey, what if the tab completion > only showed snapins that are registered but not loaded when you're adding > them, and only showed snapins that are actually loaded when you're > removing them?" A few minutes later, I have snapins.xml: > > <command pattern="add-pssnapin"> > <parameter name="Name"> > <script> > get-pssnapin -registered $pattern | foreach {$_.Name}; > </script> > </parameter> > </command> > <command pattern="remove-pssnapin"> > <parameter name="Name"> > <script> > get-pssnapin $pattern | foreach {$_.Name}; > </script> > </parameter> > </command> > > Drop it into the appropriate folder, and boom -- instant, customized tab > completion for those cmdlets, with very little danger of interfering with > any of the existing functionality. In fact, because of the way Powershell > manages the return pipeline, these things chain together very nicely -- if > you have multiple expansion or command elements that all match your input, > they'll all have an opportunity to respond, and all the results they > return will be made available, after being run through sort and unique to > keep them organized. > > -- Ryan Milligan > > "RichS" <RichS@discussions.microsoft.com> wrote in message > news:101A0A6F-C81B-4003-AA9C-2FE327BAB586@microsoft.com... >>I read it to the end! >> >> Looks like a good idea - theres a lot in there to digest >> -- >> Richard Siddaway >> Please note that all scripts are supplied "as is" and with no warranty >> Blog: http://richardsiddaway.spaces.live.com/ >> PowerShell User Group: http://www.get-psuguk.org.uk >> >> >> "Ryan Milligan" wrote: >> >>> Hi everyone, I was browsing through some of the tab expansion work that >>> people have been doing, and the big thing I noticed was the lack of >>> composability. There mostly seems to be a focus on adding new features, >>> rather than on organizing the features we already have in such a way >>> that >>> things can be simply and easily added. The result is that if I want to >>> write >>> a new tab-expansion feature, I end up appending it to a switch -regex >>> statement that just keeps getting bigger and bigger as more and more >>> people >>> contribute to it. I spent some time putting together a management >>> infrastructure that I think is pretty interesting, and I was hoping to >>> get >>> some feedback from the community on how useful people think this sort of >>> construct might be. This is going to be pretty long, but I think it's >>> worth >>> it. >>> >>> The version of tabExpansion that's in my profile right now looks like >>> this: >>> >>> param($line, $lastWord) & { >>> # extract the command name from the string >>> # first split the string into statements and pipeline elements >>> # This doesn't handle strings however. >>> $cmdlet = [regex]::Split($line, '[|;]')[-1] >>> >>> # Extract the trailing unclosed block e.g. ls | foreach { cp >>> if ($cmdlet -match '\{([^\{\}]*)$') >>> { >>> $cmdlet = $matches[1] >>> } >>> >>> # Extract the longest unclosed parenthetical expression... >>> if ($cmdlet -match '\(([^()]*)$') >>> { >>> $cmdlet = $matches[1] >>> } >>> >>> $command = $cmdlet; >>> if ($command -eq $lastWord) >>> { >>> $command = ''; >>> } >>> else { if ($lastWord -ne '') >>> { >>> # Store the arguments passed to the cmdlet separately, minus the >>> last word >>> $command = $command.Remove($command.Length - $lastWord.Length - >>> 1).Trim() >>> }} >>> >>> # take the first space separated token of the remaining string >>> # as the command to look up. Trim any leading or trailing spaces >>> # so you don't get leading empty elements. >>> $cmdlet = $cmdlet.Trim().Split()[0] >>> >>> if ($cmdlet -ne $line) >>> { >>> # Remove the name of the cmdlet itself from the argument string >>> $command = $command.Substring($cmdlet.Length).Trim() >>> } >>> else >>> { >>> $command = ''; >>> } >>> >>> # now get the info object for it... >>> $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0] >>> >>> # loop resolving aliases... >>> while ($cmdlet.CommandType -eq 'alias') { >>> $cmdlet = @(Get-Command -type 'cmdlet,alias' >>> $cmdlet.Definition)[0] >>> } >>> >>> $xml = load-xml (join-path (split-path -parent $profile) >>> "expansions") >>> $xml.root.expansion | where { invoke-expression $_.rule } | foreach >>> { >>> invoke-expression $_.script } >>> } | sort | unique >>> >>> The load-xml function is a helper that looks like this: >>> >>> param($path) [xml](dir (join-path $path "*.xml") | foreach-object ` >>> -begin { [string]$result = "" } ` >>> { $result += (gc $_) } ` >>> -end { "<root>$($result)</root>" }) >>> >>> The purpose of load-xml is to walk through all the .xml files in a >>> directory, concatenate their contents together, and wrap them in a >>> global >>> root element -- essentially, merge multiple XML files into one. This >>> model >>> gives us a very convenient way of adding new "modules" -- just drop an >>> XML >>> file that contains them into the appropriate folder. If you look at >>> tabExpansion, you'll see that I'm loading all the XML files in a folder >>> called "expansions" that's in the same folder as the profile file. The >>> location of these files and the loading mechanism are purely >>> place-holder, >>> but they get the point across. Once I've loaded the XML files, I search >>> through them for <expansion> elements whose rule attribute, when invoked >>> as >>> a PSH expression, returns true. Any such elements that I find, I invoke >>> their <script> elements as PSH expressions. >>> >>> Before writing new extensions, we need to make sure that the existing >>> functionality continues to work. One of the existing features in the >>> default >>> tabExpansion function is tab-completion on the names of parameters >>> passed to >>> cmdlets. This can be implemented in this system by adding a .xml file to >>> the >>> "expansions" folder (you can name the XML file anything you want, of >>> course) >>> with the following content: >>> >>> <expansion rule="$lastWord -match '(^.*)(\$(\w|\.)+)\.(\w*)$'"> >>> <script> >>> $method = [Management.Automation.PSMemberTypes] >>> 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'; >>> $base = $matches[1]; >>> $expression = $matches[2]; >>> Invoke-Expression ('$val=' + $expression); >>> $pat = $matches[4] + '*'; >>> Get-Member -inputobject $val $pat | sort membertype,name | >>> where { $_.name -notmatch '^[gs]et_'} | >>> foreach >>> { >>> if ($_.MemberType -band $method) >>> { >>> $base + $expression + '.' + $_.name + '('; >>> } >>> else >>> { >>> $base + $expression + '.' + $_.name; >>> } >>> } >>> </script> >>> </expansion> >>> >>> The rule attribute on the expansion element (all element and attribute >>> names >>> are completely place-holder, by the way) replaces the entry in that big >>> switch -regex in the built-in function, and the <script> element >>> contains >>> the PSH script that will generate the list of alternatives we want. >>> That's >>> pretty much it...now instead of editing one big function that has a >>> bunch of >>> switch statements in it, you can edit a bunch of small, self-contained >>> XML >>> files to add new features. Of course, if loading all those files on >>> pressing >>> tab becomes a performance problem, the XML can easily be pre-loaded and >>> cached at the beginning of the session. I simply prefer to load them >>> dynamically on demand because I'm not seeing a noticeable delay and >>> because >>> it makes it easier to test changes to the files. >>> >>> To me, the coolest feature that this enables is the ability to easily >>> tweak >>> bits and pieces of the algorithm. The thing that started me down this >>> path >>> was that I was manipulating services and I thought, "Man, wouldn't it be >>> great if I could just type start-service, and start tabbing through the >>> list >>> of services installed on my machine?" The answer of course is yes, which >>> brings me to commandParameter.xml, which I've dropped into the >>> expansions >>> folder: >>> >>> <expansion rule="$command -match '-(\w+)$'"> >>> <script> >>> $paramName = $matches[1]; >>> $_xml = load-xml (join-path (split-path -parent $profile) >>> commands); >>> $pattern = $lastWord + '*'; >>> foreach ($cmd in $_xml.root.command | where {$cmdlet -like >>> $_.pattern}) >>> { >>> foreach ($param in $cmd.parameter | where { $paramName -like >>> $_.name}) >>> { >>> invoke-expression $param.script; >>> } >>> } >>> </script> >>> </expansion> >>> >>> The concept is very similar to tabExpansion itself. It loads all the XML >>> files in a folder (this time the "commands" folder instead of >>> "expansions"), >>> walks through the resulting XML looking for applicable nodes -- in this >>> case, <command> elements with a "pattern" attribute that matches the >>> name of >>> the cmdlet we found, then walks through each <parameter> element in that >>> <command> element looking for matching names. For each one that it >>> finds, it >>> runs the corresponding script. Now, all that's left is to implement some >>> custom parameter completion... >>> >>> <command pattern="*-service"> >>> <parameter name="Name"> >>> <script> >>> get-service $pattern | foreach { $_.Name }; >>> </script> >>> </parameter> >>> </command> >>> >>> Drop that file in our "commands" folder, and we've achieved the original >>> goal: typing any cmdlet name that ends with "-service" and asking for >>> tab >>> completion on the "-Name" parameter will allow us to tab through the >>> list of >>> services available on the system. Note that if we wanted, we could >>> specialize this even further. For example, the service lifecycle >>> commands >>> are each only valid for services in a particular state: you can't start >>> a >>> service that's already running. We could easily have separate logic for >>> start-service, stop-service, etc that only returns the services that >>> actually make sense in their current state...this model of breaking >>> logic up >>> by natural category lends itself quite easily to that sort of thing. >>> >>> There are other useful things to do here as well...for example, I have a >>> <command> element that provides tab-completion on any parameter whose >>> type >>> is an enum. I also have the ability to tab through valid help topics by >>> typing "get-help -name [tab]" -- the first option in that list is >>> "about_alias", and the last is "Write-Warning". I can post the rest of >>> the >>> stuff I've done so far later, if there's a favourable response. The >>> biggest >>> real drawback to the current implementation is that it doesn't support >>> sequential parameters -- I suspect that this could be done by breaking >>> the >>> parameters into space-delimited tokens (tricky with embedded parentheses >>> and >>> the like, but probably doable) and then using the CmdletInfo to figure >>> out >>> the name of the property and set $propName to that. Pretty simple in >>> theory, >>> but I think breaking the string into those tokens will probably be more >>> trouble than it's really worth. If anyone wants to tackle it though, >>> please >>> feel free to do so and post the results. >>> >>> Like I said earlier, feedback is more than welcome -- I think this could >>> be >>> the beginnings of a pretty useful way to organize this sort of thing, >>> but I >>> want to hear what others have to say about it. Known caveats if you want >>> to >>> play around with this stuff: >>> >>> 1. invoke-expression does not support comments, and is much more rigid >>> than >>> normal scripts. For example, every statement has to end with a >>> semi-colon. >>> 2. If you want to put multiple <expansion> or <command> elements in one >>> file, the current design has you writing non-well-formed XML files with >>> multiple root elements. This is something that should probably be >>> addressed >>> later, but for now doesn't really bother me. >>> 3. Remember that all these things are sharing the same variable space. I >>> have a variable called "$xml" in the main function, so I have to call a >>> similar variable "$_xml" in the command parameter bit in order to avoid >>> clobbering the original value. Again, something that would want to be >>> addressed in the future, but can be pretty easily worked around for now. >>> >>> That's about it, really. I've been having fun writing these little >>> parameter >>> completion extensions...hopefully some of you will find it fun and >>> interesting, too. Looking back at the length of this post, I'll consider >>> myself lucky if anyone even reads all the way to the end :-) Thanks in >>> advance! >>> >>> -- Ryan Milligan >>> >>> >>> > > |
My System Specs![]() |
| | #5 (permalink) |
| Guest | Re: Tab expansion extensions True, I considered doing it that way...the main reason I went with XML instead was to bring more structure to the process of determining if a particular script should be run to find candidates, because otherwise the vast majority of scripts would need to be surrounded with an if statement to determine if they're interested in what the user is typing. Using XML this way means a) most of the time, we can avoid having PSH parse the entire script (loading and running only the initial condition if it's not a script we should run), and b) scripts take on a familiar and well-established structure, which makes it easier to tell at a glance what sorts of scenarios a particular extension is intended to handle. That said, I think either structure is perfectly fine...I would just like to push for something to be adopted on a general level so that people can continue to develop in this area and share their work with others with a level of confidence that they won't be stepping on each other's toes. Most areas of Powershell development are modular by nature -- cmdlets, providers, and extended type information can all be implemented and distributed with minimal impact on each other. Functions are also relatively easy to distribute and share without interfering with each other. However, work on tab expansion features all gets piled into one big function that everyone is editing, with multiple versions being passed around. I think there would be a lot of value in the community at large adopting a more modular model before too much more effort gets poured into it, is all. Also remember that these models can easily be mixed by implementing one of them, and then writing an extension that will implement the other -- a task which is equally easy in both directions. The main point is that adding a new feature should ideally involve adding an extra file, not appending to an existing one. Just today, I was working on a machine that didn't have Powershell installed and I needed to restart a service. Instinctively, I typed "restart-service -name d[tab]"...oops. Having tab completion on incidental parameters is surprisingly addictive, and the easier it is to mix and match and expand these capabilities, the happier I'll be :-) -- Ryan Milligan "Lee Holmes [MSFT]" <lee.holmes@online.microsoft.com> wrote in message news:en%23$Ew9SHHA.3592@TK2MSFTNGP06.phx.gbl... > That looks great -- easy extensibility is a great thing to have. > > About the plugin model, XML is one way to go ... but another more > Powershelly alternative might be to let people drop a script into a > directory. You could make your function iterate through those scripts, > supplying the line and lastword to each of them. If they return anything, > then add their output to the tabcompletion results. > > That provides an extremely clean plugin model with a minimal amount of > infrastructure overhead. > > -- > Lee Holmes [MSFT] > Windows PowerShell Development > Microsoft Corporation > This posting is provided "AS IS" with no warranties, and confers no > rights. > > "Ryan Milligan" <Ceiled@hotmail.com> wrote in message > news:xZqdnfwzveSdelfYnZ2dnUVZ_oytnZ2d@comcast.com... >> It's true that it's a bit complicated, but once it's set up...well, I was >> just loading the snapin I've been using to play around with PSH, and I >> thought to myself, "Wouldn't it be nice if I could get tab completion on >> snapin names when I'm loading them? And hey, what if the tab completion >> only showed snapins that are registered but not loaded when you're adding >> them, and only showed snapins that are actually loaded when you're >> removing them?" A few minutes later, I have snapins.xml: >> >> <command pattern="add-pssnapin"> >> <parameter name="Name"> >> <script> >> get-pssnapin -registered $pattern | foreach {$_.Name}; >> </script> >> </parameter> >> </command> >> <command pattern="remove-pssnapin"> >> <parameter name="Name"> >> <script> >> get-pssnapin $pattern | foreach {$_.Name}; >> </script> >> </parameter> >> </command> >> >> Drop it into the appropriate folder, and boom -- instant, customized tab >> completion for those cmdlets, with very little danger of interfering with >> any of the existing functionality. In fact, because of the way Powershell >> manages the return pipeline, these things chain together very nicely -- >> if you have multiple expansion or command elements that all match your >> input, they'll all have an opportunity to respond, and all the results >> they return will be made available, after being run through sort and >> unique to keep them organized. >> >> -- Ryan Milligan >> >> "RichS" <RichS@discussions.microsoft.com> wrote in message >> news:101A0A6F-C81B-4003-AA9C-2FE327BAB586@microsoft.com... >>>I read it to the end! >>> >>> Looks like a good idea - theres a lot in there to digest >>> -- >>> Richard Siddaway >>> Please note that all scripts are supplied "as is" and with no warranty >>> Blog: http://richardsiddaway.spaces.live.com/ >>> PowerShell User Group: http://www.get-psuguk.org.uk >>> >>> >>> "Ryan Milligan" wrote: >>> >>>> Hi everyone, I was browsing through some of the tab expansion work that >>>> people have been doing, and the big thing I noticed was the lack of >>>> composability. There mostly seems to be a focus on adding new features, >>>> rather than on organizing the features we already have in such a way >>>> that >>>> things can be simply and easily added. The result is that if I want to >>>> write >>>> a new tab-expansion feature, I end up appending it to a switch -regex >>>> statement that just keeps getting bigger and bigger as more and more >>>> people >>>> contribute to it. I spent some time putting together a management >>>> infrastructure that I think is pretty interesting, and I was hoping to >>>> get >>>> some feedback from the community on how useful people think this sort >>>> of >>>> construct might be. This is going to be pretty long, but I think it's >>>> worth >>>> it. >>>> >>>> The version of tabExpansion that's in my profile right now looks like >>>> this: >>>> >>>> param($line, $lastWord) & { >>>> # extract the command name from the string >>>> # first split the string into statements and pipeline elements >>>> # This doesn't handle strings however. >>>> $cmdlet = [regex]::Split($line, '[|;]')[-1] >>>> >>>> # Extract the trailing unclosed block e.g. ls | foreach { cp >>>> if ($cmdlet -match '\{([^\{\}]*)$') >>>> { >>>> $cmdlet = $matches[1] >>>> } >>>> >>>> # Extract the longest unclosed parenthetical expression... >>>> if ($cmdlet -match '\(([^()]*)$') >>>> { >>>> $cmdlet = $matches[1] >>>> } >>>> >>>> $command = $cmdlet; >>>> if ($command -eq $lastWord) >>>> { >>>> $command = ''; >>>> } >>>> else { if ($lastWord -ne '') >>>> { >>>> # Store the arguments passed to the cmdlet separately, minus >>>> the >>>> last word >>>> $command = $command.Remove($command.Length - $lastWord.Length - >>>> 1).Trim() >>>> }} >>>> >>>> # take the first space separated token of the remaining string >>>> # as the command to look up. Trim any leading or trailing spaces >>>> # so you don't get leading empty elements. >>>> $cmdlet = $cmdlet.Trim().Split()[0] >>>> >>>> if ($cmdlet -ne $line) >>>> { >>>> # Remove the name of the cmdlet itself from the argument string >>>> $command = $command.Substring($cmdlet.Length).Trim() >>>> } >>>> else >>>> { >>>> $command = ''; >>>> } >>>> >>>> # now get the info object for it... >>>> $cmdlet = @(Get-Command -type 'cmdlet,alias' $cmdlet)[0] >>>> >>>> # loop resolving aliases... >>>> while ($cmdlet.CommandType -eq 'alias') { >>>> $cmdlet = @(Get-Command -type 'cmdlet,alias' >>>> $cmdlet.Definition)[0] >>>> } >>>> >>>> $xml = load-xml (join-path (split-path -parent $profile) >>>> "expansions") >>>> $xml.root.expansion | where { invoke-expression $_.rule } | foreach >>>> { >>>> invoke-expression $_.script } >>>> } | sort | unique >>>> >>>> The load-xml function is a helper that looks like this: >>>> >>>> param($path) [xml](dir (join-path $path "*.xml") | foreach-object ` >>>> -begin { [string]$result = "" } ` >>>> { $result += (gc $_) } ` >>>> -end { "<root>$($result)</root>" }) >>>> >>>> The purpose of load-xml is to walk through all the .xml files in a >>>> directory, concatenate their contents together, and wrap them in a >>>> global >>>> root element -- essentially, merge multiple XML files into one. This >>>> model >>>> gives us a very convenient way of adding new "modules" -- just drop an >>>> XML >>>> file that contains them into the appropriate folder. If you look at >>>> tabExpansion, you'll see that I'm loading all the XML files in a folder >>>> called "expansions" that's in the same folder as the profile file. The >>>> location of these files and the loading mechanism are purely >>>> place-holder, >>>> but they get the point across. Once I've loaded the XML files, I search >>>> through them for <expansion> elements whose rule attribute, when >>>> invoked as >>>> a PSH expression, returns true. Any such elements that I find, I invoke >>>> their <script> elements as PSH expressions. >>>> >>>> Before writing new extensions, we need to make sure that the existing >>>> functionality continues to work. One of the existing features in the >>>> default >>>> tabExpansion function is tab-completion on the names of parameters >>>> passed to >>>> cmdlets. This can be implemented in this system by adding a .xml file >>>> to the >>>> "expansions" folder (you can name the XML file anything you want, of >>>> course) >>>> with the following content: >>>> >>>> <expansion rule="$lastWord -match '(^.*)(\$(\w|\.)+)\.(\w*)$'"> >>>> <script> >>>> $method = [Management.Automation.PSMemberTypes] >>>> 'Method,CodeMethod,ScriptMethod,ParameterizedProperty'; >>>> $base = $matches[1]; >>>> $expression = $matches[2]; >>>> Invoke-Expression ('$val=' + $expression); >>>> $pat = $matches[4] + '*'; >>>> Get-Member -inputobject $val $pat | sort membertype,name | >>>> where { $_.name -notmatch '^[gs]et_'} | >>>> foreach >>>> { >>>> if ($_.MemberType -band $method) >>>> { >>>> $base + $expression + '.' + $_.name + '('; >>>> } >>>> else >>>> { >>>> $base + $expression + '.' + $_.name; >>>> } >>>> } >>>> </script> >>>> </expansion> >>>> >>>> The rule attribute on the expansion element (all element and attribute >>>> names >>>> are completely place-holder, by the way) replaces the entry in that big >>>> switch -regex in the built-in function, and the <script> element >>>> contains >>>> the PSH script that will generate the list of alternatives we want. >>>> That's >>>> pretty much it...now instead of editing one big function that has a >>>> bunch of >>>> switch statements in it, you can edit a bunch of small, self-contained >>>> XML >>>> files to add new features. Of course, if loading all those files on >>>> pressing >>>> tab becomes a performance problem, the XML can easily be pre-loaded and >>>> cached at the beginning of the session. I simply prefer to load them >>>> dynamically on demand because I'm not seeing a noticeable delay and >>>> because >>>> it makes it easier to test changes to the files. >>>> >>>> To me, the coolest feature that this enables is the ability to easily >>>> tweak >>>> bits and pieces of the algorithm. The thing that started me down this >>>> path >>>> was that I was manipulating services and I thought, "Man, wouldn't it >>>> be >>>> great if I could just type start-service, and start tabbing through the >>>> list >>>> of services installed on my machine?" The answer of course is yes, >>>> which >>>> brings me to commandParameter.xml, which I've dropped into the >>>> expansions >>>> folder: >>>> >>>> <expansion rule="$command -match '-(\w+)$'"> >>>> <script> >>>> $paramName = $matches[1]; >>>> $_xml = load-xml (join-path (split-path -parent $profile) >>>> commands); >>>> $pattern = $lastWord + '*'; >>>> foreach ($cmd in $_xml.root.command | where {$cmdlet -like >>>> $_.pattern}) >>>> { >>>> foreach ($param in $cmd.parameter | where { >>>> $paramName -like >>>> $_.name}) >>>> { >>>> invoke-expression $param.script; >>>> } >>>> } >>>> </script> >>>> </expansion> >>>> >>>> The concept is very similar to tabExpansion itself. It loads all the >>>> XML >>>> files in a folder (this time the "commands" folder instead of >>>> "expansions"), >>>> walks through the resulting XML looking for applicable nodes -- in this >>>> case, <command> elements with a "pattern" attribute that matches the >>>> name of >>>> the cmdlet we found, then walks through each <parameter> element in >>>> that >>>> <command> element looking for matching names. For each one that it >>>> finds, it >>>> runs the corresponding script. Now, all that's left is to implement >>>> some >>>> custom parameter completion... >>>> >>>> <command pattern="*-service"> >>>> <parameter name="Name"> >>>> <script> >>>> get-service $pattern | foreach { $_.Name }; >>>> </script> >>>> </parameter> >>>> </command> >>>> >>>> Drop that file in our "commands" folder, and we've achieved the >>>> original >>>> goal: typing any cmdlet name that ends with "-service" and asking for >>>> tab >>>> completion on the "-Name" parameter will allow us to tab through the >>>> list of >>>> services available on the system. Note that if we wanted, we could >>>> specialize this even further. For example, the service lifecycle >>>> commands >>>> are each only valid for services in a particular state: you can't start >>>> a >>>> service that's already running. We could easily have separate logic for >>>> start-service, stop-service, etc that only returns the services that >>>> actually make sense in their current state...this model of breaking >>>> logic up >>>> by natural category lends itself quite easily to that sort of thing. >>>> >>>> There are other useful things to do here as well...for example, I have >>>> a >>>> <command> element that provides tab-completion on any parameter whose >>>> type >>>> is an enum. I also have the ability to tab through valid help topics by >>>> typing "get-help -name [tab]" -- the first option in that list is >>>> "about_alias", and the last is "Write-Warning". I can post the rest of >>>> the >>>> stuff I've done so far later, if there's a favourable response. The >>>> biggest >>>> real drawback to the current implementation is that it doesn't support >>>> sequential parameters -- I suspect that this could be done by breaking >>>> the >>>> parameters into space-delimited tokens (tricky with embedded >>>> parentheses and >>>> the like, but probably doable) and then using the CmdletInfo to figure >>>> out >>>> the name of the property and set $propName to that. Pretty simple in >>>> theory, >>>> but I think breaking the string into those tokens will probably be more >>>> trouble than it's really worth. If anyone wants to tackle it though, >>>> please >>>> feel free to do so and post the results. >>>> >>>> Like I said earlier, feedback is more than welcome -- I think this >>>> could be >>>> the beginnings of a pretty useful way to organize this sort of thing, >>>> but I >>>> want to hear what others have to say about it. Known caveats if you >>>> want to >>>> play around with this stuff: >>>> >>>> 1. invoke-expression does not support comments, and is much more rigid >>>> than >>>> normal scripts. For example, every statement has to end with a >>>> semi-colon. >>>> 2. If you want to put multiple <expansion> or <command> elements in one >>>> file, the current design has you writing non-well-formed XML files with >>>> multiple root elements. This is something that should probably be >>>> addressed >>>> later, but for now doesn't really bother me. >>>> 3. Remember that all these things are sharing the same variable space. >>>> I >>>> have a variable called "$xml" in the main function, so I have to call a >>>> similar variable "$_xml" in the command parameter bit in order to avoid >>>> clobbering the original value. Again, something that would want to be >>>> addressed in the future, but can be pretty easily worked around for >>>> now. >>>> >>>> That's about it, really. I've been having fun writing these little >>>> parameter >>>> completion extensions...hopefully some of you will find it fun and >>>> interesting, too. Looking back at the length of this post, I'll >>>> consider >>>> myself lucky if anyone even reads all the way to the end :-) Thanks in >>>> advance! >>>> >>>> -- Ryan Milligan >>>> >>>> >>>> >> >> > > |
My System Specs![]() |
| | #6 (permalink) |
| Guest | Re: Tab expansion extensions Oops, I forgot to mention in my previous post...one big advantage of doing it the way Lee suggests would be that it would avoid the issues I've been having with invoke-expression, like the fact that comments aren't supported and semi-colons are required in places where normally they aren't. Running them as named scripts instead of anonymous snippets should also improve the debugging experience when using set-psdebug -trace, which is kind of painful with the XML model, as it can be difficult to follow where things are going without file names and line numbers. The comment issue could be resolved by stripping them out before passing them to invoke-expression, but the other two are fairly sticky. Does anyone happen to know why invoke-expression snippets seem to be more rigid than scripts invoked from a file? -- Ryan Milligan "Ryan Milligan" <Ceiled@hotmail.com> wrote in message news:L5Sdnd1H0s0R4lDYnZ2dnUVZ_rCsnZ2d@comcast.com... > True, I considered doing it that way...the main reason I went with XML > instead was to bring more structure to the process of determining if a > particular script should be run to find candidates, because otherwise the > vast majority of scripts would need to be surrounded with an if statement > to determine if they're interested in what the user is typing. Using XML > this way means a) most of the time, we can avoid having PSH parse the > entire script (loading and running only the initial condition if it's not > a script we should run), and b) scripts take on a familiar and > well-established structure, which makes it easier to tell at a glance what > sorts of scenarios a particular extension is intended to handle. > > That said, I think either structure is perfectly fine...I would just like > to push for something to be adopted on a general level so that people can > continue to develop in this area and share their work with others with a > level of confidence that they won't be stepping on each other's toes. Most > areas of Powershell development are modular by nature -- cmdlets, > providers, and extended type information can all be implemented and > distributed with minimal impact on each other. Functions are also > relatively easy to distribute and share without interfering with each > other. However, work on tab expansion features all gets piled into one big > function that everyone is editing, with multiple versions being passed > around. I think there would be a lot of value in the community at large > adopting a more modular model before too much more effort gets poured into > it, is all. > > Also remember that these models can easily be mixed by implementing one of > them, and then writing an extension that will implement the other -- a > task which is equally easy in both directions. The main point is that > adding a new feature should ideally involve adding an extra file, not > appending to an existing one. Just today, I was working on a machine that > didn't have Powershell installed and I needed to restart a service. > Instinctively, I typed "restart-service -name d[tab]"...oops. Having tab > completion on incidental parameters is surprisingly addictive, and the > easier it is to mix and match and expand these capabilities, the happier > I'll be :-) > > -- Ryan Milligan > > "Lee Holmes [MSFT]" <lee.holmes@online.microsoft.com> wrote in message > news:en%23$Ew9SHHA.3592@TK2MSFTNGP06.phx.gbl... >> That looks great -- easy extensibility is a great thing to have. >> >> About the plugin model, XML is one way to go ... but another more >> Powershelly alternative might be to let people drop a script into a >> directory. You could make your function iterate through those scripts, >> supplying the line and lastword to each of them. If they return anything, >> then add their output to the tabcompletion results. >> >> That provides an extremely clean plugin model with a minimal amount of >> infrastructure overhead. >> >> -- >> Lee Holmes [MSFT] >> Windows PowerShell Development >> Microsoft Corporation >> This posting is provided "AS IS" with no warranties, and confers no >> rights. >> >> "Ryan Milligan" <Ceiled@hotmail.com> wrote in message >> news:xZqdnfwzveSdelfYnZ2dnUVZ_oytnZ2d@comcast.com... >>> It's true that it's a bit complicated, but once it's set up...well, I >>> was just loading the snapin I've been using to play around with PSH, and >>> I thought to myself, "Wouldn't it be nice if I could get tab completion >>> on snapin names when I'm loading them? And hey, what if the tab >>> completion only showed snapins that are registered but not loaded when >>> you're adding them, and only showed snapins that are actually loaded >>> when you're removing them?" A few minutes later, I have snapins.xml: >>> >>> <command pattern="add-pssnapin"> >>> <parameter name="Name"> >>> <script> >>> get-pssnapin -registered $pattern | foreach {$_.Name}; >>> </script> >>> </parameter> >>> </command> >>> <command pattern="remove-pssnapin"> >>> <parameter name="Name"> >>> <script> >>> get-pssnapin $pattern | foreach {$_.Name}; >>> </script> >>> </parameter> >>> </command> >>> >>> Drop it into the appropriate folder, and boom -- instant, customized tab >>> completion for those cmdlets, with very little danger of interfering >>> with any of the existing functionality. In fact, because of the way >>> Powershell manages the return pipeline, these things chain together very >>> nicely -- if you have multiple expansion or command elements that all >>> match your input, they'll all have an opportunity to respond, and all >>> the results they return will be made available, after being run through >>> sort and unique to keep them organized. >>> >>> -- Ryan Milligan >>> >>> "RichS" <RichS@discussions.microsoft.com> wrote in message >>> news:101A0A6F-C81B-40 |