Compare number of files in subfolders - powershell

I want to compare the number of files in subfolders of 2 given folders with a PowerShell script, but something isn't working.
I got the paths of the 2 folders through a user input and saved them
into the variables $where1 and $where2, I also saved the location where the script started of into the variable $a. I change to the folders given to get information about the subfolders in there into 2 arrays, one assioated array $folderArrayX with the numbers like this $folderArrayX["subfolder1"]=x (x being the number of child items for subfolder1) and one just with the names of the subfolders for easier comparision. The Code for the first folder given:
cd $where1;
$folderArray1 = #();
$folderArray1Keys = #();
Get-Childitem | Select-Object | ForEach-Object {
if ($_.PSIsContainer) {
$ArrayInArray = #{};
$folderArray1Keys += $_.Name.Trim();
$ArrayInArray[$_.Name.Trim()] = (Get-ChildItem ./$_).count;
$folderArray1 += $ArrayInArray;
}
}
I do the same for $where2 so I get $folderArray2 and $folderArray2Keys.
Now to compare these 2:
$r=() #something to save the returns
Compare-Object $folderArray1Keys $folderArray2Keys -IncludeEqual | ForEach-Object {
if ($_.SideIndicator -eq "=>") {
$r += ""+$_.InputObect+" only "+$where2+" : "+$folderArray2[$_.InputObject]+" files";
} elseif ($_.SideIndicator -eq "<=") {
$r += ""+$_.InputObect+" only "+$where1+" : "+$folderArray1[$_.InputObject]+" files";
} else {
if ($folderArray1[$_.InputObject] -gt $folderArray2[$_.InputObject]) {
$dif = ($folderArray1[$_.InputObject]-$folderArray2[$_.InputObject])
$r += "on both sides"+$_.InputObject+" has "+$diff+" more files in "+$where1
} elseif ($folderArray2[$_.InputObject] -gt $folderArray1[$_.InputObject]) {
$dif = ($folderArray2[$_.InputObject]-$folderArray1[$_.InputObject])
$r += "on both sides"+$_.InputObject+" has "+$diff+" more files in "+$where2
} else {
$r += ""+$_.InputObject+" is equal on both sides";
}
}
}
#Output
$r
cd $a
Some how this code doesn't work well. It indicates subfolders as equal which are clearly not (only because they are there and completely ignore the numbers), don't spell out the onesided folders (what I understand the least (it does so for the bothsided folders)) and don't show any numbers. I can't figure out the mistake because assiocated arrays work with PowerShell, I mean a code like this:
$k1="key1";
$v1=1;
$k2="key2";
$v2="value2";
$array=#{};
$array[$k1]=$v1;
$array[$k2]=$v2;
$array[$k1]
$array[$k2]
pause
# Exert
# 1
# value2
# Press any key to continue ...
works as expected.

Your comparison routine treats $folderArray1 and $folderArray2 as if they were hashtables, but you define them as arrays of hashtables. Basically you're doing this:
PS C:\> $h = #{}
PS C:\> $h['foo'] = 42
PS C:\> $a = #()
PS C:\> $a += $h
PS C:\> $a['foo']
PS C:\> $a[0]['foo']
42
when you actually want just this:
PS C:\> $a = #{}
PS C:\> $a['foo'] = 42
PS C:\> $a['foo']
42
Change the code for populating the $folderArray variables to this:
$folderArray = #{}
Get-Childitem | ? { $_.PSIsContainer } | ForEach-Object {
$folderArray[$_.Name] = #(Get-ChildItem $_.FullName).Count
}
and the Compare-Object statement to this:
Compare-Object #($folderArray1.Keys) #($folderArray2.Keys) -IncludeEqual | ...
and the problem should disappear.

Related

Powershell/ Print by filename

My English may not be perfect but I do my best.
I'm trying to write a Powershell script where the filename has a number at the end and it should print exactly that often.
Is this somehow possible ?
With the script it prints it only 1 time.
For whatever reason..
param (
[string]$file = "C:\Scans\temp\*.pdf",
[int]$number_of_copies = 1
)
foreach ($onefile in (Get-ChildItem $file -File)) {
$onefile -match '\d$' | Out-Null
for ($i = 1; $i -le [int]$number_of_copies; $i++) {
cmd /C "lpr -S 10.39.33.204 -P optimidoc ""$($onefile.FullName)"""
}
}
There is no need for parameter $number_of_copies when the number of times it should be printed is taken from the file's BaseName anyway.
I would change your code to:
param (
[string]$path = 'C:\Scans\temp'
)
Get-ChildItem -Path $path -Filter '*.pdf' -File |
# filter only files that end with a number and capture that number in $matches[1]
Where-Object { $_.BaseName -match '(\d+)$' } |
# loop through the files and print
ForEach-Object {
for ($i = 1; $i -le [int]$matches[1]; $i++) {
cmd /C "lpr -S 10.39.33.204 -P optimidoc ""$($_.FullName)"""
}
}
Inside the ForEach-Object, on each iteration, the $_ automatic variable represents the current FileInfo object.
P.S. Your script prints each file only once because you set parameter $number_of_copies to 1 as default value, but the code never changes that to the number found in the file name.
BTW. Nothing wrong with your English

