PowerShell Cookbook

Search

Categories

 

On this page

Advanced HTTP / ASP.Net Scripting with PowerShell
Converting C# to PowerShell
Getting Things Done – Outlook Task Automation with PowerShell
Calling a Webservice from PowerShell
PowerShell in Action Now Available
Controlling Robots with PowerShell
A Different Kind of Management Script
Customize Even More with AutoHotkey
PowerShell Credentials and GetNetworkCredential()
Getting Better Gas Mileage

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 220
This Year: 20
This Month: 0
This Week: 0
Comments: 533

Sign In

 Monday, March 26, 2007
Monday, March 26, 2007 10:07:35 PM (Pacific Daylight Time, UTC-07:00) ( )

Back in the good ol’ days, my domain used to support a catch-all email address. I could give out any email address (as long as it ended in @leeholmes.com,) and still be guaranteed to receive the mail. When asked to create an account at some random website, I just make one up on the spot tailored specifically to them.

I got an odd look from my dentist when I did this, though. They must have thought — “who in their right mind likes dentists so much that they pick it for their email address?” As with all computer jokes, explaining it didn’t make it any better – I just got a smile and nod.

In any case, my domain soon fell under the wrath of a dictionary attack spammer – which I noticed when I was getting spam as fast as Outlook could receive it. I quickly disabled my catch-all alias, but I immediately missed how easy it was to create a new temporary alias. Creating the aliases manually got old quickly, so I wrote a script to do it instead. It works against “SmarterMail Professional Edition” – the software used at WebHost4Life (the hosting company I use.)

If you use WebHost4Life, feel free to use this script to let you create email aliases with ease. If you don’t, you might still find the code useful for building your own script to automate HTTP / ASP.Net scenarios.

PS >New-MailAlias foobar2

cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
Credential
User: domain-admin-address
@leeholmes.com
Password for user domain-admin-address@leeholmes.com: ***********

Logging in.
Logged in.
Creating alias.
Alias created.

(Note: This script uses Send-TcpRequest, which I posted earlier as “Connect-Computer”: http://www.leeholmes.com/blog/ScriptingNetworkTCPConnectionsInPowerShell.aspx)

##############################################################################
##
## New-MailAlias.ps1
##
## Create a new mail alias on a mail host managed by a "SmarterMail 
## Professional Edition" web interface
##
## The alias provided will redirect mail to the user specified by $recipient
## The credential should be the full email address and password of the domain
## email administrator -- usually postmaster@$mailHost.
##
##############################################################################

param(
    [string] $alias = $("Specify an alias"), 
    [System.Management.Automation.PsCredential] $cred = $(Get-Credential))

[void] [Reflection.Assembly]::LoadWithPartialName("System.Web")

$mailHost = "mail.example.com"
$recipient = "me@example.com"

function Main
{
    Write-Host "Logging in."
    $session = LogIn
    Write-Host "Logged in."

    Write-Host "Creating alias."
    $viewstate = GetAliasPage $session

    SubmitNewAlias $viewstate $session
    Write-Host "Alias created."
}

function LogIn
{
    ## The template for the login request
    $loginRequest = 
@"
GET /Login.aspx HTTP/1.1
Host: $mailHost


"@

    ## Request the login page from the server
    $loginPage = Send-TcpRequest $mailHost 80 -Input $loginRequest

    ## Extract the ASP.Net session ID and viewstate for the page
    $session = GetSessionId $loginPage
    Write-Verbose "Got session: $session"
    $viewstate = GetViewState $loginPage

    ## The template for the login page submission
    $login = 
@"
POST /Login.aspx HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: __LENGTH
Host: $mailHost
Cookie: SelectedLanguage=; permcookie=1680x1050; ASP.NET_SessionId=__SESSION; screensize=1680x1050; settings=

__POSTDATA


"@

    ## Add the username, password, and viewstate into the information we want to
    ## post
    $postData = "__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=__REPLACEMENTVIEWSTATE&txtUserName=__USERNAME&txtPassword=__PASSWORD&LanguageList=&btnEnterClick.x=0&btnEnterClick.y=0"
    $postData = $postData.Replace("__REPLACEMENTVIEWSTATE", [System.Web.HttpUtility]::UrlEncode($viewstate))
    $postData = $postData.Replace("__USERNAME", [System.Web.HttpUtility]::UrlEncode($cred.GetNetworkCredential().UserName + '@' + $cred.GetNetworkCredential().Domain))
    $postData = $postData.Replace("__PASSWORD", [System.Web.HttpUtility]::UrlEncode($cred.GetNetworkCredential().Password))

    ## Replace the placeholders for the post data in the login template, as well as
    ## the ASP.Net session cookie
    $login = $login.Replace("__LENGTH"$postData.Length)
    $login = $login.Replace("__POSTDATA"$postData)
    $login = $login.Replace("__SESSION"$session)

    ## Submit the login page, and check for errors
    $output = Send-TcpRequest $mailHost 80 -Input $login
    if($output -match "frmerror.aspx")
    {
        Write-Error "Failed to log in."
        Write-Error "Sent:"
        Write-Error $login
        Write-Error "Output:"
        Write-Error $output

        exit
    }
    ##############################################################################

    $session
}

function GetAliasPage($session)
{
    ## The template for the alias page request
    $aliasRequest = 
@"
GET /DomainAdmin/frmUserAlias.aspx HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)
Host: $mailHost
Cookie: SelectedLanguage=; permcookie=1680x1050; ASP.NET_SessionId=__SESSION; screensize=1680x1050; settings=empty


"@

    ## Replace the placeholder for the ASP.Net session cookie
    $aliasRequest = $aliasRequest.Replace("__SESSION"$session)

    ## Request the alias page from the server
    $aliasPage = Send-TcpRequest $mailHost 80 -Input $aliasRequest

    ## Check for errors
    if($aliasPage -match "frmerror.aspx")
    {
        Write-Error "Failed to get alias page."
        Write-Error "Sent:"
        Write-Error $aliasRequest
        Write-Error "Got:"
        Write-Error $aliasPage

        exit
    }

    ## Extract the viewstate from the page
    GetViewState $aliasPage
}

