Return all SSRS reports in a folder with the data source name and ConnectString - powershell

This what I have so far. However, I want to list every report to it's connection string. I don't see a unique identifier in the GetDataSourceContents() method to join the report and data source lists.
$ReportServerUri = "YOUR_SERVER";
$rs = New-WebServiceProxy -Uri $ReportServerUri -UseDefaultCredential -Namespace "SSRS"
$rs.Url = "YOUR_SERVER"
$rs.Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials;
$BICItems = $rs.ListChildren("/", $true);
$BICFolders = $BICItems | Where { $_.TypeName -eq "Folder"}
$BICDataSources = $BICItems | Where {$_.typename -eq "DataSource"}
$BICDataSourceFolders = $BICFolders | Where {$_.path -like "*Data Source*"}
$BICReports = $BICItems | Where {$_.typename -eq "Report"}
foreach ($DataSource in $BICDataSources)
{
$BICDataSourceContents = $rs.GetDataSourceContents($DataSource.Path)
$MyConnectStrings = $BICDataSourceContents | Where {$_.ConnectString -like "*MY_CONNECT_STRING*"}
$MyConnectStrings
}

I don't see a unique identifier in the GetDataSourceContents method to join the report and data source lists.
Nope. Neither do I. However when were are querying for those details we already know something unique enough. The path to the datasource itself. This is also what a report would be using so that should be a good connector.
There is a series of functions that I made to serve this purpose. Find-SSRSEntities, Get-SSRSReportDataSources and Get-SSRSDatasourceDetails are what I will try and showcase here. The last one I just made since I had no reason for those details but it was easy enough to integrate into my module.
Find-SSRSEntities
Return items from a SSRS connection. Supports loads of filtering options.
function Find-SSRSEntities{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1)]
[Alias("Path")]
[string]$SearchPath="/",
[Parameter(Position=2)]
[ValidateSet("All", "Folder", "Report", "Resource", "LinkedReport", "DataSource", "Model")]
[Alias("Type")]
[String]$EntityType = "All",
[Parameter(Position=3)]
[String]$Match,
[Parameter(Position=4)]
[Switch]$Partial=$false
)
# Get all of the catalog items that match the criteria passed
# https://msdn.microsoft.com/en-us/library/reportservice2005.reportingservice2005.listchildren.aspx
$recursive = $true
$catalogItems = $ReportService.ListChildren($SearchPath,$recursive)
Write-Verbose "$($catalogItems.Count) item(s) located in the root path $SearchPath"
# Limit the results to the catalog types requested
if($EntityType -ne "All"){$catalogItems = $catalogItems | Where-Object{$_.Type -eq $EntityType}}
Write-Verbose "$($catalogItems.Count) item(s) found matching the type $EntityType"
# Set the match string based on parameters
if(-not $Partial.isPresent -and $Match){$Match = "^$Match$"}
Write-Verbose "Returning all items matching: '$Match'"
# If the regex is an empty string all object will be returned.
return $catalogItems | Where-Object{$_.Name -match $Match}
}
Get-SSRSReportDataSources
When given a valid report path it will return all associated datasources of that report.
function Get-SSRSReportDataSources{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy","SSRSService")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1,Mandatory=$true)]
[Alias("Path")]
[string]$ReportPath
)
# Test the report path to be sure it is for a valid report
if(Test-SSRSPath -ReportService $ReportService -EntityPath $ReportPath -EntityType Report){
$ReportService.GetItemDataSources($reportPath) | ForEach-Object{
[pscustomobject][ordered]#{
ReportPath = $reportPath
DataSourceName = $_.name
Reference = $_.item.reference
}
}
} else {
Write-Error "$ReportPath is not a valid report path"
}
}
Get-SSRSDatasourceDetails
When given a valid datasource path it will return all detail of that datasource. Also attaches an extra path property.
function Get-SSRSDatasourceDetails{
[CmdletBinding()]
param(
[Parameter(Position=0,Mandatory=$true)]
[Alias("Proxy")]
[Web.Services.Protocols.SoapHttpClientProtocol]$ReportService,
[Parameter(Position=1,Mandatory=$true,ValueFromPipelineByPropertyName)]
[Alias("Path")]
[string]$EntityPath
)
process{
# Split the path into its folder and entity parts
$SearchPath = Split-SSRSPath $EntityPath -Parent
$EntityName = Split-Path $EntityPath -Leaf
# Verify the path provided is to a valid datasource
if((Find-SSRSEntities -ReportService $ReportService -SearchPath $SearchPath -EntityType DataSource -Match $EntityName -Partial:$false) -as [boolean]){
Add-Member -InputObject ($ReportService.GetDataSourceContents($EntityPath)) -MemberType NoteProperty -Name "Path" -Value $EntityPath -PassThru
} else {
Write-Warning "Could not find a datasource at path: $EntityPath"
}
}
}
So armed with those lets match up all reports in a folder to their datasource connection strings. I would note that all of these functions rely on a active connection to work. Something like this
$ssrsservice = Connect-SSRSService "http://ssrsreports/ReportServer/ReportService2005.asmx" -Credential $credentials
$PSDefaultParameterValues.Add("*SSRS*:ReportService",$ssrsservice)
That will automatically apply the populated -ReportService $ssrsservice to all the SSRS functions I made below.
Else you could just add something like Find-SSRSEntities -ReportService $rs to the code below and it would work.
# Lets get all of the Marketing Datasources
$datasources = Find-SSRSEntities -SearchPath "/data sources/marketing" -EntityType DataSource | Get-SSRSDatasourceDetails
# Now gather all of their reports
Find-SSRSEntities -SearchPath "/Marketing" -EntityType Report |
# Get the report datasources
Get-SSRSReportDataSources | ForEach-Object{
# Attach the connection strings to each object
$reportDataSourceDetail = $_
# Filter the datasource for the individual datasource mapping of this report
$matchingDatasource = $datasources | Where-Object{$_.path -eq $reportDataSourceDetail.Reference}
Add-Member -InputObject $_ -MemberType NoteProperty -Name ConnectionString -Value $matchingDatasource.ConnectString -PassThru
}
This would net me results that look like this:
ReportPath : /Marketing/OandD Class Summary By Month
DataSourceName : Marketing
Reference : /Data Sources/Marketing/Marketing
ConnectionString : Data Source=SQL08R2VM; Initial Catalog=Marketing;
ReportPath : /Marketing/OandD Class YTD Summary
DataSourceName : Marketing
Reference : /Data Sources/Marketing/Marketing
ConnectionString : Data Source=SQL08R2VM; Initial Catalog=Marketing;
These, and other functions, suite me just fine. I have not really had anyone else using them so you might have issues that I have never encountered. Works fine connecting to my SSRS 2008R2 server using PowerShell v5

Here's a T-SQL statement that will return the data source name, path & connection string with the report name and path.
;WITH
XMLNAMESPACES -- XML namespace def must be the first in with clause.
(
DEFAULT 'http://schemas.microsoft.com/sqlserver/reporting/2006/03/reportdatasource'
,'http://schemas.microsoft.com/SQLServer/reporting/reportdesigner'
AS rd
)
,
shared_datasource
AS
(
SELECT
DsnSharedName = sds.[Name]
, DsnPath = sds.[Path]
, DEF = CONVERT(xml, CONVERT(varbinary(max), content))
FROM
dbo.[Catalog] AS sds
WHERE sds.[Type] = 5) --> 5 = Shared Datasource
,
data_source_name (DsnPath, DsnSharedName, DsnConnString)
AS
(
SELECT
cn.DsnPath
, cn.DsnSharedName
, cn.DsnConnString
FROM
(SELECT
sd.DsnPath
, sd.DsnSharedName
, DsnConnString = dsn.value('ConnectString[1]', 'varchar(150)')
FROM
shared_datasource AS sd
CROSS APPLY sd.DEF.nodes('/DataSourceDefinition') AS R(dsn)
) AS cn
)
SELECT
DataSourceName = lk.[Name]
, dsn.DsnPath
, dsn.DsnConnString
, ReportName = c.[Name]
, ReportFolder = c.[Path]
FROM
dbo.[Catalog] c
INNER JOIN dbo.DataSource ds ON c.ItemID = ds.ItemID
INNER JOIN dbo.[Catalog] lk ON ds.Link = lk.ItemID
INNER JOIN data_source_name dsn ON dsn.DsnSharedName = lk.[Name]
WHERE
c.[Type] = 2 --> 2 = Reports
--AND dsn.DsnConnString LIKE '%Initial Catalog%=%DatabaseNameHere%'
Then you can run the T-SQL script file in powershell with this. original post
<# Function to Check whether Server is Ping Status of the Server #>
Function Check-Ping()
{
param
(
[string]$HostName
)
$PingStatus=Get-WmiObject -Query "Select * from Win32_PingStatus where Address='$HostName'"
Return $PingStatus
}
<# Function to Check Instance name Present in the Server #>
Function Get-SQLInstances()
{
param
(
[string]$SQLServerName
)
$Status=Check-Ping($SQLServerName)
if($Status.StatusCode -ne 0)
{
Return "The Server Is Not Reachable"
}
elseif($Status.StatusCode -eq 0)
{
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $SQLServerName)
$RegKey = $Reg.OpenSubKey("SOFTWARE\\Microsoft\\Microsoft SQL Server")
$Instances=$regKey.GetValue("installedinstances")
Return $Instances
}
}
<# Function To Run TSQL and Return Results within HTML Table Tag #>
Function Run-TSQL()
{
Param
(
[string]$MachineName,
[string]$TSQLfilePath
)
$Assembly=[reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo")
$Instances=Get-SQLInstances($MachineName)
$TSQL=Get-Content $TSQLfilePath
foreach($Instance in $Instances)
{
$SQLServiceStatus=Get-Service -ComputerName $MachineName | Where-Object {$_.displayname -like "SQL Server ("+$Instance+")"}
if($SQLServiceStatus.Status -eq "Running")
{
if($Instance -eq "MSSQLSERVER")
{
$SQLServer=$MachineName
}
Else
{
$SQLServer=$MachineName+"\"+$Instance
}
$SQLServerObject = new-Object Microsoft.SqlServer.Management.Smo.Server($SQLServer)
$DatabaseObject = New-Object Microsoft.SqlServer.Management.Smo.Database
$DatabaseObject = $SQLServerObject.Databases.Item("Master")##The TSQL Script Runs in Master Database
$OutPutDataSet = $DatabaseObject.ExecuteWithResults($TSQL)
for($t=0;$t -lt $OutPutDataSet.Tables.Count;$t++)
{
$OutString+="<Table Border=2>"
$OutString+="<Tr>"
foreach($Column in $OutPutDataSet.Tables[$t].Columns)
{
$OutString+="<Th>"
$OutString+=$Column.ColumnName
$OutString+="</Th>"
}
$OutString+="</Tr>"
for($i=0;$i -lt $OutPutDataSet.Tables[$t].Rows.Count;$i++)
{
$OutString+="<Tr>"
for($j=0;$j -lt $OutPutDataSet.Tables[$t].Columns.Count;$j++)
{
$OutString+="<Td>"
$OutString+=$($OutPutDataSet.Tables[$t].Rows[$i][$j])
$OutString+="</Td>"
}
$OutString+="</Tr>"
}
$OutString+="</Table>"
$OutString+="</Br>"
$OutString+="</Br>"
}
}
}
Return $OutString
}
<# Function To Add Table Tag to with In HTML tags
Modify Title and Subject as Per yoru Requirement
#>
Function Get-HTMLOut()
{
Param
(
[String]$InputFile,
[String]$OutputFile,
[String]$TSQL
)
$Out+="<Html>"
$Out+="<Title>Run TSQL and Return HTML FIle</Title>" ## Modify 'TiTle' Tag as per your Required
$Out+="<Head><style>body {background-color:lightgray} H3{color:blue}H1{color:green}table, td, th {border: 1px solid green;}th {background-color: green;color: white;}</style></Head>" ## Modify 'Head' Tag as per your Required
$Out+="<Body><H1 Align='Center'>Run TSQL and Return HTML File</H1></Br></Br>" ## Modify 'Body' Tag as per your Required
ForEach($ServerName in Get-Content $InputFile)
{
$Out+="<H3 align='center'>--------------$ServerName--------------</H3>" ## Modify 'header Text' Tag as per your Required
$Out+="</Br>"
$Out+=Run-TSQL -MachineName $ServerName -TSQLfilePath $TSQL
}
$Out+="</Body></Html>"
Set-Content -Value $Out -Path $OutputFile
}
<# Call Get-HTMLOut Function
It Accepts 3 parameter
a. -InputFile (.txt file each server in a List withOut Instance Name)
b. -OutputFile (.Html File to which Output need to be sent)
c. -TSQL (.sql file which Contains the Script to Run)
#>
Get-HTMLOut -InputFile ".\Servers.txt" -OutputFile .\Status.Html -TSQL '.\TSQL Script.sql'

Related

SMLETS: Powershell

We want to generate an SR per row based on the criteria of a CSV file looking like:
SR templete
The additional criterion:
If the SLO countdown is less than 7 days then the due date is always 7 days for the ticket to be due. Otherwise then then countdown is number SLO _Countdown
The support group is always servicedesk
Unless the host_name does not contain "RES" then it is the support group is EITS_HW_Notes and it will be assigned to "custodian".
No matter what an SR is generated even if null.
My difficulty is my lack familiarity with smlets. I am happy to consider generating tickets via email as well. But would like help on how best to do that via powershell. But the code I came up with is below:
`#Prod
#$GLOBAL:smdefaultcomputer = "prodserver"
#Test
$GLOBAL:smdefaultcomputer = "testserver"
Import-Module SMlets
$path = "C:\Temp\Test.csv"
$csv = Import-csv -path $path
#Variable / Class Setup
$srClass = Get-SCSMClass -name System.WorkItem.ServiceRequest
$srprior = Get-SCSMEnumeration -Name ServiceRequestPriorityEnum.Medium
$srurg = Get-SCSMEnumeration -Name ServiceRequestUrgencyEnum.Medium
#$ararea = get-SCSMEnumeration -Name ServiceRequestAreaEnum.Other
$ararea = get-SCSMEnumeration -Name Enum.add3768303064ec18890170ba33cffda
$title = “Title Goes Here”
$descrip = "Description info goes here"
#Service Request Arguements
$srargs = #{
Title = $title;
Urgency = $srurg;
Priority = $srprior;
ID = “SR{0}”;
Area = $ararea;
SupportGroup = "ServiceDesk";
Description = $descrip
}
#Create Service Request
$newServiceRequest = New-SCSMOBject -Class $srClass -PropertyHashtable $srargs -PassThru
#get SR ID of the new object
$SRId = $newServiceRequest.id
#Get Projection & Object for Created Service Request
$srTypeProjection = Get-SCSMTypeProjection -name System.WorkItem.ServiceRequestProjection$
$SRProj = Get-scsmobjectprojection -ProjectionName $srTypeProjection.Name -filter “Id -eq $SRId”
#Set Afffected User
$userClass = Get-SCSMClass -Name Microsoft.AD.UserBase$
$cType = "Microsoft.EnterpriseManagement.Common.EnterpriseManagementObjectCriteria"
$cString = "UserName = 'itservicenotifications' and Domain = 'SHERMAN'"
$crit = new-object $cType $cString,$userClass
$user = Get-SCSMObject -criteria $crit
$AffectedUserRel = get-scsmrelationshipclass -name System.WorkItemAffectedUser$
New-SCSMRelationshipObject -RelationShip $AffectedUserRel -Source $newServiceRequest -Target $user -Bulk`
I tried the above code but am running into issues recognizing the column name in the CSV file and am unfamiliar with SMLETS + powershell if statements.
Columns are:
CSV Columns
CSV text with examples is: Columns with examples
Could you paste the CSV columns as text, please? Or, better, a sample CSV with one or two rows (redact any sensitive data).
I would expect a CSV to contain multiple rows - even if yours does not, it's good defensive programming to act as if it does. So the first modification I suggest is:
$path = "C:\Temp\Test.csv"
$csv = Import-csv -path $path
foreach ($Row in $csv)
{
# the rest of your code goes in here
}
I find it helpful while debugging to go step-by-step. If I understand your problem right, it's about building the right hashtable in $srargs to pass to New-SCSMOBject. So the next modification is:
foreach ($Row in $csv)
{
$srClass = Get-SCSMClass -name System.WorkItem.ServiceRequest
# etc
$srargs = #{
Title = $title
Urgency = $srurg
Priority = $srprior
ID = “SR{0}”
Area = $ararea
SupportGroup = "ServiceDesk"
Description = $descrip
}
$srargs # write the hashtable so you can inspect it
# skip the rest of the code for now
}
I understand your question as "how to express the logic of":
support group is always servicedesk
Unless the host_name does not contain "RES"
then the support group is contents of EITS_HW_Notes cell in CSV
and it will be assigned to "custodian"
I can't help you with setting the assignee. But we can rejig the rest of the statement:
if host_name contains "RES"
SupportGroup = servicedesk
else
SupportGroup = contents of EITS_HW_Notes cell
You can code that like this:
foreach ($Row in $csv)
{
$srClass = Get-SCSMClass -name System.WorkItem.ServiceRequest
# etc
if ($Row.host_name -like "*RES*")
{
$SupportGroup = "ServiceDesk"
}
else
{
$SupportGroup = $Row.EITS_HW_Notes
}
$srargs = #{
Title = $title
# etc
SupportGroup = $SupportGroup
Description = $descrip
}
}
Does that get you any closer to your solution?

Monad in PowerShell?

I have the following code
function Get($url) {
$x = Invoke-RestMethod $url
# ....
$result # return a json which some values (id, name, color) and a list
}
Get "http://...." |
% {
$id = $_.id
$url = "${$_.url}/xxx"
Get $ur |
% {
$name = $_.name
$url = $_.url
Get $url |
% {
$color = $_.color
$url = $_.url
Get $url | % {
# other layer omitted
}
}
}
}
The code looks bad. It seems the embedded layers should be resolved by monad. Is there a way to do it in PowerShell?
The following shows a pseudo F# code.
myMonad {
let! (id1, _, _, urls1, _ ) = Get ["http://..."]
let! (_, name2, _, urls2, _ ) = Get urls1
let! (_, _, color3, urls3, names) = Get urls2
// ....
printfn "%d %s %s %A" id1, name2, color3, names
}
PowerShell is clearly not a function language, though it has some functional traits and its many constructs and dynamic nature allow you to emulate functional features:
The code below defines a recurse-and-aggregate function named recurse that can invoke your Get function as follows in order to aggregate properties Id, Name, and Color from recursive calls to Get via each returned object's .URL property:
recurse -Function Get <# target function #> `
-Argument http://example.org <# input URL #> `
-RecurseOn URL <# what result property to recurse on #> `
-PropertyNames Id, Name, Color <# what properties to aggregate successively,
in each iteration #>
recurse source code with sample code:
# A fairly generic recurse-and-aggregate function that aggregates properties from
# result objects from recursive calls to a given function based on a single result property.
function recurse($Function, $Arguments, $RecurseOn, $PropertyNames, $outProperties = [ordered] #{ }) {
# Call the target function with the specified arguments.
$result = & $Function $Arguments
# Split into the names of the current and the remaining properties to aggregate.
$propName, $remainingPropNames = $PropertyNames
# Add the value of the current property of interest to the output hashtable, if present.
if ($null -ne $result.$propName) {
if ($outProperties.Contains($propName)) { # not the first value
if ($outProperties.$propName -is [array]) { # already an array -> "extend" the array.
$outProperties.$propName += $result.$propName
}
else { # not an array yet -> convert to array, with the previous value and the new one as the elements.
$outProperties.$propName = $outProperties.$propName, $result.$propName
}
}
else { # first value -> assign as-is
$outProperties.$propName = $result.$propName
}
}
if ($remainingPropNames) {
# Recurse on the value(s) of the property specfied with -RecurseOn
foreach ($newArgument in $result.$RecurseOn) {
$null = recurse -Function $function -Argument $newArgument -RecurseOn $RecurseOn -PropertyNames $remainingPropNames -outProperties $outProperties
}
}
# Return the aggregated properties.
$outProperties
}
# Sample input function:
# It makes a REST call to the given URL and returns the
# result object.
function Get($url) {
Write-Host "Get $url"
$id = 1
$name = ''
$color = ''
if ($url -eq 'http://example.org') {
$urls = 'http://example.org/1', 'http://example.org/2'
$id = 1
}
elseif ($url -match 'http://example.org/\d$') {
$urls = "$url/a", "$url/b"
$name = 'test'
}
elseif ($url -match 'http://example.org/\d/a') {
$urls = '...'
$name = 'test'
$color = "[color of $url] #1", "[color of $url] #2"
}
elseif ($url -match 'http://example.org/\d/b') {
$urls = '...'
$name = 'test'
$color = "[color of $url] #1", "[color of $url] #2"
}
[pscustomobject] #{
URL = $urls
Id = $id
Name = $name
Color = $color
}
}
# Call the recurse-and-aggregate function, passing it the name of the Get()
# function, an input URL, what result property to recurse on, and a list of properties to aggregate.
# The output is an ordered hashtable containing the aggregated property values.
recurse -Function Get <# target function #> `
-Argument http://example.org <# input URL #> `
-RecurseOn URL <# what result property to recurse on #> `
-PropertyNames Id, Name, Color <# what properties to aggregate successively,
in each iteration #>
The above yields:
Name Value
---- -----
Id 1
Name {test, test}
Color {[color of http://example.org/1/a] #1, [color of http://example.org/1/a] #2, [color of http://example.org/1/b] #1, [color of htt…
Well, you could insert F# code into Powershell with Add-Type. See Example 7: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-5.1

Fast Registry Searcher in Powershell

I'm trying to incorporate this search-registry script that I found on Github into one of my scripts.
https://github.com/KurtDeGreeff/PlayPowershell/blob/master/Search-Registry.ps1
To test it, I used one of the examples that it provided:
Search-Registry -StartKey HKLM -Pattern $ENV:USERNAME -MatchData
After executing, I was still prompted to enter a StartKey and a Pattern into the console. After providing that information once again, the command fails.
cmdlet Search-Registry.ps1 at command pipeline position 1
Supply values for the following parameters:
StartKey: HKLM
Pattern: $ENV:USERNAME -MatchData
You must specify at least one of: -MatchKey -MatchValue -MatchData
At C:\Users\Cole\Desktop\Powershell\Search-Registry.ps1:93 char:5
+ throw "You must specify at least one of: -MatchKey -MatchValue -M ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (You must specif...alue -MatchData:String) [], RuntimeException
+ FullyQualifiedErrorId : You must specify at least one of: -MatchKey -MatchValue -MatchData
Is there something that I'm doing incorrectly?
I have tested that same function on GitHub too and also came across errors with it, so I decided to do a complete rewrite myself.
The function below can use either Regular Expression matches or Wildcard comparisons using the -like operator.
To search for nameless default properties, either don't specify the Pattern or RegexPattern parameters at all, or use one of them with an empty string.
The function also has a Recurse switch, so it is up to you if you want to recursively search through all subkeys or not.
Note that although the function performs fast, searching the registry can take a long time to finish..
# If you want to run this on PowerShell < 3.0 use
# New-Object -TypeName PSObject -Property #{ ... } wherever it says [PSCustomObject]#{ ... }
# and change the -version value for 'requires' to 2
#requires -version 3
function Search-Registry {
<#
.SYNOPSIS
Searches the registry on one or more computers for a specified text pattern.
.DESCRIPTION
Searches the registry on one or more computers for a specified text pattern.
Supports searching for any combination of key names, value names, and/or value data.
The search pattern is either a regular expression or a wildcard pattern using the 'like' operator.
(both are case-insensitive)
.PARAMETER ComputerName
(Required) Searches the registry on the specified computer(s). This parameter supports piped input.
.PARAMETER Pattern
(Optional) Searches using a wildcard pattern and the -like operator.
Mutually exclusive with parameter 'RegexPattern'
.PARAMETER RegexPattern
(Optional) Searches using a regular expression pattern.
Mutually exclusive with parameter 'Pattern'
.PARAMETER Hive
(Optional) The registry hive rootname.
Can be any of 'HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU'
If not specified, the hive must be part of the 'KeyPath' parameter.
.PARAMETER KeyPath
(Optional) Starts the search at the specified registry key. The key name contains only the subkey.
This parameter can be prefixed with the hive name.
In that case, parameter 'Hive' is ignored as it is then taken from the given path.
Examples:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall
HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall
Software\Microsoft\Windows\CurrentVersion\Uninstall
.PARAMETER MaximumResults
(Optional) Specifies the maximum number of results per computer searched.
A value <= 0 means will return the maximum number of possible matches (2147483647).
.PARAMETER SearchKeyName
(Optional) Searches for registry key names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyName
(Optional) Searches for registry value names. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER SearchPropertyValue
(Optional) Searches for registry value data. You must specify at least one of -SearchKeyName, -SearchPropertyName, or -SearchPropertyValue.
.PARAMETER Recurse
(Optional) If set, the function will recurse the search through all subkeys found.
.OUTPUTS
PSCustomObjects with the following properties:
ComputerName The computer name where the search was executed
Hive The hive name used in Win32 format ("CurrentUser", "LocalMachine" etc)
HiveName The hive name used ("HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE" etc.)
HiveShortName The abbreviated hive name used ("HKCU", "HKLM" etc.)
Path The full registry path ("HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Uninstall")
SubKey The subkey without the hive ("Software\Microsoft\Windows\CurrentVersion\Uninstall")
ItemType Informational: describes the type 'RegistryKey' or 'RegistryProperty'
DataType The .REG formatted datatype ("REG_SZ", "REG_EXPAND_SZ", "REG_DWORD" etc.). $null for ItemType 'RegistryKey'
ValueKind The Win32 datatype ("String", "ExpandString", "DWord" etc.). $null for ItemType 'RegistryKey'
PropertyName The name of the property. $null for ItemType 'RegistryKey'
PropertyValue The value of the registry property. $null for ItemType 'RegistryKey'
PropertyValueRaw The raw, unexpanded value of the registry property. $null for ItemType 'RegistryKey'
The difference between 'PropertyValue' and 'PropertyValueRaw' is that in 'PropertyValue' Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in 'PropertyValueRaw' the data is returned as-is.
(Environment names return as '%SystemRoot%')
.EXAMPLE
Search-Registry -Hive HKLM -KeyPath SOFTWARE -Pattern $env:USERNAME -SearchPropertyValue -Recurse -Verbose
Searches HKEY_LOCAL_MACHINE on the local computer for registry values whose data contains the current user's name.
Searches like this can take a long time and you may see warning messages on registry keys you are not allowed to enter.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_CURRENT_USER\Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
or
Search-Registry -Hive HKEY_CURRENT_USER -KeyPath 'Printers\Settings' -Pattern * -SearchPropertyName | Export-Csv -Path 'D:\printers.csv' -NoTypeInformation
Searches HKEY_CURRENT_USER (HKCU) on the local computer for printer names and outputs it as a CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKLM:\SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
or
Search-Registry -Hive HKLM -KeyPath 'SOFTWARE\Classes\Installer' -Pattern LastUsedSource -SearchPropertyName -Recurse
Outputs the LastUsedSource registry entries on the current computer.
.EXAMPLE
Search-Registry -KeyPath 'HKCR\.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
or
Search-Registry -Hive HKCR -KeyPath '.odt' -RegexPattern '.*' -SearchKeyName -MaximumResults 10 -Verbose
Outputs at most ten matches if the specified key exists.
This command returns a result if the current computer has a program registered to open files with the .odt extension.
The pattern '.*' means match everything.
.EXAMPLE
Get-Content Computers.txt | Search-Registry -KeyPath "HKLM:\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine" -Pattern '*' -SearchPropertyName | Export-Csv -Path 'D:\powershell.csv' -NoTypeInformation
Searches for any property name in the registry on each computer listed in the file Computers.txt starting at the specified subkey.
Output is sent to the specified CSV file.
.EXAMPLE
Search-Registry -KeyPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace' -SearchPropertyName -Recurse -Verbose
Searches for the default (nameless) properties in the specified registry key.
#>
[CmdletBinding(DefaultParameterSetName = 'ByWildCard')]
Param(
[Parameter(ValueFromPipeline = $true, Mandatory = $false, Position = 0)]
[string[]]$ComputerName = $env:COMPUTERNAME,
[Parameter(Mandatory = $false, ParameterSetName = 'ByRegex')]
[string]$RegexPattern,
[Parameter(Mandatory = $false, ParameterSetName = 'ByWildCard')]
[string]$Pattern,
[Parameter(Mandatory = $false)]
[ValidateSet('HKEY_CLASSES_ROOT','HKEY_CURRENT_CONFIG','HKEY_CURRENT_USER','HKEY_DYN_DATA','HKEY_LOCAL_MACHINE',
'HKEY_PERFORMANCE_DATA','HKEY_USERS','HKCR','HKCC','HKCU','HKDD','HKLM','HKPD','HKU')]
[string]$Hive,
[string]$KeyPath,
[int32] $MaximumResults = [int32]::MaxValue,
[switch]$SearchKeyName,
[switch]$SearchPropertyName,
[switch]$SearchPropertyValue,
[switch]$Recurse
)
Begin {
# detect if the function is called using the pipeline or not
# see: https://communary.net/2015/01/12/quick-tip-determine-if-input-comes-from-the-pipeline-or-not/
# and: https://www.petri.com/unraveling-mystery-myinvocation
[bool]$isPipeLine = $MyInvocation.ExpectingInput
# sanitize given parameters
if ([string]::IsNullOrWhiteSpace($ComputerName) -or $ComputerName -eq '.') { $ComputerName = $env:COMPUTERNAME }
# parse the give KeyPath
if ($KeyPath -match '^(HK(?:CR|CU|LM|U|PD|CC|DD)|HKEY_[A-Z_]+)[:\\]?') {
$Hive = $matches[1]
# remove HKLM, HKEY_CURRENT_USER etc. from the path
$KeyPath = $KeyPath.Split("\", 2)[1]
}
switch($Hive) {
{ #('HKCC', 'HKEY_CURRENT_CONFIG') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentConfig; break }
{ #('HKCR', 'HKEY_CLASSES_ROOT') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::ClassesRoot; break }
{ #('HKCU', 'HKEY_CURRENT_USER') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::CurrentUser; break }
{ #('HKDD', 'HKEY_DYN_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::DynData; break }
{ #('HKLM', 'HKEY_LOCAL_MACHINE') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::LocalMachine; break }
{ #('HKPD', 'HKEY_PERFORMANCE_DATA') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::PerformanceData; break }
{ #('HKU', 'HKEY_USERS') -contains $_ } { $objHive = [Microsoft.Win32.RegistryHive]::Users; break }
}
# critical: Hive could not be determined
if (!$objHive) {
Throw "Parameter 'Hive' not specified or could not be parsed from the 'KeyPath' parameter."
}
# critical: no search criteria given
if (-not ($SearchKeyName -or $SearchPropertyName -or $SearchPropertyValue)) {
Throw "You must specify at least one of these parameters: 'SearchKeyName', 'SearchPropertyName' or 'SearchPropertyValue'"
}
# no patterns given will only work for SearchPropertyName and SearchPropertyValue
if ([string]::IsNullOrEmpty($RegexPattern) -and [string]::IsNullOrEmpty($Pattern)) {
if ($SearchKeyName) {
Write-Warning "Both parameters 'RegexPattern' and 'Pattern' are emtpy strings. Searching for KeyNames will not yield results."
}
}
# create two variables for output purposes
switch ($objHive.ToString()) {
'CurrentConfig' { $hiveShort = 'HKCC'; $hiveName = 'HKEY_CURRENT_CONFIG' }
'ClassesRoot' { $hiveShort = 'HKCR'; $hiveName = 'HKEY_CLASSES_ROOT' }
'CurrentUser' { $hiveShort = 'HKCU'; $hiveName = 'HKEY_CURRENT_USER' }
'DynData' { $hiveShort = 'HKDD'; $hiveName = 'HKEY_DYN_DATA' }
'LocalMachine' { $hiveShort = 'HKLM'; $hiveName = 'HKEY_LOCAL_MACHINE' }
'PerformanceData' { $hiveShort = 'HKPD'; $hiveName = 'HKEY_PERFORMANCE_DATA' }
'Users' { $hiveShort = 'HKU' ; $hiveName = 'HKEY_USERS' }
}
if ($MaximumResults -le 0) { $MaximumResults = [int32]::MaxValue }
$script:resultCount = 0
[bool]$useRegEx = ($PSCmdlet.ParameterSetName -eq 'ByRegex')
# -------------------------------------------------------------------------------------
# Nested helper function to (recursively) search the registry
# -------------------------------------------------------------------------------------
function _RegSearch([Microsoft.Win32.RegistryKey]$objRootKey, [string]$regPath, [string]$computer) {
try {
if ([string]::IsNullOrWhiteSpace($regPath)) {
$objSubKey = $objRootKey
}
else {
$regPath = $regPath.TrimStart("\")
$objSubKey = $objRootKey.OpenSubKey($regPath, $false) # $false --> ReadOnly
}
}
catch {
Write-Warning ("Error opening $($objRootKey.Name)\$regPath" + "`r`n " + $_.Exception.Message)
return
}
$subKeys = $objSubKey.GetSubKeyNames()
# Search for Keyname
if ($SearchKeyName) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
if ($useRegEx) { $isMatch = ($keyName -match $RegexPattern) }
else { $isMatch = ($keyName -like $Pattern) }
if ($isMatch) {
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = "$regPath\$keyName".TrimStart("\")
'ItemType' = 'RegistryKey'
'DataType' = $null
'ValueKind' = $null
'PropertyName' = $null
'PropertyValue' = $null
'PropertyValueRaw' = $null
}
$script:resultCount++
}
}
}
}
# search for PropertyName and/or PropertyValue
if ($SearchPropertyName -or $SearchPropertyValue) {
foreach ($name in $objSubKey.GetValueNames()) {
if ($script:resultCount -lt $MaximumResults) {
$data = $objSubKey.GetValue($name)
$raw = $objSubKey.GetValue($name, '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($SearchPropertyName) {
if ($useRegEx) { $isMatch = ($name -match $RegexPattern) }
else { $isMatch = ($name -like $Pattern) }
}
else {
if ($useRegEx) { $isMatch = ($data -match $RegexPattern -or $raw -match $RegexPattern) }
else { $isMatch = ($data -like $Pattern -or $raw -like $Pattern) }
}
if ($isMatch) {
$kind = $objSubKey.GetValueKind($name).ToString()
switch ($kind) {
'Binary' { $dataType = 'REG_BINARY'; break }
'DWord' { $dataType = 'REG_DWORD'; break }
'ExpandString' { $dataType = 'REG_EXPAND_SZ'; break }
'MultiString' { $dataType = 'REG_MULTI_SZ'; break }
'QWord' { $dataType = 'REG_QWORD'; break }
'String' { $dataType = 'REG_SZ'; break }
default { $dataType = 'REG_NONE'; break }
}
# for PowerShell < 3.0 use: New-Object -TypeName PSObject -Property #{ ... }
[PSCustomObject]#{
'ComputerName' = $computer
'Hive' = $objHive.ToString()
'HiveName' = $hiveName
'HiveShortName' = $hiveShort
'Path' = $objSubKey.Name
'SubKey' = $regPath.TrimStart("\")
'ItemType' = 'RegistryProperty'
'DataType' = $dataType
'ValueKind' = $kind
'PropertyName' = if ([string]::IsNullOrEmpty($name)) { '(Default)' } else { $name }
'PropertyValue' = $data
'PropertyValueRaw' = $raw
}
$script:resultCount++
}
}
}
}
# recurse through all subkeys
if ($Recurse) {
foreach ($keyName in $subKeys) {
if ($script:resultCount -lt $MaximumResults) {
$newPath = "$regPath\$keyName"
_RegSearch $objRootKey $newPath $computer
}
}
}
# close opened subkey
if (($objSubKey) -and $objSubKey.Name -ne $objRootKey.Name) { $objSubKey.Close() }
}
}
Process{
if ($isPipeLine) { $ComputerName = #($_) }
$ComputerName | ForEach-Object {
Write-Verbose "Searching the registry on computer '$ComputerName'.."
try {
$rootKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($objHive, $_)
_RegSearch $rootKey $KeyPath $_
}
catch {
Write-Error "$($_.Exception.Message)"
}
finally {
if ($rootKey) { $rootKey.Close() }
}
}
Write-Verbose "All Done searching the registry. Found $($script:resultCount) results."
}
}
It returns a collection of objects with the following properties:
ComputerName : MYMACHINE
Hive : LocalMachine
HiveName : HKEY_LOCAL_MACHINE
HiveShortName : HKLM
Path : HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
SubKey : SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\{F3F5824C-AD58-4728-AF59-A1EBE3392799}
ItemType : RegistryProperty
DataType : REG_SZ
ValueKind : String
PropertyName : (Default)
PropertyValue : Sticky Notes Namespace Extension for Windows Desktop Search
PropertyValueRaw : Sticky Notes Namespace Extension for Windows Desktop Search
The difference between PropertyValue and PropertyValueRaw is that in PropertyValue Environment names are expanded
('%SystemRoot%' in the data gets expanded to 'C:\Windows'), whereas in PropertyValueRaw the data is returned as-is.
(Environment names return as '%SystemRoot%')
I suspect it may have to do with the way you downloaded the file, and how you're calling it. Please try the following steps and see if that works for you.
Click the link to view the file in RAW format, or just click here
Right-click on the page and Save As... a ps1 file in your scripts folder. Note: Make sure you save it with the .ps1 extension and not as txt.
Open Powershell, browse to the directory, and call it as so...
.\Search-Registry.ps1 -StartKey HKCU -Pattern "Visual Studio" -MatchData
You can modify however you like, of course, I chose to look for Visual Studio in HKCU since I knew it would be a quick search.

Extract Pages from a PDF using itextsharp in Powershell

I have been researching this for weeks now and can't seem to make much ground on the subject. I have a large PDF (900+ pages), that is the result of a mail merge. The result is 900+ copies of the same document which is one page, with the only difference being someone's name on the bottom. What I am trying to do, is have a powershell script read the document using itextsharp and save pages that contain a specific string (the person's name) into their respective folder.
This is what I have managed so far.
Add-Type -Path C:\scripts\itextsharp.dll
$reader = New-Object iTextSharp.text.pdf.pdfreader -ArgumentList
"$pwd\downloads\TMs.pdf"
for($page = 1; $page -le $reader.NumberOfPages; $page++) {
$pageText = [iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader,$page).Split([char]0x000A)
if($PageText -match 'DAN KAGAN'){
Write-Host "DAN FOUND"
}
}
As you can see I am only using one name for now for testing. The script finds the name properly 10 times. What I cannot seem to find any information on, is how to extract pages that this string appears on.
I hope this was clear. If I can be of any help, please let me know.
Thanks!
I actually just finished writing a very similar script. With my script, I need to scan a PDF of report cards, find a student's name and ID number, and then extract that page and name it appropriately. However, each report card can span multiple pages.
It looks like you're using iTextSharp 5, which is good because so am I. iTextSharp 7's syntax is wildly different and I haven't learned it yet.
Here's the logic that does the page extraction, roughly:
$Document = [iTextSharp.text.Document]::new($PdfReader.GetPageSizeWithRotation($StartPage))
$TargetMemoryStream = [System.IO.MemoryStream]::new()
$PdfCopy = [iTextSharp.text.pdf.PdfSmartCopy]::new($Document, $TargetMemoryStream)
$Document.Open()
foreach ($Page in $StartPage..$EndPage) {
$PdfCopy.AddPage($PdfCopy.GetImportedPage($PdfReader, $Page));
}
$Document.Close()
$NewFileName = 'Elementary Student Record - {0}.pdf' -f $Current.Student_Id
$NewFileFullName = [System.IO.Path]::Combine($OutputFolder, $NewFileName)
[System.IO.File]::WriteAllBytes($NewFileFullName, $TargetMemoryStream.ToArray())
Here is the complete working script. I've removed as little as possible to provide you a near working example:
Import-Module -Name SqlServer -Cmdlet Invoke-Sqlcmd
Add-Type -Path 'C:\...\itextsharp.dll'
# Get table of valid student IDs
$ServerInstance = '...'
$Database = '...'
$Query = #'
select student_id, student_name from student
'#
$ValidStudents = #{}
Invoke-Sqlcmd -Query $Query -ServerInstance $ServerInstance -Database $Database -OutputAs DataRows | ForEach-Object {
[void]$ValidStudents.Add($_.student_id.trim(), $_.student_name)
}
$PdfFiles = Get-ChildItem "G:\....\*.pdf" -File |
Select-Object -ExpandProperty FullName
$OutputFolder = 'G:\...'
$StudentIDSearchPattern = '(?mn)^(?<Student_Id>\d{6,7}) - (?<Student_Name>.*)$'
foreach ($PdfFile in $PdfFiles) {
$PdfReader = [iTextSharp.text.pdf.PdfReader]::new($PdfFile)
$StudentStack = [System.Collections.Stack]::new()
# Map out the PDF file.
foreach ($Page in 1..($PdfReader.NumberOfPages)) {
[iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($PdfReader, $Page) |
Where-Object { $_ -match $StudentIDSearchPattern } |
ForEach-Object {
$StudentStack.Push([PSCustomObject]#{
Student_Id = $Matches['Student_Id']
Student_Name = $Matches['Student_Name']
StartPage = $Page
IsValid = $ValidStudents.ContainsKey($Matches['Student_Id'])
})
}
}
# Extract the pages and save the files
$LastPage = $PdfReader.NumberOfPages
while ($StudentStack.Count -gt 0) {
$Current = $StudentStack.Pop()
$StartPage = $Current.StartPage
$EndPage = $LastPage
$Document = [iTextSharp.text.Document]::new($PdfReader.GetPageSizeWithRotation($StartPage))
$TargetMemoryStream = [System.IO.MemoryStream]::new()
$PdfCopy = [iTextSharp.text.pdf.PdfSmartCopy]::new($Document, $TargetMemoryStream)
$Document.Open()
foreach ($Page in $StartPage..$EndPage) {
$PdfCopy.AddPage($PdfCopy.GetImportedPage($PdfReader, $Page));
}
$Document.Close()
$NewFileName = 'Elementary Student Record - {0}.pdf' -f $Current.Student_Id
$NewFileFullName = [System.IO.Path]::Combine($OutputFolder, $NewFileName)
[System.IO.File]::WriteAllBytes($NewFileFullName, $TargetMemoryStream.ToArray())
$LastPage = $Current.StartPage - 1
}
}
In my test environment this processes about 500 students across 5 source PDFs in about 15 seconds.
I tend to use constructors instead of New-Object, but there's no real difference between them. I just find them easier to read.

PowerShell cmdlet parameter value tab completion

How do you implement the parameter tab completion for PowerShell functions or cmdlets like Get-Service and Get-Process in PowerShell 3.0?
I realise ValidateSet works for a known list, but I want to generate the list on demand.
Adam Driscoll hints that it is possible for cmdlets but unfortunately hasn't elaborated.
Trevor Sullivan shows a technique for functions, but as I understand it, his code only generates the list at the time the function is defined.
I puzzled over this for a while, because I wanted to do the same thing. I put together something that I'm really happy with.
You can add ValidateSet attributes from a DynamicParam. Here's an example of where I've generated my ValidateSet on-the-fly from an xml file. See the "ValidateSetAttribute" in the following code:
function Foo() {
[CmdletBinding()]
Param ()
DynamicParam {
#
# The "modules" param
#
$modulesAttributeCollection = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
# [parameter(mandatory=...,
# ...
# )]
$modulesParameterAttribute = new-object System.Management.Automation.ParameterAttribute
$modulesParameterAttribute.Mandatory = $true
$modulesParameterAttribute.HelpMessage = "Enter one or more module names, separated by commas"
$modulesAttributeCollection.Add($modulesParameterAttribute)
# [ValidateSet[(...)]
$moduleNames = #()
foreach($moduleXmlInfo in Select-Xml -Path "C:\Path\to\my\xmlFile.xml" -XPath "//enlistment[#name=""wp""]/module") {
$moduleNames += $moduleXmlInfo.Node.Attributes["name"].Value
}
$modulesValidateSetAttribute = New-Object -type System.Management.Automation.ValidateSetAttribute($moduleNames)
$modulesAttributeCollection.Add($modulesValidateSetAttribute)
# Remaining boilerplate
$modulesRuntimeDefinedParam = new-object -Type System.Management.Automation.RuntimeDefinedParameter("modules", [String[]], $modulesAttributeCollection)
$paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("modules", $modulesRuntimeDefinedParam)
return $paramDictionary
}
process {
# Do stuff
}
}
With that, I can type
Foo -modules M<press tab>
and it will tab-complete "MarcusModule" if that module was in the XML file. Furthermore, I can edit the XML file and the tab-completion behavior will immediately change; you don't have to re-import the function.
Check the TabExpansionPlusPlus module on github, written by a former PowerShell team magician.
https://github.com/lzybkr/TabExpansionPlusPlus#readme
Classically, I used regex.
for example,
function TabExpansion {
param($line, $lastWord)
if ( $line -match '(-(\w+))\s+([^-]*$)' )
{
### Resolve Command name & parameter name
$_param = $matches[2] + '*'
$_opt = $Matches[3].Split(" ,")[-1] + '*'
$_base = $Matches[3].Substring(0,$Matches[3].Length-$Matches[3].Split(" ,")[-1].length)
$_cmdlet = [regex]::Split($line, '[|;=]')[-1]
if ($_cmdlet -match '\{([^\{\}]*)$')
{
$_cmdlet = $matches[1]
}
if ($_cmdlet -match '\(([^()]*)$')
{
$_cmdlet = $matches[1]
}
$_cmdlet = $_cmdlet.Trim().Split()[0]
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet)[0]
while ($_cmdlet.CommandType -eq 'alias')
{
$_cmdlet = #(Get-Command -type 'Cmdlet,Alias,Function,Filter,ExternalScript' $_cmdlet.Definition)[0]
}
### Currently target is Get-Alias & "-Name" parameter
if ( "Get-Alias" -eq $_cmdlet.Name -and "Name" -like $_param )
{
Get-Alias -Name $_opt | % { $_.Name } | sort | % { $_base + ($_ -replace '\s','` ') }
break;
}
}
}
Reference
http://gallery.technet.microsoft.com/scriptcenter/005d8bc7-5163-4a25-ad0d-25cffa90faf5
Posh-git renames TabExpansion to TabExpansionBackup in GitTabExpansion.ps1.
And posh-git's redifined TabExpansion calls original TabExpansion(TabExpansionBackup) when completions don't match with git commands.
So all you have to do is redefine TabExpansionBackup.
(cat .\GitTabExpansion.ps1 | select -last 18)
============================== GitTabExpansion.ps1 ==============================
if (Test-Path Function:\TabExpansion) {
Rename-Item Function:\TabExpansion TabExpansionBackup
}
function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
switch -regex ($lastBlock) {
# Execute git tab completion for all git-related commands
"^$(Get-AliasPattern git) (.*)" { GitTabExpansion $lastBlock }
"^$(Get-AliasPattern tgit) (.*)" { GitTabExpansion $lastBlock }
# Fall back on existing tab expansion
default { if (Test-Path Function:\TabExpansionBackup) { TabExpansionBackup $line $lastWord } }
}
}
===============================================================================
Redefine TabExpansionBackup(original TabExpansion)
function TabExpansionBackup {
...
### Resolve Command name & parameter name
...
### Currently target is Get-Alias & "-Name" parameter
...
}