3 Septembre, 2024
Arkage Parser -
Comment interpréter une ligne de commande ?
Normes et bonnes pratiques pour l'analyse syntaxique et l’interprétation des lignes de commande.
Introduction
Ce document vise à établir un cadre rigoureux pour la conception et l'interprétation des interfaces en ligne de commande (CLI). En définissant un ensemble de règles strictes concernant la syntaxe, les types de données et les structures de commandes, cette convention a pour objectifs :
- Améliorer la lisibilité et la cohérence des interfaces CLI, facilitant ainsi leur utilisation par les utilisateurs.
- Réduire les erreurs d'interprétation et les ambiguïtés, garantissant un traitement fiable des commandes.
- Favoriser l'interopérabilité entre les différents outils CLI en adoptant des conventions communes.
- Servir de référence pour les développeurs souhaitant créer de nouveaux outils CLI.
Cette convention abordera en détail les éléments constitutifs d'une ligne de commande et comment en faire une bonne analyse syntaxique. Elle définira également les types de données supportés et les mécanismes d'interprétation. Des exemples pratiques ainsi que des schémas illustreront l'application de ces règles dans des scénarios concrets.
Cette convention peut se révéler particulièrement utile pour les développeurs qui souhaite créer leur propre outil CLI.
Les bases de l’interprétation
Transformer une chaîne de caractères en une action précise : tel est le défi de l'interprétation des lignes de commande. Derrière la simplicité apparente d'une commande se cachent des mécanismes complexes d'analyse syntaxique et soulève de nombreuses questions :
- Comment associer les bons arguments aux options / commandes ?
- Comment vérifier la validité des données fournies par l'utilisateur ?
- Comment gérer les erreurs ?
Ce chapitre aborde les différentes techniques d'interprétation, les pièges à éviter et les bonnes pratiques à adopter pour créer des outils CLI fiables et robustes.
Convention de nommage
Dans cette convention technique, nous adopterons la convention de nommage kebab-case
pour les options, les commandes ou les sous-commandes. Ceci est pour assurer une cohérence et une lisibilité maximales dans la définition de nos commandes et options.
Le kebab-case consiste à écrire les mots en minuscules, séparés par des tirets (-). Par exemple : create-user
, list-files
, set-config
.
Ce choix est motivé par sa lisibilité accrue grâce à la séparation claire des mots et par sa compatibilité avec la syntaxe des options longues en ligne de commande (par exemple, --create-user
). Cette convention est également largement adoptée dans de nombreux domaines du développement web, ce qui la rend familière à la plupart des développeurs.
Les types
De nombreux outils CLI utilisent l'auto-complétion pour suggérer des options et des valeurs possibles. Le typage des arguments est crucial pour fournir des suggestions pertinentes et réduire les erreurs de saisie.
Les types de base sont bien entendu les string
les number
et les bool
mais il est parfois intéressant d’aller un peu plus loin dans les types attendu afin de proposer une meilleur expérience. Voici une liste non-exhaustive de types intéressant à traiter :
Nom | type | Description |
---|---|---|
Chaîne de caractère | string |
Attention, le caractère " est réservé pour contenir le string , possibilité de faire "\"" pour échapper le caractère |
Caractères | char |
N’importe quel caractère unique, ' est réservé pour contenir le char , possibilité de faire '\'' pour échapper le caractère |
Booléen | bool |
Capable de parser un TrUe en true par exemple |
Fichier | file |
Peut vérifier l’existence du fichier et son extension au lancement de la commande (optionnel) |
Dossier | folder |
Peut vérifier l’existence du dossier au lancement de la commande (optionnel) |
Nombre | number |
Type double , peuvent être signés |
Url | url |
Capable de reconnaitre le format d’un url |
Enumération | enum |
Une liste précise de valeurs possibles |
Tableau | Array<T> |
Cas particulier, ne peut être utilisé que pour les arguments de commandes en dernier argument |
Commande
Le premier mot d'une commande désigne le programme à exécuter. Ce programme, souvent appelé exécutable ou binaire, est un fichier contenant les instructions à exécuter. Le système d'exploitation recherche les exécutables dans des répertoires spécifiques, définis dans votre configuration (par exemple, dans le fichier .bashrc
). La commande est le point d’entré de votre programme.
~ which git
/usr/bin/git
Sous-commande
Les sous-commandes sont des commandes spécifiques à un programme principal. Elles permettent d'organiser les fonctionnalités d'un programme en modules plus fins.
Par exemple, git
utilise des sous-commandes pour effectuer des opérations comme checkout
, merge
ou commit
. Ces sous-commandes sont en fait des commandes à part entière qui sont exécutées dans le contexte de la commande git
.
Remarque: Tous les programmes n'utilisent pas de sous-commandes. Certains programmes exécutent une seule tâche et prennent directement leurs arguments.
# Par exemple, git a des sous-commandes :
git checkout
git merge
git commit
# Mais la commande ping n'en a pas, il prend directement un argument
ping www.google.com
Surcharge d’arguments
Les sous-commandes ont cette particularité, comme en Java, de pouvoir proposer plusieurs possibilités et combinaisons d’arguments possible pour une seule sous-commande. On parle alors de plusieurs signatures pour une sous-commande.
Les règles sont similaire à celles en Java : les signatures sont déclarées pour une seule et même sous-commande, et les signatures doivent avoir un nombre d’arguments différents ou avoir des types différents. Nous verrons un cas pratique plus bas dans ce document.
Les options
Qu'est-ce qu'une option ?
Une option est un argument spécial qui vient modifier le comportement d'une commande. Elle est précédée de deux tirets (--
) pour la distinguer des autres arguments.Son rôle est de fournir des informations supplémentaires à la commande pour qu'elle s'exécute de manière spécifique.
Une options nécessitent systématiquement une valeur typée pour fonctionner. Par exemple :
--output ./mon_fichier.txt
# Ici, `--output` est l'option et `./mon_fichier.txt` est
# la valeur qui indique où enregistrer la sortie
--output 4
# L'option attendait un chemin de fichier, un nombre ne fais pas sens ici
Les options globales
Certaines options sont définies au niveau du programme principal et sont donc disponibles pour toutes les sous-commandes. On les appelle des options globales. D'autres options sont spécifiques à une sous-commande particulière et sont donc appelées options locales.
Par exemple, l’option --verbose
pourrait être globale et disponible dans tout le programme, tandis que l’option --output
serait spécifique à la sous-commande exécutée
Options booléennes : un cas particulier
Bien qu’une option requière une valeur pour fonctionner, il existe un cas où il n’est pas nécessaire de spécifier une valeur.
Ce cas est quand une option attend une valeur de type booléen :
- Valeur par défaut: Par défaut, une option booléenne est considérée comme étant
false
. Si vous ne la spécifiez pas, la valeur par défaut sera considérée. - Activation: Pour activer une option booléenne, il suffit de l'inclure dans la commande et sa valeur passera à
true
.
Désactiver explicitement une option booléenne
Dans certains cas, vous pourriez vouloir indiquer explicitement que vous souhaitez désactiver une option booléenne, pour plus de compréhension pour l’utilisateur. Pour cela, de nombreuses commandes proposent une syntaxe spécifique, souvent en ajoutant le préfixe no-
avant le nom de l'option :
--verbose # L'option verbose sera égal à true
--no-logs # L'option no-logs sera égal à true
--logs false # Impossible d'attribuer un argument à une option de type bool
Rappel de convention de nommage
Comme expliqué au début de ce document, afin d'améliorer la clarté et la maintenabilité des scripts, il est recommandé d'utiliser la notation kebab-case pour nommer les options. Cette convention consiste à séparer les mots par des tirets, en utilisant uniquement des minuscules.
Exemple :
--output-file --set-credentials --no-logs
--outputFile # Mauvais format
Règles à retenir :
- Unicité: Chaque option doit avoir un nom unique au sein d'une commande.
- Type de données: Les options peuvent avoir différents types de données (chaîne de caractères, nombre, booléen…).
- Valeur par défaut: Les options booléennes ont une valeur par défaut (
false
). - Kebab-case: Les options doivent utiliser la convention kebab-case
Les flags
Pour simplifier la saisie des commandes, de nombreuses commandes proposent des options courtes, souvent appelées flags. Un flag est généralement représenté par un seul caractère précédé d'un tiret simple (-
).
Les flags ont la particularité de pouvoir s’accumuler. Il est fréquent de pouvoir combiner plusieurs flags en un seul argument. Par exemple, ls -al
équivaut à ls -a -l
.
Les flags sont souvent des raccourcis pour des options longues, qui sont généralement plus explicites.
Règles à retenir :
- Unicité: Chaque flag doit être unique au sein d'une commande.
- Optionnel: Chaque option ne requière pas forcément un flag, il s’agit juste d’un “raccourci”
- Explicite: Un flag doit porter une lettre explicite de l’option à laquelle il est rattaché, par exemple
-o
pour--output
Les arguments
Les arguments sont des données fournies à une commande ou à une sous-commande afin de spécifier les éléments sur lesquels elle doit opérer. Ils sont étroitement liés aux options et aux flags, mais se distinguent par leur type et leur position dans la ligne de commande.
Pour assurer la cohérence et la robustesse d'une commande, chaque argument doit correspondre à un type de données référencé. Ce typage permet de valider les entrées utilisateur et d'éviter les erreurs d'exécution. Dans le cas échéant, cela permettra de retourner des erreurs détaillées.
Tableau d’arguments
Certaines commandes peuvent accepter un nombre variable d'arguments. Pour représenter ce concept, le type Array<T>
est couramment utilisé. Un argument de type tableau peut contenir zéro, un ou plusieurs éléments du type T
.
En raison de leur nature variable, les arguments de type tableau doivent être placés en dernière position dans la liste des arguments d'une commande. Cette convention permet l'analyse de la ligne de commande et permet d'interpréter correctement les valeurs fournies par l'utilisateur.
Voici un exemple :
# Les arguments acceptés sont number, Array<string>
arkage ping 4 us uk eu-west eu-est
Ce processus est très proche des variadic
en PHP, cependant il n’est possible d’avoir un type Array<T>
uniquement pour les arguments de commandes / sous-commande. Ce type n’est pas utilisable pour une option pour des raisons de lisibilité.
Exemples et cas pratique
Déclaration complète d’une commande
Les options globales
Pour notre cas pratique, considérons une commande fictive que nous nommerons arkage
. Dans un premier temps nous pouvons déclarer nos options globales de notre commande :
Option | Flag | Type | Défaut | Description |
---|---|---|---|---|
--verbose |
-v |
Booléen | true |
Active le mode verbose |
--lang |
-l |
string |
en |
Change la lange |
--en |
Booléen | true |
Change la langue en en |
|
--fr |
Booléen | true |
Change la langue en fr |
Par déduction, on comprend qu’il serait redondant de mettre les deux options --en
et --fr
dans la même ligne de commande. Cependant, d’un point de vue syntaxique, rien ne nous l’interdit. Ça sera au programme de décider quel langage choisir ou s’il souhaite lever une erreur.
arkage help --fr --en # Syntaxe valide
Les sous-commandes
Une fois les options globales déclarées, nous avons besoin de sous-commandes pour avoir de la matière à exécuter. Pour déclarer une sous-commande, il est important de déclarer son nom (toujours en kebab-case
), ses options et ses arguments possible
Sous-commande | Options | Flag | Type | Arguments sous-commande |
---|---|---|---|---|
help |
Aucuns arguments | |||
ping |
int , Array<string> |
|||
register |
--credentials |
-c |
Booléen | string , string ou filepath |
--key |
-k |
Booléen | ||
--server |
-s |
string |
Les sous-commandes disponibles sont :
help
ping
register
La commande help
ne prend aucuns arguments mais peut prendre l’option suivantes :
--lang
ou-l
avec un argument string derrière--fr
et est de type booléen--en
et est de type booléen
Pour la sous-commande ping
, celle ci ne prend pas d’options mais peut prendre une infinité de string
arkage ping 4 us uk eu-west eu-est
Le 1er argument spécifie le nombre de tentatives, le reste les serveurs à atteindre. Les arguments sont reconvertis dans un tableau pour le traitement de l’information. Voici donc la déclaration des arguments pour cette sous-commande :
- Tentatives
int
- Serveurs
Array<string>
Un Array
doit toujours être le dernier argument car celui-ci peut contenir une infinité de valeurs
Concernant la sous-commande register
, les options --credentials
et --key
s'excluent mutuellement. Seule l'une des deux peut être utilisée dans une même commande. Car il est uniquement possible de se connecter grâce à une clé, soit avec des identifiants.
arkage register -c teo@arkage.com 1234aze*
arkage register -k ./id_rsa
arkage register -s eu-west -k ./id_rsa -c teo@arkage.com 1234aze* -v --lang en
# Syntaxe valide
# Erreur, mais côté traitement de la commande car -c et -k sont incompatible
Conclusion
Cette convention a établi un cadre rigoureux pour la conception et l'interprétation des interfaces en ligne de commande. En définissant des règles strictes concernant la syntaxe, les types de données et les structures de commandes, nous avons jeté les bases d'une expérience utilisateur cohérente et intuitive.
Les points clés à retenir sont :
- Importance de la convention de nommage: L'utilisation du kebab-case améliore la lisibilité et la maintenabilité des commandes.
- Typage des arguments: Le typage strict permet de valider les entrées utilisateur et de réduire les erreurs.
- Structure des commandes: La distinction entre commandes, sous-commandes, options et arguments est essentielle pour une organisation claire.
- Attribution des valeurs: Les valeurs et les arguments doivent être attribués dans l'ordre à leurs options et leurs sous-commances
- Gestion des erreurs: Une bonne gestion des erreurs est indispensable pour fournir des messages d'erreur clairs et constructifs.
À venir
Et ce n'est qu'un début ! Nous prévoyons d'étendre cette convention en abordant des sujets tels que :
- Complétion intelligente: Tirant parti du typage pour proposer des suggestions pertinentes.
- Messages d'erreur détaillés: Guidant l'utilisateur vers une résolution rapide des problèmes.
- Priorité des options et flags: Clarifiant les règles de résolution des conflits.
- Commandes imbriquées: Offrant une flexibilité accrue pour les utilisateurs expérimentés.
- Regex dans les commandes: Ou comment exécuter plusieurs actions grâces aux expressions régulières
N'hésitez pas à contribuer à l'évolution de cette convention via notre GitHub