Powershell - Strange output when using Get-ChildItem to search within files - powershell

I have a problem I am hoping someone could help with....
I have a powershell script containing the lines shown below:
$output = Get-ChildItem -path $target -recurse | Select-String -pattern hello | group path | select name
Write-Output "Output from the string match is $output"
The error I am getting:
Output from the string match Microsoft.Powershell.Commands.GroupInfo Microsoft.Powershell.Commands.GroupInfo
When I run this command on it's own (ie not within a script) it works perfectly and returns the two files in that location that contains the word "hello".
It appears that it knows there are two things it has found because it prints the "Microsoft.Powershell.Commands.GroupInfo" text twice (as shown above in the error). But why is it printing this and not the path to the files as it should do?
There must be something obvious I am overlooking but I dont know what.
Your help is much appreciated, thanks

The reason you're seeing this is because $output is an array of Selected.Microsoft.PowerShell.Commands.GroupInfo objects -- the objects returned by Group-Object when passed to Select-Object (without Select-Object they would just be Microsoft.PowerShell.Commands.GroupInfo objects instead). You can confirm the type of objects in $ouput by running:
$output | Get-Member
Check the TypeName that is displayed at the top of the output.
When you run these commands interactively in the console, you are seeing the paths because PowerShell knows how to display GroupInfo objects in the console so that they are human-readable. Note that when you just call $output in the console, you see a "Name" header underlined with dash characters -- this is PowerShell interpreting the GroupInfo object you gave it and displaying the Name property for you in the console.
The problem occurs when you try to output the $output array inside a string. Then PowerShell is not able to use its more advanced formatting logic and instead merely tries to convert the object to a string to insert into your string. When it does that, it doesn't have enough logic to know that what you really want to appear in your string is the Name property of these GroupInfo objects, so instead if just prints out a string with the type name of each of the objects in the $output array. So that's why you see the type name twice.
The simple solution to this problem is the -ExpandProperty parameter for Select-Object. This does what it says -- it expands the property you asked for with Select-Object and returns just that property, not the parent object. So the Name property of a GroupInfo object is a string. If you call Select-Object Name, you get a GroupInfo object with the Name property. If you call Select-Object -ExpandProperty Name, you get just the Name property as a String object. Which is what I expect that you want in this case.
So try this instead:
$output = Get-ChildItem -path $target -recurse | Select-String -pattern hello | group path | select -ExpandProperty name

A foreach would be appropriate here I believe. Try this:
$output = Get-ChildItem -path $target -recurse | where {$_.name -like "*hello*"} | select name
foreach ($file in $output) {
write-host $file.name
}
Or this:
$output = Get-ChildItem -path $target -recurse | select-string -pattern "hello" | select name
foreach ($file in $output) {
write-output $file.name
}

Related

How can I replace a string inside a pipe?

I'm trying to replace some specific parts of a selected string but am only returning the length property. Here's my code:
Get-ChildItem "StartPath/Something/Files" -Recurse -File | Select "FullName | Foreach {$_.FullName -replace "StartPath",""} | Export-Csv "ResultPath.csv"
If I omit the foreach bit, this works in that it spits out the full path. I'd like to trim the full path as I'm iterating over tons of files. I'm trying to replace a bit of the path in the beginning of the string but my code above just spits out a CSV file with just string lengths.
Looks like:
"123"
"12"
"52"
and so forth.
The intended result would be a csv file with instead of:
StartPath/Something/Files1
StartPath/Something/Files2
I'd have
Something/Files1
Something/Files2
I've tried a number of things and can't seem to figure it out. Any help is appreciated.
If you pass a string to select / Select-Object (to its positionally implied -Property parameter), it must be a property name.[1]
If you want to perform open-ended operations and/or produce open-ended output for each input object, you must use the ForEach-Object cmdlet:
Get-ChildItem "StartPath/Something/Files" -Recurse -File |
ForEach-Object {
[pscustomobject] #{ FullName = $_.FullName -replace 'StartPath' }
} |
Export-Csv "ResultPath.csv"
Note the use of a [pscustomobject] wrapper that defines a FullName property, so that Export-Csv creates a CSV with that property as its (only) column.
If you pipe [string] instances directly to Export-Csv, their properties are serialized to the output file - and a [string]'s only (public) property is its length (.Length), which is what you saw.
[1] There's also a way to create properties dynamically, using so-called calculated properties, which are defined via hash tables.

