Bien appeler un exe externe depuis PowerShell — les pièges des guillemets d'arguments, du code de sortie et de l'encodage

· · PowerShell, Windows, Automatisation, Scripts, Processus, Encodage des caractères, Amélioration opérationnelle, Traitement par lots

« Ça marche dans mon invite de commandes, mais dès que je le transpose dans un script PowerShell, l’outil externe renvoie une erreur » — un phénomène que l’on rencontre presque systématiquement en migrant des batchs vers PowerShell, ou en automatisant un EXE interne ou un CLI open source. La cause tient rarement à la logique : elle réside dans le chemin parcouru par l’argument avant d’atteindre le programme. Un chemin contenant un espace, un guillemet double dans un argument, une chaîne contenant %, ( ou ) — l’un ou l’autre de ces éléments suffit à déclencher une transformation inattendue de l’argument que l’on croyait avoir transmis.

Ce qui complique encore la situation, c’est que ce comportement a changé en PowerShell 7.3. Un contournement écrit pour fonctionner en 5.1 devient un double échappement en 7, et un script écrit pour 7 casse en 5.1. Dans un environnement japonais, le problème d’encodage de la sortie vient encore s’y ajouter.

Cet article part des spécifications pour couvrir la façon dont les arguments sont transmis lors de l’appel d’un programme externe (une commande native) depuis PowerShell, le bon usage de --%, l’usage de ProcessStartInfo quand une transmission fiable est requise, la gestion du code de sortie et de stderr, et enfin les contre-mesures à l’encodage — le tout d’un point de vue pratique. La conception de la gestion des erreurs elle-même est traitée dans « Gestion des erreurs et conception des tentatives en PowerShell », si bien que cet article se concentre sur « la frontière avec le processus externe ».

Vérifier d’abord son propre environnement

Cet article portant essentiellement sur les différences de version, vérifiez avant de commencer dans lequel des deux mondes vous vous trouvez.

$PSVersionTable.PSVersion

5.1.x signifie Windows PowerShell 5.1, 7.x signifie PowerShell 7. 7.3 marque la frontière dans la façon dont les arguments sont transmis, donc notez que 7.0 à 7.2 n’entrent pas dans la nouvelle méthode décrite au chapitre 3. Si les deux sont installés, le comportement diffère selon l’hôte en cours d’exécution.

1. La conclusion, d’abord

  • PowerShell analyse lui-même, une première fois, les arguments d’un programme externe. Ce qui suit un appel de commande est analysé en « mode argument » (argument mode) ; une valeur contenant un espace doit être entourée de guillemets. Des caractères comme , ( ) { } | & < > @ # sont des métacaractères : pour les transmettre littéralement, échappez-les avec un accent grave.1
  • PowerShell 7.3 a modifié la façon dont les arguments sont transmis (changement incompatible). Les guillemets intégrés et les arguments vides sont désormais conservés, et le comportement se choisit via $PSNativeCommandArgumentPassing. La valeur par défaut sous Windows est Windows.12
  • En mode Windows, seuls cmd.exe, cscript.exe, wscript.exe et les fichiers .bat, .cmd, .js, .vbs, .wsf conservent l’ancienne transmission (Legacy). C’est une exception destinée à préserver la compatibilité avec les anciens actifs batch.1
  • Le jeton d’arrêt d’analyse --% est le recours ultime pour « traiter tout ce qui suit comme littéral ». Cependant, seules les variables d’environnement au format %VAR% sont développées, les variables PowerShell sont totalement inutilisables, et l’effet s’arrête au saut de ligne ou au pipe. Aucune redirection ne peut non plus être écrite.1
  • Pour transmettre de manière fiable la valeur d’une variable, passer par un tableau est le premier choix. Avec le splatting & $exe @argArray, chaque élément devient un argument indépendant. ProcessStartInfo.ArgumentList permet de confier la construction des guillemets au .NET, mais c’est une API disponible à partir de .NET Core 2.1, inutilisable en 5.1.13
  • Ne transmettez jamais d’entrée non fiable à un fichier batch. Sous Windows, les arguments d’un batch sont transmis à cmd.exe sous forme de chaîne de ligne de commande brute, si bien que la documentation officielle avertit elle aussi de « transmettre les entrées non fiables par un autre moyen ».1
  • Le succès ou l’échec se détermine avec $LASTEXITCODE. Un code de sortie non nul ne devient pas, par défaut, une erreur PowerShell et n’est pas intercepté par try/catch.4
  • Corrigez les problèmes d’encodage séparément côté émission et côté réception. Le décodage de la sortie d’une commande externe relève de [Console]::OutputEncoding ; la chaîne envoyée par pipe de PowerShell vers une commande externe relève de $OutputEncoding.2
  • En cas de difficulté, utilisez Trace-Command -Name ParameterBinding pour observer les arguments réellement transmis. À partir de PowerShell 7.3, la liaison des arguments des commandes natives peut elle aussi être tracée.15

2. Pourquoi les arguments se corrompent — le principe du mode argument

