PowerShell: Set-Content having issues with "file already in use" - file

I'm working on a PowerShell script that finds all the files with PATTERN within a given DIRECTORY, prints out the relevant lines of the document with the PATTERN highlighted, and then replaces the PATTERN with a provided REPLACE word, then saves the file back. So it actually edits the file.
Except I can't get it to alter the file, because Windows complains about the file already being open. I tried several methods to solve this, but keep running into the issue. Perhaps someone can help:
param(
[string] $pattern = ""
,[string] $replace = ""
,[string] $directory ="."
,[switch] $recurse = $false
,[switch] $caseSensitive = $false)
if($pattern -eq $null -or $pattern -eq "")
{
Write-Error "Please provide a search pattern." ; return
}
if($directory -eq $null -or $directory -eq "")
{
Write-Error "Please provide a directory." ; return
}
if($replace -eq $null -or $replace -eq "")
{
Write-Error "Please provide a string to replace." ; return
}
$regexPattern = $pattern
if($caseSensitive -eq $false) { $regexPattern = "(?i)$regexPattern" }
$regex = New-Object System.Text.RegularExpressions.Regex $regexPattern
function Write-HostAndHighlightPattern([string] $inputText)
{
$index = 0
$length = $inputText.Length
while($index -lt $length)
{
$match = $regex.Match($inputText, $index)
if($match.Success -and $match.Length -gt 0)
{
Write-Host $inputText.SubString($index, $match.Index) -nonewline
Write-Host $match.Value.ToString() -ForegroundColor Red -nonewline
$index = $match.Index + $match.Length
}
else
{
Write-Host $inputText.SubString($index) -nonewline
$index = $inputText.Length
}
}
}
Get-ChildItem $directory -recurse:$recurse |
Select-String -caseSensitive:$caseSensitive -pattern:$pattern |
foreach {
$file = ($directory + $_.FileName)
Write-Host "$($_.FileName)($($_.LineNumber)): " -nonewline
Write-HostAndHighlightPattern $_.Line
%{ Set-Content $file ((Get-Content $file) -replace ([Regex]::Escape("[$pattern]")),"[$replace]")}
Write-Host "`n"
Write-Host "Processed: $($file)"
}
The issue is located within the final block of code, right at the Get-ChildItem call. Of course, some of the code in that block is now a bit mangled due to me trying to fix the problem then stopping, but keep in mind the intent of that part of the script. I want to get the content, replace the words, then save the altered text back to the file I got it from.
Any help at all would be greatly appreciated.

Removed my previous answer, replacing it with this:
Get-ChildItem $directory -recurse:$recurse
foreach {
$file = ($directory + $_.FileName)
(Get-Content $file) | Foreach-object {
$_ -replace ([Regex]::Escape("[$pattern]")),"[$replace]")
} | Set-Content $file
}
Note:
The parentheses around Get-Content to ensure the file is slurped in one go (and therefore closed).
The piping to subsequent commands rather than inlining.
Some of your commands have been removed to ensure it's a simple test.

Just a suggestion but you might try looking at the documentation for the parameters code block. There is a more efficient way to ensure that a parameter is entered if you require it and to throw an error message if the user doesn't.
About_throw: http://technet.microsoft.com/en-us/library/dd819510.aspx
About_functions_advanced_parameters: http://technet.microsoft.com/en-us/library/dd347600.aspx
And then about using Write-Host all the time: http://powershell.com/cs/blogs/donjones/archive/2012/04/06/2012-scripting-games-commentary-stop-using-write-host.aspx

Alright, I finally sat down and just typed everything sequentially in PowerShell, then used that to make my script.
It was actually really simple;
$items = Get-ChildItem $directory -recurse:$recurse
$items |
foreach {
$file = $_.FullName
$content = get-content $file
$newContent = $content -replace $pattern, $replace
Set-Content $file $newcontent
}
Thanks for all your help guys.

Related

Powershell script to check if all files from a list of files exist in given folder folder

