How can I measure the window height (number of lines) in powershell? - powershell

The maximum number of lines in my environment is 47.
Can I measure this value programmatically?

To complement Christian.K's helpful answer:
A simpler and slightly more efficient method for obtaining the host object is to use the $Host automatic variable rather than the Get-Host cmdlet.
You're only guaranteed to have access to window-size information in the PowerShell console host, i.e., when running in a console (terminal) window
It is at a given host's discretion whether to expose this information, and, as Christian states, the PowerShell ISE does not - even though it arguably should, given that it has a console subsystem built in.
To test whether your code is running in a console (a.k.a terminal) or not, use
$Host.UI.SupportsVirtualTerminal - $True indicates that the host is a console.
If running in the PowerShell console host:
You can access the console's properties either:
via $Host.UI.RawUI (as in Christian's answer), which is an object of type [System.Management.Automation.Internal.Host.InternalHostRawUserInterface] that - in the PowerShell console host only - wraps the .NET [Console] class (see below).
via the .NET [Console] class; e.g., to get the window height (count of rows) this way, use:
[Console]::WindowHeight
Sample $Host.UI.RawUI output:
PS> $Host.UI.RawUI
ForegroundColor : DarkYellow
BackgroundColor : DarkMagenta
CursorPosition : 0,58
WindowPosition : 0,0
CursorSize : 25
BufferSize : 160,9999
WindowSize : 160,75
MaxWindowSize : 160,94
MaxPhysicalWindowSize : 303,94
KeyAvailable : True
WindowTitle : Windows PowerShell
If running in the PowerShell ISE:
Written as of PowerShell version 5.1
Most of the properties in $Host.UI.RawUI are not populated and will return their data type's default value:
PS> $Host.UI.RawUI # in the ISE
ForegroundColor : -1
BackgroundColor : -1
CursorPosition : 0,0
WindowPosition :
CursorSize :
BufferSize : 166,0
WindowSize :
MaxWindowSize :
MaxPhysicalWindowSize :
KeyAvailable :
WindowTitle : Windows PowerShell ISE
The only size-related information that is available is the buffer width (166 in the sample output above).
There is no point in trying to use the .NET [Console] class in the ISE, except to query / set the character encoding used for communication with external programs, [Console]::OutputEncoding:[1]
Initially in a session, trying to use the window-related members of [Console] causes exceptions, because the ISE does not allocate a console window on startup:
# In a pristine session.
PS> [Console]::WindowHeight
The handle is invalid. # EXCEPTION
While the ISE allocates a - hidden - console window on demand, namely the first time you run a console application in your session, that hidden console window's properties, as then reported on via [Console], do not reflect the properties of the simulated console that the ISE presents to the user.
# chcp is a console application, so when it is run,
# the ISE allocates a (hidden) console window,
# after which the [Console] API becomes technically usable,
# but DOESN'T REPORT MEANINGFUL VALUES.
PS> chcp >$null; [Console]::WindowHeight
72 # No exception, but the value is meaningless.
[1] Note that the ISE defaults to the system's legacy ANSI code page, whereas regular console windows default to the OEM code page. Thus, these two environments decode output from external programs differently by default.

You could try something like
$(Get-Host).UI.RawUI.WindowSize.Height
Note a couple of things however:
Using the RawUI might not be particular "portable", depending on where your script runs. In "ISE" for example, that property returns nothing, while on the console it will work.
For more difference here between the console and ISE, run $(Get-Host).UI.RawUI and compare the output.
In other PowerShell hosts, besides the console or ISE, it might yet be still different.
There is also a BufferSize, which can be bigger than the WindowSize. The later being only the part that is currently "viewable" by the user.
Your question sounds a little like a xy problem. Consider explaining (as an edit to your question or a new question), what you're actually trying to achieve, i.e. why you need to know the number of lines?

The functionality I really need is git status, git diff in powershell_ise with highlight and output paging.
It seems that the current version of powershell_ise does not solve the problem itself.
Although not completely satisfactory, I have devised a practical solution and created the following function.
source code
function hhd-git-diff
{
[CmdletBinding()]
param
(
)
$res = git diff
hhd-git-colored-output -INPUT_OBJECT $res
}
function hhd-git-status
{
[CmdletBinding()]
param
(
)
$res = git status
hhd-git-colored-output -INPUT_OBJECT $res
}
function hhd-git-colored-output
{
[CmdletBinding()]
param
(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelinebyPropertyName=$true)]
[System.Object]
$INPUT_OBJECT
)
$lineNum = 1
$INPUT_OBJECT |
Out-String |
foreach {
$_ -split "\n" |
foreach {
if($lineNum % [Console]::WindowHeight -eq 0)
{
Read-Host "continue? press any key..."
}
$lineNum++
if($_ -like "diff --git *")
{
Write-Host ""
Write-Host ""
Write-Host ""
Write-Host ("#"*80) -ForegroundColor Cyan
Write-Host $_ -ForegroundColor Cyan
Write-Host ("#"*80) -ForegroundColor Cyan
Read-Host "continue? press any key..."
$lineNum = 1
}
elseif($_ -like "--- *")
{
}
elseif($_ -like "+++ *")
{
}
elseif($_ -like "-*")
{
Write-Host $_ -ForegroundColor Red
}
elseif($_ -like "+*")
{
Write-Host $_ -ForegroundColor Green
}
elseif($_ -like "On branch *")
{
Write-Host ("#"*80) -ForegroundColor Cyan
Write-Host $_ -ForegroundColor Cyan
}
elseif($_ -like "Your branch is*")
{
Write-Host $_ -ForegroundColor Cyan
Write-Host ("#"*80) -ForegroundColor Cyan
}
elseif($_ -like "Changes to be committed:*")
{
Write-Host ("-"*80) -ForegroundColor Green
Write-Host $_ -ForegroundColor Green
}
elseif($_ -like "Changes not staged for commit:*")
{
Write-Host ("-"*80) -ForegroundColor Red
Write-Host $_ -ForegroundColor Red
}
elseif($_ -like "Untracked files:*")
{
Write-Host ("-"*80) -ForegroundColor Black
Write-Host $_ -ForegroundColor Black
}
elseif($_ -like "*modified:*")
{
Write-Host $_ -ForegroundColor Yellow
}
elseif($_ -like "*deleted:*")
{
Write-Host $_ -ForegroundColor Red
}
else
{
Write-Host $_ -ForegroundColor Gray
}
}
}
}
screen shot