function SubmitNewAlias($viewstate$session)
{
    ## The template for the alias page submission
    $addAlias = 
@"
POST /DomainAdmin/frmUserAlias.aspx HTTP/1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)
Content-Length: __LENGTH
Host: $mailHost
Cookie: SelectedLanguage=; permcookie=1680x1050; ASP.NET_SessionId=__SESSION; screensize=1680x1050; settings=empty

__POSTDATA


"@

    ## Add the alias name and viewstate into the information we want to post
    $postData = "__EVENTTARGET=SaveTextImageButton&__EVENTARGUMENT=&__VIEWSTATE=__REPLACEMENTVIEWSTATE&headerinnercontrol_folderlist1_TreeView1_CheckedList=&headerinnercontrol_folderlist1_TreeView1_MultipleSelectedList=&sbsearchtext232=&sbsearchfolder232=&sbsearchfield232=0&headerinnercontrol_pulldownmenu1_M1_ContextData=&headerinnercontrol_pulldownmenu1_M2_ContextData=&txtName=__ALIASNAME&txtEmails=$recipient"
    $postData = $postData.Replace("__REPLACEMENTVIEWSTATE", [System.Web.HttpUtility]::UrlEncode($viewState))
    $postData = $postData.Replace("__ALIASNAME", [System.Web.HttpUtility]::UrlEncode($alias))

    ## Replace the placeholders for the post data in the login template, as well as
    ## the ASP.Net session cookie
    $addAlias = $addAlias.Replace("__LENGTH"$postData.Length)
    $addAlias = $addAlias.Replace("__POSTDATA"$postData)
    $addAlias = $addAlias.Replace("__SESSION"$session)

    ## Submit the alias page, and check for errors
    $output = Send-TcpRequest $mailHost 80 -Input $addAlias

    if($output -match "tiptextfailure")
    {
        Write-Error "Failed to add alias."
        $output = $output -replace "(?s).*<div class='tiptextfailure'>([^<]+)</div>.*",'$1'
        Write-Error $output

        exit
    }
    elseif($output -match "frmerror.aspx")
    {
        Write-Error "Failed to add alias -- protocol error"
        Write-Error "Sent:"
        Write-Error $addAlias
        Write-Error "Got:"
        Write-Error $output

        exit
    }
    elseif($output -notmatch "</html>")
    {
        Write-Error "Failed to add alias -- confirmation not present"
        Write-Error "Sent:"
        Write-Error $addAlias
        Write-Error "Got:"
        Write-Error $output

        exit
    }
}

