Environment:
Windows Server 2016
Windows 10 Pro
PowerShell 5.1
$myVariable is empty, I think and I'm expecting there to be a string value.
Invoke-Command -ComputerName WKSP000D1E3F -Credential $creds -ScriptBlock {
sqlcmd -E -Q "select top 1 FirstName from customers" -d database1 -S "(localdb)\ProjectsV13" | Tee-Object -Variable myVariable
}
Write-Host $myVariable
Cpt.Whale has provided the crucial pointer in a comment: you fundamentally cannot set local variables from a script block being executed remotely (via Invoke-Command -ComputerName) - you must use output from the script block to communicate data back to the caller.
While you could apply Tee-Object locally instead (Invoke-Command ... | Tee-Object), there's a simpler solution, which works with all cmdlets, including cmdlet-like (advanced) functions and scripts:
Use the common -OutVariable (-ov) parameter to capture a cmdlet's output in a self-chosen variable while passing that output through:
# Note the `-OutVariable myVariable` part
# and that the variable name must be specified *without* a leading "$"
# Output is still being passed through.
Invoke-Command -OutVariable myVariable -ComputerName WKSP000D1E3F -Credential $creds -ScriptBlock {
sqlcmd -E -Q "select top 1 FirstName from customers" -d database1 -S "(localdb)\ProjectsV13"
}
# $myVariable now contains the captured content.
By contrast, if you want to capture output only, without also passing it through (to the display, by default), you can heed Santiago Squarzon's advice and simply assign the Invoke-Command call to your variable ($myVariable = Invoke-Command ...).
Notes re -OutVariable(-ov):
As shown above, and as shown with Tee-Object -Variable in your question, the name of the self-chosen target variable must be specified without a leading $, e.g. -OutVariable var, not Out-Variable $var; if you did the latter, the value of a preexisting $var variable (if defined) would be used as the variable name.
Unlike directly captured output, the target variable always receives array(-like) data, specifically, an instance of the System.Collections.ArrayList class - even if only one output object is present; e.g.:
# -> 'types: v1: String vs. v2: ArrayList'
$v1 = Write-Output -OutVariable v2 'one'
"types: v1: $($v1.GetType().Name) vs. v2: $($v2.GetType().Name)"
That is, while directly capturing output captures a single output object as-is, and multiple ones in a regular PowerShell array (of type [object[]], -OutVariable always creates an ArrayList - see GitHub issue #3154 for a discussion of this inconsistency.
With commands that do not support -OutVariable, namely simple scripts and functions as well as external programs:
To pass the output through in streaming fashion, i.e. as it becomes available, pipe to Tee-Object -Variable; e.g.:
# Passes output through as it is being emitted.
some.exe | Tee-Object -Variable myVariable
Otherwise - i.e. if it is acceptable to collect all output first, before passing it through - simply enclose an assignment statement in (...) to pass its value through - this approach performs better than Tee-Object -Variable; e.g.:
# Collects all output first, then passes it through.
($myVariable = some.exe)
Related
I want to run a powershell script and save/redirect the result to another file.
My script is:
# Define time for report (default is 10 day)
$startDate = (get-date).AddDays(-10)
# Store successful logon events from security logs with the specified dates and workstation/IP in an array
$slogonevents = Get-Eventlog -LogName Security -after $startDate | where {$_.eventID -eq 4624 }
# Crawl through events; print all logon history with type, date/time, status, account name, computer and IP address if user logged on remotely
foreach ($e in $slogonevents){
# Logon Successful Events
# Local (Logon Type 2)
if (($e.EventID -eq 4624 ) -and ($e.ReplacementStrings[8] -eq 2)){
write-host "Type: Local Logon`tDate: "$e.TimeGenerated "`tStatus: Success`tUser: "$e.ReplacementStrings[5] "`tWorkstation: "$e.ReplacementStrings[11]
}
# Remote (Logon Type 10)
if (($e.EventID -eq 4624 ) -and ($e.ReplacementStrings[8] -eq 10)){
write-host "Type: Remote Logon`tDate: "$e.TimeGenerated "`tStatus: Success`tUser: "$e.ReplacementStrings[5] "`tWorkstation: "$e.ReplacementStrings[11] "`tIP Address: "$e.ReplacementStrings[18]
}
} >> D:\test.txt
but I get errors like that
>> : The term '>>' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and
try again.
At D:\Cyber_security\Python\Untitled1.ps1:26 char:3
+ } >> D:\test.txt
+ ~~
+ CategoryInfo : ObjectNotFound: (>>:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
why this is happening?
To address an incidental problem up front: even if you fix the redirection problem (see below), your foreach loop won't produce success-stream output, resulting in an empty file. You're using Write-Host, which is is typically the wrong tool to use, unless the intent is to write to the display only (though in PowerShell 5 and above you can capture Write-host output if you redirect it to the success output stream, e.g. with *>&1). Instead, use Write-Output (e.g. Write-Output "foo") or, preferably, implicit output (just "foo"). See also: the bottom section of this answer.
foreach is a language statement, and as such you cannot directly apply a redirection (> or >>) to it - see bottom section for an explantion.
You need to wrap it in a (by definition pipeline-based) command or expression first, for which there are two options:
Streaming option (preferred): Wrap the statement in a script block ({ ... }) and call it via &, the call operator (or, if you want the statement to run directly in the caller's scope as opposed to a child scope, as created by &, use ., the dot-sourcing operator)
& { foreach ($i in 1..2) { $i } } > test.txt
Collect-all-output-first option: Use $(...), the subexpression operator:
$(foreach ($i in 1..2) { $i }) > test.txt
Alternatively, use the ForEach-Object cmdlet, which is a command (as all named units of execution are in PowerShell), which also results in streaming processing (perhaps confusingly, a built-in alias for ForEach-Object is also named foreach, with the syntactical context deciding whether the cmdlet or the language statement is being referenced):
1..2 | ForEach-Object { $_ } > test.txt
As for what you tried:
The > (>>) operator is, in effect, an alias of the Out-File cmdlet (Out-File -Append), and therefor requires a pipeline to function.
However, language statements cannot directly be used in a pipeline, and by themselves are always self-contained statements, meaning that whatever comes after isn't considered part of the same statement.
This becomes more obvious when you replace >> with Out-File -Append:
# !! FAILS, because `| Out-File -Append test.txt` is considered
# !! a separate statement, resulting in the following error:
# !! "An empty pipe element is not allowed."
foreach ($i in 1..2) { $i } | Out-File -Append test.txt
The error message An empty pipe element is not allowed. implies that | was considered the start of a new statement.
The same happened with >>, albeit with the more obscure error message shown in your question, but you can easily reproduce it by executing >> test.txt in isolation.
Note: Unlike POSIX-compatible shells such as Bash, PowerShell does not allow you to place a redirection anywhere within a statement, and fails if it starts a statement; e.g., Get-Date >> test.txt' works fine and even Get-Date >>test.txt -Format g, but >> test.txt 'hi' does not.
Design musings:
Given that an expression can serve as the - first only - segment of a pipeline (e.g., 1..2 | Out-File -Append test.txt), it isn't obvious why a language statement cannot be used that way too.
The reason is a fundamental limitation in PowerShell's grammar:
A pipeline by itself is a statement,
but it cannot (directly) contain statements.
Hence the need to nest statements inside pipelines using the techniques shown above (& { ... } / $(..)).
Another unfortunate manifestation of this design is when you attempt to use language statements with && and ||, the PowerShell (Core) 7+ pipeline-chain operators:
Since exit and throw are language statements too, the following idiom - which would work in POSIX-combatible shells - does not work:
# Exit the script with an exit code of 1 if the Get-ChildItem call
# reports an error.
# !! FAILS, because `exit`, as a language statement, cannot be
# !! used directly in a pipeline.
Get-ChildItem NoSuchDir -ErrorAction SilentlyContinue || exit 1
Again, nesting of the statement is required, such as $(...):
# OK, due to $(...)
Get-ChildItem NoSuchDir -ErrorAction SilentlyContinue || $(exit 1)
Perhaps needless to say:
This requirement is obscure and easy to forget...
... and it is exacerbated by the fact that placing e.g. exit 1 after && or || does not cause a syntax (parse) error and only fails at runtime, and only when the condition is met.
That is, you may not notice the problem until the LHS command actually reports an error.
Additionally, the error message you get when it does fail can be confusing: The term 'exit' is not recognized as a name of a cmdlet, function, script file, or executable program. This is because exit in this context is then interpreted as the name of a command (such as a function or external program) rather than as a language statement.
I was trying to understand how the constants $TRUE, $FALSE and $NULL work in Powershell, and how I should test for them or compare them with variables, respectively.
Being a Powershell newbie, I did some basic tests. While $TRUE and $FALSE behaved as expected, I was baffled by what I saw when I tried to assign another value to $NULL:
PS C:\Users\Administrator> $NULL=1
PS C:\Users\Administrator> $NULL="FOO"
PS C:\Users\Administrator> $NULL
PS C:\Users\Administrator>
$NULL should be constant or read-only, shouldn't it? So why can I assign another value to it without Powershell throwing an exception, and why is that assignment silently ignored?
In contrast, $FALSE behaves as expected:
PS C:\Users\Administrator> $FALSE=1
Cannot overwrite variable false because it is read-only or constant.
At line:1 char:1
+ $FALSE=1
+ ~~~~~~~~
+ CategoryInfo : WriteError: (false:String) [], SessionStateUnauthorizedAccessException
+ FullyQualifiedErrorId : VariableNotWritable
PS C:\Users\Administrator>
The primary reason that you can assign to $null - even though as the representation of a null value it should be constant (as other PowerShell automatic variables such as $true and $false are, for instance) - is that it enables the following useful idiom for output suppression:
# Discard the success output from a command.
$null = Get-Item -ErrorAction Stop foo.txt
That is, $null can act like a regular read-write variable that you can assign to, but whatever you assign to it (a command's success output, from stream number 1 - see about_Redirection) is quietly discarded.
Effectively, $null = ... is the equivalent of >NUL (1>NUL) in cmd.exe and >/dev/null (1>/dev/null) in POSIX-compatible shells such as bash.
Note that in PowerShell you could alternatively use ... | Out-Null or > $null, though the $null = ... idiom is faster than Out-Null[1] and also signals the intent to discard the (success) output up front (unlike > $null). (There's also [void] (...), but it requires you to enclose the command in parentheses.) See this answer for more.
However, you do need redirection if you also want to suppress other output streams (too); e.g.,
*> $null discards the output from all streams.
As for inspecting the properties of variables, including automatic ones, use the Get-Variable cmdlet and pass it the name of the variable without the $ sigil; e.g., null to inspect $null.
PS> Get-Variable null | Format-List
Value :
Description : References to the null variable always return the null value. Assignments have no effect.
Options : None
Name : null
Visibility : Public
Module :
ModuleName :
Attributes : {}
Format-List * ensures that that all properties of the variable object (a System.Management.Automation.PSVariable instance or an instance of a derived class) are listed, in list form.
A constant variable such as $false would show Constant as part of the Options property value.
[1] Note: PowerShell [Core] v6+ has an optimization that makes Out-Null the fastest solution if you discard an expression's value (e.g., 1..1e6 | Out-Null vs. a command's (e.g., Write-Output (1..1e6) | Out-Null), but note that suppressing command output is the much more common use case.
This works perfectly on Windows 10 using powershell but fails badly on Win Server 2012 R2.
I have tried many forms using redirect and -outfile but at best end-up with an empty file or broken script.
Can any one help me send the one liner below to a txt file on win server 2012 R2, I'm just not getting it
$((Get-WmiObject -Query "select Name, PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor") | foreach-object { write-host "$($_.Name) : $($_.PercentProcessorTime)"}) *>&1 > output.txt
Write-Host, as the name implies, writes to the host, which in a console window is that window's display (screen), bypassing PowerShell's (success) output stream, the latter being what the pipeline operator (|) and the redirection operator > operate on.
In PowerShell v5+ only, Write-Host writes to the information output stream (stream number 6 - see the about_Redirection help topic; by default, that output still goes to the host) and can therefore be redirected - either via 6> or via *> - so the catch-all redirection *>&1, which redirects all streams to the success output stream (1), can indeed be used to redirect Write-Host to the success output stream, but not in earlier PowerShell versions - and Windows Server 2012 R2 shipped with PowerShell version 4.
However, in your case there is no good reason to use Write-Host to begin with: either use Write-Output - the cmdlet whose purpose is to write to the success output stream (1) - or, preferably, use PowerShell's implicit output feature, where any output (return value) not captured in a variable, piped or redirected is implicitly written to the success output stream:
# Note how the use of "$($_.Name) : $($_.PercentProcessorTime)"
# *by itself* implicitly causes it to be *output* (written to the pipeline).
Get-WmiObject -Query "select Name, PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor" |
Foreach-Object { "$($_.Name) : $($_.PercentProcessorTime)" } > output.txt
Note the absence of $(...), the subexpression operator in the command, which isn't needed.
If the specific spacing between the columns isn't important, you can more simply write (since only 2 properties are being select, implicit Format-Table formatting is applied):
Get-WmiObject -Query "select Name, PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor" |
Select-Object Name, PercentProcessorTime > output.txt
Note: The CIM cmdlets (e.g., Get-CimInstance) superseded the WMI cmdlets (e.g., Get-WmiObject) in PowerShell v3 (released in September 2012). Therefore, the WMI cmdlets should be avoided, not least because PowerShell [Core] (version 6 and above), where all future effort will go, doesn't even have them anymore. For more information, see this answer.
Try this... note that it is appending to the file, so if you run it twice, you will have two runs' worth of data
$((Get-WmiObject -Query "select Name, PercentProcessorTime from Win32_PerfFormattedData_PerfOS_Processor") | foreach-object { "$($_.Name) : $($_.PercentProcessorTime)" >> output.txt})
EDIT
OP accepted this as the answer, but it's not necessarily the best solution. For a thorough explanation and another solution, I recommend a different answer submitted for this question.
I'm attempting to run a PowerShell script with the input being the results of another PowerShell cmdlet. Here's the cross-forest Exchange 2013 PowerShell command I can run successfully for one user by specifying the -Identity parameter:
.\Prepare-MoveRequest.ps1 -Identity "user#domain.com" -RemoteForestDomainController "dc.remotedomain.com" $Remote -UseLocalObject -OverwriteLocalObject -Verbose
I want to run this command for all MailUsers. Therefore, what I want to run is:
Get-MailUser | select windowsemailaddress | .\Prepare-MoveRequest.ps1 -RemoteForestDomainController "dc.remotedomain.com" $Remote -LocalForestDomainController "dc.localdomain.com" -UseLocalObject -OverwriteLocalObject -Verbose
Note that I removed the -Identity parameter because I was feeding it from each Get-MailUser's WindowsEmailAddress property value. However, this returns with a pipeline input error.
I also tried exporting the WindowsEmailAddress property values to a CSV, and then reading it as per the following site, but I also got a pipeline problem: http://technet.microsoft.com/en-us/library/ee861103(v=exchg.150).aspx
Import-Csv mailusers.csv | Prepare-MoveRequest.ps1 -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote
What is the best way to feed the windowsemailaddress field from each MailUser to my Prepare-MoveRequest.ps1 script?
EDIT: I may have just figured it out with the following foreach addition to my Import-Csv option above. I'm testing it now:
Import-Csv mailusers.csv | foreach { Prepare-MoveRequest.ps1 -Identity $_.windowsemailaddress -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote }
You should declare your custom function called Prepare-MoveRequest instead of simply making it a script. Then, dot-source the script that declares the function, and then call the function. To accept pipeline input into your function, you need to declare one or more parameters that use the appropriate parameter attributes, such as ValueFromPipeline or ValueFromPipelineByPropertyName. Here is the official MSDN documentation for parameter attributes.
For example, let's say I was developing a custom Stop-Process cmdlet. I want to stop a process based on the ProcessID (or PID) of a Windows process. Here is what the command would look like:
function Stop-CustomProcess {
# Specify the CmdletBinding() attribute for our
# custom advanced function.
[CmdletBinding()]
# Specify the PARAM block, and declare the parameter
# that accepts pipeline input
param (
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int] $Id
)
# You must specify the PROCESS block, because we want this
# code to execute FOR EACH process that is piped into the
# cmdlet. If we do not specify the PROCESS block, then the
# END block is used by default, which only would run once.
process {
Write-Verbose -Message ('Stopping process with PID: {0}' -f $ID);
# Stop the process here
}
}
# 1. Launch three (3) instances of notepad
1..3 | % { notepad; };
# 2. Call the Stop-CustomProcess cmdlet, using pipeline input
Get-Process notepad | Stop-CustomProcess -Verbose;
# 3. Do an actual clean-up
Get-Process notepad | Stop-Process;
Now that we've taken a look at an example of building the custom function ... once you've defined your custom function in your script file, dot-source it in your "main" script.
# Import the custom function into the current session
. $PSScriptRoot\Prepare-MoveRequest.ps1
# Call the function
Get-MailUser | Prepare-MoveRequest -RemoteForestDomainController dc.remotedomain.com $Remote -LocalForestDomainController dc.localdomain.com -UseLocalObject -OverwriteLocalObject -Verbose;
# Note: Since you've defined a parameter named `-WindowsEmailAddress` that uses the `ValueFromPipelineByPropertyName` attribute, the value of each object will be bound to the parameter, as it passes through the `PROCESS` block.
EDIT: I would like to point out that your edit to your post does not properly handle parameter binding in PowerShell. It may achieve the desired results, but it does not teach the correct method of binding parameters in PowerShell. You don't have to use the ForEach-Object to achieve your desired results. Read through my post, and I believe you will increase your understanding of parameter binding.
My foreach loop did the trick.
Import-Csv mailusers.csv | foreach { Prepare-MoveRequest.ps1 -Identity $_.windowsemailaddress -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote }
I want to be able to get the argument portion of the previous command. $^ seems to return just the command and not the args. Get-History -count 1 returns the last full command including the command and the args. I could just .Replace the first instance, but I am not sure if it is correct.
Scenario is that sometimes I want to do something like this. Let's assume that $* are the args to the last command:
dir \\share\files\myfile.exe
copy $* c:\windows\system32
Any ideas how to get the last args correctly?
UPDATE: finished my method for doing this.
function Get-LastArgs
{
$lastHistory = (Get-History -count 1)
$lastCommand = $lastHistory.CommandLine
$errors = [System.Management.Automation.PSParseError[]] #()
[System.Management.Automation.PsParser]::Tokenize($lastCommand, [ref] $errors) | ? {$_.type -eq "commandargument"} | select -last 1 -expand content
}
Now I can just do:
dir \\share\files\myfile.exe
copy (Get-LastArgs) c:\windows\system32
To reduce typing, I did
set-alias $* Get-LastArgs
so now I still have to do
copy ($*) c:\windows\system32
if anybody has any ideas for making this better please let me know.
For the last argument (not all!) in the interactive hosts like Console and ISE it is the automatic variable $$.
Help
man about_Automatic_Variables
gets
$$
Contains the last token in the last line received by the session.
Other hosts may or may not implement this feature (as well as the $^ variable).
There is no easy way to get the last args in this fashion without parsing the history item itself, and this is no trivial matter. The reason is that the "last arguments" may not be what you think they are after you take splatting, pipelines, nested subexpressions, named and unnammed arguments/parameters into the equasion. In powershell v2 there is a parser available for tokenizing commands and expressions, but I'm not sure you want to go that route.
ps> $psparser::Tokenize("dir foo", [ref]$null) | ? {
$_.type -eq "commandargument" } | select -last 1 -expand content
foo