PowerShell:  Move from using arrays/looping into objects/pipelining

Shadow Breeze
Shadow Breeze used Ask the Experts™
on
I am working with JAMF - the Apple IT management application.  I am connecting to the Classic API (RESTful) using PowerShell (v4 and v5).  

I am capturing the name, id, serial_number, model, model_identifier, and model_display from the /mobiledevices endpoint.  I also need to capture the department field from the  /mobiledevices/id/{id}/subset/location subset - where {id} is the id captured in the first endpoint.

Re:  https://developer.jamf.com/#/mobiledevices

The results will be used to populate a different application either through SOAP or direct database updates. TBD.  We may also have to correlate the results with an Active Directory and so I thought PowerShell would be a good scripting base.  I come from a hodge-podge of scripting and admin backgrounds and only have a passing knowledge in PowerShell.

I still have problems with data structure objects and also converting my structured background to pipeline thinking.

Here is what I have:

# Authentication parameters
###########################
$RESTAPIServer = "test.jamfcloud.somewhere"

$RESTAPIUser = "WillyWonka"
$RESTAPIPassword = " OompaLoompas@r3it!"

$BaseURL = "https://" + $RESTAPIServer + "/JSSResource/mobiledevices"
$Header = @{"Authorization" = "Basic <ToBase64 string goes here>"}
$Type = "application/json;charset=UTF-8"
#--------------------------#

# Get all mobile devices
########################
Try 
{
$MobileDeviceResponse = Invoke-Restmethod -Uri $BaseURL  -Method GET -ContentType $Type -Headers $Header
}
Catch 
{
Get-Date -Format g
$_.Exception.ToString()
$error[0] | Format-List -Force
}
## Create a hastable with an array of top level fieldss
$MobileDevice = @{}
$MobileDevice.Tablet = @()
$MobileDevice.Tablet += $MobileDeviceResponse.mobile_devices.mobile_device | Select-Object -Property name, id, serial_number, model, model_identifier, model_display
#-----------------------#

# Get the department of each device based on the captured id field
##################################################################

## Create an array of all the location endpoints
$BaseLocationURI = @()

for ($i=0; $i -lt $MobileDevice.Tablet.id.Count; $i++){
    $BaseLocationURI += $BaseURL + "/id/" +  $MobileDevice.Tablet.id[$i]  + "/subset/location" | out-string
}

## Capture the device department - using the location endpoint URI
### Create a hashtable with an array for department names
$MobileDeviceDept = @{}
$MobileDeviceDept.Name = @()

### Loop through each location endpoint
for ($i=0; $i -lt $MobileDevice.Tablet.id.Count; $i++){
    Try
    {
    $MobileDeviceDeptResponse = Invoke-Restmethod -Uri $BaseLocationURI[$i] -Method GET -Headers $Header ;
    }
    Catch
    {
    Get-Date -Format g
    $_.Exception.ToString()
    $error[0] | Format-List -Force
    }

    # Update the Dept array with department name
    $MobileDeviceDept.Name +=  $MobileDeviceDeptResponse.mobile_device.location | Select-Object -Property department
}
#-------------------------------------------------------------#
# Output results
################
$MobileDevice.Tablet | FT
$MobileDeviceDept.Name
#--------------#

Open in new window


The results are accurate but I need to combine the department with the first array:

$MobileDevice.Tablet | FT

MobileDevice.Tablet.PNG
$MobileDeviceDept.Name

MobileDeviceDept.Name.PNG

My attempt to combine these so far has been limited to creating another hashtable with an array to add each element from both arrays.

$results = @{}
$results.Tablet = @()

for ($i = 0; $i -lt $MobileDevice.Tablet.id.Count; $i++) {
    $results.Tablet += $MobileDevice.Tablet.id[$i], $MobileDevice.Tablet.name[$i], $MobileDeviceDept.Name.department[$i]; #Only MobileDevice.Tablet id and name are being added here for brevity
}

# Output results
#############
$results.Tablet | Format-Table

Open in new window


But this leaves me with a list instead of a table:

results.Tablet.PNG

My questions are:
1.  (Minor) Why are the $results.Tablet forced into a list even when using Format-Table when the $MobileDevice.Tablet | FT display correctly?  Assuming += to the array with the department affected it?
2.  Main question:  I'm pretty sure from what I've started studying that I should be able to replace all of these arrays and loops by piping the id from the first endpoint call into the second endpoint call and return a formatted results with all the fields I want.  I'm not sure at what point the id value is available to pass in the pipeline and how to insert it correctly in the middle of the URI?

Looking at my current loop to create the location URIs:

...
$BaseLocationURI += $BaseURL + "/id/" +  $MobileDevice.Tablet.id[$i]  + "/subset/location" | out-string
...

Open in new window


I've tried to create a pipeline to the location call on the same line as the device call and pass the id as a pipeline variable $_.id

...
$MobileDevice.Tablet += $MobileDeviceResponse.mobile_devices.mobile_device | Select-Object -Property name, id, serial_number, model, model_identifier, model_display | Invoke-Restmethod -Method GET -Headers $Header -Uri $BaseURL + "/id/" +  $_.id  + "/subset/location" | out-string
...

Open in new window


But this errors out on the construction of the URI:

>>Invoke-RestMethod : A positional parameter cannot be found that accepts argument '+'.

Pipeline_error.PNG
All guidance is appreciated.

'Breeze
Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®

Commented:
You are close. You just needed to add the department field to the initial Select and then populate it below since you already have an object with a department field.

# Authentication parameters
###########################
$RESTAPIServer = "test.jamfcloud.somewhere"

$RESTAPIUser = "WillyWonka"
$RESTAPIPassword = " OompaLoompas@r3it!"

