Introduction

Un programme doit pouvoir interagir avec le système d’exploitation : fichiers, (et bases de données), périphériques, processus, réseau… Ce cours introduit la programmation système :

  • arguments d’un programme,
  • système de fichiers,
  • lecture et l’écriture de fichiers textes et binaires.

Arguments d’un programme

Comme pour les fonctions, les arguments d’un programme sont ses paramètres d’entrée pour adapter son comportement sans modifier son code. En Python, ils sont accessibles dans la variable sys.argv (module sys à importer) qui est de type List (tableau).

Les arguments peuvent notamment être des chemins de fichiers.

Exemple

import sys

if __name__ == "__main__":
    print("'sys.argv' est un tableau : " + str(sys.argv), end="\n\n")

    print("sys.argv[0] = " + sys.argv[0] + " - c'est toujours le programme")
    for i in range(1, len(sys.argv)):
        print("sys.argv[" + str(i) + "] = " + sys.argv[i])

Exécuter ce programme : python3 script.py essai "autre chose" …

Erreurs et codes de sortie

Habituellement, lorsqu’un programme attend des arguments (paramètres), une des premières choses à faire est de vérifier le nombre d’arguments attendus :

import sys

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("usage : " + sys.argv[0] + " argument_attendu", file=sys.stderr)
        sys.exit(1) #toute valeur de sortie ≠ 0 indique une erreur

C’est une bonne pratique d’émettre le message d’erreur vers stderr (standard error — 2 dans le shell) plutôt que vers la sortie par défaut stdout (standard output). Ils peuvent ainsi être masqués ou redirigés vers un journal d’erreur ; exemple :

python3 script.py 2> /dev/null
python3 script.py 2> error.log
cat error.log

Si les arguments sont obligatoires, il est recommandé — pour faciliter son intégration avec d’autres outils — de quitter le programme avec un code d’erreur, c’est à dire un nombre différent de 0 (succès) : 1, 2… exemple :

python3 script.py toto
code=$?             #récupère le code de retour du programme
if (( $code != 0 )); then
    bold=$(tput bold)
    normal=$(tput sgr0)
    echo -e "\n${bold}erreur : le programme a renvoyé ${code}${normal}"
fi

Exécution directe

Un programme en Python (ou dans un autre langage) peut-être exécuté sans invoquer l’interpréteur sur la ligne de commande ; cf ./script.py … par exemple. Pour cela, il faut :

  • ajouter #!/usr/bin/env python3 en première ligne du script pour indiquer à l’interpréteur de commandes (shell) qu’il faut l’exécuter avec l’interpréteur mentionné (ici python3) ;
  • rendre le script exécutable chmod u+x script.py.

Remarque : pour des raisons de sécurité, le système peut interdire d’exécuter des programmes présents sur une clé USB, sur un point de montage en réseau ou encore dans le répertoire personnel de l’utilisateur.

Système de fichiers

Un programme peut accéder au système de fichier : parcourir un dossier, vérifier si un fichier existe, renommer ou supprimer des fichiers… Cf module os de Python.

Vérifier si un fichier ou dossier existe

Les fonctions os.path.isfile(path: str) -> bool, os.path.isdir et os.path.exists renvoient True si le chemin (path) est un fichier, un dossier ou simplement si le chemin existe (quel que soit son type).

#!/usr/bin/env python3

import os
import sys

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("erreur : argument manquant", file=sys.stderr)
        print("usage : " + sys.argv[0] + " chemin/dossier", file=sys.stderr)
        sys.exit(1)

    chemin = sys.argv[1]

    if not os.path.exists(chemin): #fonctionne avec fichiers ou dossiers
        print("erreur : " + chemin + " n'existe pas", file=sys.stderr)
        sys.exit(2)

    if not os.path.isdir(chemin): #isfile pour un fichier
        print("erreur : " + chemin + " n'est pas un dossier", file=sys.stderr)
        sys.exit(2)

    print("ok : " + chemin + " existe et est un dossier")

*Rappel : “dir” siginifie “directory” (répertoire) ; c’est un synonyme de “folder” (dossier).

