While value is x - loops

Being new to PowerShell, I have this script which works when using 'switch'. I tried to get it to work using 'while' but it exits whether the value is 1 or 0.
Looking at the script below, could someone point out to me where I am going wrong!!
Your help is appreciated.
cls
Import-Module -Name ActiveDirectory
$ADpath = "OU=OU1,DC=DC1,DC=DC2,DC=DC3";
$i = 0
function yaynay(){
$i = Read-Host "`nSearch again.." "Y `tOR N";
switch -Regex ($i.ToUpper()){
"Y(es)?"{$i -eq 1}
default {$i -eq 0}
}
get-pssession | remove-pssession
}
function TableFormat(){
$x1=#{label='Full Name';Expression={$_.name};width=20},
#{label='Login Name';Expression={$_.samaccountname};width=15},
#{label='OU';Expression={(($_.DistinguishedName -split "=",4)[3] -split ",",2[0]};width=15}
$ADusr | Format-Table $x1
}
function getOut(){
Write-Host "AD Search will now exit"
exit
}
function ADUwrite (){
Write-Output "`n Cannot find user: $InputUsr"
}
while ($i -lt 1)
{
$InputUsr = Read-Host "Enter One or More Letters To Search For User"
$InputUsr1 = $InputUsr
if($InputUsr -eq ""){
write "`n No Search Criteria Entered... Exiting AD User Search"
exit
}
$InputUsr = "*"+$InputUsr+"*"
if([string]$InputUsr1 -contains "*"){
$InputUsr = $InputUsr.Replace($InputUsr, $InputUsr1)
}
$ADusr = Get-ADUser -Filter {name -like $InputUsr} -SearchBase $ADpath
$x=$ADusr.name.length
if($x -gt 0){
TableFormat
}
else{
$InputUsr = $InputUsr1.ToUpper()
ADUwrite $InputUsr
};
yaynay($i)
if($i = 1){
GetOut
}
Get-PSSession | Remove-PSSession
}

Several things here:
function yaynay(){
$i = Read-Host "`nSearch again.." "Y `tOR N";
switch -Regex ($i.ToUpper()){
"Y(es)?"{$i -eq 1}
default {$i -eq 0}
}
get-pssession | remove-pssession
}
The variable $i is local to this function and is never returned so its value won't affect anything happening outside.
yaynay($i)
You don't use parentheses to call a Powershell function (though you do to call a method), it won't matter much here as it will just pass the number 0 as an argument, but if you try it with more than one argument you'll end up passing an array. yaynaydoesn't have any arguments defined so it will ignore the argument anyway.
if($i = 1){
GetOut
}
assigns the value 1 to the variable $i. This is always true so you'll always call GetOut. The comparison should be if ($i -eq 1).

Related

Array of variables in PowerShell has null members