Related

Powershell Script working fine in Visual Code but fails running from Terminal

I'm working on writing a script which will run from AzDo Pipeline to disable F5 WebServers. Below script works fine in Visual Code and does disable the server as expected . But when running from the terminal or PS window fails with the below error . Can someone please help.
$ServerInput = 'server1.abc.com'
$BIGIPBaseURL = "https://ser-f5-1.prod.abc.com"
$usr = "nilesh"
$SecurePassword='P#assword'
Write-Host "Starting the Script..."
# Initialize variables
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$BIGIPToken = $null
Write-Host -ForegroundColor Green " done!"
$DisableWebServers = $true
# Initialize functions
Write-Host "Initializing functions..." -NoNewline
$PSVersionTable
function Disable-BIGIPNode([string]$NodeName) {
# servers should use the Disable-BIGIPTelcoNode() function
Write-Host "In the Disable function"
if ($NodeName -match "(?i).*telco.*") {
Write-Host -ForegroundColor Yellow "WARNING: `"$($NodeName.ToUpper().Split('.')[0])`" is in the wrong list. telcoo hosts should be added to the TelcoServers list in your input file."
BREAK
}
else {
if ($BIGIPToken -eq $null) {
Write-Host "Now will enter the Open-Session"
Open-BIGIPSession
}
Write-Host "Disabling node `"$($NodeName.ToUpper().Split('.')[0])`" in BIG-IP..." -NoNewline
$WebRequestInput = #{
body = #{
"session" = "user-disabled"
} | ConvertTo-Json
uri = $($BIGIPBaseURL) + "/mgmt/tm/ltm/node/~Common~" + $NodeName.ToLower()
headers = #{
"Content-Type" = "application/json"
"X-F5-Auth-Token" = "$BIGIPToken"
}
method = "PATCH"
}
Write-Host $WebRequestInput
Write-Host $WebRequestInput.body
try {
Write-Host "In the final try block"
$Request = Invoke-WebRequest #WebRequestInput -UseBasicParsing -SkipCertificateCheck
}
catch {
Write-Host -ForegroundColor Red " failed!"
Write-Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Message
}
Write-Host -ForegroundColor Green " done!"
$global:ZabbixRequestID++
}
}
function Open-BIGIPSession() {
Write-Host "Authenticating with BIG-IP API..." -NoNewline
$WebRequestInput = #{
body = #{
username = "$usr"
password = "$SecurePassword"
loginProviderName = "tmos"
} | ConvertTo-Json
uri = $ScriptInput.BIGIPBaseURL + "/mgmt/shared/authn/login"
headers = #{
"Content-Type" = "application/json"
}
method = "POST"
}
try {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$Request = Invoke-WebRequest #WebRequestInput -UseBasicParsing -SkipCertificateCheck
}
catch {
Write-Host -ForegroundColor Red " failed!"
Write-Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Message
EXIT 1
}
Write-Host -ForegroundColor Green " done!"
$global:BIGIPToken = ($Request.Content | ConvertFrom-Json).token.token
}
if ($DisableWebServers) {
Write-Host "Starting the main Methord "
foreach ($Server in $($ServerInput)) {
Disable-BIGIPNode -NodeName $Server
}
}
The PowerShell version is PSVersion 7.2.2
Disabling node "SAC-DEV-WEB2" in BIG-IP...System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry System.Collections.DictionaryEntry
{
"session": "user-disabled"
}
In the final try block
failed!
ConvertFrom-Json: C:\Temp\Testing.ps1:49:64
Line |
49 | … Host -ForegroundColor Red ($_.ErrorDetails | ConvertFrom-Json).Messag …
| ~~~~~~~~~~~~~~~~
| Conversion from JSON failed with error: Additional text encountered after finished reading JSON content: U. Path '', line
| 3, position 4.
Its working fine when running from VsCode but fails if called with the file name from the same terminal
like .\Testing.ps1
Please help
Your incidental problem is that the true error message is being obscured by a follow-up error that results from attempting to parse the error record's .ErrorDetails property as JSON, which it isn't. (You report that examining the true error reveals a 401 authentication error).
I have no specific explanation for the difference in behavior you're seeing between running in Visual Studio Code vs. in a regular PowerShell console, but I have a guess:
Your Visual Studio Code session in the so-called PowerShell Integrated Console may have lingering state from earlier debugging runs, which may mask a bug in your script.
Restarting Visual Studio Code should clarify whether that is the case, but there's also a way to configure the PowerShell extension so that the problem doesn't arise to begin with - see below.
By default, code you run (debug) via the Visual Code PowerShell extension executes in the same PowerShell session, directly in the global scope.
That is, running a script being edited, say, foo.ps1, in the debugger is effectively the same as invoking it with . .\foo.ps1, i.e. it is in effect dot-sourced.
Therefore, a given debugging run can be affected by earlier runs, because the state of earlier runs lingers.
This can result in bugs going undetected, such as in the following example:
Say your script defines variable $foo and uses it throughout the script. If you debug your script at least one, $foo is now defined in the PowerShell session in the PowerShell Integrated Console.
Say you then change the name to $bar, but you forget to also replace (all) references to $foo with $bar.
Your script is now effectively broken, but you won't notice in the same session, because $foo is still around from earlier debugging runs.
However, running the script from a regular PowerShell console would surface the problem.
The obsolescent Windows PowerShell ISE exhibits the same unfortunate behavior, invariably so, but fortunately there is a solution for the PowerShell extension - see next point.
You can avoid this problem by activating the Create Temporary Integrated Console setting (via File > Preferences > Settings or Ctrl+,), which ensure that every debugging run creates a new, temporary session to run in, which starts with a clean slate:
Whenever a new temporary session is started, any previous one is automatically discarded.
A temporary session has prefix [TEMP] in the list of running shells in the integrated terminal.
You pay a performance penalty, because a new PowerShell session must be created for every run, and you lose the previous session's display output - but I suspect avoiding the pitfalls of lingering state is worth the price.
Note that, in a given temporary session, the dot-sourced invocation described above still applies, but with the lingering-state problem out of the picture, it can now be considered an advantage: After the script finishes, and before the temporary session is replaced with a new one, the variables and functions defined in the script's top-level scope are then available for inspection.

