How to create nested Solution Folders with envdte - powershell

I've tried to create a visual studio solution with nested solution folders through Powershell (envdte). Everything works up to 1 level deep, but nesting a solution folder doesn't seem to work (interface is null). The issue is described in this SO question:
Creating a tree of Solution Folders using DTE and Package Manager Console
Unfortunately that question hasn't been answered yet. I've reachted out to the poster of that question but he had taken another route within his solution so the question is still open.
An excerpt of my code. The Find-Solution method finds the solutionfolder I'm looking for within the given context (= startfolder). When found, it returns this item:
function Find-SolutionFolder {
Param($SearchedFolderName, $StartFolder)
Write-Output "number " $StartFolder.ProjectItems.Count
For ($i = 1; $i -le $StartFolder.ProjectItems.Count; $i++) {
$Item = $StartFolder.ProjectItems.Item($i)
if ($Null -Eq $Item.Object) {
continue;
}
if ($Item.Object.Kind -eq [EnvDTE80.ProjectKinds]::vsProjectKindSolutionFolder) {
if ($Item.Name -Eq $SearchedFolderName) {
return $Item
}
Find-SolutionFolder $SearchedFolderName $Item
}
}
}
The Add-Projects method takes care of saving the structure to the solution. The structure is:
Solution
ModuleTypeFolder (ie. Foundation)
ModuleGroupFolder (optional)
Folder for project
projectfiles
This is a nested structure. Everything works without the ModuleGroupFolder but when the structure has the ModuleGroupFolder it causes the error due to the null result of the Get-Interface. I've confirmed that the correct solution folder is found. It's just the variable $moduleGroupNameFolderInterface is null.
The parameter modulePath is the path on disk
function Add-Projects {
Param(
[Parameter(Position = 0, Mandatory = $True)]
[string]$ModulePath
)
Write-Output "Adding project(s)..."
# For the sake of the example always use a folder named 'Foundation'
$moduleTypeFolder = Get-FoundationSolutionFolder
# When the literal 'Foundation' solution folder does not exist in the solution it will be created.
if (-Not $moduleTypeFolder) {
$dte.Solution.AddSolutionFolder($config.ModuleType)
$moduleTypeFolder = Get-ModuleTypeSolutionFolder
}
$moduleTypeFolderInterface = Get-Interface $moduleTypeFolder.Object ([EnvDTE80.SolutionFolder])
# Add ModuleGroup Folder if needed
if (-Not [string]::IsNullOrWhiteSpace($config.ModuleGroupName)) {
$moduleGroupNameFolder = Find-SolutionFolder $config.ModuleGroupName $moduleTypeFolder
if (-Not $moduleGroupNameFolder) {
$moduleTypeFolderInterface.AddSolutionFolder($config.ModuleGroupName)
$moduleGroupNameFolder = Find-SolutionFolder $config.ModuleGroupName $moduleTypeFolder
}
$moduleGroupNameFolderInterface = Get-Interface $moduleGroupNameFolder.Object ([EnvDTE80.SolutionFolder])
if ($Null -eq $moduleGroupNameFolderInterface) {
Write-Output "moduleGroupNameFolderInterface is null; this is wrong"
} else {
$moduleNameFolder = $moduleGroupNameFolderInterface.AddSolutionFolder($config.ModuleName)
$moduleNameFolderInterface = Get-Interface $moduleNameFolder.SubProject ([EnvDTE80.SolutionFolder])
# Search in the new module folder for csproj files and add those to the solution.
Get-ChildItem -File -Path $ModulePath -Filter "*$csprojExtension" -Recurse | ForEach-Object { $moduleNameFolderInterface.AddFromFile("$($_.FullName)")}
}
} else {
$moduleNameFolder = $moduleTypeFolderInterface.AddSolutionFolder($config.ModuleName)
$moduleNameFolderInterface = Get-Interface $moduleNameFolder.Object ([EnvDTE80.SolutionFolder])
# Search in the new module folder for csproj files and add those to the solution.
Get-ChildItem -File -Path $ModulePath -Filter "*$csprojExtension" -Recurse | ForEach-Object { $moduleNameFolderInterface.AddFromFile("$($_.FullName)")}
}
Write-Output "Saving solution..."
$dte.Solution.SaveAs($dte.Solution.FullName)
}
Note. the example is not optimized (ie. duplicate code)
Can anybody help me solve the issue.
Update - answer to question
I finally figured it out. Apparently when finding a nested solution folder where property Kind has guid {66A26722-8FB5-11D2-AA7E-00C04F688DDE} it's not the correct object yet. You have to use the object within the found item.

