I have an automation runbook in azure that receives a json string in a variable like so:
["value1","value2"]
The problem is that I can't figure out how to handle this variable and parse it.
$array = "'$jsonInput'"
$Services = $array | ConvertFrom-Json
switch($Services)
{
value1 { $requested = $true ; break }
Default { $requested = $false }
}
Using only single quotes does not resolve my input variable. Using "'$jsonInput'" does nothing and the variable $requested returns false. I believe this is because of the use of double quotes in the input variable. What is the correct way to parse this?
If I manually enter this when testing, it works.
$array = '["value1","value2"]'
But then again, not sure how to take this json string without quotes, append the quotes and resolve the variable.
Related
I have a sample PowerShell script that defines a class "Archive" with a few properties:
class Archive {
[String]$Address #JSON: archive
[Int]$Port #JSON: port
[String]$SiteKey #JSON site-key
Archive() {
$this.Address = "127.0.0.1"
$this.Port = 80
}
}
$a = [Archive]::new()
$json = $a | ConvertTo-Json
$json
I am trying to create a simple script that takes a few values and outputs it as a JSON. At first, I disregarded naming conventions in PowerShell to match the names. Then I added a property name with a hyphen (which isn't part of the naming requirements for variables).
I've come from a C# perspective where I'd use [DataMember(Name = "api-name")] attributes to hint the name to use when using APIs' JSON payloads.
Is there a way to provide naming hints to a PowerShell property so that when the object is passed to ConvertTo-Json it will use the hint rather than the actual name of the property in the script?
Edits
Underscores are allowed. Hyphens are not. Need to support something like $field-name
You can specify variable names that include special characters by surrounding the name with curly brackets.
For example, you can use:
class Archive {
[String]$address
[Int]$port
[String]${site_key} # variable / property name with special characters
Archive() {
$this.address = "127.0.0.1"
$this.port = 80
$this.{site_key} = "site key"
}
}
$a = [Archive]::new()
$json = $a | ConvertTo-Json
$json
Which should give you this JSON output:
{
"address": "127.0.0.1",
"port": 80,
"site_key": "site key"
}
When you execute ConvertTo-Json, it will still use the names but this way you can write variable names as anything.
Unfortunately, it does not seem to apply to class names.
I am trying to execute REST API within powershell using invoke-restmethod/invoke-webrequest but failed when I pass Json inputs. It works with CURL command.
curl -v --user admin:password -H Accept:application/json -H Content-Type:application/json -d "#C:\data\test.json" -X POST http://10.11.60.88:8081/artifactory/api/distribute
Test.json contents are as below
{
"targetRepo" : "ECSDigital_Bintray",
"packagesRepoPaths" : ["SNOW/org/apache/maven/maven-artifact/3.3.9/maven-artifact-3.3.9.jar"]
}
I wrote the below PowerShell and it gives me series of errors:
$user = "admin"
$pass = "password"
$secpasswd = ConvertTo-SecureString $user -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($pass, $secpasswd)
$postParams = #{targetRepo='SNOW';packagesRepoPaths='["org/apache/maven/maven-artifact/3.3.9/maven-artifact-3.3.9.jar"]'}
Invoke-WebRequest -Uri "http://10.11.60.88:8081/artifactory/api/distribute" -Credential $cred -Method Post -ContentType "application/json" -Body $postParams
Error: Invoke-WebRequest : The remote server returned an error: (400) Bad Request.
I have tried some combinations of json inputs but no go. Any help?
I have fixed the issue, the following way solved my issues, there was need of additional ""
$params = #{
uri = $ARTIFACTORY_PROTOCOL+"://"+$ARTIFACTORY_IP+":"+$ARTIFACTORY_PORT+"/artifactory/api/distribute";
Method = 'POST';
Headers = #{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$($ARTIFACTORY_USERNAME):$($ARTIFACTORY_PASSWORD)"));"Accept" = 'application/json';"Content-Type" = 'application/json'}
#body = #{""targetRepo"" : ""$DISTRIBUTION_REPOSITORY"", ""packagesRepoPaths"" : ""$ARTIFACTORY_PATH""}
}
$json = "{
""targetRepo"":""$DISTRIBUTION_REPOSITORY"",
""packagesRepoPaths"":[""$ARTIFACTORY_PATH""]
}"
$var = invoke-restmethod #params -Body $json
echo $var
While you discovered a solution here, I'd like to explain what was wrong with your original code and offer some improvements to your answer, to help others who may find it and not understand everything that was changed (it was more than just needing extra quotes as your answer implies).
A word of warning, this answer is long and also explains some constructs in detail that were in your original answer which I ended up removing from the final code. If you want to see the final code, I've labelled the section as such at the bottom of this answer.
Main Issue: The $postParams was in the wrong format
The main issue was that the remote service was expecting JSON, but you provided something different for $postParams: a PowerShell hashtable. This gets converted to a string in the end but not in JSON format. While you can type out the JSON or write a function to generate a JSON object, defining your arguments as a hashtable allows for easier maintenance and readability within your script. You can easily convert the object to JSON when it comes time to POST it by using the ConvertTo-Json cmdlet. There is a suitable example in the final code at the end of this answer.
Note: -Depth 100 makes ConvertTo-Json convert nested objects (up to 100, this is the limit) to JSON. Otherwise it will stop and provide a .ToString() representation of the value at the default depth of 3. If you have deeply nested objects, I recommend providing -Depth but this is not required in your use current example.
Suggested: Make use of Invoke-RestMethod
As you discovered can also make use of Invoke-RestMethod instead of Invoke-WebRequest so you can get a proper object returned to work with instead of having to parse the response content yourself. ConvertFrom-Json makes this relatively painless, but Invoke-RestMethod is just one more way to simplify your code. This is already used within your current answer.
Suggested: Use variable expansion and sub-expressions instead of string concatenation
String concatenation makes a script or program consume more memory than it needs to as the string is copied multiple times for each +. To use variable expansion, simply reference your variable within a double-quoted string (single quoted strings are literal strings and will not expand as the $ is not treated as a variable reference token). You can see how this is done in the final code at the bottom of this answer.
Note: With variable substitution, sometimes you will need to change "$var" to "${var}". This is an escape sequence separating the variable name from other tokens in the string. You can see this when I reference ${ARTIFACTORY_IP} and ${ARTIFACTORY_PROTOCOL} below, as both have an immediately proceeding : would be interpreted as part of a variable name.
Using sub-expressions within a string is similar, but makes use of the $() operator instead of directly referencing a variable name or using ${}. This is different because rather than inserting the value of a variable, it instead computes a full PowerShell expression and returns the result as a string. The returned value is then rendered into the string.
As I've removed the relevant code using sub-expressions from the final code at the bottom, here's an example of how they are used:
$name = 'Bender'
$age = 999
"Next year, $name will be $($age + 1)"
"$name's name has $($name.Length) letters in it"
Suggested: Use here-strings instead of multi-line strings
Here-strings are represented by #", followed by lines of text (in which variables can be expanded and sub-expressions processed), and then followed by a final line beginning with "#. This final line cannot be prepended by any whitespace regardless of where it lies in your script). This has the benefit of not needing to escape your quotes within the string content itself, and best signals your intent of multiline content to other maintainers.
As I've removed the multi-line string altogether in the final code, here is an example of defining here-strings:
$name = 'Bender'
$age = 999
# Regardless of where in the script it's used,
# the final `"#` MUST start at the first column
# of the line or it won't be parsed as the end of
# the here-string
$hereString = #"
Name: $name
Age: $age
"#
Note: Like regular strings, there is also a literal variant of the here-string using #' and '# (note the single-quotes). These are also interpreted literally so no variable expansion or processing of sub-expressions will occur. You still have the benefit of not needing to escape single-quotes.
Suggested: Hashtable definition tips
When you define a hashtable #{} in PowerShell, you do not need to end each line with a ; if each key is defined on its own line. I have removed the unnecessary ;s in the final code. You also don't need to wrap keys in quotes unless:
The key name uses a token character which PowerShell attempts to parse as syntax
Including but not limited to - or whitespace
The key name is partially determined by the value of another variable
A key name does not need to be wrapped if the key name is to be the exact value of another variable
As I've removed the Accept and Content-Type parameters from the final code (next part explains why), here is a more complete example of hashtable definition than what is in the final code at the end:
$keyName = 'Catchphrase'
$hashtable = #{
# Key name does not need to be quoted
Name = 'Bender'
Age = '999'
# This key name would render to 'Catchphrase', without the quotes
$keyName = 'Bite my pontiferous persnickity PowerShell'
# Key name needs to be quoted if parseable PowerShell tokens appear in the name
'Content-Type' = 'Key name contains a hyphen'
'This key name contains spaces' = 'Key name contains spaces'
# This key name would render to 'AnotherCatchphrase' without the quotes
"Another$keyName" = 'Antiquiqing'
}
Suggested: Use native parameters on Invoke-RestMethod instead of defining the headers yourself
Finally, you shouldn't need to define Accept: "application/json" in the header, nor do you need to provide Content-Type: "application/json" in the header either. Simply use the -ContentType parameter of Invoke-RestMethod or Invoke-WebRequest and set it to 'application/json'. I've made this change in the final code at the bottom of this answer.
Note: There may be some nuanced situations where you need to set Accept to a different format than ContentType but I don't believe Artifactory falls into this category.
Final Code
Using the code from your answer as a base, we can improve it like so:
# Define json parameters
$jsonParams = #{
targetRepo = "$DISTRIBUTION_REPOSITORY"
packagesRepoPaths = #("$ARTIFACTORY_PATH")
}
$params = #{
Uri = "${ARTIFACTORY_PROTOCOL}://${ARTIFACTORY_IP}:$ARTIFACTORY_PORT/artifactory/api/distribute"
Method = 'POST'
Headers = #{
Authorization = "Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("{$ARTIFACTORY_USERNAME}:$ARTIFACTORY_PASSWORD")))
}
ContentType = 'application/json'
Body = $jsonParams | ConvertTo-Json -Depth 100
}
# I also moved -Body to $params
$var = Invoke-RestMethod #params
I am quite new to powershell stuff. So need some help here.
I want to pass a json object as a parameter to another ps1. From what I read after searching is that I need to convert it to powershell object from json string. Please correct me if I am wrong. This is what I am doing
Calling script:
$jsonParams = "{
`"TaskName`": `"$taskName`",
`"ExitCode`": `"$exitCode`",
`"ErrorMessage`": `"$errorMessage`"
}
$jsonObject = $jsonParams | ConvertFrom-Json
$argumentList = #($param1, $param2, $jsonObject)
Invoke-Expression "& `"$scriptPath`" $argumentList"
and in called script -
param (
[string]$param1,
[string]$param2,
[Microsoft.PowerShell.Commands.JsonObject]$jsonObject
)
But, the calling script throws error
ConvertFrom-Json : Invalid object passed in, ':' or '}' expected. (21): {
What's wrong with this code. Also, after json object is passed to called script, how should I access its values in it.
Thanks!!
Your JSON is malformed. I think the core issue is that you have a trailing comma at the end of your JSON. You also don't close the opening quotation in your declaration.
You might have a much easier time if you use a here-string for this anyway. This was you don't have to use all those backticks.
$jsonParams = #"
{
"TaskName": "$taskName",
"ExitCode": "$exitCode",
"ErrorMessage": "$errorMessage"
}
"#
$jsonObject = $jsonParams | ConvertFrom-Json
$jsonObject is already a custom object and no longer JSON. You don't need to do anything special with it. Remove the type in your param block and just call the properties in your script.
param (
[string]$param1,
[string]$param2,
$jsonObject
)
$jsonObject.TaskName
I am using PowerShell v3 and the Windows PowerShell ISE. I have the following function that works fine:
function Get-XmlNode([xml]$XmlDocument, [string]$NodePath, [string]$NamespaceURI = "", [string]$NodeSeparatorCharacter = '.')
{
# If a Namespace URI was not given, use the Xml document's default namespace.
if ([string]::IsNullOrEmpty($NamespaceURI)) { $NamespaceURI = $XmlDocument.DocumentElement.NamespaceURI }
# In order for SelectSingleNode() to actually work, we need to use the fully qualified node path along with an Xml Namespace Manager, so set them up.
[System.Xml.XmlNamespaceManager]$xmlNsManager = New-Object System.Xml.XmlNamespaceManager($XmlDocument.NameTable)
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
[string]$fullyQualifiedNodePath = Get-FullyQualifiedXmlNodePath -NodePath $NodePath -NodeSeparatorCharacter $NodeSeparatorCharacter
# Try and get the node, then return it. Returns $null if the node was not found.
$node = $XmlDocument.SelectSingleNode($fullyQualifiedNodePath, $xmlNsManager)
return $node
}
Now, I will be creating a few similar functions, so I want to break the first 3 lines out into a new function so that I don't have to copy-paste them everywhere, so I have done this:
function Get-XmlNamespaceManager([xml]$XmlDocument, [string]$NamespaceURI = "")
{
# If a Namespace URI was not given, use the Xml document's default namespace.
if ([string]::IsNullOrEmpty($NamespaceURI)) { $NamespaceURI = $XmlDocument.DocumentElement.NamespaceURI }
# In order for SelectSingleNode() to actually work, we need to use the fully qualified node path along with an Xml Namespace Manager, so set them up.
[System.Xml.XmlNamespaceManager]$xmlNsManager = New-Object System.Xml.XmlNamespaceManager($XmlDocument.NameTable)
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
return $xmlNsManager
}
function Get-XmlNode([xml]$XmlDocument, [string]$NodePath, [string]$NamespaceURI = "", [string]$NodeSeparatorCharacter = '.')
{
[System.Xml.XmlNamespaceManager]$xmlNsManager = Get-XmlNamespaceManager -XmlDocument $XmlDocument -NamespaceURI $NamespaceURI
[string]$fullyQualifiedNodePath = Get-FullyQualifiedXmlNodePath -NodePath $NodePath -NodeSeparatorCharacter $NodeSeparatorCharacter
# Try and get the node, then return it. Returns $null if the node was not found.
$node = $XmlDocument.SelectSingleNode($fullyQualifiedNodePath, $xmlNsManager)
return $node
}
The problem is that when "return $xmlNsManager" executes the following error is thrown:
Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Xml.XmlNamespaceManager".
So even though I have explicitly cast my $xmlNsManager variables to be of type System.Xml.XmlNamespaceManager, when it gets returned from the Get-XmlNamespaceManager function PowerShell is converting it to an Object array.
If I don't explicitly cast the value returned from the Get-XmlNamespaceManager function to System.Xml.XmlNamespaceManager, then the following error is thrown from the .SelectSingleNode() function because the wrong data type is being passed into the function's 2nd parameter.
Cannot find an overload for "SelectSingleNode" and the argument count: "2".
So for some reason PowerShell is not maintaining the data type of the return variable. I would really like to get this working from a function so that I don't have to copy-paste those 3 lines all over the place. Any suggestions are appreciated. Thanks.
What's happening is PowerShell is converting your namespace manager object to a string array.
I think it has to do with PowerShell's nature of "unrolling" collections when sending objects down the pipeline. I think PowerShell will do this for any type implementing IEnumerable (has a GetEnumerator method).
As a work around you can use the comma trick to prevent this behavior and send the object as a whole collection.
function Get-XmlNamespaceManager([xml]$XmlDocument, [string]$NamespaceURI = "")
{
...
$xmlNsManager.AddNamespace("ns", $NamespaceURI)
return ,$xmlNsManager
}
More specifically, what is happening here is that your coding habit of strongly typing $fullyQualifiedModePath is trying to turn the result of the Get (which is a list of objects) into a string.
[string]$foo
will constrain the variable $foo to only be a string, no matter what came back. In this case, your type constraint is what is subtly screwing up the return and making it Object[]
Also, looking at your code, I would personally recommend you use Select-Xml (built into V2 and later), rather than do a lot of hand-coded XML unrolling. You can do namespace queries in Select-Xml with -Namespace #{x="..."}.
I'm writing a Powershell script that will extract a set of data files from a ZIP file and will then attach them to a server. I've written a function that takes care of the unzip and since I need to grab all of the files so that I know what I'm attaching I return that from the function:
function Unzip-Files
{
param([string]$zip_path, [string]$zip_filename, [string]$target_path, [string]$filename_pattern)
# Append a \ if the path doesn't already end with one
if (!$zip_path.EndsWith("\")) {$zip_path = $zip_path + "\"}
if (!$target_path.EndsWith("\")) {$target_path = $target_path + "\"}
# We'll need a string collection to return the files that were extracted
$extracted_file_names = New-Object System.Collections.Specialized.StringCollection
# We'll need a Shell Application for some file movement
$shell_application = New-Object -com shell.Application
# Get a handle for the target folder
$target_folder = $shell_application.NameSpace($target_path)
$zip_full_path = $zip_path + $zip_filename
if (Test-Path($zip_full_path))
{
$target_folder = $shell_application.NameSpace($target_path)
$zip_folder = $shell_application.NameSpace($zip_full_path)
foreach ($zipped_file in $zip_folder.Items() | Where {$_.Name -like $filename_pattern})
{
$extracted_file_names.Add($zipped_file.Name) | Out-Null
$target_folder.CopyHere($zipped_file, 16)
}
}
$extracted_file_names
}
I then call another function to actually attach the database (I've removed some code that checks for existence of the database, but that shouldn't affect things here):
function Attach-Database
{
param([object]$server, [string]$database_name, [object]$datafile_names)
$database = $server.Databases[$database_name]
$server.AttachDatabase($database_name, $datafile_names)
$database = $server.Databases[$database_name]
Return $database
}
I keep getting an error though, "Cannot convert argument "1", with value: "System.Object[]", for "AttachDatabase" to type "System.Collections.Specialized.StringCollection"".
I've tried declaring the data types explicitly at various points, but that just changes the location where I get the error (or one similar to it). I've also changed the parameter declaration to use the string collection instead of object with no luck.
I'm starting with a string collection and ultimately want to consume a string collection. I just don't seem to be able to get Powershell to stop trying to convert it to a generic Object at some point.
Any suggestions?
Thanks!
It looks like you should return the names using the comma operator:
...
, $extracted_file_names
}
to avoid "unrolling" the collection to its items and to preserve the original collection object.
There were several questions like this, here is just a couple:
Strange behavior in PowerShell function returning DataSet/DataTable
Loading a serialized DataTable in PowerShell - Gives back array of DataRows not a DataTable
UPDATE:
This similar code works:
Add-Type #'
using System;
using System.Collections.Specialized;
public static class TestClass
{
public static string TestMethod(StringCollection data)
{
string result = "";
foreach (string s in data)
result += s;
return result;
}
}
'#
function Unzip-Files
{
$extracted_file_names = New-Object System.Collections.Specialized.StringCollection
foreach ($zipped_file in 'xxx', 'yyy', 'zzz')
{
$extracted_file_names.Add($zipped_file) | Out-Null
}
, $extracted_file_names
}
function Attach-Database
{
param([object]$datafile_names)
# write the result
Write-Host ([TestClass]::TestMethod($datafile_names))
}
# get the collection
$names = Unzip-Files
# write its type
Write-Host $names.GetType()
# pass it in the function
Attach-Database $names
As expected, its output is:
System.Collections.Specialized.StringCollection
xxxyyyzzz
If I remove the suggested comma, then we get:
System.Object[]
Cannot convert argument "0", with value: "System.Object[]",
for "TestMethod" to type "System.Collections.Specialized.StringCollection"...
The symptoms look the same, so the solution presumably should work, too, if there are no other unwanted conversions/unrolling in the omitted code between Unzip-Files and Attach-Database calls.