|

Read SharePoint Lists, Items and Files with PnP PowerShell

You request an item from a SharePoint list, and you receive forty-five columns. You query the library for its files, and it returns fifty percent of pages generated by SharePoint, not uploaded documents. “PnP PowerShell” reading is straightforward until you start receiving something else than what you expect.

Everything above was tested against the actual site running PnP.PowerShell 2.12.0. The code output that is shown below was generated by my machine.

Listing lists

The first command everyone runs:

Get-PnPList | Measure-Object | Select-Object Count
Count
-----
   19

Nineteen, on a site where I had created three. The rest are SharePoint’s own. So you filter out the hidden ones:

Get-PnPList | Where-Object { -not $_.Hidden } | Select-Object Title, ItemCount, BaseTemplate
Title             ItemCount BaseTemplate
-----             --------- ------------
Departments               4          100
Documents                 0          101
Events                    0          106
Form Templates            0          101
Project Documents         9          101
Projects                 25          100
Site Pages                1          119
Style Library             0          101

Eight items total, yet only three of them belong to me. Documents, Events, Form Templates, Site Pages, and Style Library are all created automatically by SharePoint and not hidden.

There is no flag “list made by human”. To find out lists made by humans use filter “what you know” by title or by BaseTemplate 100 for list and 101 for document library..

Reading one item

$item = Get-PnPListItem -List 'Projects' -Id 1
$item.FieldValues

The following five-column list has forty-five items that include such names as Title, Budget, ProjectStatus, ComplianceAssetId, MetaInfo, owshiddenversion, ScopeId, SMTotalFileStreamSize, WorkflowVersion, and a few more.

To retrieve data using the property name, use the index operation with the internal name of the property

$item['Title']
$item['ProjectStatus']
$item['Budget']
Project 01
Not Started
1000

Internal names are not display names. A column shown as Project Status can have an internal name of ProjectStatus, or Project_x0020_Status if it was created through the browser with a space in the name. Check before you guess:

Get-PnPField -List 'Projects' | Select-Object Title, InternalName

Not every field is a string

This is where exports go wrong. Look at what comes back for the built-in Author field:

Author   Microsoft.SharePoint.Client.FieldUserValue
Editor   Microsoft.SharePoint.Client.FieldUserValue

Person fields, lookup fields and taxonomy fields are objects. Pipe one straight into a CSV and that is the text you get in the file. You want the property off it:

$item['Author'].LookupValue    # display name
$item['Author'].Email
$item['Author'].LookupId

Same shape for lookups: LookupValue for the text, LookupId for the ID.

Ask for less

By default you pull every one of those forty-five fields for every item. Ask only for what you need:

Get-PnPListItem -List 'Projects' -Fields 'Title','ProjectStatus' -PageSize 500

And filter on the server rather than dragging everything down and filtering locally:

$caml = "<View><Query><Where><Eq><FieldRef Name='ProjectStatus'/><Value Type='Text'>Blocked</Value></Eq></Where></Query></View>"
Get-PnPListItem -List 'Projects' -Query $caml

I compared both to my 25 items. The filtering locally took 998ms, the CAML query took 913ms. Not much difference between them, and I am not going to pretend otherwise.

This makes all the difference at scale. Beyond 5,000 items, the list view threshold renders the lazy one not just slow but erroneous, and at that point, you are forced to rewrite the script. Develop the habit while your lists are still small.

Files, and the folder nobody asks for

Listing a library looks fine:

Get-PnPFolderItem -FolderSiteRelativeUrl 'Project Documents'
Name      Type
----      ----
Archive   Folder
Contracts Folder
Forms     Folder
Reports   Folder

I created three of those. Forms is SharePoint’s, and it is in every library on every site.

Which becomes a real problem the moment you go recursive:

Get-PnPFolderItem -FolderSiteRelativeUrl 'Project Documents' -ItemType File -Recursive
Old Contract.txt        /Project Documents/Archive/2024/Old Contract.txt
Statement of Work.txt   /Project Documents/Contracts/Statement of Work.txt
AllItems.aspx           /Project Documents/Forms/AllItems.aspx
Combine.aspx            /Project Documents/Forms/Combine.aspx
DispForm.aspx           /Project Documents/Forms/DispForm.aspx
EditForm.aspx           /Project Documents/Forms/EditForm.aspx
repair.aspx             /Project Documents/Forms/repair.aspx
template.dotx           /Project Documents/Forms/template.dotx
Thumbnails.aspx         /Project Documents/Forms/Thumbnails.aspx
Upload.aspx             /Project Documents/Forms/Upload.aspx
Monthly Report.txt      /Project Documents/Reports/Monthly Report.txt
Versioned Document.txt  /Project Documents/Reports/Versioned Document.txt

Twelve files, of which four are documents. The other eight are the library’s own form pages. If you are writing a report of “every file in this library”, or worse a script that deletes or moves things, filter Forms out:

Get-PnPFolderItem -FolderSiteRelativeUrl 'Project Documents' -ItemType File -Recursive |
    Where-Object { $_.ServerRelativeUrl -notmatch '/Forms/' }

File metadata without downloading the file

Get-PnPFile -Url "/sites/Demo/Project Documents/Reports/Versioned Document.txt" -AsListItem

-AsListItem gives you the metadata only. Without it you are pulling the bytes down, which you rarely want if all you needed was the modified date.

Version history is one shorter than you think

Get-PnPFileVersion -Url "/sites/Demo/Project Documents/Reports/Versioned Document.txt"
VersionLabel Created              Size
------------ -------              ----
1.0          7/21/2026 5:17:06 PM   30
...
11.0         7/21/2026 5:17:17 PM   31

There were eleven versions but the file states that it is at version 12.0

Get-PnPFileVersion will give you the previous versions and not the current one. This means that when counting the versions for calculating storage and deleting, note that the live file is not included in the count.

Related posts

Similar Posts