I am currently writing my first script in Powershell and I am already facing the first problem.
I would like to read the value from a variable in a function so that I can use this variable in another cmd-let later. The problem now is that the variable is only recognized inside the function block and not outside.
How do I get this to work?
Thanks for the help :-)
function Write-Log([string]$logtext, [int]$level=0)
{
if($level -eq 0)
{
$logtext = "[INFO] " + $logtext
$text = "["+$logdate+"] - " + $logtext
Write-Host $text
}
}
Send-MailMessage -To "<xxx#xxx.de>" -Subject "$text" -Body "The GPO backup creation was completed with the following status: `n $text" -SmtpServer "xxx#xxx.de" -From "xxx#xxx.de"
I would like to submit $text
This has to do with variable scoping behavior in PowerShell.
By default, all variables in the caller's scope is visible inside the function. So we can do:
function Print-X
{
Write-Host $X
}
$X = 123
Print-X # prints 123
$X = 456
Print-X # prints 456
So far, so good. But when we start writing to variables outside the function itself, PowerShell transparently creates a new variable inside the function's own scope:
function Print-X2
{
Write-Host $X # will resolve the value of `$X` from outside the function
$X = 999 # This creates a new `$X`, different from the one outside
Write-Host $X # will resolve the value of the new `$X` that new exists inside the function
}
$X = 123
Print-X2 # Prints 123, and 999
Write-Host $X # But the value of `$X` outside is still 123, unchanged
So, what to do? You could use a scope modifier to write to the variable outside the function, but the real solution here is to return the value from the function instead:
function Write-Log([string]$logtext, [int]$level=0, [switch]$PassThru = $true)
{
if($level -eq 0)
{
$logtext = "[INFO] " + $logtext
$text = "["+$logdate+"] - " + $logtext
Write-Host $text
if($PassThru){
return $text
}
}
}
$logLine = Write-Log "Some log message" -PassThru
Send-MailMessage -Subject $logLine ...
if you need to access a variable outside a function in Powershell you might need to use the global variable.
$global:myglobalvariable="This is a PowerShell global variable"
or if its a null
$global:myglobalvariable2 = $null
Related
I have a PowerShell script (which I cannot change) with the following function inside it:
function Foo ([string] param1) {
[...]
$var1 = Read-Host "Test"
$var2 = Read-Host "Test2"
[...]
}
I want to call the function from my PowerShell script and want to prevent that the user has to input any values, instead I want to prepare hardcoded values.
I tried the following:
#("Var1Value", "Var2Value") | Foo "Param1Value"
But it still prompts the user. Any ideas?
During command discovery, functions take precedence over binary cmdlets, so you can "hide" Read-Host behind a fake Read-Host function:
# define local Read-Host function
function Read-Host {
param([string]$Prompt)
return #{Test = 'Var1Value'; Test2 = 'Var2Value'}[$Prompt]
}
# call foo
foo
# remove fake `Read-Host` function again
Remove-Item function:\Read-Host -Force
I have bellow script
$ErrorActionPreference = "Stop";
while($true) {
try {
Write-Host "Step 1";
Dir C:\arts #Error
Write-Host "Step 2";
exit 0
break;
}
catch {
"Error in " + $_.InvocationInfo.ScriptName + " at line: " + $_.InvocationInfo.ScriptLineNumber + ", offset: " + $_.InvocationInfo.OffsetInLine + ".";
$Error
exit 1
break;
}
}
It stops on Dir C:\arts line and that is good for me. As I understood it happens cos I have line $ErrorActionPreference = "Stop"; at the beginning.
I also have some docker params
Param(
[Parameter(Mandatory=$True,ParameterSetName="Compose")]
[switch]$Compose,
[Parameter(Mandatory=$True,ParameterSetName="ComposeForDebug")]
[switch]$ComposeForDebug,
[Parameter(Mandatory=$True,ParameterSetName="StartDebugging")]
[switch]$StartDebugging,
[Parameter(Mandatory=$True,ParameterSetName="Build")]
[switch]$Build,
[Parameter(Mandatory=$True,ParameterSetName="Clean")]
[switch]$Clean,
[parameter(ParameterSetName="Compose")]
[Parameter(ParameterSetName="ComposeForDebug")]
[parameter(ParameterSetName="Build")]
[parameter(ParameterSetName="Clean")]
[ValidateNotNullOrEmpty()]
[String]$Environment = "Debug"
)
If I put $ErrorActionPreference = "Stop" line before docker params I will have error Cannot convert value "System.String" to type "System.Management.Automation.SwitchParameter". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
In case if I put $ErrorActionPreference = "Stop"; line after docker params, script is continued to run and that is not that I want.
I do not know what I need to do here, so I will be grateful for any help
$ErrorActionPreference doesn't work with command line utilities like docker as they don't throw exceptions in PowerShell. You would have to use returncode/errorlevel or parse the output to handle those type of errors. Useful automatic variables:
$?
Contains the execution status of the last operation. It contains
TRUE if the last operation succeeded and FALSE if it failed.
$LastExitCode
Contains the exit code of the last Windows-based program that was run. Same as %errorlevel% in cmd.
If you detect an error, you can throw an exception to stop the script or use something like exit to stop the script. Example:
function Test-Error {
$ErrorActionPreference = "Stop"
Write-Host Before
ping -n 1 123.123.123.123
#If last command was not successfull.
#You can also have checked $lastexitcode, output etc.
if($? -eq $false) {
#Throw terminating error
#throw "Error"
#Or since we've chosen to stop on non-terminating errors, we could use:
Write-Error -ErrorId $LASTEXITCODE -Message "Ping failed"
}
Write-Host After
}
Test-Error
Output:
Before
Pinging 123.123.123.123 with 32 bytes of data:
Request timed out.
Ping statistics for 123.123.123.123:
Packets: Sent = 1, Received = 0, Lost = 1 (100% loss),
Test-Error : Ping failed
At line:22 char:1
+ Test-Error
+ ~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : 1,Test-Error
If you're creating a advanced function, you could set the default ErrorAction for the scope of the cmdlet like this:
function Test-Error {
[CmdLetBinding()]
param(
$Name = "World"
)
#If -ErrorAction is not specified by the user, use Stop for the scope of the function
if(-not $MyInvocation.BoundParameters.ContainsKey("ErrorAction")) { $ErrorActionPreference = "Stop" }
"Hello $Name ! My ErrorAction is: $ErrorActionPreference"
}
PS > $ErrorActionPreference
Continue
PS > Test-Error -ErrorAction Ignore
Hello World ! My ErrorAction is: Ignore
PS > Test-Error
Hello World ! My ErrorAction is: Stop
I have a Powershell script that (as one of its options) reads a user-defined pre-execution command from a file and needs to execute it.
The user-defined pre-execution command is expected to be an ordinary DOS-style command.
I can split the command by spaces and then feed it to the PowerShell "&" to get it executed:
$preExecutionCommand = "dir D:\Test"
$preExecutionArgs = $preExecutionCommand -split '\s+'
$preExecutionCmd = $preExecutionArgs[0]
$preExecutionNumArgs = $preExecutionArgs.Length - 1
if ($preExecutionNumArgs -gt 0) {
$preExecutionArgs = $preExecutionArgs[1..$preExecutionNumArgs]
& $preExecutionCmd $preExecutionArgs
} else {
& $preExecutionCmd
}
But if the user-defined command string has spaces that need to go in the arguments, or the path to the command has spaces, then I need to be much smarter at parsing the user-defined string.
To the naked eye it is obvious that the following string has a command at the front followed by 2 parameters:
"C:\Program Files\Tool\program1" 25 "the quick brown fox"
Has anyone already got a function that will parse strings like this and give back an array or list of the DOS-style command and each of the parameters?
There is a very simple solution for that. You can misuse the Powershell paramater parsing mechanism for it:
> $paramString = '1 blah "bluh" "ding dong" """foo"""'
> $paramArray = iex "echo $paramString"
> $paramArray
1
blah
bluh
ding dong
"foo"
In the end I am using CommandLineToArgvW() to parse the command line.
With this I can pass double quotes literally into parameters when needed, as well as have spaces in double-quoted parameters. e.g.:
dir "abc def" 23 """z"""
becomes a directory command with 3 parameters:
abc def
23
"z"
The code is:
function Split-CommandLine
{
<#
.Synopsis
Parse command-line arguments using Win32 API CommandLineToArgvW function.
.Link
https://github.com/beatcracker/Powershell-Misc/blob/master/Split-CommandLine.ps1
http://edgylogic.com/blog/powershell-and-external-commands-done-right/
.Description
This is the Cmdlet version of the code from the article http://edgylogic.com/blog/powershell-and-external-commands-done-right.
It can parse command-line arguments using Win32 API function CommandLineToArgvW .
.Parameter CommandLine
A string representing the command-line to parse. If not specified, the command-line of the current PowerShell host is used.
#>
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$CommandLine
)
Begin
{
$Kernel32Definition = #'
[DllImport("kernel32")]
public static extern IntPtr LocalFree(IntPtr hMem);
'#
$Kernel32 = Add-Type -MemberDefinition $Kernel32Definition -Name 'Kernel32' -Namespace 'Win32' -PassThru
$Shell32Definition = #'
[DllImport("shell32.dll", SetLastError = true)]
public static extern IntPtr CommandLineToArgvW(
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
out int pNumArgs);
'#
$Shell32 = Add-Type -MemberDefinition $Shell32Definition -Name 'Shell32' -Namespace 'Win32' -PassThru
}
Process
{
$ParsedArgCount = 0
$ParsedArgsPtr = $Shell32::CommandLineToArgvW($CommandLine, [ref]$ParsedArgCount)
Try
{
$ParsedArgs = #();
0..$ParsedArgCount | ForEach-Object {
$ParsedArgs += [System.Runtime.InteropServices.Marshal]::PtrToStringUni(
[System.Runtime.InteropServices.Marshal]::ReadIntPtr($ParsedArgsPtr, $_ * [IntPtr]::Size)
)
}
}
Finally
{
$Kernel32::LocalFree($ParsedArgsPtr) | Out-Null
}
$ret = #()
# -lt to skip the last item, which is a NULL ptr
for ($i = 0; $i -lt $ParsedArgCount; $i += 1) {
$ret += $ParsedArgs[$i]
}
return $ret
}
}
$executionCommand = Get-Content .\commands.txt
$executionArgs = Split-CommandLine $executionCommand
$executionCmd = $executionArgs[0]
$executionNumArgs = $executionArgs.Length - 1
if ($executionNumArgs -gt 0) {
$executionArgs = $executionArgs[1..$executionNumArgs]
echo $executionCmd $executionArgs
& $executionCmd $executionArgs
} else {
echo $executionCmd
& $executionCmd
}
function ParseCommandLine($commandLine)
{
return Invoke-Expression ".{`$args} $commandLine"
}
I have put together the following that seems to do what you require.
$parsercode = #"
using System;
using System.Linq;
using System.Collections.Generic;
public static class CommandLineParser
{
public static List<String> Parse(string commandLine)
{
var result = commandLine.Split('"')
.Select((element, index) => index % 2 == 0 // If even index
? element.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) // Split the item
: new string[] { String.Format("\"{0}\"", element) }) // Keep the entire item
.SelectMany(element => element).ToList();
return result;
}
}
"#
Add-Type -TypeDefinition $parsercode -Language CSharp
$commands = Get-Content .\commands.txt
$commands | % {
$tokens = [CommandLineParser]::Parse($_)
$command = $tokens[0]
$arguments = $tokens[1..($tokens.Count-1)]
echo ("Command:{0}, ArgCount:{1}, Arguments:{2}" -f $command, $arguments.Count, ([string]::Join(" ", $arguments)))
Start-Process -FilePath ($command) -ArgumentList $arguments
}
I have used some c# code posted by #Cédric Bignon in 2013 which shows a very nice C# Linq solution to your parser problem to create a parser method in [CommandLineParser]::Parse. That is then used to parse the command and Arguments to send to Start-Process.
Try it out and see if it does what you want.
I'm trying to write a custom prompt for PowerShell and I was wondering how I would filter out the 1...n directories in the output of Get-Location.
function prompt {
"PS " + $(get-location) + "> "
}
So, if the path is too long I would like to omit some of the directories and just display PS...blah\blah> or something. I tried (get-container) - 1 but it doesn't work.
Use Split-Path with the -Leaf parameter if you want just the last element of a path:
function prompt {
"PS {0}> " -f (Split-Path -Leaf (Get-Location))
}
I wanted to make a more dynamic function. I do just basic string manipulation. You could do some logic nesting Split-Path but the string manipulation approach is just so much more terse. Since what you want to be returned wont be a fully validated path I feel better offering this solution.
Function Get-PartialPath($path, $depth){
If(Test-Path $path){
"PS {0}>" -f (($path -split "\\")[-$depth..-1] -join "\")
} else {
Write-Warning "$path is not a valid path"
}
}
Sample Function call
Get-PartialPath C:\temp\folder1\sfg 2
PS folder1\sfg>
So you can use this simple function. Pass is a string for the path. Assuming it is valid then it will carve up the path into as many trailing chunks as you want. We use -join to rebuild it. If you give a $depth number that is too high the whole path will be returned. So if you only wanted to have 3 folders being shown setting the $depth for 3.
Ansgar Wiechers' answer will give you the last directory but if you want a way to do multiple directories at the end of the filepath (using the triple dot notation) you can cast the directory path to a uri and then just get and join the segments:
function prompt {
$curPath = pwd
$pathUri = ([uri] $curPath.ToString())
if ($pathUri.Segments.Count -le 3) {
"PS {0}>" -f $curPath
} else {
"PS...{0}\{1}>" -f $pathUri.Segments[-2..-1].trim("/") -join ""
}
}
Or using just a string (no uri cast)
function prompt {
$curPath = pwd
$pathString = $curPath.Tostring().split('\') #Changed; no reason for escaping
if ($pathString.Count -le 3) {
"PS {0}>" -f $curPath
} else {
"PS...{0}\{1}>" -f $pathString[-2..-1] -join ""
}
}
$a = prompt
Write-Host $a
Then just change -2 to whatever you want to be the first directory and -le 3 to match. I typically use the uri cast when I have to run stuff through a browser or over connections to Linux machines (as it uses "/" as a path separator) but there is no reason to not use the string method for normal operations.
I've got a CSV-file from HR with aprox 1000 lines (employees) that I feed to AD with Powershell.
This works, but I am a bit uncertain if I am doing this the right way.
This are my major concerns:
I am setting the attributes one at a time. Should I put the "changes" into an some kind of array/hasthable/object and do it all at once at the end of the script? But How? "New-Object"?
Should I use functions? But how can I return values (and continue based on the result from the function)?
All programming hints, corrections would be GREATLY appreciated. I really understand this wonderful community of knowledgable people so, let me have it. If you have the time please tell me how I can do this better..
This is my code:
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -ErrorAction silentlycontinue
Add-PSSnapin quest.activeroles.admanagement -ErrorAction silentlycontinue
$file = "\Scripts\employees.csv" # Location of the input file
$file2 = "\Scripts\employees2.csv" # Temp file
$logfile = "\Scripts\logfile.txt" # log file
remove-item $logfile -Force -ErrorAction SilentlyContinue
Get-Content $file | Out-File -Encoding UTF8 $file2 # Convert to UTF8 (we don't touch the original inputfile)
$ListEmployees = Import-Csv $file2 -Delimiter ";" # Import the file to CSV
foreach ($ListEmployee in $ListEmployees) {
$ListDisplayName = $ListEmployee.firstname + " " + $ListEmployee.lastname
if($ADemployee = Get-QADUser -displayname $ListDisplayName -IncludedProperties employeeid )
{
## CHECK NAME
if($($ADEmployee.displayname) -eq $($ListDisplayName))
{
echo "MATCH: $($ADEmployee.displayname)"
}
## CHECK COMPANY
if($($ADEmployee.company) -ne $($ListEmployee.company))
{
echo " CHANGE - Company: '$($ADEmployee.company)' to '$($ListEmployee.company)'"
Set-QADUser -identity $($ADEmployee.samaccountname) -Company $($ListEmployee.company) -WhatIf
}
else
{
echo " OK - Company : no change '$($ListEmployee.company)'"
}
## CHECK OFFICE
if($($ADEmployee.office) -ne $($ListEmployee.office))
{
echo " CHANGE - Office '$($ADEmployee.office)' to '$($ListEmployee.office)'"
Set-QADUser -identity $($ADEmployee.samaccountname) -Office $($ListEmployee.Office) -WhatIf
}
else
{
echo " OK - Office : no change '$($ListEmployee.office)'"
}
## CHECK MOBILE
if( $listemployee.mobile -match '\S' )
{
if($($ADEmployee.mobile) -ne $($ListEmployee.mobile))
{
echo " CHANGE - Mobile : '$($ADEmployee.mobile)' to '$($ListEmployee.mobile)'"
Set-QADUser -identity $($ADEmployee.samaccountname) -Mobile $($ListEmployee.mobile) -WhatIf
}
else
{
echo " OK - Mobile : no change '$($ListEmployee.mobile)'"
}
}
## CHECK EMPLOYEEID
if($($ADEmployee.employeeid) -ne $($ListEmployee.employeeid))
{
echo " CHANGE - EmployeeID: '$($ADEmployee.employeeid)' to '$($ListEmployee.employeeid)'"
Set-QADUser -identity $($ADEmployee.samaccountname) -ObjectAttributes #{employeeID = $($ListEmployee.employeeid)} -WhatIf
}
else
{
echo " OK - EmployeeID : no change '$($ListEmployee.employeeid)'"
}
$match++
}
else
{
if($EXContact = Get-Contact $ListDisplayName -ErrorAction SilentlyContinue)
{
echo "MATCH CONTACT: $ListDisplayName (contact)"
## CHECK MOBILE
if( $listemployee.mobile -match '\S' )
{
if($($EXContact.Mobilephone) -ne $($ListEmployee.mobile))
{
echo " CHANGE - Mobile : '$($EXContact.Mobilephone)' to '$($ListEmployee.mobile)'"
}
else
{
echo " OK - Mobile ; No change ($($ListEmployee.mobile))"
}
}
## CHECK COMPANY
if($($EXContact.company) -ne $($ListEmployee.company))
{
echo " CHANGE - Company: '$($EXContact.company)' to '$($ListEmployee.company)'"
}
else
{
echo " OK - Company : No change($($ListEmployee.company))"
}
## CHECK OFFICE
if($($EXContact.office) -ne $($ListEmployee.office))
{
echo " CHANGE - Office '$($EXContact.office)' to '$($ListEmployee.office)'"
}
else
{
echo " OK - Office : No Change($($ListEmployee.office))"
}
$contactmatch++
}
else
{
echo "$ListDisplayName" | Out-File $logfile -Append
echo "NO MATCH: $ListDisplayName"
$nomatch++
}
}
$i++
}
echo " "
echo "List contains $i accounts"
echo "Accounts: $match matches"
echo "Contacts: $contactmatch"
echo "No Match: $nomatch"
And; If you think this is cr*p, tell me! I'd rather hear it from you than you staying silent just to be polite! I am "quite" new to this so I deserve it:)
Something that seems odd about the whole thing is using display name as your identity reference. As an identity reference, it't both volatile and potentially ambiguos in AD, and seems a poor choice to use to drive a maintenance script.
Here is my opinion :
1) I really think that the problem #mjolinor point is important, and you will meet troubles (I mean need human check) if you don't use one of the identity attributes fixed by Microsoft (samAccountName, userPrincipalName or better objectGuid, objectSid ...) as a key to find your users in Active-Directory.
If it's not possible you perhaps can buid a filter on the top of multiples attributes. If you CSV comes from another LDAP Directory you perhaps can integrate their unique ID in you Schema (in this case see Microsoft Services for UNIX 3.5 (MSFU3.5) schema extensions to Active Directory).
2) Once you find one of your CSV entry in your Active-Directory, you check each attributes, and then replace 'one by one' the ones in your AD with the one in your CSV.
Here my advice will be to check all the differencies between your CSV and AD entry, and them made an unique change into the Directory. In fact, on one différence, I will change them all in one command. I don't know how Set-QADUser is written, but in the low level layers all the attributes replacement can be made one shot (LDAP_REPLACE, or in a single ADSI commit)
3) Just a remark : begining PowerShell V2 (Seven, W2K8) an Active-Directory module is given by Microsoft.