Why Isn't This Counting Correctly | PowerShell

Right now, I have a CSV file which contains 3,800+ records. This file contains a list of server names, followed by an abbreviation stating if the server is a Windows server, Linux server, etc. The file also contains comments or documentation, where each line starts with "#", stating it is a comment. What I have so far is as follows.
$file = Get-Content .\allsystems.csv
$arraysplit = #()
$arrayfinal = #()
[int]$windows = 0
foreach ($thing in $file){
if ($thing.StartsWith("#")) {
continue
}
else {
$arraysplit = $thing.Split(":")
$arrayfinal = #($arraysplit[0], $arraysplit[1])
}
}
foreach ($item in $arrayfinal){
if ($item[1] -contains 'NT'){
$windows++
}
else {
continue
}
}
$windows
The goal of this script is to count the total number of Windows servers. My issue is that the first "foreach" block works fine, but the second one results in "$Windows" being 0. I'm honestly not sure why this isn't working. Two example lines of data are as follows:
example:LNX
example2:NT
if the goal is to count the windows servers, why do you need the array?
can't you just say something like
foreach ($thing in $file)
{
if ($thing -notmatch "^#" -and $thing -match "NT") { $windows++ }
}
$arrayfinal = #($arraysplit[0], $arraysplit[1])
This replaces the array for every run.
Changing it to += gave another issue. It simply appended each individual element. I used this post's info to fix it, sort of forcing a 2d array: How to create array of arrays in powershell?.
$file = Get-Content .\allsystems.csv
$arraysplit = #()
$arrayfinal = #()
[int]$windows = 0
foreach ($thing in $file){
if ($thing.StartsWith("#")) {
continue
}
else {
$arraysplit = $thing.Split(":")
$arrayfinal += ,$arraysplit
}
}
foreach ($item in $arrayfinal){
if ($item[1] -contains 'NT'){
$windows++
}
else {
continue
}
}
$windows
1
I also changed the file around and added more instances of both NT and other random garbage. Seems it works fine.
I'd avoid making another ForEach loop for bumping count occurrences. Your $arrayfinal also rewrites everytime, so I used ArrayList.
$file = Get-Content "E:\Code\PS\myPS\2018\Jun\12\allSystems.csv"
$arrayFinal = New-Object System.Collections.ArrayList($null)
foreach ($thing in $file){
if ($thing.StartsWith("#")) {
continue
}
else {
$arraysplit = $thing -split ":"
if($arraysplit[1] -match "NT" -or $arraysplit[1] -match "Windows")
{
$arrayfinal.Add($arraysplit[1]) | Out-Null
}
}
}
Write-Host "Entries with 'NT' or 'Windows' $($arrayFinal.Count)"
I'm not sure if you want to keep 'Example', 'example2'... so I have skipped adding them to arrayfinal, assuming the goal is to count "NT" or "Windows" occurrances
The goal of this script is to count the total number of Windows servers.
I'd suggest the easy way: using cmdlets built for this.
$csv = Get-Content -Path .\file.csv |
Where-Object { -not $_.StartsWith('#') } |
ConvertFrom-Csv
#($csv.servertype).Where({ $_.Equals('NT') }).Count
# Compatibility mode:
# ($csv.servertype | Where-Object { $_.Equals('NT') }).Count
Replace servertype and 'NT' with whatever that header/value is called.

Script that checks if folder exists on users PC while counting files of those folders