## Extract the ASP.Net session ID frcom a page
function GetSessionId($page)
{
    $page -replace '(?s).*Set-Cookie: ASP.NET_SessionId=([^;]+);.*','$1'
}

## Extract the viewstate from a page
function GetViewState($page)
{
    $page -replace '(?s).*name="__VIEWSTATE" value="([^"]+)".*','$1'
}

. Main

 

Comments [1] | | # 
 Monday, March 19, 2007
Monday, March 19, 2007 5:19:57 PM (Pacific Daylight Time, UTC-07:00) ( )

Tom recently ran into a problem with the Connect-WebService script given here -- when run against a server that returns malformed headers, he received the error message, “The server committed a protocol violation.

The .NET Framework takes this approach as a security precaution, since parsing non-standard headers is one of the most common sources of vulnerabilities. It’s not that the header-parsing code has vulnerabilities (it may or may not, I have no inside knowledge,) but it is an absolute fact that you increase your risk as you expose more code to malicious input.

If you connect to a malicious server without this safeguard, then, you run an increased risk that the server might use malformed headers to exploit problems in the code intended to deal with it.

If you trust the server, then you might want work with it in all of its malformed-header glory, anyways. According to Tom’s research on the Internet, though, the only way to programmatically do this is through an internal property on the System.Net.Configuration.SettingsSection class.

The accepted solution from the MSDN Forum looks like this:

public static bool SetAllowUnsafeHeaderParsing20()
{
  //Get the assembly that contains the internal class
  Assembly aNetAssembly = Assembly.GetAssembly(typeof(System.Net.Configuration.SettingsSection));
  if (aNetAssembly != null)
  {
    //Use the assembly in order to get the internal type for the internal class
    Type aSettingsType = aNetAssembly.GetType("System.Net.Configuration.SettingsSectionInternal");
    if (aSettingsType != null)
    {
      //Use the internal static property to get an instance of the internal settings class.
      //If the static instance isn't created allready the property will create it for us.
      object anInstance = aSettingsType.InvokeMember("Section",
        BindingFlags.Static | BindingFlags.GetProperty | BindingFlags.NonPublic, null, null, new object[] { });
      if (anInstance != null)
      {
        //Locate the private bool field that tells the framework is unsafe header parsing should be allowed or not
        FieldInfo aUseUnsafeHeaderParsing = aSettingsType.GetField("useUnsafeHeaderParsing", BindingFlags.NonPublic | BindingFlags.Instance);
        if (aUseUnsafeHeaderParsing != null)
        {
          aUseUnsafeHeaderParsing.SetValue(anInstance, true);
          return true;
        }
      }
    }
  }
  return false;
}


Which translates cleanly to PowerShell as this:

$netAssembly = [Reflection.Assembly]::GetAssembly([System.Net.Configuration.SettingsSection])

if($netAssembly)
{
    $bindingFlags = [Reflection.BindingFlags] "Static,GetProperty,NonPublic"
    $settingsType = $netAssembly.GetType("System.Net.Configuration.SettingsSectionInternal")

    $instance = $settingsType.InvokeMember("Section", $bindingFlags, $null, $null, @())

    if($instance)
    {
        $bindingFlags = "NonPublic","Instance"
        $useUnsafeHeaderParsingField = $settingsType.GetField("useUnsafeHeaderParsing", $bindingFlags)

        if($useUnsafeHeaderParsingField)
        {
          $useUnsafeHeaderParsingField.SetValue($instance, $true)
        }
    }
}
Comments [0] | | # 
 Wednesday, February 28, 2007
Thursday, March 01, 2007 5:17:09 AM (Pacific Standard Time, UTC-08:00) ( )

Scott just finished writing about his boss (and now him) using Blat to help him Get Things Done.

I've been "Getting Things Done" for some time, and one thing that's always annoyed me was how difficult it is to convert a desire into a categorized task. Outlook 2007 had the opportunity to make this better with the quick-entry field in the TODO bar, but tasks entered that way unfortunately have a due date for "Tomorrow." While not terrible, it means additional fiddling around – which I wanted to avoid in the first place.

The blat solution is helpful, but still requires an intermediary. Once the reminder is in your inbox, it requires an additional step of triaging to convert it into a task.