So basically you are looking for recursion. You can recurse like this.
For the solutions folder:
function RecurseSolutionFolderProjects(){
param($solutionFolder = $(throw "Please specify a solutionFolder"))
$projectList = #()
for($i = 1; $i -le $solutionFolder.ProjectItems.Count; $i++){
$subProject = $solutionFolder.ProjectItems.Item($i).subProject
if($subProject -eq $null){
continue;
}
if($subProject.Kind -eq [EnvDTE80.ProjectKinds]::vsProjectKindSolutionFolder)
{
$projectList += RecurseSolutionFolderProjects($subProject)
} else {
$projectList += $subProject
}
}
return $projectList
}
For Project Files:
function GetProjectFiles(){
param($project = $(throw "Please specify a project"))
write-debug ("getting project files for " + $project.Name + " "+ $project.ProjectName)
$projectItems = RecurseDescendants($project.ProjectItems)
return $projectItems | Where-Object {$_.Kind -ne [EnvDTE.Constants]::vsProjectItemKindPhysicalFolder}
}
For Project Items:
function GetProjectItems(){
param($project = $(throw "Please specify a project"))
if($project.ProjectItems.count -gt 0){
write-debug "getting project items for '$project.Name' '$project.ProjectName'"
}
#example: GetProjectItems((GetSolutionProjects).get_Item(1))
$result =RecurseDescendants($project.ProjectItems)
return $result
}
Refer the Solution Hierarchy answer where the above functions are neatly explained
You can get the latest version from this GitHub Link
Hope it helps.

Related

Looking for docs/explainer on powershell syntax ":Label foreach ($item in $items) { }"

