I'm coding a progress bar for my script, and I've run into an issue where a string is being fed data in an incorrect format.
foreach ($User in $Users) {
#Set UserPrincipalName for current user
$upn = $User
#Display progress bar
$percentage = [math]::Round($loopcount / $maxcount *100)
$message = "Applying changes for $upn ($loopcount of $maxcount)" -f $percentage
Write-Progress -Activity $message -ErrorAction SilentlyContinue -PercentComplete ($percentage) -Status "Progress $percentage% :"
This is the main part of the code that makes up the progress bar (there's more, but that's irrelevant). It works great when it is being fed user identities (via $User) like "delat" and "damo", but when it gets to a user called "DiscoverySearch {D919BA05-46A6-415f-80AD-XXXXXXXXXXXXXXXX}" it understandably throws a "Error formatting a string" error.
How do I go about solving this issue?
Edit: Below is the source of $User, in case that's needed.
$MailboxRegionList = Get-Mailbox | Get-MailboxRegionalConfiguration
$Users += $MailboxRegionList | Select-Object -ExpandProperty Identity
This happens, as -f $percentage is not doing what you'd expect. It is not going to format the message as percentage a value.
$message = "Applying changes for $upn ($loopcount of $maxcount)" -f $percentage
The format operator, -f will fill in a value in string. The placeholder is to be marked with curly brackets {}, which your string doesn't usually have. When the $user is DiscoverySearch {D919BA05-46A6-415f-80AD-XXXXXXXXXXXXXXXX}, it contains curly brackets. Now, the curly brackets should follow .Net composite formatting syntax, and a GUID in account name does not do that.
As for a fix, try
$message = "Applying changes for $upn ($loopcount of $maxcount) $percentage"
Related
This question already has answers here:
How can you use an object's property in a double-quoted string?
(5 answers)
Closed 5 months ago.
I wrote a script that allows me to query the whole Azure database park:
#$ErrorActionPreference = 'SilentlyContinue'
# Connect to Azure
$azureAccount = Connect-AzAccount
# Get Azure Access Token (we will use this to query the databasees)
#$azureToken = Get-AzAccessToken -ResourceUrl https://database.windows.net
$access_token = (Get-AzAccessToken -ResourceUrl https://database.windows.net).Token
# Queries will be picked up from here
$folderPath = '.\Queries'
# Choose how to format each date ("yyyy-MM-dd") or ("yyyy-MM-dd HH:mm:ss")
$DateTime = (Get-Date).ToString("yyyy-MM-dd")
# List Azure Sunscriptions
Get-Azsubscription | ForEach-Object -Begin { $a = 1 } -Process {"$a $($_.Name)"; $a++}
$SubscriptionChoice = Read-Host -Prompt "Copy/paste the name of the Subscription that you want to investigate. If more than one separate them by a coma, Type `"All`" if you want to target all of them"
# Iterate into subscriptoins and print names
foreach ($gs in $SubscriptionChoice) {
Select-Azsubscription -Subscription "$gs" | Out-Null
Write-Host "Let's browse into Azure Sunscription: " -NoNewline
Write-Host (Get-AzContext).Subscription.Name -ForegroundColor green
# Fins all Azure SQL Server
Get-AzSqlServer | ForEach-Object -Begin { $a = 1 } -Process {"$a $($_.ServerName)"; $a++}
$SqlServerChoice = Read-Host -Prompt "Copy/paste the name of the SQL Server that you want to investigate. If more than one separate them by a coma, Type `"All`" if you want to target all of them"
if ($SqlServerChoice = "All"){
$SqlServerChoice = Get-AzSqlServer
}
Foreach ($server in $SqlServerChoice){
$DatabaseChoice = Get-AzSqlDatabase -ServerName $server.ServerName -ResourceGroupName $server.ResourceGroupName | Where-Object DatabaseName -NE "master"
Foreach ($database in $DatabaseChoice){
(Get-ChildItem $folderPath | sort-object {if (($i = $_.BaseName -as [int])) {$i} else {$_}} ).Foreach{
Invoke-Sqlcmd -ServerInstance $server.FullyQualifiedDomainName -Database $database.DatabaseName -AccessToken $access_token -InputFile $psitem.FullName | Export-Csv -Path ".\Results\$psitem.csv" -Append -NoTypeInformation
write-host "Executing $psitem on $database.DatabaseName"
}
}
}
}
However each time the query is executed against a database the Write-Hosts returns:
Executing DTU_to_vCore.sql on Microsoft.Azure.Commands.Sql.Database.Model.AzureSqlDatabaseModel.DatabaseName
Here a picture:
This Write-Hosts comes from the line:
write-host "Executing $psitem on $database.DatabaseName"
In which you can find the two variables:
$psitem : which is the name of the file that contains the query
$database.DatabaseName : which should be the database name but instead of printing the database name is printing Microsoft.Azure.Commands.Sql.Database.Model.AzureSqlDatabaseModel.DatabaseName
Why one of the two variable is not interpreted?
You need to encapsulate your variable property in a subexpression operator $().
write-host "Executing $psitem on $($database.DatabaseName)"
This is because only simple variables get expanded in an expandable string.
References
Only simple variable references can be directly embedded in an
expandable string. Variables references using array indexing or member
access must be enclosed in a subexpression.
Source: about_Quoting_Rules
Subexpression operator $( )
Returns the result of one or more statements. For a single result,
returns a scalar. For multiple results, returns an array. Use this
when you want to use an expression within another expression. For
example, to embed the results of command in a string expression.
PS> "Today is $(Get-Date)"
Today is 12/02/2019 13:15:20
PS> "Folder list: $((dir c:\ -dir).Name -join ', ')"
Folder list: Program Files, Program Files (x86), Users, Windows
Source: about_Operators
I have the following variable $Obj set to the following string value:
$Obj = '#{Version=1; Name=a;}'
How do I convert this value from a string into a custom psobject?
I would like to be able to call
$Obj.Version and get the value 1. Currently this call returns nothing.
Note: Due to how I am retrieving this variable, I can't initialize it without the single quotes.
Edit:
Here is the current code:
$Command = "script.ps1 -ExtraInfo $_"
Write-Host $Command
Invoke-Expression -Command $Command
where $_ is #{Version=1; Name=a;} (without the quote)
Originally this code was written as
. script.ps1 -ExtraInfo $_
and worked, but when I added unit tests I changed it to use Invoke-Expression so that it could be testable with Pester unit tests. Is there a better way?
Edit2:
Turns out this can be solved by putting a back tic ` before the expression and that solves the issue for me. Thank you everyone for your input.
$Command = "script.ps1 -ExtraInfo `$_"
Write-Host $Command
Invoke-Expression -Command $Command
The stringified version of a [pscustomobject] instance, which resembles a hashtable literal, is not suitable for programmatic processing, as the following example demonstrates:
# Stringify a [pscustomobject] instance.
PS> "$([pscsutomobject] #{ Version=1; Name='a value' })"
#{Version=1; Name=a value} # !! Quoting of the .Name value was lost
The problem gets worse for property values that are themselves complex objects.
Since you do appear to have access to the original [pscustomobject] instance, the solution is not to stringify.
For that, you simply need to avoid up-front string interpolation by using a verbatim (single-quoted) string literal ('...') and letting Invoke-Expression - which should generally be avoided - interpret the $_ variable as its original type:
# Use *single* quotes to prevent up-front expansion.
$Command = 'script.ps1 -ExtraInfo $_'
Write-Host $Command
Invoke-Expression -Command $Command
Note that the use of a verbatim (non-interpolating) string literal makes the use of Invoke-Expression safe here, though, as Santiago Squarzon points out, there may be a better alternatives in general, and particularly in the context of Pester.
A script-block-based solution ({ ... }) that receives the object as an argument:
$Command = { script.ps1 -ExtraInfo $args[0] }
Write-Host "Calling { $Command } with argument $_"
. $Command $_
This doesn't work with Name=a because a is not a known object (or at least not defined in my PS Session). But if this is a string, this can be done with the following script:
$Obj = '#{Version=1; Name="a";}'
$s= [System.Management.Automation.ScriptBlock]::Create("New-Object -TypeName PSObject -Property $Obj")
$o = Invoke-Command -ScriptBlock $s
$o.Version
As I stated in my comment, this is odd, and should be resolved earlier in the code base. However, if that is not possible, use Invoke-Expression
like so
$newObj = Invoke-Expression $Obj
Further reading on Invoke-Expression
I have created a script in the Azure PowerShell.
If I use the "echo" command, it displays output to the console.
However, if I use Write-Output and Write-Error, I don't see the output.
I have uploaded the script "change-to-static.ps1" to a storage account. Then I open the "Cloud Shell" with a button at the top bar. Then I type "./change-ip-to-static.ps1" in the PowerShell console.
Therefore, the script does not produce any output unless I replace "Write-Output" and "Write-Error" with "echo" or "print".
Please help me. What should I do to see the output?
The script is below.
There is a similar question at How to output something in PowerShell. I have read it, but there are no concrete examples on how to achieve my goal, i.e. how to modify my script to see the output. And in my case, it does not output even if I redirect to a text file. However, commands like "echo" and "print" in my case work but they are not covered in the above example. See the script below.
$IPs = Get-AzPublicIpAddress;
$Static = "Static";
foreach ($PublicIP in $IPs) {
$Method = $PublicIP.PublicIpAllocationMethod;
$Name = $PublicIP.Name;
if ($Method -eq $Static) {
$message = "The method of " + $Name + " is already " + $Static;
Write-Progress -Activity $message;
}
else {
Write-Progress -Activity "Changing the method of "+$Name+" from "+$Method+" to "+$Static+"...";
$PublicIP.PublicIpAllocationMethod = $Static;
Set-AzPublicIpAddress -PublicIpAddress $PublicIP;
Write-Progress -Activity "Querying the method of "+$Name+"...";
$ModifiedAddress = Get-AzPublicIpAddress -Name $Name -ResourceGroupName $PublicIP.ResourceGroupName -Location $PublicIP.Location
$NewMethod = $ModifiedAddress.PublicIpAllocationMethod;
if ($NewMethod -eq $Static) {
Write-Output "The method for "+$Name+" has successfully changed to "+$Static;
}
else {
Write-Error -Message "Cannot change the method for "+$Name+" to "+$Static+", it is still "+$NewMethod+"!!!";
}
}
}
P.S. I have updated the script (use this URL) according to the suggestions, but there is still no output. Only "echo" or "print" gives the output.
P.P.S. The Write-Progress does not even show a temporary message in the status line during Set-AzPublicIpAddress which takes a couple of seconds to complete, or if I add the Start-Sleep cmdlet. It does only set during Get-AzPublicIpAddress.
After reading your last edit to my answer, I believe you made a bit of confusion in using Write-* commandlets, and in your script logic, so I provided a more detailed answer with context.
echo in the Powershell Azure Cloud Shell is an alias of Write-Output, as executing echo without parameters clearly shows (docs here ).
PS /home/mikelangelo> echo
cmdlet Write-Output at command pipeline position 1
Supply values for the following parameters:
InputObject:
Moreover: the unix echo can also be run in the Powershell Azure Cloud Shell.
PS /home/mikelangelo> which echo
/usr/bin/echo
PS /home/mikelangelo> /usr/bin/echo ciao mondo
ciao mondo
print, on the other hand, is not a Powershell alias, so the unix counterpart is the one which always get executed when using the print keyword (presently a symlink to run-mailcap - but it's not clear to me how it comes into play into your use case.)
PS /home/mikelangelo> which print
/usr/bin/print
So, basically, echo and Write-Output will both work, because they call the same commandlet, unless you execute /usr/bin/echo directly, mixing up technologies and effectively impairing portability.
Back to the question:
Write-Output works as expected. The logic is faulty: You use = as a comparison operator, but you need to use -eq instead.
Write-Progress needs to be used differently, replace it with Write-Host or Write-Output. Refer to the docs for an explanation.
Note that Write-Output sends an object down the pipeline, which can eventually be represented as a console output.
Write-Progress and Write-Host, on the other hand, do not generate output - the latter sends an object to the host for displaying, though, so Write-Host is the recommended way to display something in the console. Refer to this question for more details on Write-Host, Write-Output and the Powershell pipeline.
Like other commenters before me I can also confirm that the code from your gist works just fine in Azure Cloud Shell.
I noticed that there is only an output if you have at least one dynamic public ip that the script can change to static. The reason is, that only in this case you use Write-Output to return a string to the console.
If there is no dynamic public ip left, your script only writes a progress message, but you never get to see it, as the script execution ends too quickly after you write the message and progress messages don't linger.
Put the command Start-Sleep -Seconds 2 under the line with Write-Progress and you will see what I mean:
$IPs = Get-AzPublicIpAddress;
$Static = "Static";
foreach ($PublicIP in $IPs) {
$Method = $PublicIP.PublicIpAllocationMethod;
$Name = $PublicIP.Name;
if ($Method -eq $Static) {
$message = "The method of $Name is already $Static";
Write-Progress -Activity $message;
Start-Sleep -Seconds 2 # This will keep the script running 2 seconds longer and the message visible.
}
else {
Write-Progress -Activity "Changing the method of $Name from $Method to $Static ...";
$PublicIP.PublicIpAllocationMethod = $Static;
Set-AzPublicIpAddress -PublicIpAddress $PublicIP;
Write-Progress -Activity "Querying the method of $Name ...";
$ModifiedAddress = Get-AzPublicIpAddress -Name $Name -ResourceGroupName $PublicIP.ResourceGroupName
$NewMethod = $ModifiedAddress.PublicIpAllocationMethod;
if ($NewMethod -eq $Static) {
Write-Output "The method for $Name has successfully changed to $Static";
}
else {
Write-Error -Message "Cannot change the method for $Name to $Static, it is still $NewMethod!!!";
}
}
}
Write-Progress is probably not the cmdlet that you want to use to write out the progress of your script (despite it's name). As I do not see how you would need to further process the output of your script, you might as well replace it with Write-Host.
What i am trying to achieve is that if the output is one line and that that line gets written away in a variable. This is the code i have right now:
Connect-AzureRmAccount
(get-azurermresourcegroup).ResourceGroupName
$filter = Read-Host -Prompt "Please filter to find the correct resource group"
$RGName = get-azurermresourcegroup | Where-Object { $_.ResourceGroupName -match $filter }
$RGName.resourcegroupname
this code filters one time and after that it writes all the lines away underneath each other so the results are this:
ResourceGroup-Test
ResourceGroup-Test-1
ResourceGroup-Test-2
but the preferred output is to keep filtering until one is left
Out-GridView
but the preferred output is to keep filtering until one is left
Depending on what the running user chooses for filters this could be a punishing approach / needlessly complicated. If you only want one result how about we instead use something like Out-GridView to allow the user to select one result from their chosen filter.
$filter = Read-Host -Prompt "Please filter to find the correct resource group"
$RGName = get-azurermresourcegroup |
Where-Object { $_.ResourceGroupName -match $filter } |
Out-GridView -OutputMode Single
$RGName.resourcegroupname
You could have used -PassThru but that allows for multiple selections. -OutputMode Single. So this would still have the potential for making a huge selection set if $filter was too vague but this is a simple way to ensure you get one result. Another caveat is that the user could click Cancel. So you might still need some loop logic: do{..}until{}. That depends on how resilient you want to make this process.
Choice
If Out-GridView is not your speed. Another option would be to make a dynamic choice system using $host.ui.PromptForChoice. The following is an example that allows users to choose a subfolder from a collection.
$possibilities = Get-ChildItem C:\temp -Directory
If($possibilities.Count -gt 1){
$title = "Folder Selection"
$message = "Which folder would you like to use?"
# Build the choices menu
$choices = #()
For($index = 0; $index -lt $possibilities.Count; $index++){
$choices += New-Object System.Management.Automation.Host.ChoiceDescription ($possibilities[$index]).Name
}
$options = [System.Management.Automation.Host.ChoiceDescription[]]$choices
$result = $host.ui.PromptForChoice($title, $message, $options, 0)
$selection = $possibilities[$result]
}
$selection
You should be able to adapt that into your code much in the same way that I suggested with Out-GridView. Be careful though about this approach. Too many options will clutter the screen.
I'm attempting to change the field contents for multiple entries in a list. So far I've gotten to the point that I can edit the list, add columns really, but can't find anything on how to edit the field text. Here is what I have currently:
EDIT:
I've found a bunch of info for 2010 which isn't applicable but I've updated the code to almost get there. I'm getting 'null array' errors when I connect to the list now. I'm hopeful because I'm able to connect, but still can't get the field to change. I've updated my if statement as well to what is I believe a better format.
#Load necessary module to connect to SPOService
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null
#Login Information for script
$User = "user#email.com"
$Pass = "password"
$creds = New-Object System.Management.Automation.PSCredential($User, (ConvertTo-SecureString $Pass -AsPlainText -Force));
#Connect to SharePoint Online service
Write-Host "Logging into SharePoint online service." -ForegroundColor Green
Connect-SPOService -Url https://site-admin.sharepoint.com -Credential $creds
#Get the Necessary List
Write-Host "Getting the required list." -ForegroundColor Green
$WebUrl = 'https://site.sharepoint.com/'
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$List = $Context.Web.Lists.GetByTitle("VacationRequestForm")
#Edit existing list items
$items = $List.items
foreach($item in $items)
{
if($item["Export Flag"] -eq "New")
{
Write-Host "Changing export flags to Complete." -ForegroundColor Green
$item["Export Flag"] = "Complete"
$item.Update()
}
}
Write-Host "Your changes have now been made." -ForegroundColor Green
I am guessing you have trimmed the script since you are missing things like defining $context and such. You don't have any ExecuteQuery() calls.
MSDN doc on SP 2013 CSOM List Item tasks, which has C# examples of what you need and can be translated to PowerShell.
It generally looks like you have everything but if you could include your whole script I can try and run your script directly.
EDIT: with the updates here is the code that you need
#Load necessary module to connect to SPOService
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null
#Login Information for script
$User = "user#email.com"
$Pass = "password"
$WebUrl = "https://site.sharepoint.com/"
#Connect to SharePoint Online service
Write-Host "Logging into SharePoint online service." -ForegroundColor Green
$Context = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$Context.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, (ConvertTo-SecureString $Pass -AsPlainText -Force))
#Get the Necessary List
Write-Host "Getting the required list." -ForegroundColor Green
$List = $Context.Web.Lists.GetByTitle("VacationRequestForm")
$Query = [Microsoft.SharePoint.Client.CamlQuery]::CreateAllItemsQuery(100);
$Items = $List.GetItems($Query);
$Context.Load($Items);
$Context.ExecuteQuery();
#Edit existing list items
foreach($item in $Items)
{
if($item["Export_x0020_Flag"] -eq "New")
{
Write-Host "Changing export flags to Complete for Id=$($item.Id)." -ForegroundColor Green
$item["Export_x0020_Flag"] = "Complete"
$item.Update()
$Context.ExecuteQuery();
}
}
Write-Host "Your changes have now been made." -ForegroundColor Green
You had a space in your Export Flag name and SharePoint will not have that in the name of the field. By default it will replace that with a _x0020_ string. This value will be based on the first name of the field. So if you change this field name in the future, you will still refer to it as 'Export_x0020_Flag' in this script. I am doing the .ExecuteQuery() for each update but you could do this once at the end of the loop. There is also the limit of 100 records in the query. If you only want records with "New" you should change the CamlQuery to just pull those records back.