I need to write a powershell script, input a list of file names and check a given folder. I need it to echo if all files from list are present in the folder( if not which are not) and the other way around - are all files in the folder listed in the inputted list of file names.
Im new to powershell, just finished my first script to rename all files in a folder and I dont have a clue how to input a list and iterate thgrough it while checking file names in the folder.
I've managed to write smomething like this:
$Dir2 = "C:\Users\Administrator\Desktop\testDir2"
$filenames = 'a.txt', 'b.txt', 'c.txt', 'd.txt'
foreach ($filename in $filenames) {
$found=$false;
Get-ChildItem -Path $Dir2 -Recurse | ForEach-Object {if($filename -eq $_.Name) {Write-Host $filename ' Ok' -foregroundcolor green; $found=$true;CONTINUE }$found=$false;} -END {if($found -ne $true){ Write-Host $filename ' missing' -foregroundcolor red}}
}
I still need to check the other way around + I need to somehow convert rows from excel sheet to the list of filenames
To retrieve the list from a text file, use the [Get-Content cmdlet]:
$FileList = Get-Content -Path .\myFileList.txt
To retrieve the files in the folder, use the Get-ChildItem cmdlet:
$Files = Get-ChildItem -Path C:\path\to\folder -File
Grab the file names with Select-Object:
$Files = $Files |Select-Object -Property Name
And finally compare the two lists with Compare-Object:
$Discrepancies = #(Compare-Object $FileList $Files)
If Compare-Object didn't return anything, there will have been no difference between the two lists:
if($Discrepancies.Count -eq 0)
{
Write-Host "Everything is as expected!"
}
Ok, I have the code that suits my needs:
(list of files is given in a file it can be csv)
$Dir2 = 'C:\Users\Administrator\Desktop\testDir2'
$filenames=Get-Content $Dir2\filenamesnoext.csv
foreach ($filename in $filenames) {
$found=$false;
Get-ChildItem -Path $Dir2 -Recurse | ForEach-Object {if($filename -eq $_.BaseName) {Write-Host 'FILE ' $filename ' Ok' -foregroundcolor green; $found=$true;CONTINUE }$found=$false;} -END {if($found -ne $true){ Write-Host 'FILE ' $filename ' missing in the folder' -foregroundcolor red}}
}
Get-ChildItem -Path $Dir2 -Recurse | ForEach-Object {$found=$false; foreach ($filename in $filenames) {if($filename -eq $_.BaseName) {Write-Host 'FILE ' $_.BaseName ' was found on the list' -foregroundcolor cyan; $found=$true;BREAK }} if($found -ne $true){ Write-Host 'FILE ' $_.BaseName ' missing on the list of files' -foregroundcolor Magenta} }
Here is another version if someone finds it more readable (no skipping loop iterations, getting file names only once):
$folder = 'D:\stuff'
$files = #(
"one.txt",
"two.txt"
)
Write-Host "Folder: $folder."
# Get only files and only their names
$folderFiles = Get-ChildItem -Path $folder -Recurse -File -Name
foreach ($f in $files) {
if ($folderFiles -contains $f) {
Write-Host "File $f was found." -foregroundcolor green
} else {
Write-Host "File $f was not found!" -foregroundcolor red
}
}

Ping a list of host names and output the results to a csv in powershell