I would like to create a script that both check if 2 folders exist (C:\Program Files (x86)\MS and C:\Program Files\MS) on several PC names (Names listed to a systems.txt file) and count how many files there are on that path.
As I am a beginner I could not write more than the following:
Get-Content C:\reports\systems.txt |
Select-Object #{Name='ComputerName';Expression={$_}},#{Name='MS Installed';Expression={ Test-Path "\\$_\c`$\Program Files (x86)\MS"}},#{Name='number of files';Expression={$numberp}}
Get-Content C:\reports\systems.txt | `
Select-Object #{Name='ComputerName';Expression={$_}},#{Name='MS Installed';Expression={ Test-Path "\\$_\c`$\Program Files (x86)\MS"}},#{Name='number of files';Expression={$numberp2}}
This returned the PC name + a ''true'' if the file exist or ''false'' if it does not. In addition to this, how can I count the number of files contained on each path of each pc?
ComputerName MS Installed number of files
------------ ------------------- ---------------
PCname1 True 0
PCname2 True 0
PCname3 True 0
PCname4 False 0
I would be so grateful if you could help me figure it out.
Let's use IO.DirectoryInfo enumeration (available since .NET framework 4, it's built-in since Win 8/10; installable on Win7/XP), which is much faster than Get-ChildItem on network paths. We'll count the number of items using a plain loop statement, which is faster than Measure-Object while being just as memory-efficient since we don't allocate an array of all files anywhere.
Get-Content R:\systems.txt | ForEach {
$pc = $_
$numFiles = 0
foreach ($suffix in '', ' (x86)') {
$dir = [IO.DirectoryInfo]"\\$pc\c`$\Program files$suffix\MS"
if ($dir.Exists) {
foreach ($f in $dir.EnumerateFiles('*', [IO.SearchOption]::AllDirectories)) {
$numFiles++
}
break
}
}
[PSCustomObject]#{
'ComputerName' = $pc
'MS Installed' = $dir.Exists
'Number of files' = $numFiles
}
}
If .NET framework 4 is not desirable, use the fast VisualBasic's FileIO.FileSystem assembly or the slow Get-ChildItem so the above code would become compatible with the ancient PowerShell 2:
Get-Content R:\systems.txt | ForEach {
$pc = $_
$numFiles = 0
foreach ($suffix in '', ' (x86)') {
$dir = [IO.DirectoryInfo]"\\$pc\c`$\Program files$suffix\MS"
if ($dir.Exists) {
Get-ChildItem $dir -recurse -force | ForEach {
$numFiles += [int](!$_.PSIsContainer)
}
break
}
}
New-Object PSObject -Property #{
'ComputerName' = $pc
'MS Installed' = $dir.Exists
'Number of files' = $numFiles
}
} | Select 'ComputerName', 'MS Installed', 'Number of files'

reconstructing path from outlined directory structure

I have a csv file in the form:
Address,L0,L1,L2,L3,L4
01,Species,,,,
01.01,,Mammals,,,
01.01.01,,,Threatened,,
...
I want to use it to create a matching directory structure. I'm new to scripting and PowerShell, and in this case I'm not sure if I'm on totally the wrong track. Should I use a separate array to store each level's Name/Address pairs and then use those arrays like a lookup table to build the path? If so, I guess I'm stuck on how to set up if-thens based on a row's Address. This is as far as I've got so suggestions on general strategy or links to similar kinds of problem would be really welcome:
$folders = Import-Csv "E:\path\to\file.csv"
$folders | foreach {
$row = new-object PSObject -Property #{
Address = $_.Address;
Level = ([regex]::Matches($_.Address, "\." )).count;
L0 = $_.L0
L1 = $_.L1
L2 = $_.L2
L3 = $_.L3
}
$array += $row
}
#top level directories
$0 = $array | ?{$_.Level -eq 0} |
Select-Object #{n="Address";e={$_.Address;}},#{n="Name";e={$_.L0}}
#2nd level directories
$1 = $array | ?{$_.Level -eq 1} |
Select-Object #{n="Number";e={$_.Address.split(".")[-1];}},#{n="Name";e={$_.L1}}
Not tested, but I think this might do what you want:
$root = 'E:\SomeDirName'
Switch -Regex (Get-Content "E:\path\to\file.csv")
{
'^01,(\w+),,,,$' { $L1,$L2,$L3,$L4 = $null; $L0=$matches[1];mkdir "$root\$L0" }
'^01\.01,,(\w+),,,$' { $L1=$matches[1];mkdir "$root\$L0\$L1" }
'^01\.01\.01,,,(\w+),,$' { $L2=$matches[1];mkdir "$root\$L0\$L1\$L2" }
'^01\.01\.01\.01,,,,(\w+),$' { $L3=$matches[1];mkdir "$root\$L0\$L1\$L2\$L3" }
'^01\.01\.01\.01\.01,,,,,(\w+)$' { $L4=$matches[1];mkdir "$root\$L0\$L1\$L2\$L3\$L4" }
}
To solve that kind of problem a programming concept called recursion is often used.
In short a recursive function is a function that call itself.
I successfully tested this code with you CSV input:
$csvPath = 'C:\Temp\test.csv'
$folderRoot = 'C:\Temp'
$csv = Import-Csv $csvPath -Delimiter ',' | Sort-Object -Property Address
# Recursive function
function Recurse-Folder( $folderAddress, $basePath )
{
# Getting the list of current folder subfolders
$childFolders = $null
if( $folderAddress -like '' )
{
$childFolders = $csv | Where-Object { $_.Address -like '??' }
}
else
{
$childFolders = $csv | Where-Object { $_.Address -like $( $folderAddress + '.??' ) }
}
# For each child folder
foreach( $childFolder in $childFolders )
{
# Get the folder name
$dotCount = $childFolder.Address.Split('.').Count - 1
$childFolderName = $childFolder.$('L'+$dotCount)
if( $childFolderName -ne '')
{
$childFolderPath = $basePath + '\' + $childFolderName
# Creating child folder and calling recursive function for it
New-Item -Path $childFolderPath -ItemType Directory
Recurse-Folder $childFolder.Address $childFolderPath
}
}
}
Recurse-Folder '' $folderRoot

