powershell -match defaulting to garbage - powershell

I have a highly used method that I use to get contents from a file, and then it returns contents between two given parameters. It works for every other one (about 15 files), but for the one I just added, it's defaulting to garbage text that isn't even in the file read. I've tried re-using the filename being fed to the method, as well as using a different filename. I've tried using different from/to strings in the file in the area returning the garbage.
This is the method called:
#Function to look for method content with to parse and return contents in method as string
#Note that $followingMethodName is where we parse to, as an end string. $methodNameToReturn is where we start getting data to return.
Function Get-MethodContents{
[cmdletbinding()]
Param ( [string]$codePath, [string]$methodNameToReturn, [string]$followingMethodName)
Process
{
$contents = ""
Write-Host "In GetMethodContents method File:$codePath method:$methodNameToReturn followingMethod:$followingMethodName" -ForegroundColor Green
$contents = Get-Content $codePath -Raw #raw gives content as single string instead of a list of strings
$null = $contents -match "($methodNameToReturn[\s\S]*)$followingMethodName" ###############?? wrong for just the last one added
Write-Host "File Contents Found: $($Matches.Item(1))" -ForegroundColor DarkYellow
Write-Host "File Contents Found: $($Matches.Item(0))" -ForegroundColor Cyan
Write-Host "File Contents Found: $($Matches[0])" -ForegroundColor Cyan
Write-Host "File Contents Found: $($Matches[1])" -ForegroundColor Cyan
return $Matches.Item(1)
}#End of Process
}#End of Function
This is the calling code. The GetMethodContents for FileHandler2 is defaulting to $currentVersion (6000) when it returns, which isn't even in the file being provided.
elseif($currentVersion -Match '^(6000)') #6000
{
$HopResultMap = [ordered]#{}
$HopResultMap2 = [ordered]#{}
#call method to return basePathFull cppFile method contents
$matchFound = Get-MethodContents -codePath $File[0] -methodNameToReturn "Build the HOP error map" -followingMethodName "CHop2Windows::getHOPError" #correct match
#call method to get what is like case info but is map in 6000 case....it's 2 files so 2 maps for 6000
$HopResultMap = (Get-Contents60 -fileContent $matchFound) #error map of ex: seJam to HOP_JAM
$FileHandler = Join-Path -Path $basePathFull -ChildPath "Hop2Windows\XXHandler.cpp"
$matchFound2 = Get-MethodContents -codePath $FileHandler -methodNameToReturn "XXHandler::populateVec" -followingMethodName "m_Warnings" #matches correctly
$HopResultMap2 = (Get-Contents60_b -fileContent $matchFound2) #used in foreach
#sdkErr uses Handler file too but it Get-methodContents is returning 6000 so try diff filename
$FileHandler2 = Join-Path -Path $basePathFull -ChildPath "Hop2Windows\XXHandler.cpp"
$matchFound3 = Get-MethodContents -codePath $FileHandler2 -methodNameToReturn "feed from No. 0" -followingMethodName "class CHop2Windows;" # returns 6000-wrong######################??
$HopResultMap3 = (Get-Contents60_c -fileContent $matchFound3) #used in foreach
#next need to put these 2 maps together
#need to test I got matches correct in 6000 map still################
#combine the maps so can reuse 7000's case/design. $hopResultMap key is the common tie with $hopResultMap2 and thrown away
$resultCase = foreach ($key in $HopResultMap.Keys){
[PSCustomObject][ordered]#{
sdkErr = "HopResultMap3[$key]" # 0x04
sdkDesc = "HopResultMap2[$key]" # Fatal Error
sdkOutErr = "$($HopResultMap[$key])"
}
}
}//else
This is with powershell 5.1 and VSCode.
Update (as requested):
$pathViewBase = 'C:\Data\CC_SnapViews\EndToEnd_view\'
$HopBase = '\Output\HOP\'
$basePathFull = Join-Path -Path $pathViewBase -ChildPath $HopBase
$Hop2PrinterVersionDirs = #('Hop2Windowsxx\Hop2Windowsxx.cpp')
...
foreach($cppFile in $Hop2VersionDirs) #right now there is only one
{
$File = #(Get-ChildItem -Path (Join-Path -Path $basePathFull -ChildPath $cppFile))
Update2:
I tried escaping like this with the problematic content returned:
$matchFound3 = Get-MethodContents -codePath $FileHandler2 -methodNameToReturn [regex]::Escape("feed from No. 0") -followingMethodName regex[Escape("class CHop2Windowsxx;")
and see this error:
Get-MethodContents : A positional parameter cannot be found that
accepts argument 'Roll feed from No. 0'.

I figured it out. I was giving it the .cpp filename, but the content I was looking for was in the .h filename. :\

Related

Log and read only last 10 minutes

I am begginer in Powershell and I have a problem with script.
I have a log and I need to send an email notification with an error. I would like to plan a task (TASK SCHEDULE) that will run my script regularly every ten minutes. This script verifies the last lines written in the last ten minutes. If the word ERROR is found, it will send an e-mail with this line where is the word ERROR.
My log:
2022-02-08 12:04:35,152 [105] ERROR RSeC.NET.RedundantHttpClient - No server found
2022-02-08 14:28:51,317 [4] DEBUG RSeC.NET.RSeC - Logging initialised
2022-02-08 14:28:53,835 [4] DEBUG RSeC.NET.JsonParser - Response binary data decoded. Size=424132
2022-02-08 14:29:20,494 [105] DEBUG RSeC.NET.RSeC - Logging initialised
2022-02-08 15:38:35,152 [105] ERROR RSeC.NET.RedundantHttpClient - No server found
2022-03-08 15:28:51,317 [4] DEBUG RSeC.NET.RSeC - Logging initialised
2022-03-08 15:28:53,835 [4] DEBUG RSeC.NET.JsonParser - Response binary data decoded. Size=424132
2022-03-08 15:39:20,494 [105] DEBUG RSeC.NET.RSeC - Logging initialised
2022-03-08 15:39:35,152 [105] ERROR RSeC.NET.RedundantHttpClient - No server found
My script :-(
$file = "C:\Soubory\esel.log"
$date = (Get-Date).AddDays(-50).Date
$cont = Get-Content -Path $file | Select-String -Pattern $date | Select-String "ERROR" | Measure-Object -line
Foreach-Object {
if ($cont -match "ERROR")
{
$kontent = Get-Content -Path $file | Select-String -Pattern $date | Select-String "ERROR" | Measure-Object -line
Write-Host $cont
}
else
{
#NOTING
}
}
Thank You for help
GILD
I think Lee_Dailey gave you the answer in his comment.
Simply figure out the worst-case scenario to read the bottom number of lines of the file, where you can be certain the last ten minutes are in there.
Then do:
$maxLines = 20 # just a guess here, but you can narrow the number of lines to read by trial and error
$lastTenMinutes = (Get-Date).AddMinutes(-10)
$errorLines = Get-Content -Path 'C:\Soubory\esel.log' -Tail $maxLines |
Where-Object { [datetime]($_ -split ',')[0] -gt $lastTenMinutes -and $_ -match 'ERROR' }
# test if there were error lines found
if (#($errorLines).Count) {
# send your email alert.
# If this email is in HTML format, use: $errorLines -join '<br>'
# if the email is plain text, join with newlines: $errorLines -join [environment]::NewLine
# for demo just output to console
Write-Host ("Errors found:`r`n{0}" -f ($errorLines -join [environment]::NewLine))
}
else {
Write-Host "No error lines found" -ForegroundColor Green
}
On my Dutch locale, [datetime]($_ -split ',')[0] parses the date correctly, but on your machine you may have to use [datetime]::ParseExact(($_ -split ',')[0], 'yyyy-MM-dd HH:mm:ss', $null)
It looks like a csv without the header, and I can compare the first column like it's a date and time, so:
import-csv log -header time,message |
where { (get-date).AddMinutes(-10) -lt $_.time -and
$_.message -match 'error' }
time message
---- -------
2022-03-09 10:11:35 152 [105] ERROR RSeC.NET.RedundantHttpClient - No server found
I would use the windows event log for easier filtering.
As I said in the comment on your question, "figure out what is the largest number of entries you could ever expect in 10 minutes". When you have that number, change the "20" in line "$MinLinesToGet = 20" in the code below to that value. Also, change the line "$MaxLengthOfALine = 100" so that it has the length of the longest line you expect to see.
If for some reason you need more than 10 minutes, change the value in the line "$MinutesOld = 10".
The code:
Uses ReadBytesFromFileEnd to read $ByteCount bytes from the end of $FilePath.
Uses ReadLinesFromFileEnd to read $MinLineCount of lines, each are expected to have less than $MaxLineLength, from the end of $FilePath. In reality, it should read several lines more than we want - which is a good thing.
Uses "[System.Text.Encoding]::UTF8.GetString" to convert the bytes to a string. And then uses Split to make an array of strings that is saved in $Lines.
The UTF8 before GetString is a type of encoding, and is the most likely file encoding the log file is in, so you shouldn't have to change anything. But, if there are problems, find the line "<## >" and remove the space when testing. The code will then give the raw text lines that are being returned by ReadLinesFromFileEnd. At that point, you can try the other encodings listed in the comment line above the GetString statement. Most encoding have a character size of 1, but in some unlikely, or absurd case where your log file has a larger character size, then change the line "$CharSize = 1" as needed.
You didn't provide your regex you were using, so I built my own and used the switch statement to loop through all the lines that are returned by ReadLinesFromFileEnd. I made the assumption that comma after the date WAS NOT part of a comma delimited line, but still part of the time stamp, and the numbers just afterwards is the milliseconds. I don't know what the numbers in the brackets are, so I just gave it the name "Code".
Those lines older than 10 minutes are used to build a PSObject that you can use in later code.
The last 3 lines are an example using the returned log entries in a Write-Host statement.
This worked well in my testing, but never know till the code is actually tried in the real world.
function ReadBytesFromFileEnd{
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$FilePath,
[Parameter(Mandatory = $true, Position = 1)]
[int]$ByteCount
)
$fs = [IO.File]::OpenRead($FilePath) # Open file
if($ByteCount -gt $fs.Length) {$ByteCount = $fs.Length} # Prevent reading more bytes than exist
$null = $fs.Seek(-$ByteCount, [System.IO.SeekOrigin]::End) # Position for reading
$Return = new-object Byte[] $ByteCount # Define the buffer $Return
$fs.Read($Return, 0, $ByteCount) | Out-Null # Fill the buffer
$fs.Close() # Close the file
return $Return
}
function ReadLinesFromFileEnd{
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$FilePath,
[Parameter(Mandatory = $true, Position = 1)]
[int]$MinLineCount,
[Parameter(Mandatory = $true, Position = 2)]
[int]$MaxLineLength
)
$CharSize = 1
[int]$ByteCount = 1.25 * ($MinLineCount + 1) * $MaxLineLength * $CharSize
# Encoding Options: ASCII, BigEndianUnicode, Default, Unicode, UTF32, UTF7,UTF8
$null, $Return = [System.Text.Encoding]::UTF8.GetString((ReadBytesFromFileEnd $FilePath $ByteCount)).Split(#("`n","`r"),[System.StringSplitOptions]::RemoveEmptyEntries)
return $Return
}
$MinLinesToGet = 20
$MaxLengthOfALine = 100
$MinutesOld = 10
$Lines = ReadLinesFromFileEnd 'C:\Soubory\esel.log' $MinLinesToGet $MaxLengthOfALine
<## >
$Lines
exit
#>
$Now = Get-Date
$RecentLogs = switch -Regex ($Lines) {
'^(?<DateTime>\d{4}-\d\d-\d\d\s+\d\d:\d\d:\d\d,\d+)\s*\[(?<Code>\d+)\]\s*(?<Description>.*)$' {
$DateTime = [DateTime]::ParseExact($Matches.DateTime, 'yyyy-MM-dd HH:mm:ss,fff', $null)
if ($Now.Subtract($DateTime).TotalMinutes -lt $MinutesOld) {
New-Object PSObject -Property #{
DateTime = $DateTime
Code = $Matches.Code
Description = $Matches.Description
}
}
continue
}
Default {
continue
}
}
$RecentLogs | ForEach-Object {
Write-Host "$($_.DateTime) [$($_.Code)] $($_.Description)"
}
EDIT:
You may want to check for lines that are not caught by the Regex. If so, place the following lines between the Default { statement and the coninue statement.
Write-Color "Regex failed to match: " -ForegroundColor Red -NoNewLine
Write-Color "$_" -ForegroundColor Yellow

PowerShell reading and writing compressed files with byte arrays

Final Update: Turns out I didn't need Binary writer. I could just copy memory streams from one archive to another.
I'm re-writing a PowerShell script which works with archives. I'm using two functions from here
Expand-Archive without Importing and Exporting files
and can successfully read and write files to the archive. I've posted the whole program just in case it makes things clearer for someone to help me.
However, there are three issues (besides the fact that I don't really know what I'm doing).
1.) Most files have this error on when trying to run
Add-ZipEntry -ZipFilePath ($OriginalArchivePath + $PartFileDirectoryName) -EntryPath $entry.FullName -Content $fileBytes}
Cannot convert value "507" to type "System.Byte". Error: "Value was either too large or too small for an unsigned byte." (replace 507 with whatever number from the byte array is there)
2.) When it reads a file and adds it to the zip archive (*.imscc) it adds a character "a" to the beginning of the file contents.
3.) The only file it doesn't error on are text files, when I really want it to handle any file
Thank you for any assistance!
Update: I've tried using System.IO.BinaryWriter, with the same errors.
Add-Type -AssemblyName 'System.Windows.Forms'
Add-Type -AssemblyName 'System.IO.Compression'
Add-Type -AssemblyName 'System.IO.Compression.FileSystem'
function Folder-SuffixGenerator($SplitFileCounter)
{
return ' ('+$usrSuffix+' '+$SplitFileCounter+')'
}
function Get-ZipEntryContent(#returns the bytes of the first matching entry
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::Open)),
[string] $EntryPath){
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Read)
$buf = New-Object byte[] (0) #return an empty byte array if not found
$ZipArchive.GetEntry($EntryPath) | ?{$_} | %{ #GetEntry returns first matching entry or null if there is no match
$buf = New-Object byte[] ($_.Length)
Write-Verbose " reading: $($_.Name)"
$_.Open().Read($buf,0,$buf.Length)
}
$ZipArchive.Dispose()
$ZipStream.Close()
$ZipStream.Dispose()
return ,$buf
}
function Add-ZipEntry(#Adds an entry to the $ZipStream. Sample call: Add-ZipEntry -ZipFilePath "$PSScriptRoot\temp.zip" -EntryPath Test.xml -Content ([text.encoding]::UTF8.GetBytes("Testing"))
[string] $ZipFilePath, #optional - specify a ZipStream or path
[IO.Stream] $ZipStream = (New-Object IO.FileStream($ZipFilePath, [IO.FileMode]::OpenOrCreate)),
[string] $EntryPath,
[byte[]] $Content,
[switch] $OverWrite, #if specified, will not create a second copy of an existing entry
[switch] $PassThru ){#return a copy of $ZipStream
$ZipArchive = New-Object IO.Compression.ZipArchive($ZipStream, [IO.Compression.ZipArchiveMode]::Update, $true)
$ExistingEntry = $ZipArchive.GetEntry($EntryPath) | ?{$_}
If($OverWrite -and $ExistingEntry){
Write-Verbose " deleting existing $($ExistingEntry.FullName)"
$ExistingEntry.Delete()
}
$Entry = $ZipArchive.CreateEntry($EntryPath)
$WriteStream = New-Object System.IO.StreamWriter($Entry.Open())
$WriteStream.Write($Content,0,$Content.Length)
$WriteStream.Flush()
$WriteStream.Dispose()
$ZipArchive.Dispose()
If($PassThru){
$OutStream = New-Object System.IO.MemoryStream
$ZipStream.Seek(0, 'Begin') | Out-Null
$ZipStream.CopyTo($OutStream)
}
$ZipStream.Close()
$ZipStream.Dispose()
If($PassThru){$OutStream}
}
$NoDeleteFiles = #('files_meta.xml' ,'course_settings.xml', 'assignment_groups.xml', 'canvas_export.txt', 'imsmanifest.xml')
Set-Variable usrSuffix -Option ReadOnly -Value 'part' -Force
$MaxImportFileSize = 1000
$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
$SplitFileCounter = 1
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog
$FileBrowser.filter = "Canvas Export Files (*.imscc)| *.imscc"
[void]$FileBrowser.ShowDialog()
$FileBrowser.FileName
$FilePath = $FileBrowser.FileName
$OriginalArchivePath = $FilePath.Substring(0,$FilePath.Length-6)
$PartFileDirectoryName = $OriginalArchive + (Folder-SuffixGenerator($SplitFileCounter)) + '.imscc'
$CourseZip = [IO.Compression.ZipFile]::OpenRead($FilePath)
$CourseZipFiles = $CourseZip.Entries | Sort Length -Descending
$CourseZip.Dispose()
<#
$SortingTable = $CourseZip.entries | Select Fullname,
#{Name="Size";Expression={$_.length}},
#{Name="CompressedSize";Expression={$_.Compressedlength}},
#{Name="PctZip";Expression={[math]::Round(($_.compressedlength/$_.length)*100,2)}}|
Sort Size -Descending | format-table –AutoSize
#>
# Add mandatory files
ForEach($entry in $CourseZipFiles)
{
if ($NoDeleteFiles.Contains($entry.Name)){
Write-Output "Adding to Zip" + $entry.FullName
# Add to Zip
$fileBytes = Get-ZipEntryContent -ZipFilePath $FilePath -EntryPath $entry.FullName
Add-ZipEntry -ZipFilePath ($OriginalArchivePath + $PartFileDirectoryName) -EntryPath $entry.FullName -Content $fileBytes
}
}```
System.IO.StreamWriter is a text writer, and therefore not suitable for writing raw bytes. Cannot convert value "507" to type "System.Byte" indicates that an inappropriate attempt was made to convert text - a .NET string composed of [char] instances which are in effect [uint16] code points (range 0x0 - 0xffff) - to [byte] instances (0x0 - 0xff). Therefore, any Unicode character whose code point is greater than 255 (0xff) will cause this error.
The solution is to use a .NET API that allows writing raw bytes, namely System.IO.BinaryWriter:
$WriteStream = [System.IO.BinaryWriter]::new($Entry.Open())
$WriteStream.Write($Content)
$WriteStream.Flush()
$WriteStream.Dispose()

Output ALL results at the end of foreach instead of during each run

I inherited a script which loops through a set of servers in a server list and then outputs some stuff for each one. It uses StringBuilder to append stuff to a variable and then spits out the results...how do I get the script to store the contents so I can display it at the VERY end with the results of the entire foreach instead of having it print (and then overwrite) on each iteration?
Currently my results look like this:
ServerName1
Text1
Next run:
ServerName2
Text 2
How do I get it to store the data and then output the following at the end so I can email it?
ServerName1
Text1
ServerName2
Text2
My code:
foreach($Machine in $Machines)
{
Invoke-Command -ComputerName $Machine -ScriptBlock{param($XML1,$XML2,$XML3,$URL)
[System.Text.StringBuilder]$SB = New-Object System.Text.StringBuilder
$X = $SB.AppendLine($env:COMPUTERNAME)
if (Test-Path <path>)
{
$PolResponse = <somestuff>
$PolResponse2 = <somestuff>
Write-Host "[1st] $PolResponse" -ForegroundColor Magenta
Write-Host "[2nd] $PolResponse2" -ForegroundColor Magenta
$X = $SB.AppendLine($PolResponse)
$X = $SB.AppendLine($PolResponse2)
}
else
{
$PolResponse = "[1st] No Response"
$PolResponse2 = "[2nd] No Response"
Write-Host $PolResponse -ForegroundColor Red
Write-Host $PolResponse2 -ForegroundColor Red
$X = $SB.AppendLine($PolResponse)
$X = $SB.AppendLine($PolResponse2)
}
} -ArgumentList $XML1, $XML2, $XML3, $URL
}
# Sending result email
<<I want to send the TOTALITY of $SB here>>
You can start by moving the StringBuilder variable declaration outside of the for loop (prior to it)
[System.Text.StringBuilder]$SB = New-Object System.Text.StringBuilder
then FOR LOOP
I don't know if this will be a good solution for what you're asking for or not, but what you could do is create a txt file and every loop in the foreach loop add the information to a txt file. This is one way to store all of the information and then have all of it together at the end.
New-Item -Path "\\Path\to\file.txt" -Itemtype File
Foreach(){
$Stuff = # Do your stuff here
Add-Content -Value $stuff -Path "\\Path\to\file.txt"
}
# Email .txt file ?
# You could use Send-MailMessage to do this possibly
Hopefully this can be helpful for your goal.

Write-Host vs Write-Information in PowerShell 5

It is well known that Write-Host is evil.
In PowerShell 5, Write-Information is added and is considered to replace Write-Host.
But, really, which is better?
Write-Host is evil for it does not use pipeline, so the input message can't get reused.
But, what Write-Host do is just to show something in the console right? In what case shall we reuse the input?
Anyway, if we really want to reuse the input, why not just write something like this:
$foo = "Some message to be reused like saving to a file"
Write-Host $foo
$foo | Out-File -Path "D:\foo.log"
Another Cons of Write-Host is that, Write-Host can specified in what color the messages are shown in the console by using -ForegroundColor and -BackgroundColor.
On the other side, by using Write-Information, the input message can be used wherever we want via the No.6 pipeline. And doesn't need to write the extra codes like I write above. But the dark side of this is that, if we want to write messages to the console and also saved to the file, we have to do this:
# Always set the $InformationPreference variable to "Continue"
$InformationPreference = "Continue";
# if we don't want something like this:
# ======= Example 1 =======
# File Foo.ps1
$InformationPreference = "Continue";
Write-Information "Some Message"
Write-Information "Another Message"
# File AlwaysRunThisBeforeEverything.ps1
.\Foo.ps1 6>"D:\foo.log"
# ======= End of Example 1 =======
# then we have to add '6>"D:\foo.log"' to every lines of Write-Information like this:
# ======= Example 2 =======
$InformationPreference = "Continue";
Write-Information "Some Message" 6>"D:\foo.log"
Write-Information "Another Message" 6>"D:\foo.log"
# ======= End of Example 2 =======
A little bit redundant I think.
I only know a little aspect of this "vs" thing, and there must have something out of my mind. So is there anything else that can make me believe that Write-Information is better than Write-Host, please leave your kind answers here.
Thank you.
The Write-* cmdlets allow you to channel the output of your PowerShell code in a structured way, so you can easily distinguish messages of different severity from each other.
Write-Host: display messages to an interactive user on the console. Unlike the other Write-* cmdlets this one is neither suitable nor intended for automation/redirection purposes. Not evil, just different.
Write-Output: write the "normal" output of the code to the default (success) output stream ("STDOUT").
Write-Error: write error information to a separate stream ("STDERR").
Write-Warning: write messages that you consider warnings (i.e. things that aren't failures, but something that the user should have an eye on) to a separate stream.
Write-Verbose: write information that you consider more verbose than "normal" output to a separate stream.
Write-Debug: write information that you consider relevant for debugging your code to a separate stream.
Write-Information is just a continuation of this approach. It allows you to implement log levels in your output (Debug, Verbose, Information, Warning, Error) and still have the success output stream available for regular output.
As for why Write-Host became a wrapper around Write-Information: I don't know the actual reason for this decision, but I'd suspect it's because most people don't understand how Write-Host actually works, i.e. what it can be used for and what it should not be used for.
To my knowledge there isn't a generally accepted or recommended approach to logging in PowerShell. You could for instance implement a single logging function like #JeremyMontgomery suggested in his answer:
function Write-Log {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
Write-Log 'foo' # default log level: Information
Write-Log 'foo' 'Information' # explicit log level: Information
Write-Log 'bar' 'Debug'
or a set of logging functions (one for each log level):
function Write-LogInformation {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
function Write-LogDebug {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message
)
...
}
...
Write-LogInformation 'foo'
Write-LogDebug 'bar'
Another option is to create a custom logger object:
$logger = New-Object -Type PSObject -Property #{
Filename = ''
Console = $true
}
$logger | Add-Member -Type ScriptMethod -Name Log -Value {
Param(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$Message,
[Parameter(Mandatory=$false, Position=1)]
[ValidateSet('Error', 'Warning', 'Information', 'Verbose', 'Debug')]
[string]$LogLevel = 'Information'
)
switch ($LogLevel) {
'Error' { ... }
'Warning' { ... }
'Information' { ... }
'Verbose' { ... }
'Debug' { ... }
default { throw "Invalid log level: $_" }
}
}
$logger | Add-Member -Type ScriptMethod -Name LogDebug -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Debug')
}
$logger | Add-Member -Type ScriptMethod -Name LogInfo -Value {
Param([Parameter(Mandatory=$true)][string]$Message)
$this.Log($Message, 'Information')
}
...
Write-Log 'foo' # default log level: Information
$logger.Log('foo') # default log level: Information
$logger.Log('foo', 'Information') # explicit log level: Information
$logger.LogInfo('foo') # (convenience) wrapper method
$logger.LogDebug('bar')
Either way you can externalize the logging code by
putting it into a separate script file and dot-sourcing that file:
. 'C:\path\to\logger.ps1'
putting it into a module and importing that module:
Import-Module Logger
To complement Ansgar's helpful and comprehensive answer:
Write-Host became (in essence) a wrapper for
Write-Information -InformationAction Continue in PSv5, presumably because:
it enables suppressing or redirecting Write-Host messages, which was not previously possible (in PowerShell 4 or below, Write-Host bypassed PowerShell's streams and output directly to the host),
while preserving backward compatibility in that the messages are output by default - unlike with Write-Information, whose default behavior is to be silent (because it respects preference variable $InformationPreference, whose default value is SilentlyContinue).
While Write-Host is therefore now (PSv5+) a bit of a misnomer — it doesn't necessarily write to the host anymore — it still has one distinct advantage over Write-Information (as you state): it can produce colored output with -ForegroundColor and -BackgroundColor.
Ansgar's answer has the conventional logging perspective covered, but PowerShell's Start-Transcript cmdlet may serve as a built-in alternative (see below).
As for your desire to output messages to the host while also capturing them in a log file:
PowerShell's session transcripts - via Start-Transcript and Stop-Transcript - may give you what you want.
As the name suggests, transcripts capture whatever prints to the screen (without coloring), which therefore by default includes success output, however.
Applied to your example:
$null = Start-Transcript "D:\foo.log"
$InformationPreference = "Continue"
Write-Information "Some Message"
Write-Information "Another Message"
$null = Stop-Transcript
The above will print messages to both the screen and the transcript file; note that, curiously, only in the file will they be prefixed with INFO:.
(By contrast, Write-Warning, Write-Verbose and Write-Debug - if configured to produce output - use prefix WARNING:, VERBOSE:, DEBUG: both on-screen and in the file; similarly, Write-Error produces "noisy" multiline input both on-screen and in the file.)
Note one bug that only affects Windows PowerShell (it has been fixed in PowerShell [Core].Thanks, JohnLBevan.): output from Write-Information shows up in the transcript file (but not on the screen) even when $InformationPreference is set to SilentlyContinue (the default); the only way to exclude Write-Information output (via the preference variable or -InformationAction parameter) appears to be a value of Ignore - which silences the output categorically - or, curiously, Continue, in which it only prints to the console, as PetSerAl points out.
In a nutshell, you can use Start-Transcript as a convenient, built-in approximation of a logging facility, whose verbosity you can control from the outside via the preference variables ($InformationPreference, $VerbosePreference, ...), with the following important differences from conventional logging:
Generally, what goes into the transcript file is also output to the console (which could generally be considered a plus).
However, success output (data output) is by default also sent to the transcript - unless you capture it or suppress it altogether - and you cannot selectively keep it out of the transcript:
If you capture or suppress it, it won't show in in the host (the console, by default) either[1].
The inverse, however, is possible: you can send output to the transcript only (without echoing it in the console), by way of Out-Default -Transcript Thanks, PetSerAl; e.g.,
'to transcript only' | Out-Default -Transcript; however, as of PowerShell 7.0 this appears to log the output twice in the transcript; also note that Out-Default is generally not meant to be called from user code - see this answer.
Generally, external redirections (applying > to a call to a script that internally performs transcription) keep streams out of the transcript, with two exceptions, as of PowerShell 7.0:
Write-Host output, even if 6> or *> redirections are used.
Error output, even if 2> or *> redirections are used.
However, using $ErrorActionPreference = 'SilentlyContinue' / 'Ignore' does keep non-terminating errors out of the transcript, but not terminating ones.
Transcript files aren't line-oriented (there's a block of header lines with invocation information, and there's no guarantee that output produced by the script is confined to a line), so you cannot expect to parse them in a line-by-line manner.
[1] PetSerAl mentions the following limited and somewhat cumbersome workaround (PSv5+) for sending success output to the console only, which notably precludes sending the output through the pipeline or capturing it:
'to console only' | Out-String -Stream | ForEach-Object { $Host.UI.WriteLine($_) }
PowerShell is about automation.
Sometimes, you run a script multiple times a day and you don't want to see the output all the time.
Write-Host has no possibility of hiding the output. It gets written on the Console, no matter what.
With Write-Information, you can specify the -InformationAction Parameter on the Script. With this parameter, you can specify if you want to see the messages (-InformationAction Continue) or not (-InformationAction SilentlyContinue)
Edit:
And please use "Some Message" | out-file D:\foo.log for logging, and neither Write-Host or Write-Information
Here's a generic version of a more specialized logging function that I used for a script of mine recently.
The scenario for this is that when I need to do something as a scheduled task, I typically create a generic script, or function in a module that does the "heavy lifting" then a calling script that handles the specifics for the particular job, like getting arguments from an XML config, logging, notifications, etc.
The inner script uses Write-Error, Write-Warning, and Write-Verbose, the calling script redirects all output streams down the pipeline to this function, which captures records the messages in a csv file with a Timestamp, Level, and Message.
In this case, it was targeted at PoSh v.4, so I basically am using Write-Verbose as a stand-in for Write-Information, but same idea. If I were to have used Write-Host in the Some-Script.ps1 (see example) instead of Write-Verbose, or Write-Information, the Add-LogEntry function wouldn't capture and log the message. If you want to use this to capture more streams appropriately, add entries to the switch statement to meet your needs.
The -PassThru switch in this case was basically a way to address exactly what you mentioned about both writing to a log file in addition to outputting to the console (or to another variable, or down the pipeline). In this implementation, I've added a "Level" property to the object, but hopefully you can see the point. My use case for this was to pass the log entries to a variable so that they could be checked for errors, and used in an SMTP notification if an error did occur.
function Add-LogEntry {
[CmdletBinding()]
param (
# Path to logfile
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, Position = 0)]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 0)]
[String]$Path,
# Can set a message manually if not capturing an alternate output stream via the InformationObject parameter set.
[Parameter(ParameterSetName = 'Normal', Mandatory = $true)]
[String]$Message,
# Captures objects redirected to the output channel from Verbose, Warning, and Error channels
[ValidateScript({ #("VerboseRecord", "WarningRecord", "ErrorRecord") -Contains $_.GetType().name })]
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $true, ValueFromPipeline = $true)]
$InformationObject,
# If using the message parameter, must specify a level, InformationObject derives level from the object.
[ValidateSet("Information", "Warning", "Error")]
[Parameter(ParameterSetName = 'Normal', Mandatory = $true, Position = 2)]
[String]$Level,
# Forward the InformationObject down the pipeline with additional level property.
[Parameter(ParameterSetName = 'InformationObject', Mandatory = $false)]
[Switch]$PassThru
)
Process {
# If using an information object, set log entry level according to object type.
if ($PSCmdlet.ParameterSetName -eq "InformationObject") {
$Message = $InformationObject.ToString()
# Depending on the object type, set the error level,
# add entry to cover "Write-Information" output here if needed
switch -exact ($InformationObject.GetType().name) {
"VerboseRecord" { $Level = "Information" }
"WarningRecord" { $Level = "Warning" }
"ErrorRecord" { $Level = "Error" }
}
}
# Generate timestamp for log entry
$Timestamp = (get-date).Tostring("yyyy\-MM\-dd\_HH\:mm\:ss.ff")
$LogEntryProps = #{
"Timestamp" = $Timestamp;
"Level" = $Level;
"Message" = $Message
}
$LogEntry = New-Object -TypeName System.Management.Automation.PSObject -Property $LogEntryProps
$LogEntry | Select-Object Timestamp, Level, Message | Export-Csv -Path $Path -NoTypeInformation -Append
if ($PassThru) { Write-Output ($InformationObject | Add-Member #{Level = $Level } -PassThru) }
}
}
Example usage would be
& $PSScriptRoot\Some-Script.ps1 -Param $Param -Verbose *>&1 | Add-LogEntry -Path $LogPath -PassThru
The -PassThru switch should essentially write the information object to the console if you don't capture the output in a variable or pass it down the pipe to something else.
Interesting: while the preference variables have been mentioned in the earlier answers, nobody has mentioned the common parameters -InformationVariable, -ErrorVariable and -WarningVariable.
They are designed to take the output from the corresponding Write-* commands in the script or command so you can do with it what you want.
They're not really suitable to use as a log of an entire session, because they need to be added to every command you run, and the command/script needs to support it. (can be as easy as adding [CmdletBinding()] at the top of you scipt if you don't have it already. See the documentation)
But it could be a good way to separate the warnings/error from the real output of a command. Just run it again with the proper parameters added.
For example -ErrorVariable ErrorList -ErrorAction SilentlyContinue.
Now all the errors are stored in the $ErrorList variable and you can write them in a file.
$ErrorList|Out-File -FilePath errors.txt
I have to admit that I hate PowerShell logging and all the Write-* commands... So I start all my scripts with the same function:
function logto{ ## Outputs data to Folder tree
Param($D,$P,$F,$C,$filename)
$LogDebug = $false
$FDomain =[System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
$SCRdir = $MyInvocation.ScriptName
$FDNSName = $FDomain.Name
$RealFile = $F
if($ScriptName -eq $null){
$ScriptName = "\LogTo\"
}
## if there is a time stamp defined make it part of the directory
if($GlobalRunTime){
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName + $GlobalRunTime + "\"
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}else{
$Flocaldrive = $env:SystemDrive + "\" + $FDNSName + $ScriptName
If ($LogDebug) {Write-host "Set path to $Flocaldrive" -foregroundcolor Magenta}
}
## do not write null data
if ($D -eq $null) {
If ($LogDebug) {Write-host "$RealFile :Received Null Data Exiting Function" -foregroundcolor Magenta}
Return
}
## if no path is chosen default to
if ($P -eq $null) {
$PT = $Flocaldrive
If ($LogDebug) {Write-host "Path was Null, setting to $PT" -foregroundcolor Magenta}
}else{
$PT = $Flocaldrive + $P
If ($LogDebug) {Write-host "Path detected as $p, setting path to $PT" -foregroundcolor Magenta}
}
## anything with no file goes to Catchall
If ($RealFile-eq $null) {
If ($LogDebug) {Write-host "$D :attempting to write to Null file name, redirected out to Catchall" -foregroundcolor Magenta}
$RealFile= "\Catchall.txt"
}
##If color is blank DONT write to screen
if ($C -eq $null) {
If ($LogDebug) {Write-host "Color was blank so not writing to screen" -foregroundcolor Magenta}
}else{
If ($LogDebug) {Write-host "Attempting to write to console in $C" -foregroundcolor Magenta}
write-host $D -foregroundcolor $C
}
###### Write standard format
$DataFile = $PT + $RealFile## define path with File
## Check if path Exists if not create it
If (Test-Path $PT) {
If ($LogDebug) {Write-host "$PT :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $PT -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $PT" -foregroundcolor Magenta}
}
## If file exist if not create it
If (Test-Path $DataFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$DataFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $DataFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$DataFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $DataFile -append ## Write our data to file
## Write to color coded files
if ($C -ne $null) {
$WriteSumDir = $Flocaldrive + "Log\Sorted"
$WriteSumFile = $WriteSumDir + "\Console.txt"
## Check if path Exists if not create it
If (Test-Path $WriteSumDir) {
If ($LogDebug) {Write-host "$WriteSumDir :Directory Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumDir -type directory | out-null ## if directory does not exist create it
If ($LogDebug) {Write-host "Creating directory $WriteSumDir" -foregroundcolor Magenta}
}
## If file does not exist create it
If (Test-Path $WriteSumFile) {
If ($LogDebug) {Write-host "$WriteSumFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteSumFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteSumFile :File Created" -foregroundcolor Magenta}
}
## Write our data to file
$D | out-file -Filepath $WriteSumFile -append ## write everything to same file
## Write our data to color coded file
$WriteColorFile = $WriteSumDir + "\$C.txt"
If (Test-Path $WriteColorFile) { ## If file does not exist create it
If ($LogDebug) {Write-host "$WriteColorFile :File Exists" -foregroundcolor Magenta}
}else{
New-Item $WriteColorFile -type file | out-null ## if file does not exist create it, we cant append a null file
If ($LogDebug) {Write-host "$WriteColorFile :File Created" -foregroundcolor Magenta}
}
## Write our data to Color coded file
$D | out-file -Filepath $WriteColorFile -append ## write everything to same file
}
## If A return was not specified
If($filename -ne $null){
Return $DataFile
}
}

A positional parameter cannot be found that accepts argument '\*'

I have a number of web services that I'm attempting to use PowerShell to install on a DEV server. I want to include, in the script, a section that copies all files and folders from a staging area to the respective folder on the DEV server.
In order to do this I've created two parallel string arrays - one containing the direct file paths of the staging folders and the other with the names of each folder on the DEV server (I use a couple other variables to store the rest of the directory).
I'd like to create a for loop and simply iterate over both arrays to complete the transfer of files/folders from staging to dev server. However when the loop runs the copy-item command gives this error each time
Copy-Item : A positional parameter cannot be found that accepts argument '*'.
If I decide not to use the loop, the error goes away and the files transfer properly. Only a couple days new to PowerShell so I'm not sure what's going on/haven't been able to find anything specific to this on the web.
Variables
#web services
$periscopeWebServices = "periscopeWebServices"
$periscopeRetrieveComments = "periscopeRetrieveComments"
$periscopeRetrieveGeneral = "periscopeRetrieveGeneral"
$periscopeRetrieveMasterSubs = "periscopeRetrieveMasterSubs"
$periscopeRetrieveProducers = "periscopeRetrieveProducers"
$periscopeRetrieveProfiles = "periscopeRetrieveProfiles"
$periscopeRetrieveRelationships = "periscopeRetrieveRelationships"
$periscopeSearch = "periscopeSearch"
$periscopeServicesArray = #($periscopeRetrieveComments, $periscopeRetrieveGeneral,
$periscopeRetrieveMasterSubs, $periscopeRetrieveProducers,
$periscopeRetrieveProfiles, $periscopeRetrieveRelationships,
$periscopeSearch)
#staging areas
$stagingComments = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveComments"
$stagingGeneral = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveGeneral"
$stagingMasterSubs = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveMasterSubs"
$stagingProducers = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveProducers"
$stagingProfiles = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveProfiles"
$stagingRelationships = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveRelationships"
$stagingSearch = "\\install\PeriscopeServices\periscopeWebServices\periscopeSearch"
$stagingArray = #($stagingComments, $stagingGeneral, $stagingMasterSubs, $stagingProducers
$stagingProfiles, $stagingRelationships, $stagingSearch)
#server variables
$domain = "InsideServices.dev.com"
$directory = "E:\webcontent"
$domainpath = "$directory\$domain"
Loop - throws error on 'copy-item'
#copy files
Write-Host "Copying Files" -ForegroundColor Yellow
For ($i=0; $i -lt $periscopeServicesArray.Length; $i++)
{
Write-Host "Copying $($periscopeServicesArray[$i])" -ForegroundColor Yellow
if(Test-Path -path "$domainpath\$periscopeWebServices\$($periscopeServicesArray[$i])")
{
copy-item -Path $($stagingArray[$i])\* -Destination $domainpath\$periscopeWebServices\$($periscopeServicesArray[$i]) -recurse -Force
}
}
Succesful copy-item command
copy-item -Path $stagingComments\* -Destination $domainpath\$periscopeWebServices\$periscopeRetrieveComments -recurse -Force
I think it's because you're using a sub-expression $() inside the loop, but not outside. Try these:
"$($stagingArray[$i])\*"
Surrounding it in quotes makes it evaluate the whole thing as a string.
$($stagingArray[$i]) + '\*'
This will concatenate the result of the sub-expression and the string containing the \*.