How can I check if the PowerShell profile script is running from an SSH session?

I'm trying to work around a bug in Win32-OpenSSH, where -NoProfile -NoLogo is not respected when using pwsh.exe (Core) and logging in remotely via SSH/SCP. One way (of several) I tried, was to add the following in the very beginning of my Microsoft.PowerShell_profile.ps1 profile.
function IsInteractive {
$non_interactive = '-command', '-c', '-encodedcommand', '-e', '-ec', '-file', '-f'
-not ([Environment]::GetCommandLineArgs() | Where-Object -FilterScript {$PSItem -in $non_interactive})
}
# No point of running this script if not interactive
if (-not (IsInteractive)) {
exit
}
...
However, this didn't work with a remote SSH, because when using [Environment]::GetCommandLineArgs() with pwsh.exe, all you get back is:
C:\Program Files\PowerShell\6\pwsh.dll
regardless whether or not you are in an interactive session.
Another way I tried, was to scan through the process tree and look for the sshd parent, but that was also inconclusive, since it may run in another thread where sshd is not found as a parent.
So then I tried looking for other things. For example conhost. But on one machine conhost starts before pwsh, whereas on another machine, it starts after...then you need to scan up the tree and maybe find an explorer instance, in which case it is just a positive that the previous process is interactive, but not a definite non-interactive current process session.
function showit() {
$isInter = 'conhost','explorer','wininit','Idle',
$noInter = 'sshd','pwsh','powershell'
$CPID = ((Get-Process -Id $PID).Id)
for (;;) {
$PNAME = ((Get-Process -Id $CPID).Name)
Write-Host ("Process: {0,6} {1} " -f $CPID, $PNAME) -fore Red -NoNewline
$CPID = try { ((gwmi win32_process -Filter "processid='$CPID'").ParentProcessId) } catch { ((Get-Process -Id $CPID).Parent.Id) }
if ($PNAME -eq "conhost") {
Write-Host ": interactive" -fore Cyan
break;
}
if ( ($PNAME -eq "explorer") -or ($PNAME -eq "init") -or ($PNAME -eq "sshd") ) {
# Write-Host ": non-interactive" -fore Cyan
break;
}
""
}
}
How can I check if the profile script is running from within a remote SSH session?
Why am I doing this? Because I want to disable the script from running automatically through SSH/SCP/SFTP, while still being able to run it manually (still over SSH.) In Bash this is a trivial one-liner.
Some related (but unhelpful) answers:
Powershell test for noninteractive mode
How to check if a Powershell script is running remotely

