Is there any way to avoid passing parameters to a function, like "-Append $outfile" to Out-File, every time? I have a script which collects data from the system, something like:
... collect OS information ... | Out-File -Append $output
... collect local users ... | Out-File -Append $output
... collect logfile permissions ... | Out-File -Append $output
etc.
The last command in the pipe is most of the time Out-File -Append $output - can this be done more elegant? I had different ideas:
Create a wrapper function which passes the needed parameters to Out-File command - already tried, but I had problems to make it pipe-compatible
Write all output into a String-Variable and write the content at the end of all commands into the file - needs a lot of memory
Create something like an Output-Writer-Object which only receives once at initialization the necessary paramters - not tried yet
Thank you very much for your help!
You dont appear to be using a lot of arguments for this to be incredibly useful but a good suggestion would be to use splatting. I added some more parameters to illustrate how clean it can make code appear while still being functional.
$options = #{
Append = $True
FilePath = $output
Encoding = "Unicode"
Width = 400
}
Build a hastable of options and splat the cmdlet with them
... collect OS information ... | Out-File #options
... collect local users ... | Out-File #options
... collect logfile permissions ... | Out-File #options
Outside of that a wrapper function (of filter if it is easier) like you suggest would be another option. Look at the options in this answer. Specifically the filter
You want to use the $PSDefaultParameterValues preference variable. Something like this:
$PSDefaultParameterValues = #{
"Out-File:Encoding"="utf8";
"Out-File:Append"=$true;
"Out-File:FilePath"=$output
}
This feature is especially useful when you must specify the same alternate parameter value nearly every time you use the command or when a particular parameter value is difficult to remember, such as an email server name or project GUID.
Or put everything inside a function or scriptblock. Note that out-file defaults to utf16 encoding, and can mix encodings, as opposed to add-content.
& {
... collect OS information ...
... collect local users ...
... collect logfile permissions ...
} | add-content $output
Related
I have this bit of simple code where I am passed a variable for the username, I need to parse that and then use it in a path for a copy command.
I have used outputting the variables to a text file to help try to troubleshoot the problem.
I parse the variable, and it seems to output to the text file properly, but when I use it in my path variable it shows as empty.
The Code:
param ([String] $mdmUserName)
$mdmUserName | Out-File "C:\Windows\Temp\test.txt"
$FullUserSplit = $mdmUserName.Split("\")
$FullUserSplit | Out-File -append "C:\Windows\Temp\test.txt"
$localusername = $FullUserSplit[2]
$localusername | Out-File -Append "C:\Windows\Temp\test.txt"
$from = "C:\Windows\Temp\Normaltest.dotm"
$to = "C:\Users\$localusername\AppData\Roaming\Microsoft\Templates\"
$to | Out-File -Append "C:\Windows\Temp\test.txt"
Copy-Item $from $to -Force
The output of the test.txt file:
Win11\User
Win11
User
C:\Users\\AppData\Roaming\Microsoft\Templates\
You can see that it outputs the $localusername variable correctly to the test.txt, but then when added to the path it is not there. I feel like I am missing something simple.
I also tried manually setting the $mdmusername manually to "Win11/User" with the same result.
Arrays are zero based (they start with 0), thus: $FullUserSplit[1] or $FullUserSplit[-1] (which selects the last entry) – iRon
iRon's comment was the answer. I needed to use $FullUserSplit[1]
Outputting the $FullUserSplit to the troubleshooting file was throwing me off, as it outputted the full array on 2 lines.
I am creating a script and want to both use Write-Host and Write-Output
As I work I want a backup of information I pull from AD to also become attached to a .txt file.
This is more of a backup in case I miss a piece of information and need to go back and recreate a ticket. Anyways I have a sample of my script, form what I can tell it should be working. If someone with a bit more experience can take a look or point me in the right direction I would appreciate it. If I need to add any more of the script I can provide this. Thanks in Advance.
Import-Module activedirectory
$object = Get-ADUser $sid -Properties * | Select-Object EmailAddress
Write-Host Email: $object.EmailAddress
Write-Output ("Email: $object.EmailAddress") >> C:\psoutput\psoutput.txt -Append
This will create the .txt file of course but is also add other information such as:
Email: #{GivenName=myfirstname; Surname=mylastname; SamAccountName=myid; DisplayName=lastname, firstname - Contingent Worker; City=; EmailAddress=myemailaddress#mywork.com; EmployeeID=; Enabled=True; OfficePhone=; MobilePhone=(555) 555-5555; LockedOut=False; LockOutTime=0; AccountExpirationDate=05/09/2020 00:00:00; PasswordExpired=False; PasswordLastSet=12/03/2019 12:16:37}.EmailAddress
-Append
I am looking to have the output like the following...
name: username
email: user email address
phone: user phone number
etc...
All general information from Active Directory
Thanks again for the suggestions
Don't use write-output. Use (Get-ADUser $sid -properties mail).mail.
Like this:
Add-Content -Path "FilePath" -Value "Email: $((Get-ADUser $sid -properties mail).mail)"
Write-Output ("Email: $object.EmailAddress")
As an aside: No need for (...) here.
This doesn't do what you expect it to: it stringifies $object as a whole and then appends .EmailAddress verbatim; in order to embed an expression, such as accessing a property inside "..." (an expandable string), you need $(), the subexpression operator.
Write-Output "Email: $($object.EmailAddress)" >> C:\psoutput\psoutput.txt
See this answer for an overview of the syntax in PowerShell expandable strings.
Or, more simply, using PowerShell's implicit output behavior (use of Write-Output is rarely necessary):
"Email: $($object.EmailAddress)" >> C:\psoutput\psoutput.txt
>> C:\psoutput\psoutput.txt -Append
>> is effectively an alias for Out-File -Append (just like > is for just Out-File), so not only is there no need for -Append, it isn't interpreted by >>, which accepts only the filename operand.
Instead, -Append was interpreted by Write-Output, which is why it ended up literally in your output file.
Perhaps surprisingly, while a redirection such as >> C:\psoutput\psoutput.txt is typically placed last on the command line, that is not a syntactic requirement: other arguments may follow.
I am looking to have the output like the following..
It sounds like you want formatting as provided by the Format-List cmdlet:
$object | Format-List >> C:\psoutput\psoutput.txt
Note that > / >> / Out-File apply the default string formatting, i.e. the same representation that would by default display in the console.
By using an explicit Format-* cmdlet, you can control that formatting, but note two things about Out-File in general:
As you're outputting for-display formats, the resulting file may not be suitable for further programmatic processing.
To prevent truncation of values, you may have to pass a -Width argument to Out-File, control the enumeration length of nested properties with $FormatEnumerationLimit, and, in the case of Format-Table, specify -AutoSize.
You don't really need to use Write-Output at all. Try this to just get your string to your file:
("Email: " + $object.EmailAddress) >> C:\psoutput\psoutput.txt
You don't need to specify append because '>>' already does that for you
I'm trying to write a small powershell script that does a few things
1) Parses Inbox items in my outlook
2) Searches for a RegEx string
3) Dumps the line that matches the RegEx string into a CSV
I can't get #3 to work. It definitely runs for about 10 minutes, but the resulting csv is empty.
Here's a snippet of what I want it to look for:
Account Name: Jbond
I tried just slapping a "Write-Host $variable" in various parts to see what was happening, but all I get is "System.____comobject". I can't find a solution online to just convert this into plain text.
Add-Type -Assembly "Microsoft.Office.Interop.Outlook"
$Outlook = New-Object -ComObject Outlook.Application
$namespace = $Outlook.GetNameSpace("MAPI")
$inbox = $namespace.GetDefaultFolder([Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderInbox)
$RE = [RegEx]'(?sm)Account Name\s*:\s*(?<AccName>.*?)$.*'
$Data = ForEach ($item in $inbox.items){
$resultText = $item.item.Value
Write-Host $resultText
if ($item.from -like "email#email.org"){
if ($item.body -match $RE){
[PSCustomObject]#{
AccName = $Matches.AccName
}
}
}
}
$Data
$Data | Export-CSv '.\data.csv' -NoTypeInformation
tl;dr:
Use:
$variable | Out-Host # or: Out-Host InputObject $variable
rather than
Write-Host $variable
to get meaningful output formatting.
Background information and debugging tips below.
Try a combination of the following approaches:
Use interactive debugging:
Use GUI editor Visual Studio Code with the PowerShell extension to place breakpoints in your code and inspect variable values interactively. (In Windows PowerShell you can also use the ISE, but it is obsolescent.)
Less conveniently, use the *-PSBreakpoint cmdlets to manage breakpoints that are hit when you run your script in a console (terminal) window. A simple alternative is to add Wait-Debugger statements to your script, which, when hit, break unconditionally.
Produce helpful debugging output:
Generally, use Write-Debug rather than Write-Host, which has two advantages:
You can leave Write-Debug calls in your code for on-demand debugging at any time:
They are silent by default, and only produce output on an opt-in basis, via (temporarily) setting $DebugPreference = 'Continue' beforehand or passing the -Debug switch (if your script / function is an advanced one, though note that in Windows PowerShell this will present a prompt whenever a Write-Debug call is hit).
You do, however, pay a performance penalty for leaving Write-Debug calls in your code.
Debug output is clearly marked as such, colored and prefixed with DEBUG:.
The problem you experienced with Write-Host is that all Write-* cmdlets perform simple .ToString() stringification of their arguments, which often results in unhelpful representations, such as System.____comobject in your case.
To get the same rich output formatting you would get in the console, use the following technique, which uses Out-String as a helper command:
$variable | Out-String | Write-Debug
If you want to control the view (list vs. table vs. wide vs. custom) explicitly, insert a Format-* call; e.g.:
$variable | Format-List | Out-String | Write-Debug
It is generally only the standard Out-* cmdlets that use PowerShell's output formatting system.
A quick-and-dirty alternative to Write-Debug is to use Out-Host rather than Write-Host - e.g., for quick insertion of debugging commands that you'll remove later; Out-Host itself performs the usual output formatting, which simplifies matters:
# Default for-display formatting
$variable | Out-Host # or: Out-Host -InputObject $variable
# Explicit formatting
$variable | Format-List | Out-Host
Caveat: Aside from formatting, another important difference between Write-Host and Out-Host is that in PSv5+ only Write-Host writes to the host via the information stream (stream number 6), whereas Out-Host truly writes directly to the host, which means that its output cannot be captured with redirections such as 6> or *> - see about_Redirection.
So my challenge today.
I have a config file (really just a txt document) that stores variables to store information passed between scripts or to be used after restarts.
I am looking for a more efficient way to read and update the file. Currently I read the file with:
Get-Content $current\Install.cfg | ForEach-Object {
Set-Variable -Name line -Value $_
$a, $b = $line.Split('=')
Set-Variable -name $a -Value $b
}
But to overwrite the contents, I recreate the file with:
ECHO OSV=$OSV >>"$ConfigLoc\tool.cfg"
ECHO OSb=$OSb >>"$ConfigLoc\tool.cfg"
ECHO cNum=$cNum >>"$ConfigLoc\tool.cfg"
ECHO cCode=$cCode >>"$ConfigLoc\tool.cfg"
ECHO Comp=$Comp >>"$ConfigLoc\tool.cfg"
Each time I have added a new saved variable, I have just hardcoded the new variable into both the original config file and the config updater.
As my next updates require an additional 30 variables to my current 15. I would like something like:
Get-Content $current\Install.cfg | ForEach-Object {
Set-Variable -Name line -Value $_
$a, $b = $line.Split('=')
ECHO $a=$$a
}
Where $$a uses the variable $a in the loop as the variable name to load the value.
Best example i can show to clarify is:
ECHO $a=$$a (in current loop)
Echo OSV=$OSV (actually appears in code as)
Not sure how to clarify this anymore, or how to achieve it with the variable title also being a variable.
If you want to create a file that has name=value parameters, here's an alternate suggestion. This is a snippet of a real script I use every day. You might modify it so it reads your .csv input and uses it instead of the hard coded values.
$Sites = ("RawSiteName|RoleName|DevUrl|SiteID|HttpPort|HttpsPort", `
"SiteName|Name of role|foo.com|1|80|443" `
) | ConvertFrom-CSV -Delimiter "|"
$site = $sites[0]
Write-Host "RawSiteName =$($site.RawSiteName)"
You might be able to use something similar to $text = Get-Content MyParameters.csv and pipe that to the ConvertFrom-CSV cmdlet. I realize it's not a direct answer to what you are doing but it will let you programmatically create a file to pass across scripts.
Thanks for the help everyone. This is the solution I am going with. Importing and exporting couldn't be simpler. If I have to manually update the XML install default I can with ease which is also amazing. I also love the fact that even if you import as $Test you can still use $original to access variables. I will be creating multiple hashtables to organize the different data I will be using going forward and just import/export it in a $config variable as the master.
$original = #{
OSV='0'
OSb='0'
cNum='00000'
cCode='0000'
Client='Unknown'
Comp='Unknown'
}
$original | Export-Clixml $Home\Desktop\sample.cfg
$Test = Import-Clixml $Home\Desktop\sample.cfg
Write $Test
Write $original.Client
In essence, you're looking for variable indirection: accessing a variable indirectly, via its name stored in another variable.
In PowerShell, Get-Variable allows you to do that, as demonstrated in the following:
# Sample variables.
$foo='fooVal'
$bar='barVal'
# List of variables to append to the config file -
# the *names* of the variables above.
$varsToAdd =
'foo',
'bar'
# Loop over the variable names and use string expansion to create <name>=<value> lines.
# Note how Get-Variable is used to retrieve each variable's value via its *name*.
$(foreach ($varName in $varsToAdd) {
"$varName=$(Get-Variable $varName -ValueOnly)"
}) >> "$ConfigLoc/tool.cfg"
With the above, the following lines are appended to the output *.cfg file:
foo=fooVal
bar=barVal
Note that you can read such a file more easily with the ConvertFrom-StringData, which outputs a hashtable with the name-value pairs from the file:
$htSettings = Get-Content -Raw "$ConfigLoc/tool.cfg" | ConvertFrom-StringData
Accessing $htSettings.foo would then return fooVal, for instance.
With a hashtable as the settings container, updating the config file becomes easier, as you can simply recreate the file with all settings and their current values:
$htSettings.GetEnumerator() |
ForEach-Object { "$($_.Key)=$($_.Value)" } > "$ConfigLoc/tool.cfg"
Note: PowerShell by default doesn't enumerate the entries of a hashtable in the pipeline, which is why .GetEnumerator() is needed.
Generally, though, this kind of manual serialization is fraught, as others have pointed out, and there are more robust - though typically less friendly - alternatives.
With your string- and line-based serialization approach, there are two things to watch out for:
All values are saved as a strings, so you have to manually reconvert to the desired data type, if necessary - and even possible, given that not all objects provide meaningful string representations.
Generally, the most robust serialization format is Export-CliXml, but note that it is not a friendly format - be careful with manual edits.
ConvertFrom-StringData will fail with duplicate names in the config file, which means you have to manually ensure that you create no duplicate entries when you append to the file - if you use the above approach of recreating the file from a hashtable every time, however, you're safe.
Currently my log function spits out the information in a single column and is hard to read. Is there a way to make it split up into different columns which each (DisplayName, PoolName, PoolSnapshot, and DesktopSVIVmSnapshot) and its respective information is put correctly?
function log ([string]$entry) {
Write-Output $entry | Out-File -Append "C:\logs\SNAPSHOT.csv"
}
Add-PSSnapin Quest.ActiveRoles.ADManagement
$date = Get-Date -Format "MM-dd-yyyy"
$time = Get-Date -Format "hh:mm:sstt"
# begin log
log $(Get-Date)
log "The below Desktops are not using the correct Snapshot."
if (#($DesktopExceptions).Count -lt 1) {
Write-Output "All desktops in $pool are currently using the correct snapshots." |
Out-File -Append "C:\logs\SNAPSHOT.csv"
} else {
Write-Output $DesktopExceptions |
Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot |
sort DisplayName |
Out-File -Append "C:\logs\SNAPSHOT.csv"
}
log $(Get-Date)
09/11/2017 12:16:17
DisplayName PoolName PoolSnapshot DesktopSVIVmSnapshot
----------- -------- ------------ --------------------
xxxc-13v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-15v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-1v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
xxxc-20v xxxc-xxx /8-11-2017/09-07-2017 /8-11-2017
Note: I removed parts of the log for in the hopes to not make the post long.
CSV files require uniform lines: a header line with column names, followed by data lines containing column values.
By writing the output from Get-Date first - a single date/time string - followed by another single-string output, followed by multi-column output from your $DesktopExceptions | Select-Object ... call, you're by definition not creating a valid CSV file.
If you still want to create such a file:
log (Get-Date) # With a single command, you don't need $(...) - (...) will do.
log "The below Desktops are not using the correct Snapshot."
If ($DesktopExceptions) # a non-empty array / non-$null object
{
log ($DesktopExceptions |
Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot |
Sort-Object DisplayName |
ConvertTo-Csv -NoTypeInformation)
}
Else
{
log "All desktops in $pool are currently using the correct snapshots."
}
log (Get-Date)
By defining your log() function's parameter as type [string], you're effectively forcing stringification of whatever object you pass to it. This stringification is the same you get when you embed a variable reference or command inside "..." (string expansion / interpolation) - but it is not the same as what you get by default, when you print to the console.
Out-File, by contrast, does result in the same output you get when printing to the console, which, however, is a format for human consumption, not for machine parsing (as CSV is, for instance).
To get CSV-formatted output, you must either use Export-Csv - to write directly to a file - or ConvertTo-Csv- to get a string representation.
Also note that there's typically no reason to use Write-Output explicitly - any command / expression's output that is not explicitly assigned to a variable / redirected (to a file or $null) is implicitly sent to PowerShell's [success] output stream; e.g., Write-Output Get-Date is the same as just Get-Date.
It looks like you're just writing an object, and taking the default PowerShell formatter behavior.
A better thing to do is make your log only responsible for one thing - writing messages to a file (no formatting). Here's an example of what you might try:
function Write-LogMessage {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, HelpMessage = "The text-content to write to the log file.",
ValueFromPipeline = $true)]
[string]$Text
)
Process {
Write-Host -ForegroundColor Green $Text
}
}
Set-Alias log Write-LogMessage
Note: This example writes directly to the PowerShell console, but you would in practice need to direct output to a file (using Out-File or one of the redirection operators - see Get-Help about_Operators).
To use it, you would write something like this:
"This is a message that would be written" | Write-LogMessage
For your specific example, you could just format the message inline, and pipe it:
Write-Output $DesktopExceptions | Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot | sort DisplayName | ForEach-Object { "{0}: Host = {1}, Pool = {2}, Pool SN = {3}, SVIV Snapshot = {4}" -f (Get-Date), $_.DisplayName, $_.PoolName, $_.PoolSnapshot, $_.DesktopSVIVmSnapshot } | log
Note that you don't need the log statement: just add formatting before piping to the Out-File cmdlet, and you'll get what you're after.
Edit: The OP asked in the original post how to format columns (tabular output). To achieve this, you can use either the ConvertTo-Csv or Export-Csv cmdlets (generally, you would use the -NoTypeInformation switch parameter with these commands, to avoid the first line of the output being a type definition). An example of this is:
$DesktopExceptions | Select-Object DisplayName,PoolName,PoolSnapshot,DesktopSVIVmSnapshot | sort DisplayName | Export-Csv C:\Temp\Datum.csv -NoTypeInformation
As pointed out in another answer, using Write-Output is not required, because PowerShell automatically writes all output to the output stream unless otherwise directed (using file redirection, a redirection operator, or the Out-Null cmdlet).
Please read my answer as part solution and part advice.
The "problem" with PowerShell is that it doesn't capture only the output of your code. It will capture output from other scripts, modules and executables. In other words, any attempt to make logging behave like it's generated by e.g. C# with NLOG, has an inherent problem.
I looked into this subject myself for a complex continuous delivery pipeline I'm building. I understood that a structured log will not be 100% possible and therefore I accepted the purpose of PowerShell transcription (Start-Transcript). But still I wanted to avoid creating functions like Write-Log and if possible provide an enhanced output for all code that uses Write-Debug, Write-Verbose functionality.
I ended up creating XWrite PowerShell module which works very well, even to my own suprize. I use it because it enhances the produced trace message by the caller's name (cmdlet or script) and a timestamp. The caller's name helps a lot with troubleshooting and the timestamp I use to implicitly benchmark. here are a couple of example
DEBUG: Test-MyXWrite.ps1: Hello
DEBUG: Script: Test-MyXWrite.ps1: 20170804: 10:57:27.845: Hello
There are some limitations though. Any binary's code trace output will not be enhanced. Also if a cmllet refers explicitly to the Write-* using their full namespace it will not work. To capture line by line all trace and output requires some very deep into the .net types of PowerShell implementation hooking. There is a guy who has done this, but I don't want to get influence the PowerShell process's behavior that aggresively. And at this moment I believe that to be the role of the transcription.
If you like the idea, install the module from XWrite
At some point, I would like to extend the module with a redirection to telemetry services, but I've still not decided I want to do that, because I will not capture the above mentioned exceptions and other executable. It will just offer me visible progress as the script is executing.