Powershell (quand on ne connaît que Linux)
Tags: beginners, french, powershell
Published: 2021-01-28 18:55:18

[Powershell](https://fr.wikipedia.org/wiki/Windows_PowerShell) est une programme de ligne de commande qui marche sur différentes plateformes mais est essentiellement utilisée sur Windows. C'est codé en .NET (prononcé: *dot net*) un framework pour C# (prononcé: *si charp*) et donc répond a certains concepts de [programmation orientée objet](https://fr.wikipedia.org/wiki/Programmation_orient%C3%A9e_objet) comme... les (surprise) objets.

En gros, chaque fonction que l'on verra reverra comme résultat une structure sur laquelle on pourra agir. Contrairement à Bash (voir articles précédents [1](https://dev.to/christalib/la-ligne-de-commande-linux-pour-les-impatient-e-s-49ne) et [2](https://dev.to/christalib/la-ligne-de-commande-linux-pour-les-impatient-e-s-2-cas-pratique-3bph)) qui nous retourne plutôt des listes ou des variables.

Si vous ne connaissez pas les concepts de programmation orientée objet, ce n'est pas grave. Dans cet article, nous allons surtout essayer de refaire les exercices des deux premiers articles en Powershell et nous n'aborderons pas des concepts trop avancés.

Powershell est bien calibré pour les environnements Windows, ce programme vous permet non seulement de travailler avec du texte et des fichiers mais aussi de gérer votre ordinateur... Scripter avec Powershell est un vrai atout lorsque vous travailler sur ce type de machines.

Si vous voulez plus d'informations, la [documentation](https://docs.microsoft.com/en-us/powershell/) de Microsoft est plutôt extensive.

## Bases

Si vous utilisez Windows 10 ou plus, Powershell devrait déjà être installé et vous pouvez trouver la ligne de commande en cherchant "powershell" dans vos programmes.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/frnsbm0w8yho5zv514t0.png)

Si vous utilisez Linux et que faire des trucs comme ça n'hérisse pas votre poil de libriste, il y a [moyen d'installer Powershell sur votre machine](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1) ! Même via Snap, si ce n'est pas beau tout ça !

Voyons quelques fonctions de base :

Linux (Bash) | Powershell                 | Options
-------------|----------------------------|-------------
`cat`        | `Get-Content`              | `-Path`, `-Tail`, `-Head`
`pwd`        | `Get-Location`             |
`cd`         | `Set-Location` (or `cd`)   |
`ls`         | `Get-ChildItem` (or `dir`) | `-Path`, `-File`, `-Directory`, `-Filter`, `-Recurse`, `-Hidden`, `-ErrorAction SilentlyContinue`
`cp`         | `Copy-Item`                | `-Path`, `-Destination`
`mv`         | `Move-Item`                | `-Path`, `-Destination`
`mkdir`      | `New-Item`                 | `-ItemType`, `-Name`
`wc`         | `Measure-Object`           | `-word`
`man`        | `Get-Help`                 |
`wget`       | `Invoke-WebRequest`        | `-Uri`, `-OutFile`

> Les majuscules ne sont pas obligatoire, `Get-Content` et `get-content` sont équivalents. Cependant, n'hésitez jamais à perdre un peu de rapidité à la frappe pour une meilleure lecture.

Ok, eh bien, réimplémentons les exercices des premiers articles en Powershell !

### Mouvements

Trouvons où nous sommes dans l'arbre (`pwd`) :

```powershell
Get-Location
```

> L'équivalent de `/` sous Linux est `C:\` sous Windows.

Déplaçons-nous :

```powershell
Set-Location C:\
```

Ou si vous êtes sous Linux avec Powershell :

```powershell
Set-Location /
```

> Lancer `Set-Location` sans arguments est comme lancer `cd` tout seul, il vous amènera au dossier de base de votre compte (`C:\Users\chris` ou `/home/chris` en fonction de votre système).

Lister le contenu d'un dossier :

```powershell
dir
# ou bien
Get-ChildItem
```

Jouez avec les options indiquées sur le table, par example que font les fonctions suivantes ?

```powershell
Get-ChildItem -File
Get-ChildItem -Directory
Get-ChildItem -Directory -Recurse
```

### Création de fichiers et de dossiers

Pour la création de fichiers et de dossiers, c'est même plus simple qu'avec
Linux, puisqu'il nous suffira d'utiliser une seule fonction : `New-Item` (nouvel
object).

Pour un fichier :

```powershell
New-Item -ItemType file -Name pouet
```

Donnera :

```powershell
    Directory: /home/chris/Documents/blog

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-----          27/01/2021    16:07              0 pouet
```