Remarque : dans cet exemple, des codes d’erreur différents sont utilisés : 1 pour paramètre(s) manquant(s), 2 pour fichier(s) / dossier(s) inexistant… c’est une bonne pratique, mais le choix est libre.

Lister un dossier

  • La fonction os.listdir(path: str) -> List[str] renvoie la liste des noms (sans le chemin) de fichiers (et sous-dossiers) d’un dossier (répertoire) ; elle déclenche des exceptions si le fichier n’existe pas, si ce n’est pas un dossier, si l’utilisateur n’a pas les permissions ou en cas d’erreur d’entrée-sortie.
  • La fonction os.path.join(part1: str, part2: str, …) -> str permet de concaténer deux (ou plusieurs) bouts de chemins en utilisant des \ sous Windows et des / sous Linux et macOS.

Exemple

#!/usr/bin/env python3

import os
import sys

if __name__ == "__main__":
    if len(sys.argv) != 2 or not os.path.isdir(sys.argv[1]):
        print("usage : " + sys.argv[0] + " dossier", file=sys.stderr)
        sys.exit(1 if len(sys.argv) != 2 else 2)

    folder = sys.argv[1]
    print("contenu du dossier " + folder + " :")
    try:
        for name in os.listdir(folder):
            path = os.path.join(folder, name)
            if os.path.isfile(path):
                print(path)
            else:
                print(path + "/") #pour faire joli ;-)
    except PermissionError:
        print("erreur : accès refusé", file=sys.stderr)
    except OSError as e: #autrefois IOError
        print(f"erreur : {e}", file=sys.stderr)

Remarques

  • L’existence du dossier a été vérifiée avant l’instruction os.listdir(…) ; la capture des exceptions FileNotFoundError et NotADirectoryError n’est donc pas nécessaire.
  • Les erreurs d’entrées sorties ne peuvent pas être anticipées.
  • Il est possible de vérifier les permissions au préalable : lecture (os.R_OK), écriture (os.W_OK) ou accès / exécution (os.X_OK) ; exemple :
if not os.access(folder, os.R_OK):
    print("erreur : accès refusé", file=sys.stderr)
    sys.exit(3) #bonne pratique: autre cas d'erreur, autre code

Lecture et écriture de fichiers textes

Pour travailler sur un fichier, il faut au préalable l’ouvrir, le parcourir (s’il est en lecture), ou y écrire, puis le fermer.

  • La fonction pour ouvrir un fichier est open(chemin: str, mode: str) ; elle prend en paramètre le chemin du fichier et le mode d’ouverture : r(ead) / w(rite), t(text) / b(binary) et renvoie un pointeur de fichier (dont le type précis varie selon le mode).
  • Les fonctions de lecture sont readline() et read(nb: int), qui prend en paramètre le nombre de charactères (texte) ou d’octets (binaire) à lire ; elles renvoient du texte (str) ou des octets (bytes).
  • La fonction d’écriture est write(content: str/bytes).
  • La fonction pour fermer un fichier est close() ; la syntaxe avec with permet de s’en affranchir (fermeture automatique à la sortie du bloc).

Les exemples présentés ici sont un minimum pour lire et écrire dans des fichiers textes ou binaires. Pour approfondir, se référer à la documentation officielle.

Lecture de texte

La fonction readline() lit une ligne entière — jusqu’au caractère de fin de ligne (\n et/ou \r). Attention, les caractères de fin de ligne sont inclus dans la valeur renvoyée ; la fonction strip() permet de les supprimer.

Exemple :

#!/usr/bin/env python3

import sys

try:
    f = open("test.txt", "rt")
    line = f.readline()
    i = 1
    while line != "":
        print(str(i).ljust(2) + " : " + line.strip("\n"))
        line = f.readline()
        i += 1
    f.close()
except FileNotFoundError:
    print("erreur : le fichier n'existe pas", file=sys.stderr)
except PermissionError:
    print("erreur : accès refusé", file=sys.stderr)
except OSError as e:
    print("erreur : " + str(e), file=sys.stderr)

Remarque: la variable i n’a qu’un rôle pédagogique et pourrait être omise.

Syntaxe avec “with" :

with open("test.txt", "rt") as f:
    line = f.readline()
    i = 1
    while line != "":
        print(str(i).ljust(2) + " : " + line.strip())
        line = f.readline()
        i += 1
#le close peut être omis

Syntaxe compacte :

with open("test.txt", "rt") as f:
    while "" != (line := f.readline()):
        print(line.strip().upper()) #avec mise en majuscules

Écriture texte

L’écriture dans un fichier texte peut s’effectuer ligne par ligne ; Penser à ajouter les caractères de fins de ligne.

with open("test.txt", "wt") as f:
    line = input("Saisir une ligne : ")
    while "" != line:
        f.write(line + "\n")
        line =input("Saisir une ligne : ")

Remarque pour le professeur : “Basthon Notebook -> Fichier -> Ouvrir”, et import basthon; basthon.download("test.txt").

Lecture et écriture de fichiers binaires

Écriture binaire

L’écriture binaire demande un peu de rigueur : contrairement aux lignes d’un fichier texte qui peuvent être de longueur variables, il n’est pas possible d’utiliser un délimiteur dans le cas de données binaires.

Dans le cas de nombres, il est par exemple nécessaire de les écrire sur un nombre déterminé d’octets (1, 2, 4 ou 8) — selon le nombre de bits nécessaires pour représenter la plus grande valeur. Il y a plusieurs possibilités pour obtenir la séquence d’octets (bytes) représentant un nombre :

  • par le calcul (base 256)
  • utiliser la méthode to_bytes(nb_octets: int) sur un nombre entier ;
  • utiliser la fonction struct.pack(format: str, valeur) ; les formats sont :
    • préfixe < pour ordonner les octets en petit boutiste (“little-endian”) — par défaut sur architecture “x86” / “x86_64” ou > pour les ordonner en grand boutiste (les octets de poids fort en premier) ;
    • b pour un nombre signés codé sur 8 bits (de -128 à + 127) ;
    • B pour un nombre non signé sur 8 bits (de 0 à 255) ;
    • h / H pour des nombres signés / non signés sur 16 bits ;
    • i / I pour des nombres signés / non signés sur 32 bits ;
    • q / Q pour des nombres signés / non signés sur 64 bits.
import struct

with open("test.bin", "wb") as f:
    nbr = input("Saisir un nombre entre 0 et 65535 : ")
    while "" != nbr:
        #b_nbr = bytes([nbr//256, nbr%256]) #calcul : grand boutiste
        #b_nbr = int(nbr).to_bytes(2)       #grand boutiste
        b_nbr = struct.pack(">H", int(nbr))
        f.write(b_nbr)
        nbr = input("Saisir un nombre entre 0 et 65535 : ")

La commande hexdump -X test.bin permet de visualiser le contenu du fichier généré. Comparer le résultat selon les formats petit et grand boutiste.

Lecture binaire

Pour la lecture d’un fichier binaire, il faut être également être rigoureux, notamment pour la lecture des nombres. Le programme suivant utilise la fonction struct.unpack pour décoder les octets lus ; le résultat devrait être le même que celui obtenu par le calcul (base 256).

import struct

with open("test.bin", "rb") as f:
    while len(b_nbr := f.read(2)) != 0:
        u_nbr = struct.unpack(">H", b_nbr)[0]   #unpack renvoie un tuple
        nbr = b_nbr[0]*256 + b_nbr[1]           #grand boutiste
        #nbr = b_nbr[1]*256 + b_nbr[0]          #petit boutiste
        print(b_nbr, u_nbr, nbr)

Le choix entre petit et grand boutiste doit être conforme à celui utilisé pour l’écriture.

Approfondissement / réflexion (rappel mathématiques) : soit “b0” une séquence binaire (“101” par exemple) ; “b1” est cette même séquence décalée d’un bit vers la gauche (on ajoute un “0” à droite : b1 = b0<<1 = “1010”) ; indiquer à quelles opérations correspondent un décalage de “n” bits vers la gauche (ou la droite).