$BaseURL = "https://" + $RESTAPIServer + "/JSSResource/mobiledevices"
$Header = @{ "Authorization" = "Basic <ToBase64 string goes here>" }
$Type = "application/json;charset=UTF-8"
#--------------------------#

# Get all mobile devices
########################
Try
{
	$MobileDeviceResponse = Invoke-Restmethod -Uri $BaseURL -Method GET -ContentType $Type -Headers $Header
}
Catch
{
	Get-Date -Format g
	$_.Exception.ToString()
	$error[0] | Format-List -Force
}
## Create a hastable with an array of top level fieldss
$MobileDevice = @{ }
$MobileDevice.Tablet = @()
$MobileDevice.Tablet += $MobileDeviceResponse.mobile_devices.mobile_device | Select-Object -Property name, id, serial_number, model, model_identifier, model_display, department
#-----------------------#

# Get the department of each device based on the captured id field
##################################################################

## Create an array of all the location endpoints
$BaseLocationURI = @()

for ($i = 0; $i -lt $MobileDevice.Tablet.id.Count; $i++)
{
	$BaseLocationURI += $BaseURL + "/id/" + $MobileDevice.Tablet.id[$i] + "/subset/location" | out-string
}

## Capture the device department - using the location endpoint URI
### Create a hashtable with an array for department names
$MobileDeviceDept = @{ }
$MobileDeviceDept.Name = @()

### Loop through each location endpoint
for ($i = 0; $i -lt $MobileDevice.Tablet.id.Count; $i++)
{
	Try
	{
		$MobileDeviceDeptResponse = Invoke-Restmethod -Uri $BaseLocationURI[$i] -Method GET -Headers $Header;
	}
	Catch
	{
		Get-Date -Format g
		$_.Exception.ToString()
		$error[0] | Format-List -Force
	}
	
	# Update the Dept array with department name
	$MobileDevice.Tablet[$i].department = $MobileDeviceDeptResponse.mobile_device.location | Select-Object -ExpandProperty department
}
#-------------------------------------------------------------#
# Output results
################
$MobileDevice.Tablet | FT
# $MobileDeviceDept.Name
#--------------#

Open in new window

Top Expert 2014
Commented:
Not sure if there's a point to creating a hashtable for the results outside of the script.  I think you could simplify things a lot with something like below.
# Authentication parameters
###########################
$RESTAPIServer = "test.jamfcloud.somewhere"

$RESTAPIUser = "WillyWonka"
$RESTAPIPassword = " OompaLoompas@r3it!"

$BaseURL = "https://" + $RESTAPIServer + "/JSSResource/mobiledevices"
$Header = @{"Authorization" = "Basic <ToBase64 string goes here>"}
$Type = "application/json;charset=UTF-8"
#--------------------------#

# Get all mobile devices
########################
Try 
{
    $MobileDeviceResponse = Invoke-Restmethod -Uri $BaseURL  -Method GET -ContentType $Type -Headers $Header
    $MobileDeviceResponse.mobile_devices.mobile_device |
     Select-Object -Property name, 
                         id,
                         serial_number,
                         model, 
                         model_identifier, 
                         model_display,
                         @{n="department";e={ 
                                (Invoke-Restmethod -Uri "$BaseURL/id/$($_.id)/subset/location" -Method GET -Headers $Header).mobile_device.location |
                                 Select-Object -ExpandProperty department 
                                 }}

}
Catch 
{
    Get-Date -Format g
    $_.Exception.ToString()
    $error[0] | Format-List -Force
}

Open in new window

Author

Commented:
Thank you:  @DBAduck - Ben Miller and @footech

Both of these solutions resolve the issue of combining the results from different endpoints.  

@DBAduck - Ben Miller - I did not realize I could include a property name (department) as a placeholder in the first query, even though the attribute does not exist at the endpoint.  It was straightforward to update the table with the results of the second query.

@footech's solution resolves my question on how to invoke a command in a pipeline and pass a variable from upstream - eliminating the need to collect values from one query and then calling a separate query outside the pipeline.  Once I saw the ...$($_id)... syntax, I was able to look up the references to command substitution inside a string.

These have helped solve my problem and have given me a better understanding of PowerShell concepts.

Thank you!
Top Expert 2014

Commented:
The technique I used for adding the property (@{n="<blah>";e={<expression>}}) is known as a "calculated property".  If we needed to add more than one calculated property that relied on the result of the second Invoke-RestMethod call, then I would probably pipe the results from the first to a ForEach-Object loop and store the result in an intermediate variable just for efficiency's sake.  Example below.
    $MobileDeviceResponse = Invoke-Restmethod -Uri $BaseURL  -Method GET -ContentType $Type -Headers $Header
    $MobileDeviceResponse.mobile_devices.mobile_device | ForEach-Object {
        $response2 = Invoke-Restmethod -Uri "$BaseURL/id/$($_.id)/subset/location" -Method GET -Headers $Header
        $_ | Select-Object -Property name, 
                         id,
                         serial_number,
                         model, 
                         model_identifier, 
                         model_display,
                         @{n="department";e={ $response2.mobile_device.location |
                                 Select-Object -ExpandProperty department 
                                 }},
                         @{n="other";e={ $response2.mobile_device.location |
                                 Select-Object -ExpandProperty other #this is completely made up 
                                 }}
     }

Open in new window

You may have found this out already, but the $() notation is known as a subexpression.  One of the more common uses for it is referencing the property of an object inside a string.

Author

Commented:
Thank you for the additional instruction!  Expanding your explanation using my sample problem really helps my thinking of other uses.  The target application also has subsets of data in the main collection and I was able to pull these into the main variable at the same time using this construct.  Nothing like repetition to help my old brain!

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial