Monday, 5 September 2011

Replacing Visual Studio Command Prompt

Working a lot with Visual Studio, I naturally wanted to be able to use Powershell as the preferred command line tool instead of cmd.exe that Visual Studio Command Prompt uses. Not being aware of the Invoke-Batchfile functionality available in Powershell Community Extensions, and seeing that the script logic could be made so much more efficient in Powershell, I ended up re-writing the whole script in Powershell.

One thing that simplified it for me was the fact that I only need the x64 variant, but it should be ver easy to extend this to cover all other variants. In fact, I started that but when I found that I didn't need them myself that part naturally got demoted... And it could surely be bettered some day.

Anyway, here's the script:
#vcvars64.ps1, for setting Visual Studio environment variables for 64-bit architecture
[CmdletBinding()]
param()

function Set-EnvVar
{
 param (
  [string]$Variable,
  [string]$Value,
  [switch]$Append
 )
 
 if ($Append)
 {
  if (Test-Path ('env:' + $Variable))
  {
   $Value = (Get-Item ('env:' + $Variable)).Value + ";$Value"
  }
 }
 
 [environment]::SetEnvironmentVariable($Variable, $Value)
}

function Set-EnvVarFromRegistry
{
 param (
  [string[]]$keys,
  [string]$property,
  [string]$environmentVariable,
  [string]$suffix,
  [switch]$append
 )
 
 foreach ($key in $keys)
 {
  if (Test-Path $key)
  {
   $regValue = (Get-ItemProperty $key).$property
   if (-not ($regValue -eq ""))
   {
    Set-EnvVar $environmentVariable ($regValue + $suffix)
    break
   }
  }
 }
}

Write-Host -ForegroundColor Yellow "Setting environment for using Microsoft Visual Studio 2010 x64 tools.`n"

$VSRegKeys32 = @("HKLM:\SOFTWARE\Microsoft\VisualStudio\SxS\VS7", "HKCU:\SOFTWARE\Microsoft\VisualStudio\SxS\VS7")
$VSRegKeys64 = @("HKLM:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\SxS\VS7", "HKCU:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\SxS\VS7")
$VSRegKeys = ($VSRegKeys32 + $VSRegKeys64)
$VCRegKeys32 = @("HKLM:\SOFTWARE\Microsoft\VisualStudio\SxS\VC7", "HKCU:\SOFTWARE\Microsoft\VisualStudio\SxS\VC7")
$VCRegKeys64 = @("HKLM:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\SxS\VC7", "HKCU:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\SxS\VC7")
$VCRegKeys = ($VCRegKeys32 + $VCRegKeys64)
$FSharpRegKeys = @("HKLM:\SOFTWARE\Microsoft\VisualStudio\10.0\Setup\F#", "HKCU:\SOFTWARE\Microsoft\VisualStudio\10.0\Setup\F#", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\10.0\Setup\F#", , "HKCU:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\10.0\Setup\F#")
$WindowsSDKRegKeys = @("HKLM:\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A", "HKCU:\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v7.0A")

Set-EnvVarFromRegistry $VSRegKeys "10.0" VS100COMNTOOLS "Common7\Tools\"
Set-EnvVarFromRegistry $VSRegKeys "10.0" VSINSTALLDIR
Set-EnvVarFromRegistry $VCRegKeys "10.0" VCINSTALLDIR
Set-EnvVarFromRegistry $FSharpRegKeys ProductDir FSHARPINSTALLDIR
Set-EnvVarFromRegistry $VCRegKeys64 FrameworkDir64 FrameworkDir
Set-EnvVarFromRegistry $VCRegKeys64 FrameworkVer64 FrameworkVersion
Set-EnvVar Framework35Version "v3.5"
Set-EnvVar VCbuildToolsPath ($env:VCINSTALLDIR + "VCPackages")

Set-EnvVarFromRegistry $WindowsSDKRegKeys InstallationFolder WindowsSdkDir
if ($env:WindowsSdkDir -eq "") { Set-EnvVar "WindowsSdkDir" "$env:VCINSTALLDIR\PlatformSDK\" }

if ($env:WindowsSdkDir)
{
 Set-EnvVar PATH ($env:WindowsSdkDir + "bin\NETFX 4.0 Tools\x64;" + $env:WindowsSdkDir + "bin\x64;" + $env:WindowsSdkDir + "bin") -Append
 Set-EnvVar INCLUDE ($env:WindowsSdkDir + "include") -Append
 Set-EnvVar LIB ($env:WindowsSdkDir + "lib\x64") -Append
}

# PATH
Set-EnvVar PATH ($env:VSINSTALLDIR + "Common7\Tools;" + $env:VSINSTALLDIR + "Common7\IDE;$($env:VCINSTALLDIR)VCPackages;$env:FrameworkDir\$env:FrameworkVersion;$env:FrameworkDir\$env:Framework35Version;" + $env:VCINSTALLDIR + "bin\amd64") -Append
if (Test-Path "$env:VSINSTALLDIR\Team Tools\Performance Tools\x64") { Set-EnvVar PATH ($env:VSINSTALLDIR + "Team Tools\Performance Tools\x64;" + $env:VSINSTALLDIR + "Team Tools\Performance Tools") -Append }
if (Test-Path "$env:ProgramFiles\HTML Help Workshop") { Set-EnvVar PATH "$env:ProgramFiles\HTML Help Workshop" -Append }
if (Test-Path "$env:ProgramFiles(x86)\HTML Help Workshop") { Set-EnvVar PATH "$env:ProgramFiles(x86)\HTML Help Workshop" -Append }
Write-Verbose "PATH set to $env:Path"

# INCLUDE
if (Test-Path "$env:VCINSTALLDIR\ATLMFC\INCLUDE") {Set-EnvVar INCLUDE ($env:VCINSTALLDIR + "ATLMFC\INCLUDE") -Append }
Set-EnvVar INCLUDE ($env:VCINSTALLDIR + "INCLUDE;") -Append
Write-Verbose "INCLUDE set to $env:INCLUDE"

# LIB
Set-EnvVar LIB ($env:VCINSTALLDIR + "LIB\amd64;") -Append
if (Test-Path "$env:VCINSTALLDIR\ATLMFC\LIB\amd64") {Set-EnvVar LIB ("$($env:VCINSTALLDIR)ATLMFC\LIB\amd64") -Append }
Write-Verbose "LIB set to $env:LIB"

# LIBPATH
Set-EnvVar LIBPATH ("$env:FrameworkDir\$env:FrameworkVersion;$env:FrameworkDir\$env:Framework35Version;$($env:VCINSTALLDIR)LIB\amd64") -Append
if (Test-Path "$env:VCINSTALLDIR\ATLMFC\LIB\amd64") {Set-EnvVar LIBPATH ($env:VCINSTALLDIR + "ATLMFC\LIB\amd64") -Append }
Write-Verbose "LIBPATH set to $env:LIBPATH"

Set-EnvVar Platform X64
Write-Verbose "Platform set to X64"
Set-EnvVar CommmandPromptType Native
Write-Verbose "CommmandPromptType set to Native"

Monday, 15 November 2010

Powershell: Catching an unspecified number of arguments into an array

I recently had the need for creating a command to be executed on an unspecified number of objects, and being picky regarding the command syntax, I wanted it to be called in the "usual" way, like:

My-Command namedParameter file1 file2 ... file
I.e. the user should not have to know that the list of objects (in this case, files) had to be specified as an array.

This was fairly easy in Powershell, making use of the $MyInvocation predefined variable: simply define those parameters you want as named parameters, and catch the 'rest' into an array.
function My-Command {
   param ($namedParameter)
   #Catch the items to be labelled from the unbound arguments
   if ($MyInvocation.UnboundArguments.Count -gt 0) {
      [string[]]$items = $MyInvocation.UnboundArguments
   }
   else
   {
      [string[]]$items = (($pwd).toString())
   }
   ...
}
In this case, if no unbound arguments are given, the $items array at least gets one value consisting of the current directory path, but you could easily replace that with for example a throw()-statement instead.

Friday, 12 November 2010

Enhancing ClearCase with Powershell

Using Powershell to access IBM Rational ClearCase data gives (as with just about everything that you use with PS) enormous enhancements. Here's just an example: since you can interface with COM objects from Powershell and ClearCase has a (rather useful) COM application, you gain a much more flexible and powerful access to ClearCase data and commands than with the usual cleartool commands. And it's faster.

Here's a replacement for the cleartool command, using the COM interface instead. I haven't done any real tests, but a simple "eye-measure" test with an advanced search told me it's about three times faster:

function Invoke-Cleartool
{
    $CCCleartool = New-Object -COM ClearCase.Cleartool    #Get all arguments as one command-line
    $command = ""
    $MyInvocation.UnboundArguments | Foreach-Object {
        if ($_ -eq ".") {$command += " $pwd"}
        else { $command += " $_" }
    }
   
    # Create a "real" array to store the result in
    $result = New-Object System.Collections.ArrayList
   
    #Parse the output and store as items in the array
    ($CCCleartool.CmdExec($command)).replace($pwd,".").split("`n") | Foreach-Object { $result.Add($_) >$null}
   
    #Remove the last item (which will always be an empty string)
    $result.RemoveAt($result.Count - 1)
   
    $result
}


Update: Just noted that there's a bug here when cleartool requires quotations around the arguments (for example when calling find -name). Powershell is good at stripping any unneeded quotations from arguments, but in this case that doesn't suit me very well...

Tuesday, 9 November 2010

Testing for parameters in Powershell functions

I recently needed to test for parameter existence in a Powershell function. The idea was that if parameters were provided, the command should run directly in shell; else a GUI should be presented where the user can enter any of the parameters in a user-friendly way.

I started out with the "standard" test, using $args:
    if ($args.Count -gt 0)
    {
        ...
    }
    else
    {
        Show-GUI
    }




Unfortunately, $Args turned out to always be zero (at least when using from within a module as in this case). I found help at this post at EggheadCafé though, switching $args for $MyInvocation.BoundParameters:

    if ($MyInvocation.BoundParameters.Count -gt 0)
    {
        ...
    }
    else
    {
        Show-GUI
    }

A small word of warning though: $MyInvocation always refers to the "current context", which means that if you debug and try getting data, it will show the data for the current shell, not the function being debugged.

Wednesday, 29 September 2010

Powershell: Error-handling in modules

I've recently started using modules for my Powershell commands for managing my custom commands, since I this neing a great way to encapsulate and distribute such commands. Besides, I can choose the purpose of my session by selecting what modules to import.

However, there seems to be a BIG caveat with modules: the global variables seems not to be available. In particular, the automatic $error variable cannot be used inside modules. You can try this example:
New-Module -ScriptBlock {
    function div ($number) {
       try { 1/$number }
       catch { Write-Host $Error[0] }
    }
} -name DivMod | Import-module

PS > div 1

1

PS > div 0

Note the empty line: the $Error variable is empty! I need to dig further into this, but since not even Googling seems to have any answers(!), I think I've come up with a workaround, at least: use Invoke-Expression with the -ErrorVariable parameter for 'sensitive' commands (e.g. those that you need good error-handling for).
New-Module -ScriptBlock {
   function div ($number) {
      try { Invoke-Expression '1/$number' -ErrorVariable e }
      catch { Write-Error $e }
   }
} -name DivMod | Import-module

PS > div 1

1

PS > div 0
System.Management.Automation.CmdletInvocationException: Attempted to divide by zero...
Using this, you can use try..catch in modules almost as you would in scripts.

If anyone out there has a better solution (I'd really like to be able to use the $error variable...) don't hesitate to comment/point me in the right direction!

Thursday, 8 October 2009

copy-merge in ClearCase (on Windows)

For various reasons, there are occasions when you want to merge exactly what has been changed on a branch to another without having ClearCase trying to perform the merge itself. A common issue is for binary elements, but I use it for deliveries as well: an identical copy of the element is "merged" from development to integration.

The solutions I've seen (for example from IBM) aren't satisfactory since they perform the operation on every element, disregarding if they need merging or not. I want to have traceability so that only the files needed are merged, allowing me to easily find what changes have been done between merges.

Here's my solution, made as an MS-DOS batch. Note that it's currently only working for snapshot views, since this was one of my requirements. It should be easy to adapt for dynamic views as well (I might just do that in a "near future" myself).

:: Clear previous mergeset
if exist %MERGESET% del /q %MERGESET%

::Automatically merge all directories
cleartool findmerge . -type d -fversion %VERSION% -merge -nc -log NUL

:: Find items eligible to merge, check these out and record them to file
echo Finding files to merge...
cleartool findmerge . -type f -fversion %VERSION% -log NUL -co -nc -exec "cmd /v:on /c echo !CLEARCASE_FXPN!;!CLEARCASE_PN! >>%MERGESET%"

:: Delete the target
echo Deleting targets...
for /f "tokens=2 delims=;" %%a in (%MERGESET%) do del /q "%%a"

:: Use cleartool get to copy contents from source
echo Copying from source to target...
for /f "tokens=1,2 delims=;" %%a in (%MERGESET%) do cleartool get -to "%%b" "%%a"
for /f "tokens=2 delims=;" %%a in (%MERGESET%) do attrib -R "%%a"

:: Create a merge arrow for the these items
echo Creating Merge arrows...
for /f "tokens=1,2 delims=;" %%a in (%MERGESET%) do cleartool mkhlink -unidir Merge "%%a" "%%b"
Some explanation needed: the %VERSION% is an input parameter holding the exact version merging from (e.g. /main/dev_branch/LATEST).

One "tough nut" for me was to find the /v:on-flag for calling cmd, in conjunction with the !-signs around environment variables. This is what makes them set at runtime, and not at time of calling the script (when it's empty).

Wednesday, 7 October 2009

MS-DOS Parameter handling

Configuration Management involves a lot of scripting, in the OS used by the system. Unfortunately for Windows systems, this means scripting in MS-DOS. Yes, I know of Powershell, but as long as that's not default part of OS (comes with Windows 7) it remains not being a real alternative.

Parameter handling in MS-DOS is weak by default, so here's a routine I stole from an old colleague (Jimmy, if you ever read this, pleas shut up. And I guess you stole it from someone first! ;)

First, set up the internal variables needed. The first gets the dir the script is run from, the second the name of the script:
set BASEDIR=%~dp0
set BASENAME=%~n0
I put the Usage-description early so it's easily readable within the script. Note the ^-signs to allow special chars within echo:
goto GET_PARAMETERS

:USAGE
echo Usage:
echo %BASENAME% ^/f ^ /v
echo    ^/f file : enter file with full path
echo    ^/v      : verbose mode
goto END
Start of parameter loop, with a label. I always put the "help"-flag first out of convenience:
:GET_PARAMETERS

:HELP
if /i [%1] neq [/?] goto FILE
goto USAGE
For each parameter, enter a block in the following way. Check first parameter for the current flag, if found then set a variable and "shift out" from parameter stack and start over. If not found, go to next parameter.
:FILE
if /i [%1] neq [/f] goto VERBOSE
set FILE=%2
shift
shift
goto GET_PARAMETERS
If the parameter is a flag only like in the following case, only one shift is needed:
:VERBOSE
if /i [%1] neq [/v] goto ENDPARAM
set VERBOSE=1
shift
goto GET_PARAMETERS

End of parameter loop, check for any other parameters that aren't covered above and give error message if found:
:ENDPARAM
if [%1] equ [] goto VALIDATE_PARAMETERS
echo Error! Incorrect parameters given
goto USAGE
Validate parameters that are mandatory:
:VALIDATE_PARAMETERS
:: Check for mandatory parameters
if /i [%FILE%] neq [] goto EXECUTION
echo Error! Missing parameter Version
goto USAGE
Start the actual script (finally!). Don't forget the end-label at bottom:
:EXECUTION
...
:END
What does this give you? Well, most importantly you don't need to enter parameters in any order and you only allow prescripted input. As a bonus, you get help and usage description.

If you think it's awkwars, all I can say is that MS-DOS is awkward to script in ;).

Here's the script in one go, for easy copy-paste:

set BASEDIR=%~dp0
set BASENAME=%~n0
 
goto GET_PARAMETERS

:USAGE
echo Usage:
echo %BASENAME% ^/f ^ /v
echo ^/f file : enter file with full path
echo ^/v : verbose mode
goto END

:GET_PARAMETERS

:HELP
if /i [%1] neq [/?] goto FILE
goto USAGE

:FILE
if /i [%1] neq [/f] goto VERBOSE
set FILE=%2
shift
shift
goto GET_PARAMETERS

:VERBOSE
if /i [%1] neq [/v] goto ENDPARAM
set VERBOSE=1
shift
goto GET_PARAMETERS

:ENDPARAM
if [%1] equ [] goto VALIDATE_PARAMETERS
echo Error! Incorrect parameters given
goto USAGE

:VALIDATE_PARAMETERS
:: Check for mandatory parameters
if /i [%FILE%] neq [] goto EXECUTION
echo Error! Missing parameter Version
goto USAGE

:EXECUTION
...
:END