I have a large list of hostnames I need to ping to see if they are up or down. I'm not really that great at scripting but I managed to figure this much out:
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name is up" -ForegroundColor Green
}
else{
Write-Host "$name is down" -ForegroundColor Red
}
}
This gets me what I need but i now need to write out these results to a csv file and i have no idea how to do that.
Please Help!
You can use the following code instead (I simply altered the write-host calls to CSV formatting) and execute it with "PowerShell.exe script.ps > output.csv"
Note that you must execute it from the folder that contains hnames.txt, or simply change the "hnames.txt" to a full path.
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
Write-Host "$name,up"
}
else{
Write-Host "$name,down"
}
}
P.S. You can also use the Out-File Cmdlet to create the CSV file
I am a complete newbie to Powershell, so I took this on as a learning task, as I needed a quick and simple way to check a list of PC's for up/down status. These tweaks were needed to get it to output cleanly to the screen and to a txt file
$Output= #()
$names = Get-content "hnames.txt"
foreach ($name in $names){
if (Test-Connection -ComputerName $name -Count 1 -ErrorAction SilentlyContinue){
$Output+= "$name,up"
Write-Host "$Name,up"
}
else{
$Output+= "$name,down"
Write-Host "$Name,down"
}
}
$Output | Out-file "C:\support\result.csv"
$Output= #()
$names = Get-Content ".\input\Servers.txt"
foreach ($name in $names){
if (Test-Connection -Delay 15 -ComputerName $name -Count 1 -ErrorAction SilentlyContinue -quiet){
$Output+= "$name,up"
Write-Host "$Name,up" -ForegroundColor Green
}
else{
$Output+= "$name,down"
Write-Host "$Name,down" -ForegroundColor Red
}
}
$Output | Out-file ".\output\result.csv"
This is a tad cleaner, and includes the original foreground options but, BTW, the 'delay' switch seems to be ignored -PB
I would do it this way. Using a list of computers and -asjob works very well. The Responsetime property (confusingly the header is "Time(ms)") will be non-null if the host is up.
$names = Get-content hnames.txt
test-connection $names -asjob -count 1 | receive-job -wait -auto
Source Destination IPV4Address IPV6Address Bytes Time(ms)
------ ----------- ----------- ----------- ----- --------
COMP001 yahoo.com 74.6.231.21 32 39
COMP001 microsoft.com 40.113.200.201 32
Lately I do it this way. It requires threadjobs installed in powershell 5.1. Or just use get-port. I stick it in a mymod\mymod.psm1 module file somewhere in $env:psmodulepath. I can check a classroom in under 10 seconds.
function get-pport { # multi-threaded
param($list)
$list |
% { $_ | start-threadjob { get-port $input } -throttlelimit 20 } |
receive-job -wait -auto
}
function Get-Port {
Param (
[parameter(ValueFromPipeline)]
[string[]]$Hostname='yahoo.com'
)
begin {
$ports = 22,5988,3389,5985
$ping = New-Object System.Net.Networkinformation.ping
$Timeout = 200 # ms
}
process {
$hostname | foreach {
$openPorts = #()
foreach ($port in $ports) {
$client = New-Object System.Net.Sockets.TcpClient
$beginConnect = $client.BeginConnect($_,$port,$null,$null)
Start-Sleep -Milli $TimeOut
if($client.Connected) { $openPorts += $port }
$client.Close()
}
$result = $Ping.Send($_, $timeout)
if (! $result) { write-error "hostname $_ not found" }
$pingstatus = ($result.status -eq 'Success')
New-Object -typename PSObject -Property #{
HostName = $_
Port = $openPorts
Ping = $pingstatus
} | select hostname,port,ping
} # end foreach
} # end process
}
Example:
$avid = cat afile.txt
pport $avid
HostName Port Ping
-------- ---- ----
A006 {3389, 5985} True
A011 {3389, 5985} True
A015 {3389} True

Trying to thin out backup files but Get-ChildItem isn't returning usable list

We have a backup that runs every other day, but the files are large and we want to just remove every other one once we get a certain amount of backup files with our file signature.
I've tried this:
$Drive = "E:\temp\"
$deleteTime = -42;
$limit = (Get-Date).AddDays($deleteTime)
#this is finding the correct files but I don't think it's really in an array
$temp1 = Get-ChildItem -Path $Drive -filter "*junk.vhd*" | Where-Object {$_.LastWriteTime -lt $limit} | Select -Expand Name
for($i=$temp1.GetLowerBound(0); $i -le $temp1.GetUpperBound(0); $i+=2) {
Write-Host "removing $temp1[$i]" #this is listing the entire array with a [0] for first one and the third [2] element also, whether I cast to an array or not
}
I tried this instead of the above (Get-ChildItem) line currently but it listed the entire set of junk files for [0] instead of just the first junk.vhd at [0]:
[array]$temp1 =#( Get-ChildItem -Path $Drive -filter "*junk.vhd*" | Where-Object {$_.LastWriteTime -lt $limit} | Foreach-Object {$_.Name} )
I tried this too:
$limit = (Get-Date).AddDays(-42)
$list = (dir -Filter *junk.ps1 | where LastWriteTime -lt $limit).FullName
$count = $list.Length
for ($i = 0; $i -lt $count; $i += 2)
{
Write-Verbose "[$i] $($list[$i])"
#it's not getting in here because I'm not sure how
#to add the $Drive location and list is empty
}
Does anyone have a suggestion how to get an array of the filenames from $Drive location with the signature *junk.vhd so I can loop through them and remove every other one?
An internet search isn't turning much up.
This works for me:
$deleteTime = -12;
$limit = (Get-Date).AddDays($deleteTime)
$t = Get-ChildItem -Path $pwd -filter "p*.txt" | Where-Object {$_.LastWriteTime -lt $limit} | Select -Expand Name
foreach ($a in $t) { Write-Host "Name : $a" }
What have I missed from what you were looking for?
(Obviously, you will need to maintain a counter and do some modulo arithmetic in the body of the foreach() statement... )
This works, too:
for($i=$t.GetLowerBound(0); $i -le $t.GetUpperBound(0); $i+=2) {
$n = $t[$i]
Write-Host "removing $n"
}

Powershell - combining arrays

I am new to powershell and in need of help. My first script for work is automate the new and termed users in AD environment.
A CSV dump will be done once daily from our Peoplesoft system. I use Import-CSV and create 3 arrays (new, term and processed).
The trouble I'm having is with combining the 3 arrays once i loop through all the users and try putting it back into the file. The code breaks at the $New += $Term lines. I believe this is due to the fact that there is only 1 record of each user type (new, term and processed) in my test file (I know, add more users…can't. This may be a real world outcome for any particular day). Below is my sample code:
#Get Credentials from user
$c = Get-Credential
#Get Date for $Term array population
$e = Get-Date -format M/d/yyyy
#Set file location and variable for said file
$File = "c:\users\nmaddux\desktop\adduserstuff\test.csv"
#Import record sets for New and Term users
$New = #()
$Term = #()
$Procd = #()
$New = Import-Csv $File | Where-Object {
$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""
}
$Term = Import-Csv $File | Where-Object {
$_.TermDate -ne "" -and $_.Processdate -eq "" -and $_.TermDate -le $e
}
$Procd = Import-Csv $File | Where-Object { $_.Processdate -ne "" }
#Process both new and term users provided there are records to process for each
If ($New -ne $NULL -and $Term -ne $NULL) {
# Some code to process users
}
$new += $term
$new += $Procd
$new | Export-Csv $file -NoTypeInformation -ErrorAction SilentlyContinue
So it will export but only partial results.
error - Method invocation failed because [System.Management.Automation.PSObject] doesn't contain a method named 'op_Addition'.
If Import-Csv only returns 1 result, then you are correct that your variable is assumed NOT to be an array, then concatenation will fail. This is not change by the fact that you have pre-initialized your variables with #(). In fact, that step isn't necessary.
To force the result to be treated as an array, you can either wrap your whole Import-Csv line in #(), or do something similar afterward.
$new = #( Import-Csv $File | Where-Object {...} )
# or
$new = Import-Csv $File | Where-Object {...}
$new = #($new)
So you are importing the same CSV file 3 times? isn't it better to import it once and then set the arrays to be filtered "views" of it?
Sort of like this. You should also be able to use the "Count" value from each array as well to say whether 1 or more results were returned.
#Get Credentials from user
$c = Get-Credential
#Get Date for $Term array population
$e = Get-Date -format M/d/yyyy
#Set file location and variable for said file
$File = "c:\users\nmaddux\desktop\adduserstuff\test.csv"
#Import record sets for New and Term users
[array]$New
[array]$Term
[array]$Procd
[array]$Import = Import-Csv $File
[array]$New = $Import | ? {$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""}
[array]$Term = $Import | ? {$_.TermDate -ne "" -and $_.Processdate -eq "" -and $_.TermDate -le $e}
[array]$Procd = $Import | ? {$_.Processdate -ne ""}
#Process both new and term users provided there are records to process for each
if (($New.Count -gt 0) -and ($Term.Count -gt 0))
{
# Some code to process users
}
$new += $term
$new += $Procd
$new | Export-Csv $file -NoTypeInformation -ErrorAction SilentlyContinue
You can also enforce the type by typecasting the variable:
$array = #()
$array = gci test.txt
$array.GetType()
[array]$array = #()
$array = gci test.txt
$array.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
True True Object[] System.Array
I know I'm coming into this discussion late, but for someone else that comes along...
Since you already defined $new as an empty array, when you import from the csv you want to ADD the output to your pre-defined array, not set it equal to the output of import-csv.
$new = #()
$new += Import-Csv $File | Where-Object {
$_.TermDate -eq "" -and $_.LastName -ne "" -and $_.Processdate -eq ""
}

How to remove item from an array in PowerShell?

I'm using Powershell 1.0 to remove an item from an Array. Here's my script:
param (
[string]$backupDir = $(throw "Please supply the directory to housekeep"),
[int]$maxAge = 30,
[switch]$NoRecurse,
[switch]$KeepDirectories
)
$days = $maxAge * -1
# do not delete directories with these values in the path
$exclusionList = Get-Content HousekeepBackupsExclusions.txt
if ($NoRecurse)
{
$filesToDelete = Get-ChildItem $backupDir | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
else
{
$filesToDelete = Get-ChildItem $backupDir -Recurse | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
foreach ($file in $filesToDelete)
{
# remove the file from the deleted list if it's an exclusion
foreach ($exclusion in $exclusionList)
{
"Testing to see if $exclusion is in " + $file.FullName
if ($file.FullName.Contains($exclusion)) {$filesToDelete.Remove($file); "FOUND ONE!"}
}
}
I realize that Get-ChildItem in powershell returns a System.Array type. I therefore get this error when trying to use the Remove method:
Method invocation failed because [System.Object[]] doesn't contain a method named 'Remove'.
What I'd like to do is convert $filesToDelete to an ArrayList and then remove items using ArrayList.Remove. Is this a good idea or should I directly manipulate $filesToDelete as a System.Array in some way?
Thanks
The best way to do this is to use Where-Object to perform the filtering and use the returned array.
You can also use #splat to pass multiple parameters to a command (new in V2). If you cannot upgrade (and you should if at all possible, then just collect the output from Get-ChildItems (only repeating that one CmdLet) and do all the filtering in common code).
The working part of your script becomes:
$moreArgs = #{}
if (-not $NoRecurse) {
$moreArgs["Recurse"] = $true
}
$filesToDelete = Get-ChildItem $BackupDir #moreArgs |
where-object {-not $_.PsIsContainer -and
$_.LastWriteTime -lt $(Get-Date).AddDays($days) -and
-not $_.FullName.Contains($exclusion)}
In PSH arrays are immutable, you cannot modify them, but it very easy to create a new one (operators like += on arrays actually create a new array and return that).
I agree with Richard, that Where-Object should be used here. However, it's harder to read.
What I would propose:
# get $filesToDelete and #exclusionList. In V2 use splatting as proposed by Richard.
$res = $filesToDelete | % {
$file = $_
$isExcluded = ($exclusionList | % { $file.FullName.Contains($_) } )
if (!$isExcluded) {
$file
}
}
#the files are in $res
Also note that generally it is not possible to iterate over a collection and change it. You would get an exception.
$a = New-Object System.Collections.ArrayList
$a.AddRange((1,2,3))
foreach($item in $a) { $a.Add($item*$item) }
An error occurred while enumerating through a collection:
At line:1 char:8
+ foreach <<<< ($item in $a) { $a.Add($item*$item) }
+ CategoryInfo : InvalidOperation: (System.Collecti...numeratorSimple:ArrayListEnumeratorSimple) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
This is ancient. But, I wrote these a while ago to add and remove from powershell lists using recursion. It leverages the ability of powershell to do multiple assignment . That is, you can do $a,$b,$c=#('a','b','c') to assign a b and c to their variables. Doing $a,$b=#('a','b','c') assigns 'a' to $a and #('b','c') to $b.
First is by item value. It'll remove the first occurrence.
function Remove-ItemFromList ($Item,[array]$List(throw"the item $item was not in the list"),[array]$chckd_list=#())
{
if ($list.length -lt 1 ) { throw "the item $item was not in the list" }
$check_item,$temp_list=$list
if ($check_item -eq $item )
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-ItemFromList -item $item -chckd_list $chckd_list -list $temp_list )
}
}
This one removes by index. You can probably mess it up good by passing a value to count in the initial call.
function Remove-IndexFromList ([int]$Index,[array]$List,[array]$chckd_list=#(),[int]$count=0)
{
if (($list.length+$count-1) -lt $index )
{ throw "the index is out of range" }
$check_item,$temp_list=$list
if ($count -eq $index)
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-IndexFromList -count ($count + 1) -index $index -chckd_list $chckd_list -list $temp_list )
}
}
This is a very old question, but the problem is still valid, but none of the answers fit my scenario, so I will suggest another solution.
I my case, I read in an xml configuration file and I want to remove an element from an array.
[xml]$content = get-content $file
$element = $content.PathToArray | Where-Object {$_.name -eq "ElementToRemove" }
$element.ParentNode.RemoveChild($element)
This is very simple and gets the job done.

Resources