File Output in Powershell without Extension

Here is what I have so far:
Get-ChildItem "C:\Folder" | Foreach-Object {$_.Name} > C:\Folder\File.txt
When you open the output from above, File.txt, you see this:
file1.txt
file2.mpg
file3.avi
file4.txt
How do I get the output so it drops the extension and only shows this:
file1
file2
file3
file4
Thanks in advance!
EDIT
Figured it out with the help of the fellows below me. I ended up using:
Get-ChildItem "C:\Folder" | Foreach-Object {$_.BaseName} > C:\Folder\File.txt
Get-ChildItem "C:\Folder" | Select BaseName > C:\Folder\File.txt
Pass the file name to the GetFileNameWithoutExtension method to remove the extension:
Get-ChildItem "C:\Folder" | `
ForEach-Object { [System.IO.Path]::GetFileNameWithoutExtension($_.Name) } `
> C:\Folder\File.txt
I wanted to comment on #MatthewMartin's answer, which splits the incoming file name on the . character and returns the first element of the resulting array. This will work for names with zero or one ., but produces incorrect results for anything else:
file.ext1.ext2 yields file
powershell.exe is good for me. Let me explain to thee..doc yields powershell
The reason is because it's returning everything before the first . when it should really be everything before the last .. To fix this, once we have the name split into segments separated by ., we take every segment except the last and join them back together separated by .. In the case where the name does not contain a . we return the entire name.
ForEach-Object {
$segments = $_.Name.Split('.')
if ($segments.Length -gt 1) {
$segmentsExceptLast = $segments | Select-Object -First ($segments.Length - 1)
return $segmentsExceptLast -join '.'
} else {
return $_.Name
}
}
A more efficient approach would be to walk backwards through the name character-by-character. If the current character is a ., return the name up to but not including the current character. If no . is found, return the entire name.
ForEach-Object {
$name = $_.Name;
for ($i = $name.Length - 1; $i -ge 0; $i--) {
if ($name[$i] -eq '.') {
return $name.Substring(0, $i)
}
}
return $name
}
The [String] class already provides a method to do this same thing, so the above can be reduced to...
ForEach-Object {
$i = $_.Name.LastIndexOf([Char] '.');
if ($i -lt 0) {
return $_.Name
} else {
return $_.Name.Substring(0, $i)
}
}
All three of these approaches will work for names with zero, one, or multiple . characters, but, of course, they're a lot more verbose than the other answers, too. In fact, LastIndexOf() is what GetFileNameWithoutExtension() uses internally, while what BaseName uses is functionally the same as calling $_.Name.Substring() except it takes advantage of the already-computed extension.
And now for a FileInfo version, since everyone else beat me to a Path.GetFileNameWithoutExtension solution.
Get-ChildItem "C:\" | `
where { ! $_.PSIsContainer } | `
Foreach-Object {([System.IO.FileInfo]($_.Name)).Name.Split('.')[0]}
(ls).BaseName > C:\Folder\File.txt
Use the BaseName property instead of the Name property:
Get-ChildItem "C:\Folder" | Select-Object BaseName > C:\Folder\File.txt