I have a PowerShell script, where I want to make sure certain variables have value before proceeding.
So I have the following:
$dataRow = $sheet.Cells.Find($country).Row
$serverCol = $sheet.Cells.Find($serverString).Column
$databaseCol = $sheet.Cells.Find($databaseString).Column
$userCol = $sheet.Cells.Find($userString).Column
$passwordCol = $sheet.Cells.Find($passString).Column
$partnerCol = $sheet.Cells.Find($partnerString).Column
#All variables in this array are required. If one is empty - the script cannot continue
$requiredVars = #($dataRow, $serverCol, $databaseCol, $userCol, $passwordCol, $partnerCol)
But when I foreach over the array like so:
foreach ($var in $requiredVars)
{
Write-Host DataRow = ($dataRow -eq $var)
Write-Host ServerCol = ($serverCol -eq $var)
Write-Host DatabaseCol = ($databaseCol -eq $var)
Write-Host UserCol = ($userCol -eq $var)
Write-Host PasswordCol = ($passwordCol -eq $var)
Write-Host PartnerCol = ($partnerCol -eq $var)
if ($var -eq $null)
{
[System.Windows.Forms.MessageBox]::Show("No data found for given string!")
$excel.Quit()
return
}
}
I always get the MessageBox. I added the "Write-Host" part to see the value of each variable, then changed it to see which variable was null but all variables have values in them and all the checks you see here return "False".
I'd like to know what I'm doing wrong and if the $requiredVars array only copies values, not references or something.
Instead of using separate variables, you may consider using a Hashtable to store them all.
This makes checking the individual items a lot simpler:
# get the data from Excel and store everything in a Hashtable
# to use any of the items, use syntax like $excelData.passwordCol or $excelData['passwordCol']
$excelData = #{
'dataRow' = $sheet.Cells.Find($country).Row
'serverCol' = $sheet.Cells.Find($serverString).Column
'databaseCol' = $sheet.Cells.Find($databaseString).Column
'userCol' = $sheet.Cells.Find($userString).Column
'passwordCol' = $sheet.Cells.Find($passString).Column
'partnerCol' = $sheet.Cells.Find($partnerString).Column
}
# check all items in the hash. If any item is $null then exit
foreach ($item in $excelData.Keys) {
# or use: if ($null -eq $excelData[$item])
if (-not $excelData[$item]) {
[System.Windows.Forms.MessageBox]::Show("No data found for item $item!")
$excel.Quit()
# IMPORTANT: clean-up used COM objects from memory when done with them
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sheet) | Out-Null
# Your code doesn't show this, but you'll have a $workbook object in there too
# [System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
return
}
}
One way to directly solve your question is this:
$a = "foo"
$b = "bar"
$c = $null
$requiredVariables = $a, $b, $c
# How many total entries in array?
($requiredVariables).Count
# How many of them have a value?
($requiredVariables | Where-Object {$_}).Count
# So one option for a single check would be:
if (($requiredVariables.Count) -ne ($requiredVariables | Where-Object {$_}).Count) {
Write-Warning "Not all values provided"
}
However an alternative [and better] approach is to make your code in to a function that includes parameter validation
function YourCustomFunction {
Param (
[ValidateNotNullOrEmpty()]
$a
,
[ValidateNotNullOrEmpty()]
$b
,
[ValidateNotNullOrEmpty()]
$c
)
Process {
Write-Output "Your function code goes here..."
}
}
# Call your function with the params
YourCustomFunction -a $a -b $b -c $c
Example output:
Test-YourCustomFunction: Cannot validate argument on parameter 'c'. The argument is null or empty. Provide an argument that is not null or empty, and
then try the command again.
At line:39 char:48

Can I convert comma-separated string back to AD value objects?

So I'm trying to write a script that will generate an AD report based on parameters chosen by the user, and it's caused me a lot more headache than I expected. Here's an excerpt of what I'm trying to do:
#import the ActiveDirectory Module
Import-Module ActiveDirectory
#report parameter variables
$firstname = {givenname}
$lastname = {surname}
$displayname = {DisplayName}
$logonname = {sAMAccountName}
#set array initially to number of possible parameters
$inputarray = (0..3)
#display menu
cls
Write-Host "Please select the parameters you would like in your report:"
Write-Host "Enter q when finished"
Write-Host `n
Write-Host " 1) First Name"
Write-Host " 2) Last Name"
Write-Host " 3) Display Name"
Write-Host " 4) Logon Name"
Write-Host `n
#read in selections from user while input isn't Q, and not bigger than array
#bounds
for ($i=0; (($i -le 3) -and ($inputarray[$i] -ne 'q')); $i++){
$selection = ($i + 1)
$inputarray[$i] = Read-host "Enter report parameter $selection"
#exit loop for quit selection
if ($inputarray[$i] -eq 'q'){
break
}
switch ($inputarray[$i])
{
"1" { $result = $firstname }
"2" { $result = $lastname }
"3" { $result = $displayname }
"4" { $result = $logonname }
}
$inputarray[$i] = $result
}
$arraylen = $i
$test = ''
for ($x=0; $x -lt $arraylen; $x++){
if($x -lt ($arraylen -1)){
$test = ($test + $inputarray[$x] + ',')
}
else{
$test = ($test + $inputarray[$x])
}
}
Get-ADUser -searchbase "my targeted OU" -Properties * -Filter * |
Select-Object $test |
Export-Csv -Path "export path here.csv" -NoTypeInformation
I thought creating a string from the array with comma separated values would work the same as typing them in (like Select-Object givenname,surname,lastlogin) but that clearly isn't working. Any ideas how to change it back from a string value to individual objects, so maybe it will accept it?
1st problem: Statements such as $firstname = {givenname} assign a script block ({...}) to the variable, which is not your intent; instead, you're looking to assign property names as strings, e.g., $firstname = 'givenname'.
2nd problem: You're building variable $test as a string, whereas what you need to pass to Select-Object is an array of strings (property names), so you can just use $inputArray directly.

Using ForEach-Object on Array of Structs - Powershell

I'm evolving my Surveillance script, so i can choose a Service/Maintenance Window. Where all errors are ignored between two time intervals.
This is what i got:
Add-Type -TypeDefinition #"
public struct ServiceWindow
{
public int SWStart;
public int SWEnd;
}
"#
[array]$SWArray = New-Object ServiceWindow
$time = Get-Date -Format HHMM
$time
$ActiveBatchVar = "1000-1005;1306-1345;2300-2305"
$ActiveBatchVar = $ActiveBatchVar.Split(";")
For ($i = 0; $i -lt $ActiveBatchVar.Length; $i++)
{
$tempSW = New-Object ServiceWindow
$tempSW.SWStart = $ActiveBatchVar[$i].Split("-")[0]
$tempSW.SWEnd = $ActiveBatchVar[$i].Split("-")[1]
If ($i -eq 0) { $SWArray = $tempSW } else { $SWArray += $tempSW }
}
Write-Host Complete array...
$SWArray
ForEach-Object ($SWArray) {
Get-Date -Format HHMM
If ($time -ge $_.SWStart -and $time -lt $_.SWEnd) {Write-Host Wohoo we have hit a service window service window...}
}
I get an error in my last ForEach-Object loop. and can't figure out what is wrong.
The point is that I would like to check if the current time is between two given times, like "1000-1005".
Anyone got a clue what’s missing, or maybe a way to simplify the whole thing ;)
Ok, a few things here... You really seem to like the Split() method. You may want to look into some alternatives, like this:
$ActiveBatchVar = #(#("1000","1005"),#("1306","1345"),#("2300","2305"))
See what we did there? It's an array of arrays. #() is the array notation. So I have an array, with 3 arrays in it.
I'm not real familliar with structs, but I am familliar with custom objects, so I would use that if it were me. Then you could do something like:
$SWArray = #() #That's an empty array, we'll add things to it now that it exists
ForEach ($Batch in $ActiveBatchVar){
$SWArray += New-Object PSObject -Property #{
SWStart = $Batch[0]
SWEnd = $Batch[1]
}
}
So then we change the last bit so that you are assigning $time just before your next loop to keep it as accurate as possible, and correct the ForEach just a little and the whole thing would look like this:
$ActiveBatchVar = #(#("1000","1005"),#("1306","1345"),#("2300","2305"))
$SWArray = #()
ForEach ($Batch in $ActiveBatchVar){
$SWArray += New-Object PSObject -Property #{
SWStart = $Batch[0]
SWEnd = $Batch[1]
}
}
Write-Host Complete array...
$SWArray
$time = date -f HHmm
ForEach($SW in $SWArray) {
If ($time -ge $SW.SWStart -and $time -lt $SW.SWEnd) {
Write-Host "Wohoo we have hit a service window service window..."
}
}
Minimum changes:
ForEach-Object ($SWArray) {
to
$SWArray | % {
Also your last Write-Host should enclose the message in quoes ie
{Write-Host "Wohoo..."}
ForEach-Object ($SWArray) {}
This is the wrong syntax, you should use the keyword in
Foreach-Object ($array in $SWArray) {}
if you have a small array...
($SWArray).foreach({
Get-Date -Format HHMM
If ($time -ge $_.SWStart -and $time -lt $_.SWEnd)
{Write-Host Wohoo we have hit a service window service window...}
})

PowerShell: Set-Content having issues with "file already in use"

I'm working on a PowerShell script that finds all the files with PATTERN within a given DIRECTORY, prints out the relevant lines of the document with the PATTERN highlighted, and then replaces the PATTERN with a provided REPLACE word, then saves the file back. So it actually edits the file.
Except I can't get it to alter the file, because Windows complains about the file already being open. I tried several methods to solve this, but keep running into the issue. Perhaps someone can help:
param(
[string] $pattern = ""
,[string] $replace = ""
,[string] $directory ="."
,[switch] $recurse = $false
,[switch] $caseSensitive = $false)
if($pattern -eq $null -or $pattern -eq "")
{
Write-Error "Please provide a search pattern." ; return
}
if($directory -eq $null -or $directory -eq "")
{
Write-Error "Please provide a directory." ; return
}
if($replace -eq $null -or $replace -eq "")
{
Write-Error "Please provide a string to replace." ; return
}
$regexPattern = $pattern
if($caseSensitive -eq $false) { $regexPattern = "(?i)$regexPattern" }
$regex = New-Object System.Text.RegularExpressions.Regex $regexPattern
function Write-HostAndHighlightPattern([string] $inputText)
{
$index = 0
$length = $inputText.Length
while($index -lt $length)
{
$match = $regex.Match($inputText, $index)
if($match.Success -and $match.Length -gt 0)
{
Write-Host $inputText.SubString($index, $match.Index) -nonewline
Write-Host $match.Value.ToString() -ForegroundColor Red -nonewline
$index = $match.Index + $match.Length
}
else
{
Write-Host $inputText.SubString($index) -nonewline
$index = $inputText.Length
}
}
}
Get-ChildItem $directory -recurse:$recurse |
Select-String -caseSensitive:$caseSensitive -pattern:$pattern |
foreach {
$file = ($directory + $_.FileName)
Write-Host "$($_.FileName)($($_.LineNumber)): " -nonewline
Write-HostAndHighlightPattern $_.Line
%{ Set-Content $file ((Get-Content $file) -replace ([Regex]::Escape("[$pattern]")),"[$replace]")}
Write-Host "`n"
Write-Host "Processed: $($file)"
}
The issue is located within the final block of code, right at the Get-ChildItem call. Of course, some of the code in that block is now a bit mangled due to me trying to fix the problem then stopping, but keep in mind the intent of that part of the script. I want to get the content, replace the words, then save the altered text back to the file I got it from.
Any help at all would be greatly appreciated.
Removed my previous answer, replacing it with this:
Get-ChildItem $directory -recurse:$recurse
foreach {
$file = ($directory + $_.FileName)
(Get-Content $file) | Foreach-object {
$_ -replace ([Regex]::Escape("[$pattern]")),"[$replace]")
} | Set-Content $file
}
Note:
The parentheses around Get-Content to ensure the file is slurped in one go (and therefore closed).
The piping to subsequent commands rather than inlining.
Some of your commands have been removed to ensure it's a simple test.
Just a suggestion but you might try looking at the documentation for the parameters code block. There is a more efficient way to ensure that a parameter is entered if you require it and to throw an error message if the user doesn't.
About_throw: http://technet.microsoft.com/en-us/library/dd819510.aspx
About_functions_advanced_parameters: http://technet.microsoft.com/en-us/library/dd347600.aspx
And then about using Write-Host all the time: http://powershell.com/cs/blogs/donjones/archive/2012/04/06/2012-scripting-games-commentary-stop-using-write-host.aspx
Alright, I finally sat down and just typed everything sequentially in PowerShell, then used that to make my script.
It was actually really simple;
$items = Get-ChildItem $directory -recurse:$recurse
$items |
foreach {
$file = $_.FullName
$content = get-content $file
$newContent = $content -replace $pattern, $replace
Set-Content $file $newcontent
}
Thanks for all your help guys.

How to remove item from an array in PowerShell?

I'm using Powershell 1.0 to remove an item from an Array. Here's my script:
param (
[string]$backupDir = $(throw "Please supply the directory to housekeep"),
[int]$maxAge = 30,
[switch]$NoRecurse,
[switch]$KeepDirectories
)
$days = $maxAge * -1
# do not delete directories with these values in the path
$exclusionList = Get-Content HousekeepBackupsExclusions.txt
if ($NoRecurse)
{
$filesToDelete = Get-ChildItem $backupDir | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
else
{
$filesToDelete = Get-ChildItem $backupDir -Recurse | where-object {$_.PsIsContainer -ne $true -and $_.LastWriteTime -lt $(Get-Date).AddDays($days)}
}
foreach ($file in $filesToDelete)
{
# remove the file from the deleted list if it's an exclusion
foreach ($exclusion in $exclusionList)
{
"Testing to see if $exclusion is in " + $file.FullName
if ($file.FullName.Contains($exclusion)) {$filesToDelete.Remove($file); "FOUND ONE!"}
}
}
I realize that Get-ChildItem in powershell returns a System.Array type. I therefore get this error when trying to use the Remove method:
Method invocation failed because [System.Object[]] doesn't contain a method named 'Remove'.
What I'd like to do is convert $filesToDelete to an ArrayList and then remove items using ArrayList.Remove. Is this a good idea or should I directly manipulate $filesToDelete as a System.Array in some way?
Thanks
The best way to do this is to use Where-Object to perform the filtering and use the returned array.
You can also use #splat to pass multiple parameters to a command (new in V2). If you cannot upgrade (and you should if at all possible, then just collect the output from Get-ChildItems (only repeating that one CmdLet) and do all the filtering in common code).
The working part of your script becomes:
$moreArgs = #{}
if (-not $NoRecurse) {
$moreArgs["Recurse"] = $true
}
$filesToDelete = Get-ChildItem $BackupDir #moreArgs |
where-object {-not $_.PsIsContainer -and
$_.LastWriteTime -lt $(Get-Date).AddDays($days) -and
-not $_.FullName.Contains($exclusion)}
In PSH arrays are immutable, you cannot modify them, but it very easy to create a new one (operators like += on arrays actually create a new array and return that).
I agree with Richard, that Where-Object should be used here. However, it's harder to read.
What I would propose:
# get $filesToDelete and #exclusionList. In V2 use splatting as proposed by Richard.
$res = $filesToDelete | % {
$file = $_
$isExcluded = ($exclusionList | % { $file.FullName.Contains($_) } )
if (!$isExcluded) {
$file
}
}
#the files are in $res
Also note that generally it is not possible to iterate over a collection and change it. You would get an exception.
$a = New-Object System.Collections.ArrayList
$a.AddRange((1,2,3))
foreach($item in $a) { $a.Add($item*$item) }
An error occurred while enumerating through a collection:
At line:1 char:8
+ foreach <<<< ($item in $a) { $a.Add($item*$item) }
+ CategoryInfo : InvalidOperation: (System.Collecti...numeratorSimple:ArrayListEnumeratorSimple) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration
This is ancient. But, I wrote these a while ago to add and remove from powershell lists using recursion. It leverages the ability of powershell to do multiple assignment . That is, you can do $a,$b,$c=#('a','b','c') to assign a b and c to their variables. Doing $a,$b=#('a','b','c') assigns 'a' to $a and #('b','c') to $b.
First is by item value. It'll remove the first occurrence.
function Remove-ItemFromList ($Item,[array]$List(throw"the item $item was not in the list"),[array]$chckd_list=#())
{
if ($list.length -lt 1 ) { throw "the item $item was not in the list" }
$check_item,$temp_list=$list
if ($check_item -eq $item )
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-ItemFromList -item $item -chckd_list $chckd_list -list $temp_list )
}
}
This one removes by index. You can probably mess it up good by passing a value to count in the initial call.
function Remove-IndexFromList ([int]$Index,[array]$List,[array]$chckd_list=#(),[int]$count=0)
{
if (($list.length+$count-1) -lt $index )
{ throw "the index is out of range" }
$check_item,$temp_list=$list
if ($count -eq $index)
{
$chckd_list+=$temp_list
return $chckd_list
}
else
{
$chckd_list+=$check_item
return (Remove-IndexFromList -count ($count + 1) -index $index -chckd_list $chckd_list -list $temp_list )
}
}
This is a very old question, but the problem is still valid, but none of the answers fit my scenario, so I will suggest another solution.
I my case, I read in an xml configuration file and I want to remove an element from an array.
[xml]$content = get-content $file
$element = $content.PathToArray | Where-Object {$_.name -eq "ElementToRemove" }
$element.ParentNode.RemoveChild($element)
This is very simple and gets the job done.

Resources