PowerShell If Statement not returning expected results using array values

I'm trying to condense a PS script from 30+ lines of original code, down to a handful using an array. My original script has about 30+ (functional) lines of code to check the status of various web sites like this:
$icelb = Invoke-WebRequest -Uri https://interchange.ctdssmap.com/Connecticut/
Followed by the status that gets reported to the console:
if ($icelb.StatusDescription -ne "OK") {
Write-Host -BackgroundColor Black -ForegroundColor Red "Interchange Load Balancer Results: FAILED"
}
}
As you can imagine, after 30+ lines of this, the script operates rather slowly, to say the least. So I am playing around with using arrays for the URL's instead, like this (the $websites variable contains an array of all the URL's to be tested):
$websites | ForEach-Object {
Invoke-WebRequest -Uri $_ | select $_.StatusDescription
if ($_.StatusDescription -ne "OK") {
Write-Host -BackgroundColor Black -ForegroundColor Red "'$_': FAILED"
} else {
Write-Host -BackgroundColor Black -ForegroundColor Green "'$_': " $_.StatusDescription
}
}
My problem is in the reporting phase of the script. I can't get the if statement to properly recognize and report correct status. The only thing I repeatedly get is "'$_': FAILED", even though statusdescription is "OK".
I have just noticed that the variable $_.StatusDescription is empty, which is likely causing the problem, but I can't get the correct syntax to have the if statement properly evaluate it.
The "current object" variable doesn't work the way you seem to expect. In a statement
Invoke-WebRequest -Uri $_ | select $_.StatusDescription
both occurrences of $_ are expanded at parse time, so upon execution the statement effectively looks like this:
Invoke-WebRequest -Uri 'https://example.com/foo/bar' | select 'https://example.com/foo/bar'.StatusDescription
To make your code work simply expand the property StatusDescription, assign the value to a variable, and use that variable in your if statement:
$websites | ForEach-Object {
$status = Invoke-WebRequest -Uri $_ |
Select-Object -Expand StatusDescription
if ($status -ne "OK") {
Write-Host -BackgroundColor Black -ForegroundColor Red "'$_': FAILED"
} else {
Write-Host -BackgroundColor Black -ForegroundColor Green "'$_': $status"
}
}

How to configure a timeout for Read-Host in PowerShell

Like I said, this code works in PowerShell version 2, but not in PowerShell version 5.
function wait
{
$compte = 0
Write-Host "To continue installation and ignore configuration warnings type [y], type any key to abort"
While(-not $Host.UI.RawUI.KeyAvailable -and ($compte -le 20))
{
$compte++
Start-Sleep -s 1
}
if ($compte -ge 20)
{
Write-Host "Installation aborted..."
break
}
else
{
$key = $host.ui.rawui.readkey("NoEcho,IncludeKeyup")
}
if ($key.character -eq "y")
{Write-Host "Ignoring configuration warnings..."}
else
{Write-Host "Installation aborted..."
}}
The official documentation or Read-Host -? will tell that it's not possible to use Read-Host in that manner. There is no possible parameter to tell it to run with some kind of timeout.
But there are various other questions detailing how to do this in PowerShell (usually utilizing C#).
The idea seems to be to check whenever the user pressed a key using $Host.UI.RawUI.KeyAvailable and check that for the duration of your timeout.
A simple working example could be the following:
$secondsRunning = 0;
Write-Output "Press any key to abort the following wait time."
while( (-not $Host.UI.RawUI.KeyAvailable) -and ($secondsRunning -lt 5) ){
Write-Host ("Waiting for: " + (5-$secondsRunning))
Start-Sleep -Seconds 1
$secondsRunning++
}
You could use $host.UI.RawUI.ReadKey to get the key that was pressed. This solution probably would not be acceptable if you need more complex input than a simple button press. See also:
Windows PowerShell Tip of the Week - Pausing a Script Until the User Presses a Key
PowerTip: Use PowerShell to Wait for a Key Press (Hey, Scripting Guy!)
Seth, thank you for your solution. I expanded on the example you provided and wanted to give that back to the community.
The use case is a bit different here - I have a loop checking if an array of VMs can be migrated and if there are any failures to that check the operator can either remediate those until the checks clear or they can opt to "GO" and have those failing VMs excluded from the operation. If something other than GO is typed state remains within the loop.
One downside to this is if the operator inadvertently presses a key the script will be blocked by Read-Host and may not be immediately noticed. If that's a problem for anyone I'm sure they can hack around that ;-)
Write-Host "Verifying all VMs have RelocateVM_Task enabled..."
Do {
$vms_pivoting = $ph_vms | Where-Object{'RelocateVM_Task' -in $_.ExtensionData.DisabledMethod}
if ($vms_pivoting){
Write-Host -ForegroundColor:Red ("Some VMs in phase have method RelocateVM_Task disabled.")
$vms_pivoting | Select-Object Name, PowerState | Format-Table -AutoSize
Write-Host -ForegroundColor:Yellow "Waiting until this is resolved -or- type GO to continue without these VMs:" -NoNewline
$secs = 0
While ((-not $Host.UI.RawUI.KeyAvailable) -and ($secs -lt 15)){
Start-Sleep -Seconds 1
$secs++
}
if ($Host.UI.RawUI.KeyAvailable){
$input = Read-Host
Write-Host ""
if ($input -eq 'GO'){
Write-Host -ForegroundColor:Yellow "NOTICE: User prompted to continue migration without the blocked VM(s)"
Write-Host -ForegroundColor:Yellow "Removing the following VMs from the migration list"
$ph_vms = $ph_vms | ?{$_ -notin $vms_pivoting} | Sort-Object -Property Name
}
}
} else {
Write-Host -ForegroundColor:Green "Verified all VMs have RelocateVM_Task method enabled."
}
} Until(($vms_pivoting).Count -eq 0)
Also note that all this $Host.UI stuff doesn't work from the Powershell ISE.
To find out from within a script you could test for $Host.Name -eq "ConsoleHost". When true you can use the code from this topic. Otherwise you could use $Host.UI.PromptForChoice or any other way of showing a dialog box. With System.Windows.Forms.Timer you can then set a timer, and code to close the dialog box or form can be run when it expires.

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
}
}