Et pour un dossier :

```powershell
New-item -ItemType directory -Name dirdir
```

Résultat :

```powershell
    Directory: /home/chris/Documents/blog

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          27/01/2021    16:07                dirdir
```

Déplaçons le fichier `pouet` dans `dirdir` :

```powershell
# Windows
Move-Item -Path pouet -Destination dirdir\pouet

# Linux
Move-Item -Path pouet -Destination dirdir/pouet
```

## Que les choses sérieuses commencent !

Téléchargeons notre premier fichier :

```powershell
Invoke-Webrequest -Uri "https://archive.org/stream/Frankenstein1818Edition/frank-a5_djvu.txt" -OutFile texte_complet.txt
```

Normalement, ici nous utiliserions `grep`. Sauf qu'en Powershell ça n'existe par vraiment. Il faudra utiliser `Where-Object` si vous recherchez des objets retournés par une autre fonction Powershell (comme `Get-Process`) ou bien `Select-String` quand vous travaillez avec des chaînes de caractères (des *strings* en anglais).

Donc :

```powershell
Select-String -Path texte_complet.txt -Pattern "love"
```

Vous remarquez que le résultat est plus précis par défaut que la version par défaut de `grep`. En effet, sans devoir le préciser, `Select-String` indique le numéro de la ligne ainsi que le fichier où la résultat a été trouvé.

Et la version plus précise :

```powershell
Select-String -Path texte_complet.txt -Pattern " love "
```

Pour compter le nombre d'occurrences :

```powershell
Select-String -Path texte_complet.txt -Pattern " love " | Measure-Object -Word
```

Ok, super. Passons à la liste de mots de passe :

```powershell
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Passwords/Common-Credentials/10k-most-common.txt" -OutFile liste_mdp.txt
```

Pour trier de façon unique, nous utiliserons la même logique sur Linux : afficher le texte et trier le résultat.

```powershell
Get-Content liste_mdp.txt | Sort-Object -Unique
```

Et créons un fichier d'un coup

```powershell
Get-Content liste_mdp.txt | Sort-Object -Unique > nouvelle_liste.txt
```

Et voilà le travail !

## Fonctions plus avancées

Passons maintenant au second article, et réessayons de refaire tout cela en Powershell !

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zzsk0tnz9v4thphthc94.gif)

Cette instruction est parfaitement valide en Powershell :

```powershell
echo 'Robert;Dupont;rue du Verger, 12;
"Michel";"Durand";" av. de la Ferme, 89 ";
"Michel ""Michele""";"Durand";" av. de la Ferme, 89";
"Michel;Michele";"Durand";"av. de la Ferme, 89";
' > exemple.csv
```

Maintenant, les choses se compliquent. Powershell n'a pas vraiment d'équivalent 1 à 1 pour `cut` (voir cette réponse sur [stackoverflow](https://stackoverflow.com/questions/24634022/what-is-an-equivalent-of-nix-cut-command-in-powershell) -- en anglais). Il y a plusieurs solutions sur Internet, nous allons prendre une qui parait un peu moins simple mais qui pourrait être utile dans le futur.

```powershell
Get-Content exemple.csv | ForEach-Object {$_.Split(";")[1]}
```

Que se passe-t-il ici ? La première partie avant le `|` récupère le contenu de `exemple.csv` et l'envoie via `|` à la fonction suivante. `ForEach-Object` est une fonction qui va nous permettre de travail sur un objet énumérable.

> En programmation, un énumérable est une structure, ordonnée ou non qui contient multiples valeurs, dès fois de type différent mais pas nécessairement. Par exemple, un dictionnaire en Python : [1, "un", True]. Retenez que ce type de structure contient multiple valeurs et peut donc être passée dans une boucle qui va... (vous le devinez) l'énumérer.

Ensuite, `$_` est une variable, ce type de variable et de notation est assez commun en Powershell lorsqu'on utilise des *pipes* (`|`). Donc ici `$_` fait référence à `exemple.csv`.

La fonction `split` (assez commune, elle existe en Ruby et en Python entre autres), va couper la ligne à un délimiteur donné ici `;`.

`[1]` va prendre la... *seconde* occurrence après le délimiteur !

> Attention, les ordinateurs comptent à partir de 0. Donc pour prendre le premier objet après le split, ça sera [0] et non [1].

Donc, nous aurons comme résultat :

```powershell
PS /home/chris/Documents/blog/dirdir> Get-Content exemple.csv | ForEach-Object {$_.Split(";")[1]}
Dupont
"Durand"
"Durand"
Michele"
```

> Essayez de jouer avec les délimiteurs et les index (`[1]`, `[0]`). Par exemple que donne l'index `[-2]` (`Get-Content exemple.csv | ForEach-Object {$_.split(";")[-2]}`) ?

**Variables**

Pour déclarer une variable en Powershell, `$` sera aussi nécessaire pour la
déclaration. Donc :

```powershell
$nombre_deux=2
echo $nombre_deux
```

Et la réassignation fonctionne aussi de la même manière :

```powershell
$nombre_deux=2
echo $nombre_deux
```
Résultat : 2

```powershell
$nombre_deux=3
echo $nombre_deux
```

Résultat : 3

**boucles**

En ce qui concerne les boucles, nous avons déjà vu la fonction `ForEach-Object` qui fonctionne avec des *objets*, donc des structures assez spécifiques. Il existe un autre manière de faire des boucles avec `ForEach`.

```powershell
ForEach ($ligne in Get-Content exemple.csv) {echo $ligne}
```

> Pour plus d'information sur les boucles en Powershell, la [documentation](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/foreach-object?view=powershell-7.1) de Microsoft est assez bien écrite.

Vous vous souvenez de cette ligne cryptique ?

```sh
somme=0; for i in $(seq 0 100); do somme=$(expr $somme + $i); done; echo $somme
```

Essayons de le faire en Powershell !

```powershell
$global:somme=0; ForEach ($i in 0..100) {$somme = $somme + $i}; echo $somme
```

Pourquoi `global` devant la déclaration de la variable `somme` ? Parce que les variables en Powershell on un *scope*.

Revenons à la notion de bloc mentionnée plus haut.

Exemple 1 :

```
function {
    bloc
}
```

Exemple 2 :

```
if condition {
    bloc
} else if condition 2 {
    bloc 2
} else {
    bloc 3
}
```

Un bloc est une instruction ou une suite d'instructions de code qui se trouvent à l'intérieur d'une fonction (que ça soit une fonction en tant que telle ou dans une branche d'une fonction conditionnelle comme un `if`). En fonction des langages de programmation, les variables peuvent voyager entre les blocs ou non.

Dans le cas ou ce n'est pas possible, on parle de variable "locale" (à savoir locales au bloc) ou globale (globales au programme entier).

Donc ici, Powershell a besoin qu'on définisse la variable `$somme` comme globale afin de réaliser la dernière instruction `echo $somme` qui se trouve à l'extérieur du bloc de la fonction `ForEach`.

Si nous ne le faisons pas, `$somme` à la fin de l'instruction sera égal à 0. En effet, `$somme` dans le bloc de la fonction `ForEach` est une variable locale et donc n'est pas la même que la variable `$somme` définie en-dehors de la fonction.

À part cela, `seq 0 100` a son équivalent `0..100` et pas besoin d'utiliser `expr` pour réaliser une fonction arithmétique.

## Cas pratique : Fouiller une page web

Allons-y, Alonso !

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/fhyfwp7277a8s64t3t7w.gif)

```powershell
Invoke-WebRequest -Uri "https://en.wikipedia.org/wiki/Hedy_Lamarr" -OutFile page.html
```

Ensuite :

```powershell
Select-String -Path page.html -Pattern 'href=\"/wiki/'
```

Ce coup-ci, on utilisera la pleine puissance de `Select-String` plutôt que de le faire passer dans différents processus. De plus, nous avons déjà récupéré la liste des liens internes de la page qui commencent avec `/wiki/`.

> *Note sur les regex* : `Select-String` peut parfaitement utiliser des regex pour des recherches avancées. Par exemple : `Select-String -Path page.html -Pattern '\w'`. Voir l'[article sur les regex](https://dev.to/christalib/les-regex-attrapez-les-tous-5alg) pour plus d'informations sur ce type d'instructions.

Cependant, la liste n'est pas "propre", il est encore difficile de travailler avec car les objets retournés par la fonction `Select-String` ne nous permettent pas de travailler dessus directement. Du coup, nous allons préciser notre
recherche et puiser dans les ressources de Powershell.

Nous devons d'abord récupérer la liste des chaînes de caractères qui commencent par `href="/wiki` et qui se terminent par un espace. En regex, ça donne : `href=\"/wiki/\S+` :

```powershell
Select-String -Path page.html -Pattern 'href=\"/wiki/\S+'
```

> Rappel : `\S` tout caractère sauf espace (donc, alphanumérique et spéciaux) et `+` cherchera de une à infini nombre d'occurrences.

Résultat :

```
(...)
/home/chris/Documents/blog/page.html:1250:	<li id="footer-places-about"><a href="/wiki/Wikipedia:About"
title="Wikipedia:About">About Wikipedia</a></li>
/home/chris/Documents/blog/page.html:1251:	<li id="footer-places-disclaimer"><a href="/wiki/Wikipedia:General_disclaimer"
title="Wikipedia:General disclaimer">Disclaimers</a></li>
```

Nous allons maintenant itérer sur chaque objet de notre recherche

```powershell
Select-String -Path page.html -Pattern 'href=\"/wiki/\S+' | ForEach-Object {$_.Matches}
```

Résultat :

```
(...)
Groups   : {0}
Success  : True
Name     : 0
Captures : {0}
Index    : 38
Length   : 41
Value    : href="/wiki/Wikipedia:General_disclaimer"
```

Voilà quelque chose d'intéressant ! Ce que nous voyons, ce sont les objets résultat de la recherche. Donc en réitérant dessus, nous pourrons récupérer simplement la valeur de ces recherches avec les champs `.Value` pour l'objet `.Match` qui appartient à `.Matches`.


```powershell
Select-String -Path page.html -Pattern 'href=\"/wiki/\S+' | ForEach-Object {$_.Matches} | ForEach-Object {$_.Value}
```

Résulat :

```
(...)
href="/wiki/Main_Page"
href="/wiki/Main_Page"
href="/wiki/Help:Contents"
href="/wiki/Special:WhatLinksHere/Hedy_Lamarr"
href="/wiki/Wikipedia:About"
href="/wiki/Wikipedia:General_disclaimer"
```

Il faut encore trier les résultats :

```powershell
Select-String -Path page.html -Pattern 'href=\"/wiki/\S+' | ForEach-Object {$_.Matches} | ForEach-Object {$_.Value} | Sort-Object -Unique
```

Maintenant rajouter les liens pour les télécharger :

```powershell
Select-String -Path ../page.html -Pattern 'href=\"/wiki/\S+' | ForEach-Object {$_.Matches} | ForEach-Object {$_.Value} | Sort-Object -Unique | ForEach-Object {$_.replace('href="/wiki/', "https://fr.wikipedia.org/wiki/")} > liens.txt
```

Avant de tout télécharger, vérifions quel est le format de nos liens :

```powershell
Get-Content -Head 5 -Path liens.txt
```

Résultat :

```powershell
PS /home/chris/Documents/blog/dirdir> Get-Content -Head 5 -Path liens.txt
https://fr.wikipedia.org/wiki//Film"
https://fr.wikipedia.org/wiki/%C3%91uSat"
https://fr.wikipedia.org/wiki/A_Lady_Without_Passport"
https://fr.wikipedia.org/wiki/Alexis_Granowsky"
https://fr.wikipedia.org/wiki/Alfred_Abel"
```

Nous risquons d'avoir un soucis ici, car le `"` à la fin des lignes risque de casser le lien. Donc, nettoyons cela rapidement :

```
ForEach ($ligne in Get-Content liens.txt) {$ligne.replace('"', '') >> liens_propres.txt}
```

Et téléchargeons le tout !

`Invoke-WebRequest` retourne un objet avec les [champs suivants](https://docs.microsoft.com/en-us/dotnet/api/microsoft.powershell.commands.basichtmlwebresponseobject?view=powershellsdk-7.0.0). Les champs les plus intéressants sont :
* Content (le contenu de la page)
* Links (voir la dernière partie de cet article)

```powershell
ForEach ($ligne in Get-Content liens_propres.txt) {Invoke-WebRequest -Uri $ligne -OutFile $ligne.replace("https://", "").replace("/", "_")  }
```

Ici, nous nettoyons les liens des `/` qui peuvent casser le chemin du fichier (particulièrement sous Linux), c'est pour cela que l'on enchaîne deux méthodes `replace` lorsque l'on crée le fichier.

## Go go powershell

Powershell a encore quelques astuces :

* `ForEach-Object` peut être remplacé par le caractère `%` et donc les boucles
  ressemblent à `% { bloc }`
* `GetChild-Item` peut être remplacé par `gci`
* `Set-Location` peut être remplacé par `cd`
* `Invoke-WebRequest` peut télécharger les liens d'une page web [d'un coup](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7.1#example-3--get-links-from-a-web-page) : `(Invoke-WebRequest -Uri "https://aka.ms/pscore6-docs").Links.Href`.

Essayez de refaire l'exercice avec ces astuces.

Quelques liens pour aller plus loin :

* [Awesome Powershell](https://github.com/janikvonrotz/awesome-powershell)
* [Github Officiel de Powershell](https://github.com/PowerShell/PowerShell)