Powershell - How to create array of filenames based on filename?

I'm looking to create an array of files (pdf's specifically) based on filenames in Powershell. All files are in the same directory. I've spent a couple of days looking and can't find anything that has examples of this or something that is close but could be changed. Here is my example of file names:
AR - HELLO.pdf
AF - HELLO.pdf
RT - HELLO.pdf
MH - HELLO.pdf
AR - WORLD.pdf
AF - WORLD.pdf
RT - WORLD.pdf
HT - WORLD.pdf
....
I would like to combine all files ending in 'HELLO' into an array and 'WORLD' into another array and so on.
I'm stuck pretty early on in the process as I'm brand new to creating scripts, but here is my sad start:
Get-ChildItem *.pdf
Where BaseName -match '(.*) - (\w+)'
Updated Info...
I do not know the name after the " - " so using regex is working.
My ultimate goal is to combine PDF's based on the matching text after the " - " in the filename and the most basic code for this is:
$file1 = "1 - HELLO.pdf"
$file2 = "2 - HELLO.PDF"
$mergedfile = "HELLO.PDF"
Merge-PDF -InputFile $file1, $file2 -OututFile $mergedfile
I have also gotten the Merge-PDF to work using this code which merges all PDF's in the directory:
$Files = Get-ChildItem *.pdf
$mergedfiles = "merged.pdf"
Merge-PDF -InputFile $Files -OutputFile $mergedfiles
Using this code from #Mathias the $suffix portion of the -OutputFile works but the -InputFile portion is returning an error "Exception calling "Close" with "0" argument(s)"
$groups = Get-ChildItem *.pdf |Group-Object {$_.BaseName -replace
'^.*\b(\w+)$','$1'} -AsHashTable
foreach($suffix in $groups.Keys) {Merge-PDF -InputFile $(#($groups[$suffix]))
-OutputFile "$suffix.pdf"}
For the -InputFile I've tried a lot of different varieties and I keep getting the "0" arguments error. The values in the Hashtable seem to be correct so I'm not sure why this isn't working.
Thanks
This should do the trick:
$HELLO = Get-ChildItem *HELLO.pdf |Select -Expand Name
$WORLD = Get-ChildItem *WORLD.pdf |Select -Expand Name
If you want to group file names by the last word in the base name and you don't know them up front, regex is indeed an option:
$groups = Get-ChildItem *.pdf |Group-Object {$_.BaseName -replace '^.*\b(\w+)$','$1'} -AsHashTable
And then you can do:
$groups['HELLO'].Name
for all the file names ending with the word HELLO, or, to iterate over all of them:
foreach($suffixGroup in $groups.GetEnumerator()){
Write-Host "There are $($suffixGroup.Value.Count) files ending in $($suffixGroup.Key)"
}
Another option is to get all items with Get-ChildItem and use Where-Object to filter.
$fileNames = Get-ChildItem | Select-Object -ExpandProperty FullName
#then filter
$fileNames | Where-Object {$_.EndsWith("HELLO.PDF")}
#or use the aliases if you want to do less typing:
$fileNames = gci | select -exp FullName
$fileNames | ? {$_.EndsWith("HELLO.PDF")}
Just wanted to show more options -especially the Where-Object cmdlet which comes in useful when you're calling cmdlets that don't have parameters to filter.
Side note:
You may be asking what -ExpandProperty does.
If you just call gci | select -exp FullName, you will get back an array of PSCustomObjects (each of them with one property called FullName).
This can be confusing for people who don't really see that the objects are typed as it is not visible just by looking at the PowerShell script.

Powershell, Loop through CSV files and search for a string in a row, then Export

I have a directory on a server called 'servername'. In that directory, I have subdirectories whose name is a date. In those date directories, I have about 150 .csv file audit logs.
I have a partially working script that starts from inside the date directory, enumerates and loops through the .csv's and searches for a string in a column. Im trying to get it to export the row for each match then go on to the next file.
$files = Get-ChildItem '\\servername\volume\dir1\audit\serverbeingaudited\20180525'
ForEach ($file in $files) {
$Result = If (import-csv $file.FullName | Where {$_.'path/from' -like "*01May18.xlsx*"})
{
$result | Export-CSV -Path c:\temp\output.csv -Append}
}
What I am doing is searching the 'path\from' column for a string - like a file name. The column contains data that is always some form of \folder\folder\folder\filename.xls. I am searching for a specific filename and for all instances of that file name in that column in that file.
My issue is getting that row exported - export.csv is always empty. Id also like to start a directory 'up' and go through each date directory, parse, export, then go on to the next directory and files.
If I break it down to just one file and get it out of the IF it seems to give me a result so I think im getting something wrong in the IF or For-each but apparently thats above my paygrade - cant figure it out....
Thanks in advance for any assistance,
RichardX
The issue is your If block, when you say $Result = If () {$Result | ...} you are saying that the new $Result is equal what's returned from the if statement. Since $Result hasn't been defined yet, this is $Result = If () {$null | ...} which is why you are getting a blank line.
The If block isn't even needed. you filter your csv with Where-Object already, just keep passing those objects down the pipeline to the export.
Since it sounds like you are just running this against all the child folders of the parent, sounds like you could just use the -Recurse parameter of Get-ChildItem
Get-ChildItem '\\servername\volume\dir1\audit\serverbeingaudited\' -Recurse |
ForEach-Object {
Import-csv $_.FullName |
Where-Object {$_.'path/from' -like "*01May18.xlsx*"}
} | Export-CSV -Path c:\temp\output.csv
(I used a ForEach-Object loop rather than foreach just demonstrate objects being passed down the pipeline in another way)
Edit: Removed append per Bill_Stewart's suggestion. Will write out all entries for the the recursed folders in the run. Will overwrite on next run.
I don't see a need for appending the CSV file? How about:
Get-ChildItem '\\servername\volume\dir1\audit\serverbeingaudited\20180525' | ForEach-Object {
Import-Csv $_.FullName | Where-Object { $_.'path/from' -like '*01May18.xlsx*' }
} | Export-Csv 'C:\Temp\Output.csv' -NoTypeInformation
Assuming your CSVs are in the same format and that your search text is not likely to be present in any other columns you could use a Select-String instead of Import-Csv. So instead of converting string to object and back to string again, you can just process as strings. You would need to add an additional line to fake the header row, something like this:
$files = Get-ChildItem '\\servername\volume\dir1\audit\serverbeingaudited\20180525'
$result = #()
$result += Get-Content $files[0] -TotalCount 1
$result += ($files | Select-String -Pattern '01May18\.xlsx').Line
$result | Out-File 'c:\temp\output.csv'

Powershell Get-ChildItem, filtered on date with Owner and export to txt or csv

I am trying to export a list of documents modified files after a set date, including its owners from a recursive scan using Get-ChildItem.
For some reason I cannot get it to port out to a file/csv:
$Location2 = "\\fs01\DATAIT"
$loc2 ="melb"
cd $Location2
Get-ChildItem -Recurse | Where-Object { $_.lastwritetime -gt [datetime]"2017/05/01" } | foreach { Write-Host $_.Name "," $_.lastwritetime "," ((get-ACL).owner) } > c:\output\filelisting-$loc2.txt
Could any of the PowerShell gurus on here shed some light please?
The problem with your code is that you are using Write-Host which explicitly sends output to the console (which you then can't redirect elsewhere). The quick fix is as follows:
Get-ChildItem -Recurse | Where-Object { $_.lastwritetime -gt [datetime]"2017/05/01" } | foreach { "$($_.Name),$($_.lastwritetime),$((get-ACL).owner)" } > filelisting-$loc2.txt
This outputs a string to the standard output (the equivalent of using Write-Output). I've made it a single string which includes the variables that you wanted to access by using the subexpression operator $() within a double quoted string. This operator is necessary to access the properties of objects or execute other cmdlets/complex code (basically anything more than a simple $variable) within such a string.
You could improve the code further by creating an object result, which would then allow you to leverage other cmdlets in the pipeline like Export-CSV. I suggest this:
Get-ChildItem -Recurse | Where-Object { $_.lastwritetime -gt [datetime]"2017/05/01" } | ForEach-Object {
$Properties = [Ordered]#{
Name = $_.Name
LastWriteTime = $_.LastWriteTime
Owner = (Get-ACL).Owner
}
New-Object -TypeName PSObject -Property $Properties
} | Export-CSV $Loc2.csv
This creates a hashtable #{} of the properties you wanted and then uses that hashtable to create a PowerShell Object with New-Object. This Object is then returned to standard output, which goes into the pipeline so when the ForEach-Object loop concludes all the objects are sent in to Export-CSV which then outputs them correctly as a CSV (as it takes object input).
As an aside, here is an interesting read from the creator of PowerShell on why Write-Host is considered harmful.
[Ordered] requires PowerShell 3 or above. If you're using PowerShell 2, remove it. It just keeps the order of the properties within the object in the order they were defined.

Variable referencing. How to create arrays getting their names from elements of another array

This is as simplified version of what I'd like to achieve... I think it's called 'variable referencing'
I have created an array containing the content of the folder 'foo'
$myDirectory(folder1, folder2)
Using the following code:
$myDirectory= Get-ChildItem ".\foo" | ForEach-Object {$_.BaseName}
I'd like to create 2 arrays named as each folders, with the contained files.
folder1(file1, file2)
folder2(file1, file2, file3)
I tried the following code:
foreach ($myFolder in $myDirectory) {
${myFolder} = Get-ChildItem ".\$myFolders" | forEach-Object {$_.BaseName}
}
But obviously didn't work.
In bash it's possible create an array giving it a variable's name like this:
"${myForder[#]}"
I tried to search on Google but I couldn't find how to do this in Powershell
$myDirectory = "c:\temp"
Get-ChildItem $myDirectory | Where-Object{$_.PSIsContainer} | ForEach-Object{
Remove-Variable -Name $_.BaseName
New-Variable -Name $_.BaseName -Value (Get-ChildItem $_.FullName | Where-Object{!$_.PSIsContainer} | Select -ExpandProperty Name)
}
I think what you are looking for is New-Variable. Cycle through all the folders under C:\temp. For each folder make a new variable. It would throw errors if the variable already exists. What you could do for that is remove a pre-exising variable. Populate the variable with the current folders contents in the pipeline using Get-ChildItem. The following is a small explanation of how the -Value of the new variable is generated. Caveat Remove-Variable has the potiential to delete unintended variables depending on your folder names. Not sure of the implications of that.
Get-ChildItem $_.FullName | Where-Object{!$_.PSIsContainer} | Select -ExpandProperty Name
The value of each custom variable is every file ( not folder ). Use -ExpandProperty to just gets the names as strings as supposed to a object with Names.
Aside
What do you plan on using this data for? It might just be easier to pipe the output from the Get-ChildItem into another cmdlet. Or perhaps create a custom object with the data you desire.
Update from comments
$myDirectory = "c:\temp"
Get-ChildItem $myDirectory | Where-Object{$_.PSIsContainer} | ForEach-Object{
[PSCustomObject] #{
Hotel = $_.BaseName
Rooms = (Get-ChildItem $_.FullName | Where-Object{!$_.PSIsContainer} | Select -ExpandProperty Name)
}
}
You need to have at least PowerShell 3.0 for the above to work. Changing it for 2.0 is easy if need be. Create and object with hotel names and "rooms" which are the file names from inside the folder. If you dont want the extension just use BaseName instead of Name in the select.
This is how I did it at the end:
# Create an array containing all the folder names
$ToursArray = Get-ChildItem -Directory '.\.src\panos' | Foreach-Object {$_.Name}
# For each folder...
$ToursArray | ForEach-Object {
# Remove any variable named as the folder's name. Check if it exists first to avoid errors
if(Test-Path variable:$_.BaseName){ Remove-Variable -Name $_.BaseName }
$SceneName=Get-ChildItem ".\.src\panos\$_\*.jpg"
# Create an array using the main folder's name, containing the names of all the jpg inside
New-Variable -Name $_ -Value ($SceneName | Select -ExpandProperty BaseName)
}
And here it goes some code to check the content of all the arrays:
# Print Tours information
Write-Verbose "Virtual tours list: ($($ToursArray.count))"
$ToursArray | ForEach-Object {
Write-Verbose " Name: $_"
Write-Verbose " Scenes: $($(Get-Variable $_).Value)"
}
Output:
VERBOSE: Name: tour1
VERBOSE: Scenes: scene1 scene2
VERBOSE: Name: tour2
VERBOSE: Scenes: scene1