Une réorganisation documentaire, trois sites SharePoint, une quarantaine de bibliothèques et près de trois cents dossiers à créer. À la main, dans l’interface, c’est deux jours de clics et au moins une faute de frappe qui remontera six mois plus tard, quand quelqu’un cherchera Procédures qualité dans un dossier orthographié Procedures qualite. Ce guide montre comment déléguer ce travail à un script PowerShell piloté par un simple fichier Excel — celui que le métier remplit déjà.
Le résultat : la structure documentaire complète est provisionnée en quelques minutes, elle est rejouable à l’identique sur un autre tenant, et la source de vérité reste un classeur que n’importe quel chef de projet sait modifier.
Points clés
- Une ligne Excel = une bibliothèque + son arborescence complète de dossiers.
- Depuis septembre 2024, PnP.PowerShell exige votre propre application Entra ID : l’app multi-tenant PnP Management Shell a été supprimée.
Resolve-PnPFoldercrée toute la hiérarchie manquante en un appel — il remplace à lui seul la boucle récursive que la plupart des scripts réinventent.- SharePoint Online n’interdit que neuf caractères (
" * : < > ? / \ |). Nettoyer plus que ça abîme les noms métier pour rien. - Le chemin décodé complet est plafonné à 400 caractères, nom de domaine exclu : à vérifier avant de créer, pas après.
Ce que le script automatise
Le principe tient en trois blocs : un classeur Excel décrit la cible, PnP.PowerShell traduit chaque ligne en appels SharePoint, et le tenant se retrouve provisionné. Aucune interface, aucun clic, et surtout aucune divergence entre ce que le métier a validé dans le tableau et ce qui existe réellement dans SharePoint.

Prérequis
Comptez une vingtaine de minutes pour la mise en place initiale, dont l’essentiel part dans l’enregistrement de l’application Entra ID. Les exécutions suivantes ne demandent plus qu’une authentification interactive.
- PowerShell 7.4 ou supérieur. PnP.PowerShell v3 repose sur .NET 8 et refuse de se charger sur Windows PowerShell 5.1.
- Module PnP.PowerShell 3.x.
- Module ImportExcel 7.8.x de Douglas Finke — il lit les fichiers .xlsx sans qu’Excel soit installé sur la machine.
- Un compte disposant du rôle permettant de créer des listes sur les sites cibles (propriétaire de site suffit).
- Le droit d’enregistrer une application dans Entra ID, ou un administrateur qui le fera pour vous.
# Vérifier la version : 7.4 minimum pour PnP.PowerShell v3
$PSVersionTable.PSVersion
Install-Module -Name PnP.PowerShell -Scope CurrentUser
Install-Module -Name ImportExcel -Scope CurrentUser
Import-Module PnP.PowerShell
Import-Module ImportExcel
Si Install-Module échoue sur la politique d’exécution, lancez d’abord Set-ExecutionPolicy -Scope CurrentUser RemoteSigned.
Étape 1 — Enregistrer votre application Entra ID
Cette étape n’est plus optionnelle. L’application multi-tenant PnP Management Shell, qui servait d’identité par défaut à tous les scripts PnP, a été supprimée le 9 septembre 2024 pour des raisons de sécurité. Depuis, chaque organisation doit enregistrer sa propre application Entra ID et passer son identifiant à Connect-PnPOnline (documentation PnP PowerShell). Un script qui appelle Connect-PnPOnline -Url … -Interactive sans -ClientId ne fonctionne plus.
Bonne nouvelle : PnP fournit le cmdlet qui fait le travail à votre place.
# À lancer une seule fois par tenant, avec un compte habilité
Register-PnPEntraIDAppForInteractiveLogin `
-ApplicationName 'PnP.PowerShell.Provisioning' `
-Tenant 'contoso.onmicrosoft.com'
# Le cmdlet retourne l'AppId : conservez-le, c'est votre -ClientId
Par défaut, l’application demande le consentement pour quatre permissions déléguées : AllSites.FullControl, Group.ReadWrite.All, User.ReadWrite.All et TermStore.ReadWrite.All. Pour un usage limité au provisionnement de bibliothèques, restreignez-les via les paramètres -SharePointDelegatePermissions et -GraphDelegatePermissions : AllSites.Manage couvre la création de listes et de dossiers sans donner le contrôle total du tenant.
Étape 2 — Construire la matrice Excel
Quatre colonnes suffisent, et c’est volontaire : plus le classeur est simple, plus le métier le remplit sans erreur. La colonne Folders porte toute l’intelligence de l’arborescence, avec deux séparateurs à retenir.
| Colonne | Rôle | Exemple |
|---|---|---|
SiteUrl | Site SharePoint cible | https://contoso.sharepoint.com/sites/Operations |
LibraryName | Titre de la bibliothèque | QUALITE |
Description | Description affichée (facultative) | Documentation du système qualité |
Folders | Arborescence — ; sépare les branches, / les niveaux | Audits/Rapports;Procédures/Modes opératoires |
Une ligne comme Audits/Rapports;Audits/Plans d'action;Procédures/Modes opératoires produit deux dossiers racine (Audits et Procédures) et trois sous-dossiers. Inutile de déclarer les dossiers parents séparément : ils sont créés au passage.
Attention au point-virgule. Si vos noms de dossiers métier contiennent déjà des points-virgules, changez le séparateur dans le script (par exemple |) plutôt que de demander au métier de modifier sa nomenclature.
Étape 3 — Lire le classeur et créer les bibliothèques
La lecture du classeur tient en une ligne avec Import-Excel, qui renvoie un objet PowerShell par ligne, avec les en-têtes comme propriétés. Le point important vient juste après : on regroupe les lignes par site pour n’ouvrir qu’une seule connexion par site, et on inventorie les listes existantes une bonne fois pour toutes.
$rows = Import-Excel -Path $ExcelPath -WorksheetName $WorksheetName |
Where-Object { $_.SiteUrl -and $_.LibraryName } # ignore les lignes vides
foreach ($group in $rows | Group-Object -Property SiteUrl) {
Connect-PnPOnline -Url $group.Name -Interactive -ClientId $ClientId
# Un seul appel réseau, réutilisé pour toutes les lignes du site
$existingLists = @((Get-PnPList).Title)
foreach ($row in $group.Group) {
$libName = ConvertTo-SharePointSafeName -Name $row.LibraryName
if ($existingLists -contains $libName) {
Write-Host " = $libName (existe déjà)"
continue
}
$newList = New-PnPList -Title $libName -Template DocumentLibrary -OnQuickLaunch
if ($row.Description) {
Set-PnPList -Identity $newList.Title -Description ([string]$row.Description)
}
$existingLists += $libName
}
Disconnect-PnPOnline
}
Le filtre Where-Object mérite son existence : les classeurs remplis par des humains contiennent presque toujours des lignes fantômes en bas de tableau, invisibles à l’écran mais bien présentes pour Import-Excel. Sans ce filtre, le script tente de se connecter à une URL vide et s’arrête net.
Étape 4 — Créer l’arborescence en un seul appel
C’est ici que la plupart des scripts trouvés en ligne se compliquent inutilement. Ils découpent le chemin, testent l’existence de chaque niveau, reconstruisent le chemin parent, appellent Add-PnPFolder niveau par niveau. Une trentaine de lignes de logique récursive, avec les bugs de découpage qui vont avec.
Resolve-PnPFolder fait tout cela en un appel. La documentation PnP est explicite : le cmdlet « retourne un dossier à partir d’un chemin relatif au site, et le crée s’il n’existe pas » — hiérarchie complète incluse (Resolve-PnPFolder, PnP PowerShell). Il est idempotent par nature : rejoué sur une arborescence existante, il ne fait rien et ne lève pas d’erreur.
foreach ($rawPath in ([string]$row.Folders -split ';')) {
if ([string]::IsNullOrWhiteSpace($rawPath)) { continue }
# Chaque niveau est nettoyé séparément, puis le chemin est reconstruit
$segments = $rawPath.Split('/') |
ForEach-Object { ConvertTo-SharePointSafeName -Name $_ } |
Where-Object { $_ }
$target = "$libName/$($segments -join '/')"
# Crée toute la hiérarchie manquante, ne fait rien si elle existe
Resolve-PnPFolder -SiteRelativePath $target | Out-Null
}
Nettoyer chaque segment avant de rejoindre le chemin, et non le chemin entier, est le détail qui évite le bug le plus vicieux de cet exercice : si la fonction de nettoyage remplace les /, l’appliquer au chemin complet écrase la structure de dossiers et produit un unique dossier au nom absurde.
Étape 5 — Nettoyer les noms sans les mutiler
Les scripts de provisionnement héritent souvent d’une liste de caractères interdits recopiée d’un article de 2013. Résultat : Facturation 50% devient Facturation 50_, et le métier ne retrouve plus son dossier. La liste réelle est bien plus courte.
Microsoft documente neuf caractères refusés dans les noms de fichiers et de dossiers OneDrive et SharePoint : " * : < > ? / \ |, auxquels s’ajoutent les espaces en début et fin de nom, la séquence _vti_ et une poignée de noms réservés (CON, PRN, AUX, NUL, COM0 à COM9, LPT0 à LPT9, desktop.ini, .lock) — voir la page Restrictions et limitations dans OneDrive et SharePoint.
| Caractère | Statut réel | Ce qu’il faut faire |
|---|---|---|
" * : < > ? / \ | | Refusés | Remplacer par un tiret |
# et % | Autorisés dans les bibliothèques de documents depuis 2017 | Ne rien toucher |
. (point) | Autorisé, sauf en première ou dernière position | Rogner les points aux extrémités uniquement |
~ & { } ' | Autorisés | Ne rien toucher |
| Espace initial ou final | Refusé | .Trim() |
Le support de # et % dans les noms de fichiers et de dossiers a été annoncé par Microsoft en 2017 et activé par défaut pour les nouveaux tenants dès juin 2017 (Microsoft Community Hub). Concrètement : si votre fonction de nettoyage remplace encore ces deux caractères, elle dégrade des noms parfaitement valides depuis presque dix ans. La restriction subsiste en dehors des bibliothèques de documents — noms de sites et objets web — mais pas pour les dossiers que vous créez ici.
function ConvertTo-SharePointSafeName {
[OutputType([string])]
param([Parameter(Mandatory)][AllowEmptyString()][string]$Name)
if ([string]::IsNullOrWhiteSpace($Name)) { return '' }
# 1. Les neuf caractères réellement refusés
$clean = $Name -replace '["*:<>?/\\|]', '-'
# 2. Espaces et points en début / fin
$clean = $clean.Trim().Trim('.').Trim()
# 3. Séquence _vti_ interdite partout
$clean = $clean -replace '_vti_', 'vti'
# 4. Noms réservés hérités de MS-DOS
if ($clean -match '^(\.lock|CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9]|desktop\.ini)$') {
$clean = "$clean-1"
}
# 5. Un segment ne dépasse pas 255 caractères
if ($clean.Length -gt 255) { $clean = $clean.Substring(0, 255).Trim() }
return $clean
}
Le budget des 400 caractères
La contrainte qui casse les provisionnements ambitieux n’est pas la liste des caractères : c’est la longueur du chemin. Le chemin décodé complet — nom du site, nom de la bibliothèque, dossiers et nom de fichier — ne peut pas dépasser 400 caractères pour OneDrive et SharePoint dans Microsoft 365, le nom de domaine n’étant pas compté. Chaque nom de fichier ou de dossier est par ailleurs limité à 255 caractères.
Le compte monte vite. Un chemin de site, une bibliothèque au nom explicite et trois niveaux de sous-dossiers consomment facilement une centaine de caractères avant même qu’un document soit déposé. Il ne reste alors plus grand-chose pour les noms de fichiers que produiront les utilisateurs — et ce sont eux qui déclencheront l’erreur, six mois après votre livraison. D’où l’intérêt de contrôler la longueur avant d’appeler Resolve-PnPFolder : mieux vaut refuser le dossier et le tracer dans le journal que laisser une arborescence provisionnée à moitié.
Les cinq pièges du script qui « marche quand même »
La version initiale de ce script tournait correctement en démonstration, sur trois bibliothèques et neuf dossiers. Passée sur une matrice de production, elle a révélé cinq défauts qu’on retrouve dans la quasi-totalité des scripts de provisionnement partagés en ligne. Les voici, du plus sournois au plus visible.
1. La variable écrasée au milieu de la boucle
Une ligne $libName = $libName.Replace("'","") placée à l’intérieur de la boucle de création des dossiers, pour contourner un problème d’apostrophe dans les URL. Le problème : la variable est modifiée de façon persistante. Le premier dossier est créé dans Audits d'exploitation, les suivants dans Audits dexploitation — une bibliothèque qui n’existe pas. L’erreur ne se déclenche que sur les noms contenant une apostrophe et seulement à partir du deuxième dossier, ce qui la rend très difficile à reproduire.
Correctif : ne jamais réassigner une variable de boucle externe depuis une boucle interne. Nettoyez le nom une seule fois, au moment de le lire.
2. Une reconnexion par ligne de tableau
Connect-PnPOnline appelé dans la boucle principale rouvre une session pour chaque bibliothèque, y compris quand quarante lignes visent le même site. En authentification interactive, cela peut déclencher une invite de connexion par ligne. Le Group-Object -Property SiteUrl de l’étape 3 supprime le problème.
3. Get-PnPList appelé pour chaque bibliothèque
Tester l’existence d’une liste avec Get-PnPList | Where-Object { $_.Title -eq $libName } télécharge l’intégralité des listes du site à chaque itération. Sur un site qui en compte cinquante, c’est cinquante allers-retours réseau pour une information qu’on pouvait récupérer une fois. Le même raisonnement vaut pour Get-PnPFolderItem utilisé comme test d’existence de dossier : Resolve-PnPFolder rend ce test inutile.
4. Aucune gestion d’erreur
Sans try/catch, une seule ligne problématique — un site supprimé, un droit manquant, un nom trop long — interrompt tout le traitement. Vous découvrez alors que vingt bibliothèques sur quarante ont été créées, sans savoir lesquelles. Un bloc try/catch par ligne, plus un journal CSV en sortie, transforme un incident en simple relance : le script étant idempotent, il reprend là où il en était.
5. Pas de mode simulation
Un script qui crée des objets sur un tenant de production sans possibilité de tourner à blanc est une invitation à l’accident. [CmdletBinding(SupportsShouldProcess)] et un appel à $PSCmdlet.ShouldProcess() avant chaque création donnent gratuitement le paramètre -WhatIf. Une matrice de quarante lignes se relit alors en dix secondes, avant d’engager quoi que ce soit.
Réflexe à prendre : avant toute exécution sur un tenant client, lancez le script avec -WhatIf et faites relire la sortie par la personne qui a rempli le classeur. C’est le moment le moins coûteux pour corriger une nomenclature.
Assembler la version de production
Les extraits des étapes 3 à 5 se combinent en un script unique. La version consolidée ajoute six garde-fous : regroupement des lignes par site, inventaire des listes en un appel, nettoyage conforme aux règles réelles de SharePoint, contrôle de la longueur de chemin avant création, bloc try/catch par ligne et journal CSV en sortie. Déclarez-la avec [CmdletBinding(SupportsShouldProcess)] et encadrez chaque création d’un $PSCmdlet.ShouldProcess() : vous héritez gratuitement de -WhatIf.
L’ordre d’exécution est ensuite toujours le même : une passe à blanc, relue par la personne qui a rempli le classeur, puis la passe réelle.
# 1. Simulation : rien n'est créé, tout est affiché
.\New-SPOLibraryStructure.ps1 -ExcelPath .\Matrice.xlsx `
-ClientId '00000000-0000-0000-0000-000000000000' -WhatIf
# 2. Exécution réelle, avec journal horodaté
.\New-SPOLibraryStructure.ps1 -ExcelPath .\Matrice.xlsx `
-WorksheetName 'Bibliotheques' `
-ClientId '00000000-0000-0000-0000-000000000000'
Vérifier le résultat
Le journal CSV donne le premier niveau de contrôle : une ligne par objet, avec son statut. Pour une vérification côté tenant, deux commandes suffisent.
Connect-PnPOnline -Url 'https://contoso.sharepoint.com/sites/Operations' `
-Interactive -ClientId $ClientId
# Les bibliothèques créées (BaseTemplate 101 = bibliothèque de documents)
Get-PnPList | Where-Object BaseTemplate -eq 101 |
Select-Object Title, ItemCount, DefaultViewUrl
# L'arborescence d'une bibliothèque
Get-PnPFolderItem -FolderSiteRelativeUrl 'QUALITE' -ItemType Folder -Recursive |
Select-Object Name, ServerRelativeUrl
Vérifiez aussi que les bibliothèques apparaissent bien dans le lancement rapide si vous avez utilisé -OnQuickLaunch, et que les descriptions ont été appliquées : Set-PnPList échoue silencieusement si la valeur passée est $null, d’où le transtypage [string] dans le script.
Dépannage : les erreurs les plus fréquentes
| Symptôme | Cause | Correctif |
|---|---|---|
AADSTS700016: Application not found in the directory | -ClientId absent ou pointant vers l’ancienne app PnP Management Shell | Enregistrer votre application avec Register-PnPEntraIDAppForInteractiveLogin |
The term 'Connect-PnPOnline' is not recognized | Session lancée dans Windows PowerShell 5.1 | Ouvrir pwsh (PowerShell 7.4+) |
Import-Excel: Worksheet 'Feuil1' not found | Nom de feuille différent, souvent lié à la langue d’Excel | Passer le vrai nom via -WorksheetName |
File Not Found sur Resolve-PnPFolder | Le nom de bibliothèque utilisé dans le chemin ne correspond pas au titre réel | Reconstruire le chemin à partir de $newList.Title, pas de la valeur Excel brute |
The specified name is already in use | Une liste porte déjà ce titre après nettoyage (deux noms Excel différents ont convergé) | Vérifier les doublons dans la matrice avant exécution |
| Vue de bibliothèque en erreur au-delà de 5 000 éléments | Seuil d’affichage des listes SharePoint Online | Filtrer et indexer les vues ; le seuil de 5 000 n’est pas modifiable dans SharePoint Online |
Ce dernier point mérite une précision : une bibliothèque SharePoint Online peut stocker jusqu’à 30 millions d’éléments, mais une vue ne peut en afficher que 5 000 à la fois (Microsoft Support). Concevoir l’arborescence, c’est donc aussi anticiper la répartition des documents : un dossier prévu pour recevoir 20 000 factures a besoin d’un sous-découpage par année ou par mois, décidé dans le classeur Excel plutôt que six mois après la mise en service.
Questions fréquentes
Peut-on créer des bibliothèques SharePoint sans droits d’administrateur global ?
Oui. La création de listes et de dossiers relève des permissions du site : être propriétaire des sites cibles suffit. En revanche, l’enregistrement initial de l’application Entra ID demande un rôle habilité à créer et consentir des applications dans l’annuaire. Faites réaliser cette étape une fois par un administrateur, puis réutilisez l’AppId indéfiniment.
Faut-il Excel installé sur la machine qui exécute le script ?
Non. Le module ImportExcel de Douglas Finke lit et écrit les fichiers .xlsx sans dépendance à Microsoft Excel — sa description officielle sur PowerShell Gallery est explicite : « import/export Excel spreadsheets, without Excel ». Le script tourne donc aussi bien sur un serveur d’administration que dans un conteneur.
Le script peut-il tourner sans interaction, dans une tâche planifiée ?
Pas dans cette version : -Interactive ouvre une fenêtre d’authentification. Pour un usage non supervisé, remplacez la connexion par une authentification par certificat (Connect-PnPOnline -ClientId … -Tenant … -CertificatePath …) et attribuez les permissions d’application, et non déléguées, lors de l’enregistrement Entra ID. Le reste du script est inchangé.
Faut-il créer des dossiers ou utiliser des métadonnées ?
Les métadonnées restent préférables pour le classement et la recherche. Les dossiers gardent deux avantages concrets : ils portent des permissions spécifiques et ils correspondent au modèle mental des utilisateurs qui arrivent d’un lecteur réseau. En migration, une arborescence reconnaissable réduit fortement la résistance au changement — quitte à ajouter des colonnes de métadonnées dans un second temps.
Comment gérer les bibliothèques qui existent déjà ?
Le script les détecte et passe à la suite, sans les modifier, puis complète leur arborescence si des dossiers manquent. C’est ce qui le rend rejouable : ajoutez une ligne à la matrice, relancez, seul le delta est créé. Pour forcer une mise à jour de description ou de paramètres sur les bibliothèques existantes, ajoutez un appel Set-PnPList dans la branche « existe déjà ».
Pour aller plus loin
Ce script résout un problème précis : transformer une décision d’architecture documentaire, prise dans un tableau, en structure SharePoint réelle et reproductible. Sa vraie valeur n’est pas le gain de temps sur la première exécution — c’est la capacité à rejouer la même structure sur un site de recette, puis en production, puis chez le client suivant, sans écart.
- Ajoutez une colonne
ContentTypeà la matrice pour associer un type de contenu à chaque bibliothèque. - Complétez avec
Set-PnPListPermissionpour provisionner aussi les droits, à partir d’une colonneGroupe. - Versionnez le classeur avec le script : la matrice devient la documentation de l’architecture documentaire.
Vous automatisez Microsoft 365 au quotidien ? Recevez les prochains scripts et retours de terrain directement par mail — un envoi, pas de bruit. [CTA : inscription newsletter powerplatform-addict.com]


Laisser un commentaire