So hard to Google this one...
Looking for docs/explainer on the syntax :Label foreach ($item in $items) { }
I came across an interesting example in the official docs and I'm trying to wrap my head around some of the concepts used. The example I'm referencing is at the very bottom of the about_foreach page (using Get-Help) and also online here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_foreach?view=powershell-7.3#:~:text=%3AtokenLoop%20foreach
The example defines an AST/parser utility for showing info about where functions are defined within a given script file (pretty cool advanced example imo). There are a few concepts in the example that I've seen before and understand the usefulness of, but haven't used personally, like do/until statements and enumerator methods like $foreach.MoveNext()
But it's the first time I've seen the :myLabel for () {} syntax which seems to only be relevant to specific expressions like loops, and I'm curious about the usage of this construct (like how/can you reference this label), does anyone here make use of this or know where to find docs/explainer on it?
Thanks in advance !
Here's the full raw example from the docs in case you like clicking links:
function Get-FunctionPosition {
[CmdletBinding()]
[OutputType('FunctionPosition')]
param(
[Parameter(Position = 0, Mandatory,
ValueFromPipeline, ValueFromPipelineByPropertyName)]
[ValidateNotNullOrEmpty()]
[Alias('PSPath')]
[System.String[]]
$Path
)
process {
try {
$filesToProcess = if ($_ -is [System.IO.FileSystemInfo]) {
$_
} else {
Get-Item -Path $Path
}
$parser = [System.Management.Automation.Language.Parser]
foreach ($item in $filesToProcess) {
if ($item.PSIsContainer -or
$item.Extension -notin #('.ps1', '.psm1')) {
continue
}
$tokens = $errors = $null
$ast = $parser::ParseFile($item.FullName, ([REF]$tokens),
([REF]$errors))
if ($errors) {
$msg = "File '{0}' has {1} parser errors." -f $item.FullName,
$errors.Count
Write-Warning $msg
}
:tokenLoop foreach ($token in $tokens) {
if ($token.Kind -ne 'Function') {
continue
}
$position = $token.Extent.StartLineNumber
do {
if (-not $foreach.MoveNext()) {
break tokenLoop
}
$token = $foreach.Current
} until ($token.Kind -in #('Generic', 'Identifier'))
$functionPosition = [pscustomobject]#{
Name = $token.Text
LineNumber = $position
Path = $item.FullName
}
Add-Member -InputObject $functionPosition `
-TypeName FunctionPosition -PassThru
}
}
}
catch {
throw
}
}
}
A label example from Windows Powershell in Action. Labels don't come up that often. In your example, it's breaking out of both the do loop and the token loop with the label.
# loop label, break out of both loops
$target = 'outer'
:outer while (1) {
while(1) {
break $target # break or continue label
}
}

Powershell script to check subfolders exist or not

I'm trying to write a script in PowerShell. There will be the main folder like eg: movies. Inside I will have a subfolder of which language of movie it is and another subfolder inside it for the movie name. I will be giving only the main folder path ie: F:\Movies, And I will be taking the language of the movie folder and movie name folder as parameters I want to verify if the folder is available inside the main folder or not. I wrote the below script but it's not working. Could you please help me to figure it out?
Function Folder_Check {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory=$true)]$Languageofmovie,
[Parameter(Mandatory=$true)]$nameofmovie)
$Foldertocheck = 'F:\Movies'
if ($result = Get-childitem -path $Foldertocheck -Recurse -Directory) {
Write-Host "Folder found in $($result)"
}
else {
Write-Host "No it is not available"
}
}
You can fix this by leveraging the parameters passed to the function.
Since you know the folder structure you can create a variable that combines the root folder, the language and the movie name:
Function Folder_Check {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory=$true)]$Languageofmovie,
[Parameter(Mandatory=$true)]$nameofmovie)
$Foldertocheck = '~\Documents\Movies'
$MovieFolder = "$Foldertocheck\$Languageofmovie\$nameofmovie"
if (Test-Path $MovieFolder -PathType Container) {
Write-Host "$MovieFolder found"
}
else {
Write-Host "$MovieFolder is not available"
}
}
Using -PathType Container to make sure the movie name is a folder and not a file.
Then you can call the function - something like:
Folder_Check English Godfather
Folder_Check English Armageddon
Folder_Check Russian TheIdiot
Did I understand correctly?
Function Folder_Check {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory=$true)]$Languageofmovie,
[Parameter(Mandatory=$true)]$nameofmovie)
$Foldertocheck = $Foldertocheck = 'F:\Movies' + '$($Languageofthemovie)'
if ((Get-childitem -path $Foldertocheck -Directory).name -match '$($nameofmovie)') {
Write-Host "Folder found in $($Foldertocheck)"
}
else {
Write-Host "No it is not available"
}
}

How to check if Certain programs are installed and if they are display message

I'm trying to create a script that will check if Program A,B,C and D is installed. If so display message to say they are all installed else say they are not installed.
From research i have created the following script.
$ProgramList = #("A","B","C","D")
ForEach ($Program in $ProgramList){
Function Get-InstalledApps
{
$Regpath = #(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
Get-ItemProperty $Regpath | .{Process{If($_.DisplayName) { $_ } }}
}
$Result = Get-InstalledApps | Where {$_.DisplayName -like "*$Program*"}
If ($Result) {
[Windows.Forms.Messagebox]::Show("INSTALLED")
} Else {
[Windows.Forms.Messagebox]::Show("NOT INSTALLED")
}
}
My issue is when i run this i get 4 message boxes popup to say the program is installed. i'm trying to make this so it will just give a single message box. if all are installed and if one or more is not installed another message box to say the programs are not installed.
Any help is greatly Appreciated.
You're getting four pop-ups because your calling the msgbox four times (as it's within your loop). Just moving it out doesn't completely solve your problem since it's going to only look at the last one, but if you need to do it the way you are, then something like this would work:
$ProgramList = #("A","B","C","D")
$allInstalled = $true # Assume they're all installed
ForEach ($Program in $ProgramList){
Function Get-InstalledApps {
$Regpath = #(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
Get-ItemProperty $Regpath | .{Process{If($_.DisplayName) { $_ } }}
}
If(-not(Get-InstalledApps | Where {$_.DisplayName -like "*$Program*"})) {
# We know at least one isn't installed
$allInstalled = $false
}
}
If($allInstalled) {
[Windows.Forms.Messagebox]::Show("INSTALLED")
} Else {
[Windows.Forms.Messagebox]::Show("NOT INSTALLED")
}
If you're able to tweak the function a bit, you can speed it up by only pulling in the registry information once. The BEGIN section here runs just once when you call the function with multiple applications.
Function Test-InstalledApps {
Param(
[Parameter(ValueFromPipeline)]
[string[]]$appName
)
Begin {
$Regpath = #('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*','HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')
$allApps = Get-ItemProperty $Regpath | Select DisplayName
$allAppsInstalled = $true
}
Process {
ForEach($app in $appName) {
If(-Not($allApps | Where-Object { $_.DisplayName -like "*$app*" })) {
# We know at least one isn't installed
$allAppsInstalled = $false
}
}
}
End {
Return $allAppsInstalled
}
}
If(Test-InstalledApps #("A","B","C")) {
[Windows.Forms.Messagebox]::Show("INSTALLED")
} Else {
[Windows.Forms.Messagebox]::Show("NOT INSTALLED")
}
I think this might be a bit better approach, using Out-GridView. In my opinion this would look cleaner, I know this is not answering your question but it might suit you better:
$programList = #(
'SomeRandomProgram1'
'Microsoft Visual'
'7-Zip'
'SomeRandomProgram2'
)
$Regpath = #(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$installedPrograms = (Get-ItemProperty $Regpath).where({$_.DisplayName})
$result = foreach($program in $programList)
{
$check = $installedPrograms.DisplayName -match $program
if($check)
{
foreach($match in $check)
{
[pscustomobject]#{
Program = $program
Status = 'Found'
Match = $match
}
}
continue
}
[pscustomobject]#{
Program = $program
Status = 'Not Found'
Match = $null
}
}
$result | Out-GridView
In my computer the OGV looks like this:
I know I'm a bit late to the game here and my answer is you might say completely different buy I did it on a quest to learn a few things and thought others might find it interesting. It produces output like this.
The interesting part is that the menu is self adjusting, within screen size limits, so you can search for more or fewer programs just by passing them in an array. The program searched directories vs the Registry so you can locate programs which are not installed (portable). By default it searches the two Windows locations but you can also pass it an array with additional search locations.
I'm sure it wouldn't take to much to modify to have it search the registry keys instead.
If you're interested you can download a zip file (needed to include the graphics files for the check mark and red x from my OneDrive here.

Powershell - Checking variable for value in foreach - If no value then log other output

I'm having issue with my foreach method. I am checking in the registry whether a good amount of programs are installed. How would I write it to say something is not installed one time versus it saying something's not installed for each key it checks? Now, If I place a ElseIf it executes "PowerBroker not installed." about 16 times. This is due to it checking every key and writing it out for each key it does not find a match to the displayname. How do I go about it checking the key and only writing it out one time if it's not installed?? Thanks!
$UninstallKeys = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
foreach($Key in $UninstallKeys){
if($Key.GetValue("DisplayName") -Match "BeyondTrust"){
$PBW = $Key.GetValue("DisplayName")
$PBWV = $Key.GetValue("DisplayVersion")
if ($PBW) {
$PBW = $PBW, $PBWV
}
else {
$PBW = "PowerBroker not installed."
$installsmissing = "True"
}
}
Give this script a whirl. If I've understood the requirement correctly it should give you what you need.
$displayName = "BeyondTrust"
$uninstallKeys = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
# Filter the keys down by their display name property
$specificUninstallKeys = $uninstallKeys |
Where-Object {
$_.GetValue("DisplayName") -eq $displayName
}
# Did we find any keys of that name?
if ($specificUninstallKeys) {
Write-Output "Keys found: $($specificUninstallKeys.Length)"
}
else {
Write-Output "Sorry pal, no keys by that name here!"
}
# There may be more than one; hence the loop-y requirement here.
foreach ($specificUninstallKey in $specificUninstallKey) {
Write-Output $displayName
Write-Output $specificUninstallKey.GetValue("DisplayVersion")
}

Locate MSTest.exe using powershell

I'm in the process of automating my .Net solution build to be completely in PowerShell. I want to locate MSTest.exe using PowerShell.
I used the following script to locate MSBuild.exe and I hope that I can have something similar to locate MSTest.exe
$msBuildQueryResult = reg.exe query "HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0" /v MSBuildToolsPath
$msBuildQueryResult = $msBuildQueryResult[2]
$msBuildQueryResult = $msBuildQueryResult.Split(" ")
$msBuildLocation = $msBuildQueryResult[12] + "MSBuild.exe"
Any directions ?
The following works with Visual Studio 2010 and higher[1]:
# Get the tools folder location:
# Option A: Target the *highest version installed*:
$vsToolsDir = (
Get-Item env:VS*COMNTOOLS | Sort-Object {[int]($_.Name -replace '[^\d]')}
)[-1].Value
# Option B: Target a *specific version*; e.g., Visual Studio 2010,
# internally known as version 10.0.
# (See https://en.wikipedia.org/wiki/Microsoft_Visual_Studio#History)
$vsToolsDir = $env:VS100COMNTOOLS
# Now locate msbuild.exe in the "IDE" sibling folder.
$msTestExe = Convert-Path -EA Stop (Join-Path $vsToolsDir '..\IDE\MSTest.exe')
The approach is based on this answer and is generalized and adapted to PowerShell.
It is based on system environment variables VS*COMNTOOLS, created by Visual Studio setup, where * represents the VS version number (e.g., 100 for VS 2010).
Re option A: Sort-Object is used to ensure that the most recent Visual Studio installation is targeted, should multiple ones be installed side by side:
The script block used for sorting first extracts only the embedded version number from the variable name ($_.Name -replace '[^\d]'; e.g., 100 from VS100COMNTOOLS) and converts the result to an integer ([int]); [-1] then extracts the last element from the sorted array - i.e., the variable object whose names has the highest embedded version number - and accesses its value (.Value).
The IDE subfolder, in which MSTest.exe is located is a sibling folder of the tools folder that VS*COMNTOOLS points to.
If MSTest.exe is NOT in the expected location, Convert-Path will throw a non-terminating error by default; adding -EA Stop (short for: -ErrorAction Stop) ensures that the script is aborted instead.
[1]
- I've tried up to Visual Studio 2015; do let me know whether or not it works on higher versions.
- Potentially also works with VS 2008.
Perhaps you are wanting something like this?
$regPath = "HKLM:\SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0"
$regValueName = "MSBuildToolsPath"
$msBuildFilename = "MSBUild.exe"
if ( Test-Path $regPath ) {
$toolsPath = (Get-ItemProperty $regPath).$regValueName
if ( $toolsPath ) {
$msBuild = Join-Path $toolsPath $msBuildFilename
if ( -not (Test-Path $msBuild -PathType Leaf) ) {
Write-Error "File not found - '$msBuild'"
}
}
}
# Full path and filename of MSBuild.exe in $msBuild variable
My way of getting mstest path.
GetMSTestPath function is main function which you call and then if first GetMsTestPathFromVswhere function will find something it returns path if not your will be making a long search for mstest.exe. Usually, it takes approximately 10 sec. I know that this is not the best but at least it is something when you struggle to find mstest.exe. Hope it will be helpful for somebody. :)))
function GetMSTestPath
{
function GetTime()
{
$time_now = Get-Date -format "HH:mm:ss"
return $time_now;
}
function GetMsTestPathFromVswhere {
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$path = & $vswhere -latest -prerelease -products * -requires Microsoft.Component.MSBuild -property installationPath
#write-host $path
if ($path) {
$tool = join-path $path 'Common7\IDE\MSTest.exe'
if (test-path $tool) {
return $tool
}
return ""
}
}
function SeachForMsTestPath
{
write-host $(GetTime)
$path = Get-ChildItem C:\ -Filter MSTest.exe -Recurse -ErrorAction Ignore | ? { $_.VersionInfo.FileDescription -eq 'Test Execution Command Line Tool' } | Select -First 1
write-host $(GetTime)
return $path
}
$msTestExePath = GetMsTestPathFromVswhere
if ([string]::IsNullOrEmpty($msTestExePath))
{
$msTestExePath = SeachForMsTestPath;
if ([string]::IsNullOrEmpty($msTestExePath))
{
Write-host "MsTest path is not found. Exiting with error"
Exit -1
}
}
return $msTestExePath;
}
Thanks #Bill_Stewart , I used your comments to write this working function:
function Get-MSTest-Location {
$msTests = #()
$searchResults = Get-ChildItem C:\* -Filter MSTest.exe -Recurse -ErrorAction Ignore
foreach($searchResult in $searchResults) {
try{
if(($searchResult.VersionInfo -ne $null) -and ($searchResult.VersionInfo.FileDescription -eq "Test Execution Command Line Tool"))
{ $msTests = $msTests + $searchResult.FullName }
}
catch{}
}
if($msTests.Length -eq 0)
{return "MSTest not found."}
return $msTests[0]
}