PowerShell décompose la ligne de commande en tokens et l’interprète soit en mode expression, soit en mode argument. Dès qu’un appel de commande apparaît, tout ce qui suit est analysé en mode argument. Dans ce mode, l’entrée est fondamentalement traitée comme une « chaîne extensible » : un $ en tête indique une référence de variable, un guillemet ouvre une chaîne, ( ) ouvre une expression, et ainsi de suite — les symboles portent une signification syntaxique.1

Autrement dit, une chaîne qui fonctionnerait telle quelle collée dans l’invite de commandes peut être interprétée différemment par PowerShell. icacls en est l’exemple classique.1

# Fonctionne avec cmd.exe, mais à l'époque de PowerShell 2.0, les parenthèses étaient interprétées comme une expression et provoquaient une erreur
icacls X:\VMS /grant Dom\HVAdmin:(CI)(OI)F

# Échapper les métacaractères avec un accent grave (peu lisible)
icacls X:\VMS /grant Dom\HVAdmin:`(CI`)`(OI`)F

# Déclarer avec le jeton d'arrêt d'analyse que « tout ce qui suit est littéral » (PowerShell 3.0 et ultérieur)
icacls X:\VMS --% /grant Dom\HVAdmin:(CI)(OI)F

L’autre principe à connaître est le chemin par lequel les arguments analysés parviennent au programme. Sous Windows PowerShell 5.1, les arguments analysés sont recomposés en une seule chaîne séparée par des espaces avant d’être transmis au processus. C’est au cours de cette « recomposition » que les guillemets contenus dans un argument disparaissent, ou qu’un argument vide s’efface — c’est l’incident classique de l’ère 5.1.1

3. Le changement incompatible de PowerShell 7.3 — $PSNativeCommandArgumentPassing

PowerShell 7.3 a modifié cette recomposition. C’est un point sur lequel la documentation officielle écrit explicitement : « changement incompatible par rapport au comportement de Windows PowerShell 5.1 ».1

Le nouveau comportement se règle via la variable de préférence $PSNativeCommandArgumentPassing, dont la valeur peut être Legacy (ancien comportement), Standard ou Windows. La valeur par défaut sur la plateforme Windows est Windows, et Standard sur les autres plateformes.12

La différence entre Windows et Standard tient à un seul point : en mode Windows, les appels suivants basculent automatiquement en mode Legacy.1

Appels auxquels le mode Legacy s’applique automatiquement
cmd.exe / cscript.exe / wscript.exe
Fichiers dont l’extension est .bat, .cmd, .js, .vbs, .wsf

C’est une exception destinée à éviter que d’anciens batchs ou scripts WSH ne « cassent parce que la façon de recevoir les arguments change dès le passage à PowerShell 7 ». Inversement, si vous définissez explicitement $PSNativeCommandArgumentPassing sur Standard ou Legacy, ce test n’est plus effectué.1

Le nouveau mode apporte deux améliorations.1

Dans les exemples qui suivent, TestExe -echoargs est un outil de vérification qui se contente d’afficher un par un les arguments reçus, sous la forme Arg 0 is <...>. Il fait partie des ressources de test de PowerShell lui-même et n’est pas une commande fournie en standard sous Windows. La façon de vérifier la même chose sur votre poste est résumée au chapitre 8 ; pour la suite, considérez-le simplement comme « une fenêtre qui laisse voir les arguments tels quels ».

# (1) Les guillemets intégrés dans la chaîne sont conservés
$a = 'a" "b'
TestExe -echoargs $a 'c" "d' e" "f
# Arg 0 is <a" "b>
# Arg 1 is <c" "d>
# Arg 2 is <e f>

# (2) Un argument vide ne disparaît plus
TestExe -echoargs '' a b ''
# Arg 0 is <>
# Arg 1 is <a>
# Arg 2 is <b>
# Arg 3 is <>

Si vous voulez transmettre tel quel un chemin entre guillemets comme "C:\Program Files (x86)\Microsoft\", les modes Windows / Standard permettent de l’écrire directement.1

# 7.3 et ultérieur (mode Windows / Standard)
TestExe -echoargs '"C:\Program Files (x86)\Microsoft\"'

