Tag: PowerShell

  • How to filter for PowerShell objects easily

    If you are working interactively with PowerShell, It might be unhandy to define all the filtering options with Where-Object. I will show you how to filter for PowerShell objects the traditional way and doing it with Out-Gridview.

    Filter for PowerShell objects – the traditional way with Where-Object

    If we are looking forward to filter in PowerShell, we normaly make use of Where-Object this. In my example I am looking forward for all proccesses, which contain the name msedge and the value for Handles is greater than 292. My query would look like this:

    #traditional way
    Get-Process  | Where-Object {$_.processname -contains "msedge" -and $_.Handles -gt 292}
    Output of all processes in PowerShell
    Get-Process the traditional way with Where-Object

    Filter for PowerShell objects – Out-Gridview

    I want to show you how to filter for Powershell objects easily by using following cmdlet:

    Out-GridView -PassThru

    Example:

    Get-Process | Out-GridView -PassThru

    Doing that results in a grid popping up.

    output of processes as a grid in PowerShell

    Now you have multiple options.

    You can make use of the filter of the out of the box filtering options. Let’s say I just want to find the msedge processes.

    So I am adding the Processname as a filter criteria:

    filtering options in the grid in PowerShell

    Adding ProcessName eligibles setting filtering following operators:

    contains
    does not contains
    beginns with
    is equal to
    is not equal to
    ends with
    is empty
    is not empty

    Entering msedge to the contains field, shows only process entries, which contain msedge

    filtered results in PowerShell grid
    filtering for processes which contain msedge in the name

    Now you can add additional filters.

    Additional filters in PowerShell
    I do add NPM(K) for the value 19
    Filter for npmk equals 19

    We can even now sort all the stuff by clicking on the column, which we want to sort for.

    Sort for CPU in GUI
    Sorting for highest CPU usage

    Export objects to Excel

    If you mark everything with CTRL + A, you can copy all the stuff and put it e.g. straigth to Excel

    Exporting objects to Excel
    Easy way to export to excel

    If you want to process further with powershell, you can mark the entities needed and click on OK. I selected the first and the thridh entity.

    Picture selecting only two entities
    selecting only two entities

    As you see, PowerShell returns my selection

    PowerShell return

    Conclusio

    As you can see you can save time and coding lines, when use are working inteactively on PowerShell by filter forPowerShell objects with Out-Gridview.

    You might find intersting

    Filtering items with CAML. For further learnings check out the post: Filtering for SharePoint items with CAML Queries | SPO Scripts

    Official documentation of Microsoft for the cmdlet Out-Gridview: Out-GridView (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Docs

  • How I easily add Webparts to SharePoint Pages by PowerShell

    How I easily add Webparts to SharePoint Pages by PowerShell

    A page consists of multiple webparts. With the webparts you can refer to other lists, libraries and apps within a page. You can add webparts SharePoint Pages manually and programmatically to a page. Adding webparts to single sites, can be done straight forwards in the edit section of the pages. If you want to do it in multiple sites and want to ensure, that the pages do look identical in terms of the structure, add webparts to SharePoint Pages with PowerShell! In this post I will show you how to do add webparts to pages manually and with PowerShell.

    Add Webparts to SharePoint Pages manually

    If you want to add webparts to SharePoint pages manually, you have to edit the page by clicking on edit.

    webpart page edit

    Now you can add the webpart to areas, where you get displayed a red line with a plus:

    Like here

    Add webpart to homepage

    or here:

    Add webpart to homepage

    After clicing on the red cross, you have to choose the webpart and can add. In my example, I want to display a user.

    Add people webpart
    Add the webpart name and set a person.
    set webpart properties

    If you want to display the changes to all users, click on publish (1) , otherwise click save as draft (2), so only you can see the changes.

    save or publish the page

    I have published the page, so every user can now see my change:

    published webpart page

    Add Webparts to SharePoint pages with PowerShell

    Recommendations

    • Use a code editor, which can format JSON properly – otherwise your code will look like a mess. I would recommend Visual Studio Code
    • Refresh the $Page variable fore each change you make on your page – Otherwise you will experience, that the homepage will be messed up

    Building webpart Components

    In the first step, I would recommend to design the webpart in the worbench page. You can find the workbench page of your site, by calling following URL:

    https://DOMAIN.sharepoint.com/sites/SITE/_layouts/15/workbench.aspx

    For my demo page it is:

    https://devmodernworkplace.sharepoint.com/sites/Sales/_layouts/15/workbench.aspx

    You can ignore the warning:

    warning, which you can ignore in workbench

    Now add the webpart, which you want manually.

    demo webpart in workbench

    Now click on “Web part data” and copy the yellow marked contents

    webpart code from web part data

    Put it in an editor like visual studio code and I would recommend changing to language to JSON.

    visual studio code changing language

    After doing that, make a right click and click on format document.

    Formatting document in visual studio code

    Now copy the content of WebPartData into a new tab (CTRL + N).

    Add { as the first character and } as the last character. After doing this, format the document.

    formatting document in visual studio code

    Your JSON should look like this:

    {
        "webPartData": {
            "id": "7f718435-ee4d-431c-bdbf-9c4ff326f46e",
            "instanceId": "ad75d0d7-81be-4271-b809-405a30d161d2",
            "title": "People",
            "description": "Display selected people and their profiles",
            "audiences": [],
            "serverProcessedContent": {
                "htmlStrings": {},
                "searchablePlainTexts": {
                    "title": "Added by PowerShell",
                    "persons[0].name": "Serkar Aydin",
                    "persons[0].email": "Serkar@devmodernworkplace.onmicrosoft.com"
                },
                "imageSources": {},
                "links": {}
            },
            "dataVersion": "1.3",
            "properties": {
                "layout": 1,
                "persons": [
                    {
                        "id": "Serkar@devmodernworkplace.onmicrosoft.com",
                        "upn": "",
                        "role": "",
                        "department": "",
                        "phone": "",
                        "sip": ""
                    }
                ]
            }
        }
    }

    Adding Webpart to Page with PowerShell

    In order to add webparts to SharePoint pages with PowerShell, we have to connect to SharePoint. If you are doing this the first time, check out the post: Connect to SharePoint Online with PowerShell (workplace-automation.com/)

    $Credential = Get-Credential
    $SiteUrl = "https://devmodernworkplace.sharepoint.com/sites/sales"
    Connect-PnPOnline -Url $SiteUrl -Credential $Credential

    Get the page, where you want to add the webparts to:

    $Page = Get-PnPPage -Identity "Home"

    After connecting, you can add the webpart by the previously created JSON content:

    $PersonJSON = @"
    {
        "webPartData": {
            "id": "7f718435-ee4d-431c-bdbf-9c4ff326f46e",
            "instanceId": "ad75d0d7-81be-4271-b809-405a30d161d2",
            "title": "People",
            "description": "Display selected people and their profiles",
            "audiences": [],
            "serverProcessedContent": {
                "htmlStrings": {},
                "searchablePlainTexts": {
                    "title": "Added by PowerShell",
                    "persons[0].name": "Serkar Aydin",
                    "persons[0].email": "Serkar@devmodernworkplace.onmicrosoft.com"
                },
                "imageSources": {},
                "links": {}
            },
            "dataVersion": "1.3",
            "properties": {
                "layout": 1,
                "persons": [
                    {
                        "id": "Serkar@devmodernworkplace.onmicrosoft.com",
                        "upn": "",
                        "role": "",
                        "department": "",
                        "phone": "",
                        "sip": ""
                    }
                ]
            }
        }
    }
    "@
    
    Add-PnPPageWebPart -Page $Page -DefaultWebPartType People -WebPartProperties $PersonJSON -Section 1 -Column 1 -Order 3

    The result of the added webpart looks like this:

    added webpart with powershell

    Bonus: Ready-to-use script

    The ready to use script looks like this:

    $Credential = Get-Credential
    $SiteUrl = "https://devmodernworkplace.sharepoint.com/sites/sales"
    Connect-PnPOnline -Url $SiteUrl -Credential $Credential
    
    
    $Page = Get-PnPPage -Identity "Home"
    
    $PersonJSON = @"
    {
        "webPartData": {
            "id": "7f718435-ee4d-431c-bdbf-9c4ff326f46e",
            "instanceId": "ad75d0d7-81be-4271-b809-405a30d161d2",
            "title": "People",
            "description": "Display selected people and their profiles",
            "audiences": [],
            "serverProcessedContent": {
                "htmlStrings": {},
                "searchablePlainTexts": {
                    "title": "Added by PowerShell",
                    "persons[0].name": "Serkar Aydin",
                    "persons[0].email": "Serkar@devmodernworkplace.onmicrosoft.com"
                },
                "imageSources": {},
                "links": {}
            },
            "dataVersion": "1.3",
            "properties": {
                "layout": 1,
                "persons": [
                    {
                        "id": "Serkar@devmodernworkplace.onmicrosoft.com",
                        "upn": "",
                        "role": "",
                        "department": "",
                        "phone": "",
                        "sip": ""
                    }
                ]
            }
        }
    }
    "@
    
    Add-PnPPageWebPart -Page $Page -DefaultWebPartType People -WebPartProperties $PersonJSON -Section 1 -Column 1 -Order 3

    Further Documentation

    If you are curious about webpart development and the workbench page check: Build your first SharePoint client-side web part (Hello World part 1) | Microsoft Docs

    Alternative to Visual Studio Code, you can use following link: JSON Formatter & Validator (curiousconcept.com)

  • Access SharePoint via Graph API in PowerShell

    Access SharePoint via Graph API in PowerShell

    Sometimes the use of PNP.PowerShell might not be sufficient. I encountered this experience, when I wanted to find out the usage of all sites. The Graph API provides methods, which you can use in your PowerShell Scripts. So in my example I wanted to get unused Sites with PowerShell. If you want to make use of it, you have to register an enterprise application and afterwards you can retrieve the information with an HTTP-Webrequest. In the following I will show you step by step how to access your SharePoint tenant with Graph API in PowerShell.


    Considerations – Find the right Graph API Method

    The Graph API has multiple methods, which we can use to analyze and change the content of our M365 services. In order to find the right method for your plan, check folllowing resources to see what the Graph API is capable of Microsoft Graph REST API v1.0 reference – Microsoft Graph v1.0 | Microsoft Docs. Based on the needed methods, you have to set up your enterprise application.

    Let’s assume, that you want to see the site usage of all sites in your tenant. In order to do this, you have to make use of following API method:

    GET /reports/getSharePointSiteUsageDetail(period='{period_value}’)
    GET /reports/getSharePointSiteUsageDetail(date={date_value})

    This API requires following permissions. We will consider them in this article. I want to analyze the sharepoint usage and want to update it to a list afterwards, that’s why I will make use of Application – Reports.Read.All

    Permission typePermissions (from least to most privileged)
    Delegated (work or school account)Reports.Read.All
    Delegated (personal Microsoft account)Not supported.
    ApplicationReports.Read.All
    SharePointSiteUsage Method Screenshot Graph API
    reportRoot: getSharePointSiteUsageDetail – Microsoft Graph v1.0 | Microsoft Docs

    Register the Enterprise Application

    After we figured out what permissions we need, we register the app.

    Prerequisites

    You have to have the role ‘Global Administrator’ to grant the permissions for an Enterprise Application.

    Registration

    Visit the Azure Portal URL and switdh to the app registrations sites. Directlink: App registrations – Microsoft Azure

    Click on new registration

    Register New App for Graph Api

    Give your application a name, click on Accounts in this organizational directory only, select mobile as platform, after that click on register.

    Application Registration for Graph Api

    Take a note of the Application (client) ID, you will need it to authenticate against the Graph API.

    Enterprise Application

    Grant API Permissions for App Registration

    After creating the app, we have to give it the permissions, which we have defined in the first step.

    Enterprise Application API permission

    Click on Microsoft Graph.

    Graph API screenshot

    Grant it Application permissions

    Application Permissions

    Now you have to select the permissions, for which you want to use the Graph API. I just need the information for Reports.Read.All. If you don’t know which permission to take, check the considerations part of this post.

    reports.read.all permission for Graph API

    As you can see, the permission is not granted for this tenant.

    not granted permissions screenshot for Graph API

    Create Client Secret for App Registration

    In order to authenticate to the Graph API in PowerShell, you have to create a client secret.

    Click on Certificates & secrets and then on New client secret

    Create client secret for Graph API enterprise application

    Set a Description and define when it will be expiring. I would recommend to give it a description, which you can recognize, for what it will be used in future. I have set 24 months, because I want to make use it in an automation, which should run for a long term. When finished, click Add.

    Usage Scripts client secret for Graph API

    Take Note of the value! You wont see it again, if you leave the site.

    Client Secret for Graph API obfuscated

    Consent the Requested permissions for App Registration

    Caution: You have to consent the created application with the global administrator role.

    https://login.microsoftonline.com/TENANTDomain/adminconsent?client_id=CLIENTID
    

    The URL for my dev tenant is like:

    https://login.microsoftonline.com/devmodernworkplace.onmicrosoft.com/adminconsent?client_id=949710fd-8d80-48ee-8c1b-a6f5e9e32be3

    Choose an account with global administrator role.

    Global administrator account login to grant permission for Graph API

    As you can see the permissions, which we have configured, are showing up:

    permission grant for created app for Graph API

    Since you have not set a redirect url, you will encounter this issue, which you can ignore.

    this ocurs, since we have not configured a redirect url

    Check Permission consent

    You can check that the permission is granted, if you see the green check marks.

    granted permission for enterprise application for Graph API

    Script To Acess SharePoint via the Graph API (PowerShell)

    The script contains two parts. The first part is about authentication and the second is about getting the data provided.

    Authentication

    I am making use of a credential export to be sure, that nobody steals the credentials, when it is in plain text. If you don’t know how to, check out: Use credentials in PowerShell – SPO Scripts

    Function Export-CredentialFile 
    {
        param(
        [Parameter(Mandatory=$true,Position=0)]
        $Username,
        [Parameter(Mandatory=$true,Position=1)] 
        $Path
        )
        
        
        While ($Path -eq "")
        {
            $Path = Read-Host "The path does not exist. Where should the credentials be exported to?"
        }
        $ParentPath = Split-Path $Path
        If ((Test-Path $ParentPath) -eq $false)
        {
            New-Item -ItemType Directory -Path $ParentPath
        }
        $Credential = Get-Credential($Username)
        $Credential | Export-Clixml -Path $Path
        Return $Credential
    }
    Function Import-CredentialFile ($Path)
    {
        if (! (Test-Path $Path))
        {
            Write-Host "Could not find the credential object at $Path. Please export your credentials first"
            Export-CredentialFile
        }
        Import-Clixml -Path $Path
    }
    $AppId = '949710fd-8d80-48ee-8c1b-a6f5e9e32be3'
    $CredentialPath = "C:\temp\$AppId.key"
    Export-CredentialFile -Username $AppId -Path $CredentialPath

    After doing this, we notice, that the file with the app id as name, has an encrypted password. So we splitted credentials from script to increase the security. This credential file can only be used on the machine and with the user, who has created it.

    PowerShell credential object

    If we run follwing script afterwards, we will notice, that the $AuthorizationRequest will show us a token with an bearer token.

    $AppId = '949710fd-8d80-48ee-8c1b-a6f5e9e32be3'
    $CredentialPath = "C:\temp\$AppId.key"
    $AppCredential = Import-CredentialFile -Path $CredentialPath
    
    $Scope = "https://graph.microsoft.com/.default"
    $Url = "https://login.microsoftonline.com/devmodernworkplace.onmicrosoft.com/oauth2/v2.0/token"
    
    $Body = @{
        client_id = $AppCredential.UserName
        client_secret = $AppCredential.GetNetworkCredential().password
        scope = $Scope
        grant_type = 'client_credentials'
    }
    
    $AuthorizationRequest = Invoke-RestMethod -Uri $Url -Method 'post' -Body $Body
    $AuthorizationRequest
    
    answer to the authorization request

    Access SharePoint Online with Authorization Token

    Now that we got the access token, we can connect to SharePoint Online with following script. You can use the uris (methods), defined in Microsoft docs.

    $Uri = "YOURURI"
    
    $Header = @{Authorization = "$($AuthorizationRequest.token_type) $($AuthorizationRequest.access_token)"}
    $SitesRequest = Invoke-RestMethod -Uri $Uri -Method 'Get'  -Headers $Header

    Get Site Usage Details

    You can get the site usage with following uri “https://graph.microsoft.com/beta/reports/getSharePointSiteUsageDetail(period='{D90}’)?`$format=application/json”. The number next to the D means the amount of days. So for my example it shows the usage of all sites for the last 90 days. You can replace D90 with D7, D30, and D180.

    With this script you can get the site usage for the last 90 days:

    $Uri = "https://graph.microsoft.com/beta/reports/getSharePointSiteUsageDetail(period='{D90}')?`$format=application/json"
    
    $Header = @{Authorization = "$($AuthorizationRequest.token_type) $($AuthorizationRequest.access_token)"}
    $SitesRequest = Invoke-RestMethod -Uri $Uri -Method 'get'  -Headers $Header 
    
    $Sites.value | Out-GridView -PassThru

    Bonus: Ready-to-Use Script

    If you want to make use of the script, you have to change the parameters $GraphUrl and $AppID.

    Param(
        $AppId = '949710fd-8d80-48ee-8c1b-a6f5e9e32be3',
        $GraphUrl = "https://login.microsoftonline.com/devmodernworkplace.onmicrosoft.com/oauth2/v2.0/token",
        $Scope = "https://graph.microsoft.com/.default",
        $Uri = "https://graph.microsoft.com/beta/reports/getOffice365GroupsActivityDetail`(`period=`'`D90`'`)?`$format=application/json",
    )
    
    Function Export-CredentialFile 
    {
        param(
        [Parameter(Mandatory=$true,Position=0)]
        $Username,
        [Parameter(Mandatory=$true,Position=1)] 
        $Path
        )
        
        
        While ($Path -eq "")
        {
            $Path = Read-Host "The path does not exist. Where should the credentials be exported to?"
        }
        $ParentPath = Split-Path $Path
        If ((Test-Path $ParentPath) -eq $false)
        {
            New-Item -ItemType Directory -Path $ParentPath
        }
        $Credential = Get-Credential($Username)
        $Credential | Export-Clixml -Path $Path
        Return $Credential
    }
    Function Import-CredentialFile ($Path)
    {
        if (! (Test-Path $Path))
        {
            Write-Host "Could not find the credential object at $Path. Please export your credentials first"
            Export-CredentialFile
        }
        Import-Clixml -Path $Path
    }
    
    $CredentialPath = "C:\temp\$AppId.key"
    Export-CredentialFile -Username $AppId -Path $CredentialPath
    
    $AppCredential = Import-CredentialFile -Path $CredentialPath
    
    $Body = @{
        client_id = $AppCredential.UserName
        client_secret = $AppCredential.GetNetworkCredential().password
        scope = $Scope
        grant_type = 'client_credentials'
    }
    
    $AuthorizationRequest = Invoke-RestMethod -Uri $GraphUrl -Method 'post' -Body $Body
    
    $Header = @{Authorization = "$($AuthorizationRequest.token_type) $($AuthorizationRequest.access_token)"}
    $SitesRequest = Invoke-RestMethod -Uri $Uri -Method 'get'  -Headers $Header 
    
    $SitesRequest.value | Out-GridView -PassThru

    Conclusio

    In this article you saw how to find the right permission for the enterprise application, which you need to access the SharePoint via the Graph API with PowerShell. After doing this, you can authenticate and analyze the data.

    Further Docs

    reportRoot: getSharePointSiteUsageDetail – Microsoft Graph v1.0 | Microsoft Docs

  • Use credentials in PowerShell

    Use credentials in PowerShell

    Credentials are necessary, if you want to access systems or APIs. Credentials can be used interactively and within a script. If you want to use credentials in PowerShell to automate processes, you might have to export your credentials and import them within your automation solution.

    In this article I will show you a few ways to make use of credentials, how to export and import credential objects and security considerations.

    Functionality

    You export the credentials with PowerShell the context the user whith which you are running the cmdlets, also the machine will be considered. So if you copy the credential file to another machine, it won’t work with the same user and you have to re-export the credential file.

    Security considerations

    Every initialization of credentials (interactive or automated) will lead to a prompt like this. When you export credentials with PowerShell consider following advice:

    Be sure, that you do this close to your authentication and do not enter your credentials on client/ servers, which are not trusted.

    Credential Prompt

    Why?

    You might think this is safe. If we try to read the password, it looks like this:

    Credential Secure String

    But there is still a way to read the password:

    Credential Clear String

    The risk will be there, when you enter your credentials to a shell, which you are not owner of.

    • Recommendation
      1. If you have to enter it in a Shell, which you do not controll, ensure, that the credential object will be initialized like this $Credential = $null
      2. If possible, try to run your scripts within windows authentication. In PNP.PowerShell it looks like this:
    Connect-PNPOnline -Url $Url -UseCurrentCredentials

    Using credentials interactively

    This will lead to a prompt.

    Credential Prompt
    $UserName = "John.Doe@Contoso.com"
    $Credential = Get-Credential ($UPN)

    You can than make use of the credentials like this:

    Connect-PnPOnline -Url "https://devmodernworkplace.sharepoint.com/sites/sales" -Credentials $Credential

    Using credentials non-interactively

    For this use case I wrote two ready-to-use functions.

    Function Export-CredentialFile 
    {
        param(
        [Parameter(Mandatory=$true,Position=0)]
        $Username,
        [Parameter(Mandatory=$true,Position=1)] 
        $Path
        )
        
        
        While ($Path -eq "")
        {
            $Path = Read-Host "The path does not exist. Where should the credentials be exported to?"
        }
    
        $ParentPath = Split-Path $Path
        If ((Test-Path $ParentPath) -eq $false)
        {
            New-Item -ItemType Directory -Path $ParentPath
        }
    
        $Credential = Get-Credential($Username)
        $Credential | Export-Clixml -Path $Path
        Return $Credential
    }
    
    Function Import-CredentialFile ($Path)
    {
        if (! (Test-Path $Path))
        {
            Write-Host "Could not find the credential object at $Path. Please export your credentials first"
            Export-CredentialFile
        }
        Import-Clixml -Path $Path
    }
    

    You can call the functions like this:

    $Path = "C:\keys\serkar.key"
    $UserName = "Serkar@devmodernworkplace.onmicrosoft.com"
    
    Export-CredentialFile -Username $UserName -Path $Path
    $Credential = Import-CredentialFile -Path $Path

    And afterwards you can connect to SharePoint. If you feel unsure about it, check out this post: Connect to SharePoint Online with PowerShell (workplace-automation.com/)

    Connect-PnPOnline -Url https://devmodernworkplace.sharepoint.com/sites/sales -Credentials $Credential

    But be sure to initialize the $Credential afterwards, so the potential security vector is as small as possible when you export credentials with PowerShell.

    Additional links

    Get-Credential (Microsoft.PowerShell.Security) – PowerShell | Microsoft Docs

  • How to use LINQ in PowerShell to compare arrays

    How to use LINQ in PowerShell to compare arrays

    LINQ stands for Language Integrated Query and if you use LINQ in PowerShell it might boost the performance of your scripts. In this article, I’ll show you how to use LINQ in PowerShell for comparing arrays.

    In this article, I will show you except and intersect of LINQ. There are many more methods. If you are interested in other methods, check out the official documentation of Microsoft.

    Concept of using LINQ in PowerShell

    We have three amounts red, orange and yellow. The aim is to only get a specific amount e.g. only the red portion.

    Amounts with strings
    $Red = @( "A", "B", "C")
    $Yellow = @("C","D","E")

    We also can do this for integer values.

    amounts with integers

    $Red = @( 1..5)
    $Yellow = @(4..10)

    Using LINQ for Strings in arrays

    I will show you how to use LINQ for string in this chapter. We will cover the methods except and intersect.

    String – Get the red amount

    In order to get the red amount, you have to do following:

    $Red = @( "A", "B", "C")
    $Yellow = @("C","D","E")
    
    $Left  = [string[]]$Red
    $Right = [string[]]$Yellow
    
    [string[]][Linq.Enumerable]::Except($Left, $Right)

    By doing this, you’ll get only A and B:

    LINQ get the red amount in PowerShell

    String – Get the yellow amount

    In order to get the red amount, you have to change Right and Left in the LINQ cmdlet:

    $Red = @( "A", "B", "C")
    $Yellow = @("C","D","E")
    
    $Left  = [string[]]$Red
    $Right = [string[]]$Yellow
    
    [string[]][Linq.Enumerable]::Except($Right, $Left)

    By doing this, you’ll get only D and E:

    LINQ get the yellow amount in PowerShell

    String – Get the orange amount

    If we want to get the orange amount, we have to make use of intersect

    $Red = @( "A", "B", "C")
    $Yellow = @("C","D","E")
    
    $Left  = [string[]]$Red
    $Right = [string[]]$Yellow
    
    [string[]][Linq.Enumerable]::Intersect($Left, $Right)
    LINQ get the orange amount in PowerShell

    String – Get everything except orange

    $Red = @( "A", "B", "C")
    $Yellow = @("C","D","E")
    
    $Left  = [string[]]$Red
    $Right = [string[]]$Yellow
    
    [string[]]([Linq.Enumerable]::Except($Left, $Right) + [Linq.Enumerable]::Except($Right, $Left))

    By doing this, you’ll everything but not C:

    linq - get everything except the orange amount powershell

    Using LINQ for integers in arrays

    I will show you how to use LINQ for integers in this chapter. We will cover the methods except and intersect.

    Integer – Get the red amount

    In order to get the red amount, you have to do following:

    $Red = @( 1..5)
    $Yellow = @(4..10)
    
    $Left  = [int[]]$Red
    $Right = [int[]]$Yellow
    
    [int[]][Linq.Enumerable]::Except($Left, $Right)

    By doing this, you’ll get only 1, 2, 3:

    LINQ get only the red amount in PowerShell

    Integer – Get the yellow amount

    In order to get the red amount, you have to change Right and Left in the LINQ cmdlet:

    $Red = @( 1..5)
    $Yellow = @(4..10)
    
    $Left  = [int[]]$Red
    $Right = [int[]]$Yellow
    
    [int[]][Linq.Enumerable]::Except($Right, $Left)

    By doing this, you’ll get only 6, 7, 8, 9, 10:

    LINQ get the yellow amount in PowerShell for integers

    Integer – Get the orange amount

    If we want to get the orange amount, we have to make use of intersect

    $Red = @( 1..5)
    $Yellow = @(4..10)
    
    $Left  = [int[]]$Red
    $Right = [int[]]$Yellow
    
    [int[]][Linq.Enumerable]::Intersect($Right, $Left)
    LINQ get the orange amount in PowerShell for integers

    Integer – Get everything except orange

    $Red = @( 1..5)
    $Yellow = @(4..10)
    
    $Left  = [int[]]$Red
    $Right = [int[]]$Yellow
    
    [int[]]([Linq.Enumerable]::Except($Left, $Right) + [Linq.Enumerable]::Except($Right, $Left))

    By doing this, you’ll everything but not 5:

    LINQ get the everything but not the orange amount in PowerShell for integers
    Read more
  • Dealing with existing SharePoint connections

    Dealing with existing SharePoint connections

    We all can Imagine the scenario. You create sites in sharepoint and now you want to edit multiple sites afterwards with PowerShell. In order to be safe, we have to check, wether an connection exists and if yes to disconnect the current connection to have a clean processing of the sites. In this article I want to show you how can achieve dealing with existing SharePoint connections. If you don’t know how to connect to SharePoint Online, check the article.


    Symptoms – How I tried it first

    Multiple paths as needles

    I tried to check the connection by a normal if query, but as you can see it throws everytime an error, so the script will be halted under normal circumstances. Changing the ErroActionPreference is something you could do for sure, but I would not recommend it, if you want to handle other upcomming potential errors of the API. So as you can see dealing with existing SharePoint connections in terms of checking, wether an connection exists, is not that easy.

    if ((Get-PnPConnection) )
    {
        Write-Host "Connection found"
    }
    Get-PnPConnection : The current connection holds no SharePoint context. Please use one of the Connect-PnPOnline commands which uses the -Url argument to connect.
    In Zeile:2 Zeichen:6
    + if ((Get-PnPConnection) )
    +      ~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [Get-PnPConnection], InvalidOperationException
        + FullyQualifiedErrorId : System.InvalidOperationException,PnP.PowerShell.Commands.Base.GetPnPConnection
    PS H:>> 
    if ((Get-PnPConnection) -ne $Null )
    {
        Write-Host "Connection found"
    }
    Get-PnPConnection : The current connection holds no SharePoint context. Please use one of the Connect-PnPOnline commands which uses the -Url argument to connect.
    In Zeile:2 Zeichen:6
    + if ((Get-PnPConnection) -ne $Null )
    +      ~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [Get-PnPConnection], InvalidOperationException
        + FullyQualifiedErrorId : System.InvalidOperationException,PnP.PowerShell.Commands.Base.GetPnPConnection
    PS H:>> 
    if ((Get-PnPConnection|out-null) -ne $Null )
    {
        Write-Host "Connection found"
    }
    Get-PnPConnection : The current connection holds no SharePoint context. Please use one of the Connect-PnPOnline commands which uses the -Url argument to connect.
    In Zeile:2 Zeichen:6
    + if ((Get-PnPConnection|out-null) -ne $Null )
    +      ~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [Get-PnPConnection], InvalidOperationException
        + FullyQualifiedErrorId : System.InvalidOperationException,PnP.PowerShell.Commands.Base.GetPnPConnection
    PS H:>> 
    if ((Get-PnPConnection -ErrorAction SilentlyContinue) -ne $Null )
    {
        Write-Host "Connection found"
    }
    Get-PnPConnection : The current connection holds no SharePoint context. Please use one of the Connect-PnPOnline commands which uses the -Url argument to connect.
    In Zeile:2 Zeichen:6
    + if ((Get-PnPConnection -ErrorAction SilentlyContinue) -ne $Null )
    +      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : NotSpecified: (:) [Get-PnPConnection], InvalidOperationException
        + FullyQualifiedErrorId : System.InvalidOperationException,PnP.PowerShell.Commands.Base.GetPnPConnection

    Solutions

    sewing kit

    Try Catch Solution

    In order to handle this situation, you have to catchup the error.

    The snippet tries to check wether there is an connection and if there is one, it will proceed and disconnect it. After disconnecting it I have set the variable $connection to $null, so I can process it later on.

    try 
    {
        Get-PnPConnection -ErrorAction Stop
        Disconnect-PnPOnline
        $Connection = $null
    }
    catch 
    {
        $Connection = $null
    }

    BONUS 1: Invoke-PNPConnection with Credential object (No MFA enforcement)

    A function which handles the whole procedure of cutting of the connection and reconnecting, makes the handling easier. In this case I have added an additional check of the contents of lists, because sometimes you do connect, but experience that the webserver is not ready yet – basically you get an 403 FORBIDDEN message in PowerShell.

    NOTE: This will only work If your user has no MFA enforcement. If you have MFA enabled, I have another function for you.

    Function Invoke-PNPConnection ($Url, $Cred)
    {
        try 
        {
            Get-PnPConnection -ErrorAction Stop
            Disconnect-PnPOnline
            $Connection  = $null
        }
        catch
        {
            $Connection  = $null
        }
        $i = 1
        while ($null -eq $Connection -and $i -le 6 -and $null -eq $Lists)
        {
            Write-Verbose "Trying to connect to $Url for the $i time" 
            $Lists = $null
            Connect-PnPOnline -Url $Url -Credentials $Cred
            $Lists = Get-PnPList -ErrorAction SilentlyContinue
            $i++
            if ($i -ne 1 -and $null -eq $Lists)
            {
                Start-Sleep -Seconds 30
                Write-Verbose "Wait 30 Seconds"
            }
        }
        Write-Verbose "Connection to $Url established"
    }

    You can call the function like this

    $Cred = get-credential
    $Url = "https://contoso.sharepoint.com/sites/Sales"
    Invoke-PNPConnection -Url $Url -Cred $Cred

    Bonus 2: Invoke-PNPConnection interactively (MFA enforced)

    So if you use the scripts interactively (with MFA enforced users), you can make use of this function

    Function Invoke-PNPConnection ($Url)
    {
        try 
        {
            Get-PnPConnection -ErrorAction Stop
            Disconnect-PnPOnline
            $Connection  = $null
        }
        catch
        {
            $Connection  = $null
        }
        $i = 1
        while ($null -eq $Connection -and $i -le 6 -and $null -eq $Lists)
        {
            Write-Verbose"Trying to connect to $Url for the $i time" 
            $Lists = $null
            Connect-PnPOnline -Url $Url -Interactive
            $Lists = Get-PnPList -ErrorAction SilentlyContinue
            $i++
            if ($i -ne 1 -and $null -eq $Lists)
            {
                Start-Sleep -Seconds 30
                Write-Verbose "Wait 30 Seconds"
            }
        }
        Write-Verbose "Connection to $Url established"
    }

    Start the function like this


    $Url = "https://contoso.sharepoint.com/sites/Sales"
    Invoke-PNPConnection -Url $Url

    Conclusio

    There are ways to deal with the connections, you just have to think a bit OOTB 🙂

    patched teddy

    You might find intersting

    If you are not familiar with connecting to SharePoint, check out this Post: Connect to SharePoint Online with PowerShell

    Original article of PNP Connecting with PnP PowerShell | PnP PowerShell




    Images:
    Bild von Meine Reise geht hier leider zu Ende. Märchen beginnen mit auf Pixabay

    Bild von vargazs auf Pixabay

    Bild von Ina Hoekstra auf Pixabay

    Bild von saulhm auf Pixabay

  • Filtering for SharePoint items with CAML Queries

    Filtering for SharePoint items with CAML Queries

    Most of our times, we just need just a bunch of items, to export them or to change their values. This post should help you to show, how to handle filtering for SharePoint items. Besides filtering for SharePoint items with Where-Object, you can also make use of CAML (Collaborative Application Markup Language), which lets you get only the items, you need. It might increase the performance of your queries, when you are dealing with large amounts of data.

    Where are the items, which I am looking for?

    Preqrequistes

    If we want to achieve filtering for SharePoint items, with a CAML query, we have to fulfill following prerequisites:

    1. Permissions to access the list
    2. Installed Module PNP.Powershell. If you don’t know how to, check the post.
    3. Connection to the site via PNP.PowerShell. If you don’t know how to, check the post.

    Considerations

    1. You should take care of the case sensitivity of operands and column names
    2. You should take care of the <view> part. Sometimes it is needed, sometimes not – so I would rely on the examples.

    Query Schema

    A query is structured like this

     "<View><Query><Where><LOGICAL OPERATOR><FieldRef Name='INTERNAL NAME OF COLUMN'/><Value Type='VALUE TYPE'>VALUE</Value></LOGICAL OPERATOR></Where></Query></View>"

    You can find the internal name of columns in two ways:

    PowerShell or GUI.

    Explanation for PowerShell: Getting FieldValues of Items | SPO Scripts
    Explanation for GUI: Determine internal name of SharePoint Columns with GUI (workplace-automation.com/)

    Value types

    TypeMeaningExamples
    BooleanIt means true or false. You can find this in yes/no checkboxestrue, false
    true reflects 1
    false reflects 0
    ChoiceIt reflects the choices in your sharepoint listapple, banana
    CurrencyIt reflects the amount of an defined currency5$
    DateTimeIt reflects a timestamp23.06.2021 15:30
    GUIDGlobally Unique Identifier (GUID)6154ff96-8209-457b-86dd-ee7dcd80b584
    IntegerIt reflects a number as an integer 10
    LookupLinks to another list: for example an Orders list may have a lookup field that links to customers in a Customer list;Füller AG
    NoteReflects a multi line text field. Not sortable or groupableLorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec odio.

    Quisque volutpat mattis eros. Nullam malesuada erat ut turpis. Suspendisse urna nibh, viverra non, semper suscipit, posuere a, pede.
    TextReflects a single line text field. Sortable and groupable. Corresponds to the nvarchar SQL data type and represented by the SPFieldText class.Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
    UserA Lookup field that references the UserInfo database table.Email TypeId LookupId LookupValue
    —– —— ——– ———–
    Serkar@devmodernworkplace.onmicrosoft.com {c956ab54-16bd-4c18-89d2-996f57282a6f} 11 Serkar Aydin
    Source: Field element (Field) | Microsoft Docs

    Logical Comparison Operators

    In this case X means your entry

    OperatorMeaning
    BeginsWithThe existing value begins with X
    ContainsThe existing value contains x
    DateRangesOverlapThe existing date overlaps the date range defined in x
    |---- 01.01.-07.01 ------|
    |---02.01-09.01 -----|
    EqThe existing value equals x
    GeqThe existing value is greater or equal x
    GtThe existing value is greater than x
    InX is one of the existing values
    Includeschecks, whether x is in the defined values
    NotIncluseschecks, whether x is not in the defined values
    IsNotNullChecks wheter the existing value is not null
    IsNullChecks wheter the existing value is null
    LeqThe existing value is lower equal x
    LtThe existing value is lower than x
    Source: Query schema in CAML | Microsoft Docs

    In order to filter by query paramter, you have to define a filter query, depending on your datatype (string, integer, boolean..) you have to choose a different query value type.

    Logical Joins

    OperatorMeaning
    AndBoth query operations have to be fulfilled
    OrOnly one query operation have to be fulfilled
    Source: Query schema in CAML | Microsoft Docs

    Query Examples

    My blog would not keep it’s promise, If you would not find examples, which give you a fast way to adapt the scripts, so here we go!

    In my example, I am using my demo opportunities list. I have marked the names of the columns, the value types, the operands and the actual values bold. Mostly I am using the logical operator “eq”, but I think if you got the basic concept of this, you can adapt it to your solution easily and if not, we will find a way together.

    Example for boolean

    If you want to find items with TRUE values, you have to enter 1. For FALSE values, you have to make use of 0.

    If boolean should be true:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Win'/><Value Type='Boolean'>1</Value></Eq></Where></Query></View>"

    If boolean should be false:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Win'/><Value Type='Boolean'>0</Value></Eq></Where></Query></View>" 

    Example for choice

    If you want to filter for values choice values, you have to make use of a query like this:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Product'/><Value Type='Choice'>SAP</Value></Eq></Where></Query></View>"

    Example for currency

    You have to enter the value of the amount without the currency sign.

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='DealSize'/><Value Type='Currency'>40000</Value></Eq></Where></Query></View>"

    Example for DateTime

    You have to format date times according to this format (ISO8601).

    yyyy-MM-ddTHH:mm:ssZ

    You can do this by appending -Format s, when creating the variable

    $CreationDate = Get-Date "16.06.2021 20:04" -Format s

    If DateTime should exactly match a specific date

    $CreationDate = Get-Date "16.06.2021 20:04" -Format s
    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Eq></Where></Query></View>"

    If DateTime should be after a specific date

    Example: I want to find all items, created after 15.06.2021.

    $CreationDate = Get-Date "15.06.2021" -Format s
    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Gt></Where></Query></View>"

    If DateTime should be before a specific date

    Example: I want to find all items, created before 15.06.2021.

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Lt><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Lt></Where></Query></View>"

    Example for GUID

    [GUID]$UniqueID= "b4ae9e9f-7103-459a-acb2-73573d035b36"
    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='UniqueId'/><Value Type='GUID'>$UniqueID</Value></Eq></Where></Query></View>"

    Example for integer

    In this case I want to find all opportunites with 2 stakeholders.

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Stakeholder'/><Value Type='Integer'>2</Value></Eq></Where></Query></View>"

    Example for lookup

    Get-PnPListItem -List "Opportunities" -Query "<Query><Where><Eq><FieldRef Name='Contact'/><Value Type='Lookup'>Sus Spicious</Value></Eq></Where></Query>"

    Example for Note aka multi line text

    Get-PnPListItem -List "Opportunities" -Query "<Query><Where><Eq><FieldRef Name='Notes'/><Value Type='Note'>He was really curious.</Value></Eq></Where></Query>"

    Example for text aka string

    In this Query, I am looking for items, where the title equals Opp 3.

    Get-PnPListItem -List "Opportunities" -Query "<Query><Where><Eq><FieldRef Name='Title'/><Value Type='Text'>Opp 3</Value></Eq></Where></Query>"

    Example for user

    In this query, I am looking for items, where the authors UPN is Serkar@devmodernworkplace.onmicrosoft.com.

    Get-PnPListItem -List "Opportunities" -Query "<Query><Where><Eq><FieldRef Name='Author' /><Value Type='User'>Serkar@devmodernworkplace.onmicrosoft.com</Value></Eq></Where></Query>"

    Example for OR

    In this query, I am looking for items, where the value for Stakeholder is 1 or the value Win is yes.

    Get-PnPListItem -List $ListName -Query "<View><Query><Where><Or><Eq><FieldRef Name='Stakeholder'/><Value Type='Integer'>1</Value></Eq><Eq><FieldRef Name='Win'/><Value Type='Boolean'>1</Value></Eq></Or></Where></Query></View>"

    Example for AND

    In this query, I am looking for items, where the value for Stakeholder is 1 and the value Win is yes.

    Get-PnPListItem -List $ListName -Query "<View><Query><Where><And><Eq><FieldRef Name='Stakeholder'/><Value Type='Integer'>1</Value></Eq><Eq><FieldRef Name='Win'/><Value Type='Boolean'>1</Value></Eq></And></Where></Query></View>"

    Complete example

    $Url = "https://devmodernworkplace.sharepoint.com/sites/Sales" 
    $ListName = "Opportunities"
    
    Connect-PnPOnline -Url $Url -Interactive
    
    $AmountOfStakeholders = 2
    $ColumName = "Stakeholder"
    
    Get-PnPListItem -List $ListName -Query "<View><Query><Where><Eq><FieldRef Name='$ColumName'/><Value Type='Integer'>$AmountOfStakeholders</Value></Eq></Where></Query></View>"

    Troubleshooting

    I am getting to many items

    Error

    You get nearly every item in the list, but you are filtering for specific SharePoint items

    Cause

    Maybe you forgot the <View> part?

    Resolution

    Without view:

    With view:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Eq><FieldRef Name='Stakeholder'/><Value Type='Integer'>2</Value></Eq></Where></Query></View>"

    Exception from HRESULT: 0x80131904

    Error message:

    Get-PnPListItem : Exception from HRESULT: 0x80131904
    In Zeile:1 Zeichen:1
    + Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><gt ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : WriteError: (:) [Get-PnPListItem], ServerException
        + FullyQualifiedErrorId : EXCEPTION,PnP.PowerShell.Commands.Lists.GetListItem

    Cause:

    You did not care about the case sensitivity of the logical operands

    Resolution:

    Wrong:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><gt><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></gt></Where></Query></View>"

    Right:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Gt></Where></Query></View>"

    Field types are not installed properly

    Error message in german

    Get-PnPListItem : Mindestens ein Feld ist nicht richtig installiert. Wechseln Sie zur Listeneinstellungsseite, um diese Felder zu löschen.
    In Zeile:1 Zeichen:1
    + Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : WriteError: (:) [Get-PnPListItem], ServerException
        + FullyQualifiedErrorId : EXCEPTION,PnP.PowerShell.Commands.Lists.GetListItem

    Error message in english

    Get-PnPListItem : One or more field types are not installed properly. Go to the list settings page to delete these fields.
    In Zeile:1 Zeichen:1
    + Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : WriteError: (:) [Get-PnPListItem], ServerException
        + FullyQualifiedErrorId : EXCEPTION,PnP.PowerShell.Commands.Lists.GetListItem

    Cause

    You did not care of the case sensitivity of the column name

    Resolution

    Wrong:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt><FieldRef Name='created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Gt></Where></Query></View>"

    Right:

    Get-PnPListItem -List "Opportunities" -Query "<View><Query><Where><Gt><FieldRef Name='Created'/><Value Type='DateTime' IncludeTimeValue='FALSE'>$CreationDate</Value></Gt></Where></Query></View>"

    Bild von Deedee86 auf Pixabay

  • Getting Fieldvalues of Items

    Getting Fieldvalues of Items

    Getting all values of all items

    If you want to retrieve all fieldvalues of an item, you can use following steps. I am getting the opportunities of the sales department

    Screenshot of SharePoint List with values of items
    • Connect to SharePoint Online with PNP. If you don’t know how to, check the post. I am connecting with the sales site

      Connect-PnPOnline -Url "https://devmodernworkplace.sharepoint.com/sites/Sales" -Interactive
    • After connecting, get the list.

      $Items =Get-PnPListItem -List "YourList"

      I am using the list opportunities:

      $Items =Get-PnPListItem -List "Opportunities"

      If you don’t know what the internal name of your list is, you can make use of the cmdlet

      $Items = Get-PNPListItem | Out-Gridview -Passthru
    out grid view with lists

    After getting the items, you can get all fieldvalues like this:
    $Items.Fieldvalues

    Screenshot of all values of items

    Filter items by specific value

    You can filter items like this. You have to use the key, which you get with $Items. FieldValues

    Filter by Where Object

    $Item = $Items | Where-Object {$_.Fieldvalues["KEY"] -eq "VALUE"}

    I filter for items with the title Opp 2

    $Item = $Items | Where-Object {$_.Fieldvalues["Title"] -eq "Opp 2"}

    Filter by -Query Parameter

    I have created a separate post for this. Check it out.

    Getting only specific amount of Items

    It might happen, that your list contains many items. In order to get only 1000 Items, make use of the parameter -PageSize

    Get-PnPListItem -List "Opportunities" -PageSize 1000

    Getting a specific value for specific item

    First get one single item, either by filtering, or by using the index

    You can use indexing with following method

    $Item= $Items[INDEX]

    For the first Item: $Item = $Items[0]

    For the second Item: $Item = $Items[1] etc,

    If you got a single item, you can get the fieldvalues like this:

    $item.FieldValues.KEY

    In my case I use the key “Product”

    $item.FieldValues.Product

    screenshot of specific value

    Or “Contact”

    screenshot of specific contact value

    Example script to get all fieldvalues for all items

    $Url = "https://devmodernworkplace.sharepoint.com/sites/Sales" 
    $ListName = "Opportunities"
    
    Connect-PnPOnline -Url $Url -Interactive
    $Items = Get-PnPListItem -List $ListName
    
    $Items.Fieldvalues

    Example script to get all fieldvalues for a specific item

    $Url = "https://devmodernworkplace.sharepoint.com/sites/Sales" 
    $ListName = "Opportunities"
    
    Connect-PnPOnline -Url $Url -Interactive
    $Items = Get-PnPListItem -List $ListName
    
    $Item =  $Items | Where-Object {$_.Fieldvalues["Title"] -eq "Opp 2"}
    
    $item.FieldValues