Some time back, I wrote the following script to make it easy to directly enter tasks into Outlook from PowerShell. It even validates the category (if you specify one,) so you don't get items in the wrong category:

## Add-OutlookTask.ps1
## Add a task to the Outlook Tasks list

param( $description = $(throw "Please specify a description"), $category, [switch$force )

## Create our Outlook and housekeeping variables. 
## Note: If you don't have the Outlook wrappers, you'll need
## the commented-out constants instead

$olTaskItem = "olTaskItem"
$olFolderTasks = "olFolderTasks"

#$olTaskItem = 3
#$olFolderTasks = 13

$outlook = New-Object -Com Outlook.Application
$task = $outlook.Application.CreateItem($olTaskItem)
$hasError = $false


## Assign the subject
$task.Subject = $description

## If they specify a category, then assign it as well.
if($category)
{
    if(-not $force)
    {
        ## Check that it matches one of their already-existing categories, but only
        ## if they haven't specified the -Force parameter.
        $mapi = $outlook.GetNamespace("MAPI")
        $items = $mapi.GetDefaultFolder($olFolderTasks).Items
        $uniqueCategories = $items | Sort -Unique Categories | % { $_.Categories }
        if($uniqueCategories -notcontains $category)
        {
            $OFS = ", "
            $errorMessage = "Category $category does not exist. Valid categories are: $uniqueCategories. " +
                            "Specify the -Force parameter to override this message in the future."
            Write-Error $errorMessage
            $hasError = $true
        }
    }

    $task.Categories = $category 
}

## Save the item if this didn't cause an error, and clean up.
if(-not $hasError) { $task.Save() }
$outlook = $null


 

One valid point that Scott brought up for using a batch file instead of a PowerShell script is that it was easier to run batch files from places like Start | Run, SlickRun, and apps like that. One thing I sometimes do is write a wrapper batch file:

REM todo.bat
powershell -command "Add-OutlookTask '%1'
'@TODO' -Force"

Another option is to associate PowerShell as the interpreter for .PS1 files. Which reminds me...

[C:\Temp]
PS:44 > Add-OutlookTask
"Write about associating PowerShell with PS1" "@Write"

 

Comments [3] | | # 
 Tuesday, February 27, 2007
Wednesday, February 28, 2007 5:17:41 AM (Pacific Standard Time, UTC-08:00) ( )

One question that comes up fairly frequently in our internal mailing list, the newsgroup, and the internet at large is how to call a webservice from PowerShell. In fact, several excellent PowerShellers have written about it: Keith, and Geert. In general, the guidance has been to use wsdl.exe to generate the webservice proxy, compile that proxy into a DLL, then finally load that DLL into memory.

This is a topic that I cover in my upcoming book, and initially wrote a script to automate these proxy generation steps. However, the prerequisite to running a script designed in that matter is fairly huge. Wsdl.exe doesn't come with the .NET Framework, so you need to have the .NET SDK installed. That was something that made me uncomfortable, so I instead opted for a solution that could generate the web service proxy without wsdl.exe.

The .NET Framework supports a few classes that make this possible, although the documentation for them is terrible at best :)

To give a glimpse into the writing process behind my upcoming "Windows PowerShell - The Definitive Guide" (O'Reilly,) I'll occasionally post entries "as the author sees it." This entry discusses calling a webservice from PowerShell.

 

Program: Connect-WebService

Although "screen scraping" (parsing the HTML of a web page) is the most common way to obtain data from the internet, web services are becoming increasingly common. Web services provide a significant advantage over HTML parsing, as they are much less likely to break when the web designer changes minor features in their design.

The only benefit to web services isn't their more stable interface, however. When working with web services, the .NET Framework allows you to generate proxies that let you interact with the web service as easily as you would work with a regular .NET object. That is because to you, the web service user, these proxies act almost exactly the same as any other .NET object. To call a method on the web service, simply call a method on the proxy.

The primary difference you will notice when working with a web service proxy (as opposed to a regular .NET object) is the speed and internet connectivity requirements. Depending on conditions, a method call on a web service proxy could easily take several seconds to complete. If your computer (or the remote computer) experiences network difficulties, the call might even return a network error message instead of the information you had hoped for.

 The following script allows you to connect to a remote webservice, if you know the location of its service description file (WSDL.) It generates the web service proxy for you, allowing you to interact with it as you would any other .NET object.

Example 9-3. Connect-WebService.ps1

##############################################################################

## Connect-WebService.ps1

##

## Connect to a given web service, and create a type that allows you to

## interact with that web service.

##

## Example:

##

##     $wsdl = "http://terraserver.microsoft.com/TerraService2.asmx?WSDL"

##     $terraServer = Connect-WebService $wsdl

##     $place = New-Object Place

##     $place.City = "Redmond"

##     $place.State = "WA"

##     $place.Country = "USA"

##     $facts = $terraserver.GetPlaceFacts($place)

##     $facts.Center

##############################################################################

param(

    [string] $wsdlLocation = $(throw "Please specify a WSDL location"),

    [string] $namespace,

    [Switch] $requiresAuthentication)

 

## Create the web service cache, if it doesn't already exist

if(-not (Test-Path Variable:\Lee.Holmes.WebServiceCache))

{

    ${GLOBAL:Lee.Holmes.WebServiceCache} = @{}

}

 

## Check if there was an instance from a previous connection to

## this web service. If so, return that instead.

$oldInstance = ${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation]

if($oldInstance)

{

    $oldInstance

    return

}

 

## Load the required Web Services DLL

[void] [Reflection.Assembly]::LoadWithPartialName("System.Web.Services")

 

## Download the WSDL for the service, and create a service description from

## it.

$wc = new-object System.Net.WebClient

 

if($requiresAuthentication)

{

    $wc.UseDefaultCredentials = $true

}

 

$wsdlStream = $wc.OpenRead($wsdlLocation)

 

## Ensure that we were able to fetch the WSDL

if(-not (Test-Path Variable:\wsdlStream))

{

    return

}

 

$serviceDescription =

    [Web.Services.Description.ServiceDescription]::Read($wsdlStream)

$wsdlStream.Close()

 

## Ensure that we were able to read the WSDL into a service description

if(-not (Test-Path Variable:\serviceDescription))

{

    return

}

 

## Import the web service into a CodeDom

$serviceNamespace = New-Object System.CodeDom.CodeNamespace

if($namespace)

{

    $serviceNamespace.Name = $namespace

}

 

$codeCompileUnit = New-Object System.CodeDom.CodeCompileUnit

$serviceDescriptionImporter =

    New-Object Web.Services.Description.ServiceDescriptionImporter

$serviceDescriptionImporter.AddServiceDescription(

    $serviceDescription, $null, $null)

[void] $codeCompileUnit.Namespaces.Add($serviceNamespace)

[void] $serviceDescriptionImporter.Import(

    $serviceNamespace, $codeCompileUnit)

 

## Generate the code from that CodeDom into a string

$generatedCode = New-Object Text.StringBuilder

$stringWriter = New-Object IO.StringWriter $generatedCode

$provider = New-Object Microsoft.CSharp.CSharpCodeProvider

$provider.GenerateCodeFromCompileUnit($codeCompileUnit, $stringWriter, $null)

 

## Compile the source code.

$references = @("System.dll", "System.Web.Services.dll", "System.Xml.dll")

$compilerParameters = New-Object System.CodeDom.Compiler.CompilerParameters

$compilerParameters.ReferencedAssemblies.AddRange($references)

$compilerParameters.GenerateInMemory = $true

 

$compilerResults =

    $provider.CompileAssemblyFromSource($compilerParameters, $generatedCode)

 

## Write any errors if generated.        

if($compilerResults.Errors.Count -gt 0)

{

    $errorLines = ""

    foreach($error in $compilerResults.Errors)

    {

        $errorLines += "`n`t" + $error.Line + ":`t" + $error.ErrorText

    }

 

    Write-Error $errorLines

    return

}

## There were no errors.  Create the webservice object and return it.

else

{

    ## Get the assembly that we just compiled

    $assembly = $compilerResults.CompiledAssembly

 

    ## Find the type that had the WebServiceBindingAttribute.

    ## There may be other "helper types" in this file, but they will

    ## not have this attribute

    $type = $assembly.GetTypes() |

        Where-Object { $_.GetCustomAttributes(

            [System.Web.Services.WebServiceBindingAttribute], $false) }

 

    if(-not $type)

    {

        Write-Error "Could not generate web service proxy."

        return

    }

 

    ## Create an instance of the type, store it in the cache,

    ## and return it to the user.

    $instance = $assembly.CreateInstance($type)

    ${GLOBAL:Lee.Holmes.WebServiceCache}[$wsdlLocation] = $instance

    $instance

}

 

[Update: Several readers have requested that this script support web services that reqire credentials, and support web services that return the same type of object. Added parameters to allow this.]

Comments [7] | | # 
 Monday, February 26, 2007
Tuesday, February 27, 2007 1:07:21 AM (Pacific Standard Time, UTC-08:00) ( )

Allow me to be tragically late to the party in pointing out that Bruce Payette’s PowerShell in Action is now available.

As shown by its Amazon page, the reviews are immensely positive.

Unfortunately for me, I’ve sequestered myself from reading any of the PowerShell books until my own is complete. This one, however, calls longingly from co-workers’ desks as I walk down the halls :) Conversations with Bruce are always interesting, and this book is sure to not disappoint.

Comments [0] | | # 
 Monday, February 19, 2007
Tuesday, February 20, 2007 6:33:41 AM (Pacific Standard Time, UTC-08:00) ( )

Over the last while, Scott Hanselman has been blogging a lot about some pretty cool ideas – controlling an IR port through C#, monkeying around with robots, and of course, PowerShell.

This all came together today, with the publishing of the second part of his Coding4Fun series, "Microbric Viper Robot with an Iguanaworks IR Serial Port and PowerShell."

In this final part, he extends the PowerShell Logo example to make a real robot execute your PowerShell script. Check it out – it's mind blowing!

 

Comments [0] | | # 
 Monday, February 12, 2007
Monday, February 12, 2007 6:54:26 PM (Pacific Standard Time, UTC-08:00) ( )

It turns out that the chisel nib on a thick whiteboard marker is actually exceedingly good for whiteboard calligraphy. It gives an x-height of about 20mm, though :)


The only problem is the guide lines – since on a whiteboard, there are none :) I’ve tried penciling them in, but that doesn’t work very well. I think my solution for that will be the true geek way – lasers.

Comments [1] | | # 
 Friday, February 09, 2007
Saturday, February 10, 2007 12:06:59 AM (Pacific Standard Time, UTC-08:00) ( )

One of the things people commonly ask about is support for rich keyboard macros and hotkeys in PowerShell.  For many, a pointer to the Doskey hotkey reference (which provides the history management functionality in PowerShell’s “cooked mode”) is enough: http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/doskey.mspx?mfr=true.

Once you’ve become used to those features, though, you start to want more – especially if you’ve grown accustomed to some of the power available on traditional Unix shells. Typical questions include:

  • How do I use Emacs keystrokes to navigate my history?
  • How do I use the keyboard to copy and paste in PowerShell?
  • How do I clear the screen with a keystroke?
  • How do exit the shell with a keystroke?
  • How do I use the keyboard to adjust the font size in PowerShell?

Although PowerShell itself doesn’t directly offer these additional customization options, there is still an outlet – UI Automation and Keystroke macro programs.

Programs that support keystroke macros are great, as they let you automate anything that you can already do with a keyboard (and sometimes the mouse.) For example, you can use the Alt+Space combination in any console window to open up its system menu.  A console system window includes all kinds of tasty treats – copy, paste, find, and even properties.

Using these menus and a program that supports keystroke macros, we can customize most of the things we want to.

I personally use a program called AutoHotkey as my macro program. It has a lot of power, although its scripting language leaves a lot to be desired. I think it would be the coolest thing ever if somebody wrote a macro program that hosted PowerShell – giving you all the benefits of PowerShell scripting for UI automation.

AutoHotkey uses a script to let you write keystroke macros. You tell it what keys to listen for, and what it should do when it sees those keys. Let’s take some simple examples.

One thing you might wonder is how to use Emacs keystrokes to navigate your history. The console cooked mode already supports many of the concepts that you want, but just doesn’t use the keystrokes that you want. You want to push Ctrl-A, but the console uses HOME for that. You want to push Ctrl-E, but the console uses END for that. So in your macro, tell AutoHotkey to send the HOME key when you push Ctrl-A:

;;
;; Beginning of line with ^A if we're in a console window
;;
#IfWinActive, ahk_class ConsoleWindowClass
^a::SendInput {HOME}
#IfWinActive

That sequence tells AutoHotkey: if the class of the current window is ConsoleWindowClass, then run these commands when I press ^a. Those commands are to send the HOME key as input. Similar macros work for the other navigation commands you might want to remap.
These keystrokes can get even more complex, if desired. For example, you want Ctrl-Y (the Emacs keystroke for Paste) to paste into a console window. The console doesn’t directly support that with a keystroke substitution, but the glorious system menu does:

You can automate that with Alt+Space, E, P:

;;
;; Paste to the console window with Ctrl-Y if we're in one
;;
#IfWinActive, ahk_class ConsoleWindowClass
^y::SendInput ! ep
#IfWinActive

Once you move past simple keystroke automation, you can also think of things you’d like to do that automate PowerShell itself – such as clear the screen. For that, you press ESC to clear the current line, “type” CLS, and then press ENTER:

;;
;; Clear screen with ^L if we're in a console window
;;
#IfWinActive, ahk_class ConsoleWindowClass
^l::SendInput {ESC}cls{ENTER}
#IfWinActive

Or, an exceedingly useful command to wrap the current line in parentheses:

;;
;; Wrap current line in brackets
;;
#IfWinActive, ahk_class ConsoleWindowClass
^w::SendInput {HOME}({END})
#IfWinActive

While you’re at it, you might have found that it is extremely annoying to demonstrate a text-based prompt to colleagues, or during a presentation. You fumble with the properties window to pick a font and text size, but these do it all for you:

;;
;; Font larger with ^UP if we're in a console window
;;
#IfWinActive, ahk_class ConsoleWindowClass
^UP::
Sleep 300
SendInput ! p
Sleep 200
SendInput {CTRLDOWN}{TAB}{CTRLUP}
Send !s
Sleep 200
Send {DOWN}
Sleep 200
Send {CTRLDOWN}{SHIFTDOWN}{TAB}{SHIFTUP}{CTRLUP}
Sleep 200
Send {ENTER}
Sleep 200
Send {ENTER}
return
#IfWinActive

;;
;; Font smaller with ^DOWN if we're in a console window
;;
#IfWinActive, ahk_class ConsoleWindowClass
^DOWN::
Sleep 300
SendInput ! p
Sleep 200
SendInput {CTRLDOWN}{TAB}{CTRLUP}
Send !s
Sleep 200
Send {UP}
Sleep 200
Send {CTRLDOWN}{SHIFTDOWN}{TAB}{SHIFTUP}{CTRLUP}
Sleep 200
Send {ENTER}
Sleep 200
Send {ENTER}
return
#IfWinActive

All-in-all, quite a useful way to customize the console environment to your liking.

Comments [0] | | # 
 Thursday, January 25, 2007
Thursday, January 25, 2007 4:27:34 PM (Pacific Standard Time, UTC-08:00) ( )

Martin Zugec recently wrote about a security concern he has with PowerShell: Once you have a credential (i.e.: from Get-Credential,) it is quite easy to get the plain-text password by calling its GetNetworkCredential() method.

The reason we don’t make this more difficult is that you already have the credential. In security, something is either possible or impossible -- "hard" is just another way of saying "possible." We designed the GetNetworkCredential() method to give you the System.Net.NetworkCredential class required by many network classes in the .NET Framework, so we definitely want it to be possible.

You might point out that the password in a credential is a SecureString, so why is it so easy to get access to? To be clear, a SecureString doesn’t protect you from yourself (or anybody using your account,) it protects the credential from people that don’t have access to your user account (but have access to a crash dump, page file, or the like.)

The core point here is that if PowerShell has access to the password in any way, so do you. In fact, getting the plain text of a SecureString is already a one-liner:

[C:\temp]
PS:23 > $secureString = Read-Host -AsSecureString
***********

[C:\temp]
PS:24 > [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString))
Hello World

Now, let’s say that you are concerned about calling GetNetworkCredential() while somebody watches over your shoulder, hence exposing your password. For this, you can modify the way we display all types of the System.Net.NetworkCredential class with this view definition:

        <View>
            <Name>System.Net.NetworkCredential</Name>
            <ViewSelectedBy>
                <TypeName>System.Net.NetworkCredential</TypeName>
            </ViewSelectedBy>

            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Label>UserName</Label>
                        <Width>50</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Label>Domain</Label>
                        <Width>50</Width>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                            <TableColumnItem>
                                <PropertyName>UserName</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Domain</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                 </TableRowEntries>
            </TableControl>
        </View>        

 

For more information about how to make custom formatting files, these posts are a useful resource:

http://www.leeholmes.com/blog/AddFileDescriptionsToYourDirectoryListingsDESCRIPTIONInMonad.aspx
http://www.leeholmes.com/blog/DESCRIPTIONSupportInMonadPart2.aspx
http://www.leeholmes.com/blog/DESCRIPTIONSupportInMonadPart3.aspx

And, in action:

[C:\temp]
PS:4 > $cred = Get-Credential

cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
Credential

[C:\temp]
PS:6 > $cred.GetNetworkCredential()

UserName                                           Domain
--------                                           ------
leeholm                                            CONTOSO

Comments [3] | | # 
 Wednesday, January 24, 2007
Thursday, January 25, 2007 4:37:17 AM (Pacific Standard Time, UTC-08:00) ( )

Ask any driver where they get their gas, and you've opened a deep discussion. There are the few that get it from wherever (including the canteen in the back of their trunk because they always run out,) but most drivers have a preference.

The first question is, "Where do you get your gas?" Do you go for the cheapest gas station you can find, or go to the expensive places because it must be better? For that question, it seems that there really isn't a difference:

A joint study by ABC News and the Maryland State Comptroller's Office examined the difference between name brand and generic gas. Chemists at the Maryland Fuel Testing Laboratory conducted a battery of tests: They verified that the gas was formulated correctly for the season, checked for contaminants like excessive sediments or diesel accidentally mixed with the gas, and they ran the gas through an elaborate engine to ensure that it was all the same 87 octane level.

Here's some good news for consumer: Regular and discount gas are basically similar.

"By and large, it's one and the same," said Bob Crawford of the Maryland Fuel Testing Lab. "You will find results will almost mirror each other. There are going to be slight variations, but gasoline is gasoline."

The primary difference between competing brands is the amount of detergent the distributors add to it. Even still, the difference is generally minor: The EPA requires all gas to have a minimum amount of detergent to keep a car's engine clean.

"You would be paying for brand loyalty, primarily," said Crawford, explaining why brand name gas costs more. "Some people feel more comfortable dealing with a particular brand."

Crawford, who has been studying fuel for 36 years, uses whatever gas is the cheapest. Some cars require premium gas, but generally, regular is fine.

That last quote raises a good point. If the choice of station really doesn't make a difference, what about the octane? Some cars explicitly require Premium gas, but what about the others? Just as with software, the only way to be sure about the value of your gas is to measure it.

For example, I used to drive a 1998 Hyundai Accent. I gave it the cheapest gas, because it didn’t deserve any better :) Then one day, I tried Super Premium Ultra Deluxo gas on a lark, and got 19% better gas mileage.  Since premium fuel is only about 9-10% more expensive than dirt cheap fuel, it is well worth the money. So I turned into one of the people I used to laugh at: dumping super premium gas into a car with 107 HP.

It really comes down to figuring out the gas that gives you the best “miles per dollar,” and stick with it.

(Miles per dollar = Miles per Gallon / Dollars per Gallon)

The exact mileage you get from a given fill-up is easy to determine. Reset your trip odometer to zero just after you fill up. All the way up. The next time you fill your tank, make sure to fill it all the way up again. The pump will tell you how many gallons it gave you. Then, divide the measurement on your trip odometer by the number of gallons you just put in the tank, and you have your car’s mileage from the last fill up. Then, reset your trip odometer and start again.

Different octanes may have an effect on Miles per Gallon for your car, and will most definitely have an effect on Dollars per Gallon. Using the same octane from different suppliers may have an effect on Miles per Gallon, and will probably have an effect on Dollars per Gallon.

So, to figure out the best fuel for you, make a simple chart:

Place / Fuel      Miles per Gallon        Dollars per Gallon            Miles per Dollar
Safeway 89        17                      1.40                          10
Safeway 92        21                      1.65                          12.72

(etc)

For the Dollars per Gallon number, make sure to use the real price. For example, AARCO AM/PM adds a 45-cent surcharge per tank, which increases their advertized dollars per gallon by about 4 cents per gallon.

Experiment for awhile, and settle on the place that gives you the most Miles per Dollar.

Comments [1] | | #