# Pour obtenir le même résultat en mode Legacy (équivalent 5.1), un double échappement des guillemets est nécessaire
TestExe -echoargs "\""C:\Program Files (x86)\Microsoft\\"""

Une remarque importante ici : la barre oblique inverse (\) n’est pas un caractère d’échappement de PowerShell. Si \" apparaît dans l’exemple ci-dessus, c’est parce que l’API .NET sous-jacente (ProcessStartInfo.ArgumentList) traite la barre oblique inverse comme caractère d’échappement, alors que l’échappement propre à la syntaxe PowerShell est l’accent grave (`). C’est le mélange de ces deux mécanismes qui rend ce domaine particulièrement difficile à appréhender.13

La règle pratique est simple : dans un environnement où 5.1 et 7 coexistent, cessez d’écrire en littéral les arguments contenant des guillemets. En vous orientant vers « transmettre via un tableau » ou « utiliser ProcessStartInfo », présentés dans les chapitres suivants, vous êtes presque à l’abri des différences de version. Pour la politique de coexistence entre 5.1 et 7 elle-même, voir « Les différences entre Windows PowerShell 5.1 et PowerShell 7 ».

4. Le bon usage et les limites de --% (jeton d’arrêt d’analyse)

--% est un jeton qui permet de transmettre tel quel, sans interprétation par PowerShell, tout ce qui le suit (à partir de PowerShell 3.0). La documentation officielle précise explicitement qu’il est « destiné uniquement à l’usage avec les commandes natives sur la plateforme Windows ».1

PS> cmd /c echo "a|b"
'b' is not recognized as an internal or external command,
operable program or batch file.

PS> cmd /c --% echo "a|b"
"a|b"

C’est puissant, mais il faut en connaître précisément les restrictions.1

Restriction Contenu
Seules les variables d’environnement sont développées Une forme %<nom>% comme %USERPROFILE% est toujours développée. L’échappement par %% est impossible. Un nom non défini passe tel quel
Les variables PowerShell sont inutilisables $path et similaires ne sont pas développées ; elles sont transmises comme chaîne littérale
Portée de l’effet Jusqu’au prochain saut de ligne ou pipe (\|). Ne peut être prolongée par une continuation de ligne avec l’accent grave, ni terminée par ;
Redirection impossible >file.txt et similaires sont transmis tels quels comme argument à la commande cible

Autrement dit, --% ne peut être utilisé que lorsque « le contenu transmis est une chaîne entièrement fixe, ne contenant pas de % ». Dans l’automatisation typique où un script construit des valeurs à partir de variables, cette condition n’est presque jamais remplie. La pratique consistant à « ajouter --% par précaution » s’effondre dès qu’on transmet un mot de passe ou un caractère générique contenant un %.

5. Transmettre une variable de manière fiable — splatting de tableau et ProcessStartInfo

Le premier choix pour transmettre un argument contenant la valeur d’une variable est d’écrire chaque argument dans un tableau et de faire du splatting. Chaque élément du tableau étant transmis comme un argument indépendant, un chemin contenant un espace n’exige pas que vous écriviez vous-même les guillemets.

$exe  = 'C:\Program Files\MyTool\convert.exe'
$args = @(
    '--input',  'D:\Données de commandes\2026-07.csv'   # Aucun problème avec des espaces ou des caractères non ASCII
    '--output', 'D:\Sortie\result.json'
    '--mode',   'strict'
)

& $exe @args                      # Splatting de tableau. Chaque élément devient un argument
if ($LASTEXITCODE -ne 0) { throw "Échec de la conversion (ExitCode=$LASTEXITCODE)" }

L’opérateur d’appel & est également nécessaire pour exécuter un exe dont le chemin contient un espace ('C:\Program Files\...' seul serait simplement évalué comme un littéral de chaîne, sans être exécuté).

Si vous n’êtes pas sûr que la transmission s’est bien passée, vérifiez les arguments réellement reçus avec la méthode du chapitre 8 avant d’ajouter des échappements au hasard. Le plus rapide est de comparer les arguments reçus avant et après avoir changé votre façon d’écrire.

Lorsqu’une fiabilité encore plus grande est requise — contrôler chaque argument un par un, ou spécifier l’encodage de la sortie par processus — utilisez directement ProcessStartInfo de .NET. Les valeurs ajoutées à ArgumentList sont correctement entourées de guillemets par le .NET lui-même, si bien que cela n’est influencé ni par l’analyse de PowerShell, ni par une nouvelle analyse de la ligne de commande.3

Cependant, ArgumentList est une API disponible à partir de .NET Core 2.1, et n’existe pas dans le ProcessStartInfo de Windows PowerShell 5.1, qui tourne sur .NET Framework.3 En 5.1, construisez vous-même la chaîne Arguments du chapitre suivant, ou utilisez le splatting de tableau décrit ci-dessus.

# [PowerShell 7 et ultérieur] Confier la construction des arguments au .NET
$psi = [System.Diagnostics.ProcessStartInfo]::new()
$psi.FileName = 'C:\Program Files\MyTool\convert.exe'
foreach ($a in '--input', $inputPath, '--output', $outputPath) {
    $psi.ArgumentList.Add($a)     # 1 élément = 1 argument. Ne pas écrire les guillemets soi-même (spécifique à 7)
}
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError  = $true
$psi.UseShellExecute        = $false
# Pouvoir spécifier explicitement l'encodage de la sortie est aussi un avantage de ProcessStartInfo (chapitre 6)
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
$psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8

$proc = [System.Diagnostics.Process]::Start($psi)

# Lire la sortie standard et l'erreur standard « en même temps ». Si l'on
# lit intégralement l'une de façon synchrone avant de lire l'autre, le tampon
# du pipe non lu se remplit pendant l'attente, le processus enfant se bloque
# en écriture, et cela mène directement à un interblocage
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$stderrTask = $proc.StandardError.ReadToEndAsync()
$proc.WaitForExit()
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()

if ($proc.ExitCode -ne 0) {
    throw "Échec de la conversion (ExitCode=$($proc.ExitCode)) : $stderr"
}

L’incident le plus fréquent lors de la redirection de sortie est l’interblocage (deadlock). Le tampon d’un pipe a une capacité limitée ; une fois plein, le processus enfant se bloque en écriture. Il est donc évident qu’appeler WaitForExit() avant de lire est dangereux, mais lire intégralement l’un des flux avec ReadToEnd() de façon synchrone avant de lire l’autre l’est tout autant (si le tampon du flux non lu se remplit en premier, le processus enfant s’arrête, et ReadToEnd() ne revient jamais).

Voici, en une seule image, l’ordre dans lequel tout se bloque.

Processus parentAttend de lire stderr jusqu'au boutavec StandardError.ReadToEndProcessus enfantContinue d'écrire vers stdoutLe tampon du pipe stdout est pleinIl ne se vide pas car le parent ne lit pasLe processus enfant se bloque en écritureIl ne peut ni se terminer ni fermer stderrLe ReadToEnd du parent ne revient jamaisWaitForExit ne revient pas non plus= interblocage

Figure 1 : le déroulement du blocage lorsqu’on essaie de lire intégralement, de façon synchrone, un seul des deux flux

Comme dans le code ci-dessus, commencez la lecture des deux flux de façon asynchrone avant d’attendre, ou concevez le programme pour ne rediriger qu’un seul flux. Pour la gestion générale des processus enfants, voir aussi « Liste de vérification pour gérer les processus enfants en toute sécurité dans une application Windows ».

Si vous utilisez ProcessStartInfo sous Windows PowerShell 5.1, vous devrez transmettre à Arguments une seule chaîne dans laquelle vous aurez vous-même posé les guillemets. Écrire cette construction à la main est source d’incidents ; en 5.1, faites donc du splatting de tableau (& $exe @args) votre premier choix.

# [5.1] Comme il n'y a pas d'ArgumentList, construire soi-même une seule chaîne contenant les guillemets
$quote = {
    param([string] $s)
    if ($s -eq '') { return '""' }                   # Une chaîne vide doit devenir "" sinon l'argument entier disparaît
    if ($s -notmatch '[\s"]') { return $s }          # Pas besoin d'entourer si ce n'est pas nécessaire

    # Se conformer aux règles d'analyse de la ligne de commande Windows :
    #   (1) Doubler la suite de barres obliques inverses précédant un ", et transformer le " lui-même en \"
    #   (2) Doubler aussi la suite de barres obliques inverses en fin de chaîne. Comme elle se trouve
    #       juste avant le guillemet fermant, la laisser telle quelle serait interprétée comme \" et
    #       empêcherait la fermeture du guillemet, cassant les arguments suivants
    #       (exemple : 'C:\Program Files\input\' → "C:\Program Files\input\\")
    $e = $s -replace '(\\*)"', '$1$1\"'
    $e = $e -replace '(\\+)$', '$1$1'
    '"' + $e + '"'
}
$psi.Arguments = (@('--input', $inputPath, '--output', $outputPath) |
                  ForEach-Object { & $quote $_ }) -join ' '

Comme l’expression régulière seule ne rend pas évident ce qui se passe, voici la correspondance entre entrée et sortie, ligne par ligne. Une fois ces six lignes comprises, il n’est pas nécessaire de mémoriser la règle elle-même.

Valeur à transmettre (contenu de la variable) Chaîne renvoyée par $quote Règle qui s’applique
strict strict Ni espace ni guillemet, donc renvoyée telle quelle sans être entourée
Chaîne vide "" Si l’on n’écrit rien, l’argument entier disparaît ; on place donc des guillemets vides
D:\Données\2026-07.csv D:\Données\2026-07.csv Même avec des caractères non ASCII, pas d’entourage tant qu’il n’y a pas d’espace
C:\Program Files\input "C:\Program Files\input" Un espace est présent, donc l’ensemble est simplement entouré de guillemets
C:\Program Files\input\ "C:\Program Files\input\\" Règle (2). Le \ final est doublé. Laissé seul, il fusionnerait avec le guillemet fermant, serait lu comme \", et le guillemet ne se fermerait pas, entraînant l’argument suivant
say "hi" "say \"hi\"" Règle (1). Le " contenu dans la valeur devient \", transmis comme caractère et non comme séparateur
a\"b "a\\\"b" Vue d’ensemble de la règle (1). La suite de \ précédant le " est d’abord doublée, puis le " devient \"

La dernière ligne résume à elle seule la raison pour laquelle cette fonction paraît compliquée. La barre oblique inverse ne fonctionne comme caractère d’échappement que « lorsqu’elle est suivie d’un " » ; c’est cette asymétrie propre aux règles d’analyse de la ligne de commande Windows qui explique ce résultat.

C’est précisément la finesse de cette règle qui constitue la raison d’éviter de la construire à la main en 5.1. Cela dit, en Windows PowerShell 5.1, le splatting de tableau n’est pas non plus une solution universelle. La valeur transmise finit tout de même par être recomposée en chaîne de ligne de commande selon l’ancienne méthode, si bien qu’un argument vide disparaît et qu’une valeur contenant des guillemets se déforme.1 En 5.1, répartissez donc l’usage comme suit.

Argument à transmettre en 5.1 Méthode
Valeur ordinaire contenant un espace ou du japonais Le splatting de tableau (& $exe @args) suffit
Chaîne vide, valeur contenant des guillemets ProcessStartInfo + l’échappement ci-dessus, ou --% (chaîne fixe uniquement)

En PowerShell 7, ces deux problèmes sont résolus, si bien que cette répartition devient inutile.

Par ailleurs, la documentation officielle avertit de ne pas transmettre d’entrée non fiable à un fichier batch, car les arguments d’un batch sont transmis à cmd.exe sous forme de chaîne de ligne de commande brute.1 Concevoir un système qui concatène une saisie utilisateur ou un nom de fichier avant de le transmettre à un batch crée un terreau propice à l’injection de commande. Faites transiter les valeurs par un fichier temporaire ou une variable d’environnement, ou migrez le batch lui-même vers PowerShell (voir « Ce fichier batch devrait-il migrer vers PowerShell ? »).

6. Corriger les problèmes d’encodage — [Console]::OutputEncoding et $OutputEncoding

Dans un environnement japonais, on se heurte immanquablement aux problèmes d’encodage. Le point clé est que le réglage utilisé diffère selon le sens du flux.

Sens Réglage utilisé Symptôme
PowerShell reçoit la sortie d’une commande externe [Console]::OutputEncoding Le résultat d’un outil qui produit de l’UTF-8 se corrompt en quelque chose comme « 譁�喧縺� »
PowerShell envoie une chaîne par pipe vers une commande externe $OutputEncoding Le japonais envoyé se corrompt côté destinataire

La façon dont le texte se corrompt devient beaucoup plus claire en la mettant côte à côte avec la chaîne d’origine. Voici ce que donne du japonais produit en UTF-8, décodé comme s’il s’agissait de CP932 (Shift_JIS).

Chaîne d’origine Résultat du décodage d’une sortie UTF-8 en CP932
こんにちは 縺薙s縺ォ縺。縺ッ
エラー 繧ィ繝ゥ繝シ
日本語 譌・譛ャ隱 + octets non décodables

Le signe distinctif est que les hiragana et katakana se corrompent en paires de caractères commençant par , ou (les hiragana et katakana en UTF-8 commencent par E3 81, E3 82, E3 83, et ces deux premiers octets correspondent justement à ces caractères en CP932). Lorsque des octets non décodables se mélangent, comme pour les kanjis, ils deviennent des caractères de remplacement ou disparaissent, si bien que la longueur ne correspond plus non plus, comme à la troisième ligne du tableau ci-dessus.

$OutputEncoding est une variable de préférence qui détermine « l’encodage utilisé par PowerShell pour envoyer une chaîne à une commande native ».2 De son côté, [Console]::OutputEncoding a pour rôle de décoder en chaîne la séquence d’octets émise par une commande externe. Le cas « ça se corrompt encore » après n’avoir corrigé que l’un des deux résulte, dans la plupart des cas, d’une confusion entre ces deux réglages.

# Contre-mesure classique lors de l'appel, depuis Windows PowerShell 5.1, d'un outil externe qui produit de l'UTF-8
$prevOut = [Console]::OutputEncoding
$prevPs  = $OutputEncoding
try {
    [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)  # UTF-8 sans BOM
    $OutputEncoding           = [System.Text.UTF8Encoding]::new($false)
    $result = & $exe --list
}
finally {
    # Cela affecte toute la session, donc toujours remettre en place
    [Console]::OutputEncoding = $prevOut
    $OutputEncoding           = $prevPs
}

Si vous utilisez ProcessStartInfo, comme indiqué au chapitre précédent, StandardOutputEncoding / StandardErrorEncoding peuvent être spécifiés par processus, ce qui évite de toucher aux réglages de la session. C’est cette approche qui a le moins d’effets de bord. Les questions d’encodage propres à Windows en général sont résumées dans « Encodage des caractères et fins de ligne sous Windows ».

7. Code de sortie et stderr — éviter le « traité comme un échec alors que c’est un succès »

Le succès ou l’échec d’un programme externe se détermine avec $LASTEXITCODE. Un code de sortie non nul ne génère pas d’ErrorRecord par défaut et n’est pas intercepté par try/catch.4 Cette base a été traitée en détail dans « Gestion des erreurs et conception des tentatives en PowerShell » ; nous nous limitons ici à deux compléments propres aux processus externes.

(1) Une écriture sur stderr n’est pas un « échec ». De nombreux outils en ligne de commande écrivent leur progression ou leurs journaux sur stderr. PowerShell fait passer la sortie stderr des commandes natives par le flux d’erreur, ce qui fait apparaître l’écran en rouge et donne l’impression d’un « échec », mais si le code de sortie est 0, c’est un succès. Sous Windows PowerShell 5.1, il arrivait qu’une simple écriture sur stderr fasse passer $? à $false ; en PowerShell 7, cela a été corrigé pour que $? ne passe à $false que lorsque le code de sortie est non nul.6

(2) Le type obtenu après fusion avec 2>&1 diffère selon la version. Sous Windows PowerShell 5.1, chaque ligne de stderr se mélange sous forme d’ErrorRecord, mais à partir de PowerShell 7.4, la sortie redirigée des commandes natives est traitée comme un flux d’octets, et devient, après fusion, une donnée de type chaîne.78 Autrement dit, une écriture qui « trie selon qu’il s’agit ou non d’un ErrorRecord » ne fonctionne plus à partir de 7.4. Si vous avez besoin des deux sorties, il est plus sûr de les recevoir séparément sans les fusionner.

# [Recommandé] Recevoir stdout et stderr séparément (insensible aux différences de version)
$errFile = [System.IO.Path]::GetTempFileName()
try {
    $stdout = & $exe --import $csvPath 2> $errFile
    $stderr = Get-Content -Path $errFile -Raw

    # Conserver les deux dans le journal
    $stdout | Add-Content -Path $logPath -Encoding utf8
    if ($stderr) { $stderr | Add-Content -Path $logPath -Encoding utf8 }

    if ($LASTEXITCODE -ne 0) {
        throw "Échec de l'import (ExitCode=$LASTEXITCODE) : $stderr"
    }
    # Si l'on arrive ici, c'est un succès. Une sortie sur stderr n'est pas traitée comme un échec
}
finally {
    Remove-Item $errFile -ErrorAction SilentlyContinue
}

# [Si la distinction n'est pas nécessaire] Pour simplement tout journaliser en bloc, la fusion suffit
(& $exe --import $csvPath 2>&1) | ForEach-Object { $_.ToString() } |
    Add-Content -Path $logPath -Encoding utf8

8. Vérifier ce qui a réellement été transmis

Ajouter des échappements au hasard aggrave la situation. Le plus rapide est de regarder les arguments réellement transmis. À partir de PowerShell 7.3, la liaison des arguments des commandes natives peut être tracée avec Trace-Command.15

Trace-Command -Name ParameterBinding -PSHost -Expression {
    & $exe --input 'D:\Données de commandes\2026-07.csv' --mode strict
}
# DEBUG: ... BIND cmd line arg [--input] to position [0]
# DEBUG: ... BIND cmd line arg [D:\Données de commandes\2026-07.csv] to position [1]

Si l’outil appelé est développé en interne, prévoir un mode de vérification qui se contente d’afficher tel quel le tableau args reçu permet de régler ce genre d’investigation en un instant (c’est la même idée que TestExe -echoargs, présent dans les outils de test de PowerShell).1 Pour faire la même chose dans un environnement 5.1 actuel, il suffit de préparer un petit .ps1 qui énumère simplement $args, ou un petit EXE qui se contente d’afficher les arguments reçus.

9. Les bonnes pratiques (tableau de décision)

Situation Options Critère de décision
Argument sous forme de chaîne fixe (sans %) --% / appel normal Si l’échappement est trop lourd, --% est le plus rapide. Mais aucune variable n’est utilisable1
Transmettre la valeur d’une variable Splatting de tableau & $exe @args Premier choix. Pas besoin d’écrire soi-même les guillemets, même avec espaces, japonais ou symboles
Vouloir la même écriture en 5.1 et en 7 Splatting de tableau L’écriture est commune, mais en 5.1 une chaîne vide ou un argument contenant des guillemets se casse (voir la remarque ci-dessus)1
Transmettre en 5.1 une chaîne vide ou un argument contenant des guillemets ProcessStartInfo + échappement personnalisé Fiable car cela évite la reconstruction de la ligne de commande propre à 5.1 (chapitre 5)
Vouloir contrôler chaque argument un par un (spécifique à 7) ProcessStartInfo + ArgumentList La construction des guillemets peut être confiée au .NET. API à partir de .NET Core 2.13
Besoin d’une autre fenêtre, d’un autre utilisateur ou d’une élévation Start-Process Récupérer ExitCode avec -Wait -PassThru. Rediriger la sortie vers un fichier9
Transmettre une valeur à un batch (.bat) Via une variable d’environnement ou un fichier temporaire Ne pas transmettre d’entrée non fiable en argument (avertissement officiel)1
La sortie est mal encodée [Console]::OutputEncoding (réception) / $OutputEncoding (émission) Le réglage diffère selon le sens. Par processus, utiliser StandardOutputEncoding2
Détermination du succès ou de l’échec $LASTEXITCODE Une valeur non nulle n’est pas interceptée par catch par défaut. Une sortie sur stderr ne signifie pas un échec46
Vouloir distinguer stdout et stderr Les rediriger et les recevoir séparément Le type obtenu après fusion avec 2>&1 diffère entre 5.1 et 7.4+78
On ne sait pas ce qui a été transmis Trace-Command -Name ParameterBinding Mesurer réellement avant d’ajouter des échappements au hasard5

10. Conclusion

  • PowerShell analyse lui aussi, en mode argument, les arguments d’un programme externe. Les symboles ayant une signification syntaxique, une chaîne qui fonctionne avec cmd.exe ne passe pas forcément telle quelle.
  • PowerShell 7.3 a modifié la façon de transmettre les arguments (changement incompatible) : les guillemets intégrés et les chaînes vides sont désormais conservés. Windows utilise par défaut le mode Windows, où seuls cmd.exe, les batchs, etc. adoptent le mode Legacy.
  • --% est un recours ultime réservé aux chaînes fixes. Utilisez-le en gardant à l’esprit ses restrictions : %VAR% est toujours développé, les variables PowerShell sont inutilisables, et l’effet s’arrête au saut de ligne ou au pipe.
  • Pour transmettre une variable, le splatting de tableau est le premier choix. En PowerShell 7, ArgumentList de ProcessStartInfo est également utilisable (il n’existe pas en 5.1). Le fait que la barre oblique inverse ne soit pas le caractère d’échappement de PowerShell est la source de la confusion.
  • Lorsque vous redirigez à la fois la sortie standard et l’erreur standard, lisez toujours les deux en même temps. Une écriture qui lit intégralement l’une des deux de façon synchrone provoque un interblocage.
  • Les problèmes d’encodage se traitent différemment selon le sens : réception via [Console]::OutputEncoding, émission via $OutputEncoding, et StandardOutputEncoding par processus.
  • Le succès ou l’échec se juge avec $LASTEXITCODE. Une sortie sur stderr n’est pas un échec. Si vous voulez distinguer stdout et stderr, recevez-les séparément sans les fusionner avec 2>&1 (le type obtenu après fusion diffère entre 5.1 et 7.4+).

Téléchargement du code d’exemple

Le code traité dans cet article est distribué sous une forme directement exécutable. Il contient un module de mise entre guillemets des arguments et une exécution via ProcessStartInfo.

Télécharger le code d’exemple (zip)

Les exemples de cet article ont été réellement exécutés et validés sous PowerShell 7.6 (22 tests Pester). En exécutant Invoke-SampleTests.ps1 inclus dans le zip, vous pouvez reproduire la même validation sur votre poste.

# Analyse syntaxique + analyse statique + tests Pester
./Invoke-SampleTests.ps1

Les valeurs de configuration (chemins, noms de serveurs, ID de locataire, etc.) sont des exemples. Ne les exécutez pas telles quelles en production : adaptez-les à votre propre environnement.

Articles connexes

Domaines de conseil associés

合同会社小村ソフト (Komura Software LLC) prend en charge la transformation des actifs batch en PowerShell, la conception de traitements automatisés combinant outils externes et EXE internes, ainsi que l’investigation des causes derrière des scripts qui « fonctionnent ou non selon l’environnement ».

Références

  1. Microsoft Learn, about_Parsing. Sur la distinction entre mode expression et mode argument, les métacaractères du mode argument, l’échappement par accent grave, le fait que les arguments transmis à une commande native soient recombinés après analyse en une seule chaîne séparée par des espaces, les spécifications du jeton d’arrêt d’analyse --% (à partir de PowerShell 3.0 ; seules les variables d’environnement sont développées, l’échappement %% est impossible, l’effet s’arrête au saut de ligne ou au pipe, aucune redirection n’est possible), le changement incompatible apporté par PowerShell 7.3 à l’analyse de la ligne de commande des commandes natives, les valeurs de $PSNativeCommandArgumentPassing (Legacy/Standard/Windows) et sa valeur par défaut sous Windows, le fait qu’en mode Windows, cmd.exe, cscript.exe, wscript.exe ainsi que .bat/.cmd/.js/.vbs/.wsf adoptent le mode Legacy, le fait que la barre oblique inverse ne soit pas le caractère d’échappement de PowerShell, l’avertissement de ne pas transmettre d’entrée non fiable à un fichier batch, et le fait qu’à partir de 7.3 la liaison des arguments des commandes natives puisse être tracée.  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

  2. Microsoft Learn, about_Preference_Variables. Sur le fait que $PSNativeCommandArgumentPassing est une variable de préférence dont la valeur par défaut dépend de la plateforme, et sur le fait que $OutputEncoding détermine l’encodage utilisé lorsque PowerShell envoie une chaîne à une autre application.  2 3 4 5

  3. Microsoft Learn, Propriété ProcessStartInfo.ArgumentList. Sur le fait que les arguments peuvent être indiqués individuellement dans une collection, le runtime se chargeant de la mise entre guillemets et de l’échappement nécessaires, sur le fait que la barre oblique inverse est traitée comme caractère d’échappement, et sur le fait que cette propriété n’est disponible qu’à partir de .NET Core 2.1 (elle n’existe pas dans .NET Framework), ce qui la rend inutilisable dans Windows PowerShell 5.1, qui fonctionne sur .NET Framework. Voir également la remarque sur l’interblocage pouvant survenir lorsqu’on redirige et lit de façon synchrone à la fois la sortie standard et l’erreur standard, ainsi que sur la façon de l’éviter (lire l’un des deux de façon asynchrone), dans Propriété Process.StandardOutput 2 3 4 5

  4. Microsoft Learn, about_Error_Handling. Sur le fait qu’un code de sortie non nul d’une commande native fait passer $? à $false et est stocké dans $LASTEXITCODE, sans pour autant générer d’ErrorRecord ni être intercepté par try/catch.  2 3

  5. Microsoft Learn, Trace-Command. Sur le traçage de la liaison des paramètres via -Name ParameterBinding, la sortie vers l’hôte via -PSHost, et la spécification de la cible du traçage via -Expression 2 3

  6. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sur le fait qu’en PowerShell 7, une commande native écrivant sur stderr ne fait plus passer $? à $false à elle seule, ce comportement n’intervenant désormais que sur un code de sortie non nul.  2

  7. Microsoft Learn, about_Redirection. Sur la numérotation des flux de sortie de PowerShell, la fusion du flux d’erreur dans le flux de succès via 2>&1, et le traitement de la sortie stderr des commandes natives.  2

  8. Microsoft Learn, What’s New in PowerShell 7.4. Sur le fait que les opérateurs de redirection conservent désormais la sortie des commandes natives sous forme de flux d’octets, sans que PowerShell n’en interprète ni n’en reformate le contenu (changement incompatible), et sur le fait que, par conséquent, le stderr fusionné avec 2>&1 est traité comme une donnée de type chaîne.  2

  9. Microsoft Learn, Start-Process. Sur le fait que, par défaut, il n’attend pas la fin du nouveau processus, sur l’attente via -Wait, sur l’obtention de l’objet Process et de son ExitCode via -PassThru, sur la redirection vers un fichier via -RedirectStandardOutput / -RedirectStandardError, sur l’élévation via -Verb RunAs, et sur l’exécution sous un autre utilisateur via -Credential

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Quand j'appelle un exe depuis PowerShell, les guillemets doubles de mes arguments disparaissent. Pourquoi ?
C'est dû au mécanisme par lequel PowerShell analyse les arguments avant de les transmettre au programme externe. Sous Windows PowerShell 5.1, les arguments analysés sont recomposés en une seule chaîne séparée par des espaces avant d'être transmis, ce qui fait perdre les guillemets intégrés ou disparaître les arguments vides. PowerShell 7.3 a modifié ce comportement : les guillemets intégrés et les arguments vides sont désormais conservés. Un point d'attention : en 5.1, même la répartition en tableau (splatting, & $exe @args) passe par cette recomposition. Si vous devez transmettre de manière fiable une valeur contenant des guillemets ou une chaîne vide, le splatting ne résout pas le problème. Pour une chaîne fixe, le jeton d'arrêt d'analyse --% peut être utilisé, mais pas s'il contient des variables. La méthode fiable consiste à ajouter vous-même les guillemets selon les règles de la ligne de commande Windows et à les transmettre via la chaîne Arguments de ProcessStartInfo (ArgumentList est une API disponible à partir de .NET Core 2.1 et n'existe pas en 5.1). Le corps de l'article donne un exemple d'implémentation.
Le jeton d'arrêt d'analyse --% permet-il de transmettre n'importe quel argument en toute sécurité ?
Non, il comporte de nombreuses restrictions et n'est pas une solution universelle. Tout ce qui suit --% est traité littéralement, mais les références aux variables d'environnement comme %USERPROFILE% sont, elles seules, développées, ce qui remplace involontairement toute chaîne contenant un % (l'échappement par %% n'est pas non plus disponible). De plus, les variables PowerShell ne peuvent absolument pas être développées, l'effet s'arrête au prochain saut de ligne ou symbole de pipe, et aucune redirection ne peut être écrite. Dès lors que vous devez transmettre la valeur d'une variable, --% n'est pas utilisable ; envisagez alors ProcessStartInfo ou Start-Process.
Peut-on transmettre à un fichier batch (.bat) une chaîne reçue d'une source externe ?
Évitez de transmettre une entrée non fiable à un fichier batch. Sous Windows, les arguments d'un fichier batch sont transmis à cmd.exe sous forme de chaîne de ligne de commande brute, si bien que la documentation officielle indique explicitement de « transmettre les entrées non fiables par un autre moyen ». Concaténer directement un nom de fichier ou une saisie utilisateur laisse une possibilité d'injection de commande. Il est plus sûr de faire transiter la valeur par un fichier temporaire ou une variable d'environnement, ou de remplacer le batch par un script PowerShell.
La sortie d'une commande externe est mal encodée en japonais. Que faut-il corriger ?
Réglez [Console]::OutputEncoding, l'encodage que PowerShell utilise pour décoder la sortie standard de la commande externe, sur l'encodage réellement produit par cette commande. Pour un outil qui produit de l'UTF-8, définissez [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 avant de l'appeler. À l'inverse, lorsque PowerShell envoie une chaîne par pipe vers une commande externe, c'est $OutputEncoding qui est utilisé : le truc consiste à traiter séparément l'émission et la réception. Le réglage est propre à la session, donc si vous le modifiez temporairement dans un script, remettez-le en place ensuite.
Comment choisir entre Start-Process et l'appel direct (&) ?
Dans le cas courant où l'on veut récupérer la sortie via le pipeline ou simplement observer le code de sortie, l'appel direct (& ou un simple nom de commande) est la base. Start-Process s'utilise lorsqu'on veut contrôler la « façon de démarrer » : lancer dans une autre fenêtre, exécuter sous un autre utilisateur, s'élever en administrateur (-Verb RunAs), ou rediriger la sortie standard vers un fichier. Notez toutefois que Start-Process n'attend pas la fin de l'exécution par défaut ; si vous avez besoin du code de sortie, combinez -Wait et -PassThru, puis consultez la propriété ExitCode.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog