Lorsqu’il s’agit de développement logiciel, une majorité des outils à notre disposition sont des applications en ligne de commande. Il est également bien de noter que de nombreux outils utilisés en ligne de commande sont assez puissants dans ce qu’ils peuvent accomplir, du trivial au fastidieux. Pour aller plus loin, vous pouvez combiner des applications en ligne de commande pour enchaîner une séquence de travail et obtenir le résultat souhaité. L’apprentissage des commandes en ligne de commande existantes vous permet d’augmenter votre productivité et de mieux comprendre les capacités à portée de main et les tâches que vous devrez peut-être mettre en œuvre par vous-même.
Lorsque vous concevez un outil en ligne de commande, vous devez vraiment prêter attention à qui, ou quoi, est votre public cible. Par exemple, si toutes les personnes qui utiliseront cet outil utilisent le même système d’exploitation et le même environnement, alors vous avez la plus grande flexibilité pour profiter de l’écosystème complet. Cependant, si vous devez implémenter votre application en ligne de commande pour qu’elle fonctionne sur plusieurs systèmes d’exploitation, vous avez maintenant limité les outils que vous pouvez utiliser à ce qui est disponible sur tous ces systèmes — ou vous devez implémenter des exceptions pour chaque système d’exploitation dans votre application. (Cela peut être très fastidieux à rechercher et à tester.)
La solution la plus souvent utilisée pour gérer les divers environnements OS est d’éviter tout outil externe spécifique à l’OS lorsque cela est possible ou de déléguer la responsabilité à des bibliothèques qui ont déjà fait le dur travail d’implémentation des fonctionnalités pour fonctionner sur plusieurs architectures. Plus vous pouvez maintenir l’implémentation à un langage de base sans dépendre de dépendances externes, plus il sera facile de maintenir votre projet.
Les composants d’une application en ligne de commande en Ruby
Il y a trois domaines de préoccupation à aborder lors de la construction d’une application en ligne de commande : l’entrée, la sortie, et tout ce qui se trouve entre les deux. Si vous développez un outil de ligne de commande unique qui prend simplement une entrée ou une source et la traite/formate et crache l’information, alors votre travail est plutôt simple. Cependant, si vous développez une interface utilisateur avec des menus à parcourir, les choses commencent à se compliquer.
Traitement de l’entrée
Pour commencer à traiter l’entrée, nous avons des paramètres qui peuvent être donnés à votre application. Ces arguments sont la façon la plus typique de démarrer une commande, avec des détails sur la façon dont vous voulez qu’elle s’exécute et éviter le besoin de systèmes de menus. Dans cet exemple:
ruby -e "puts RUBY_VERSION"
Ruby est le programme en ligne de commande en cours d’exécution, -e
est appelé un drapeau, et "puts RUBY_VERSION"
est la valeur donnée pour le drapeau. Dans ce cas, l’indicateur -e
de Ruby signifie qu’il faut exécuter ce qui suit en tant que code Ruby. Lorsque j’exécute ce qui précède sur la ligne de commande, je récupère 2.4.0
imprimé sur la sortie standard (qui s’affiche simplement sur la ligne suivante).
Argument input
L’argument input, ou paramètres, sont tous les bits supplémentaires de texte entrés après une commande et séparés par un espace. Presque toutes les commandes permettent un argument drapeau d’aide. Un argument drapeau a un tiret ou deux devant lui.
Les drapeaux d’aide standard sont -h
, et --help
. À l’époque du MSDOS, c’était (et c’est peut-être encore) /?
. Je ne sais pas si la tendance pour les drapeaux dans Windows a continué à utiliser le forward-slash comme marqueur de drapeau, mais les scripts de ligne de commande multiplateformes utilisent de nos jours des tirets.
En Ruby, l’entrée d’une ligne de commande est placée dans deux variables différentes : ARGV et ARGF. ARGV gère les paramètres d’entrée sous la forme d’un tableau de chaînes de caractères ; ARGF sert à gérer les flux de données. Vous pouvez utiliser ARGV pour les paramètres directement, mais cela peut représenter plus de travail que nécessaire. Il existe quelques bibliothèques Ruby construites pour travailler avec les arguments de la ligne de commande.
OptionParser
OptionParser a l’avantage d’être inclus avec Ruby, ce n’est donc pas une dépendance externe. OptionParser vous donne un moyen facile à la fois d’afficher les options de la ligne de commande qui sont disponibles et de traiter l’entrée dans n’importe quel objet Ruby que vous souhaitez. Voici un extrait d’un de mes outils de ligne de commande dfm:
require 'optionparser'options = {}printers = Array.newOptionParser.new do |opts| opts.banner = "Usage: dfm \nDefaults: dfm -xd ." + File::SEPARATOR opts.on("-f", "--filters FILTERS", Array, "File extension filters") do |filters| options = filters end opts.on("-x", "--duplicates-hex", "Prints duplicate files by MD5 hexdigest") do |dh| printers << "dh" end opts.on("-d", "--duplicates-name", "Prints duplicate files by file name") do |dh| printers << "dn" end opts.on("-s", "--singles-hex", "Prints non-duplicate files by MD5 hexdigest") do |dh| printers << "sh" end opts.on("-n", "--singles-name", "Prints non-duplicate files by file name") do |dh| printers << "sn" endend.parse!
Dans cet exemple, chaque bloc opts.on
comporte du code à exécuter si l’option a été passée en ligne de commande. Les quatre options du bas sont (dans leur bloc) simplement en train d’annexer les infos du drapeau dans un tableau pour que je puisse les utiliser plus tard.
Le premier a Array
donné comme l’un des paramètres à on
, donc l’entrée pour ce drapeau sera convertie en un tableau Ruby et stockée dans mon hachage nommé options
sous la clé filters
.
Le reste des paramètres donnés à la méthode on
sont les détails du drapeau et la description. Les drapeaux court et long fonctionneront pour exécuter le code donné dans le bloc suivant.
OptionParser a également les drapeaux -h
et --help
intégrés par défaut, donc vous n’avez pas besoin de réinventer la roue. Tapez simplement dfm -h
pour l’outil en ligne de commande ci-dessus, et il sort gentiment une description utile:
Usage: dfm Defaults: dfm -xd ./ -f, --filters FILTERS File extension filters -x, --duplicates-hex Prints duplicate files by MD5 hexdigest -d, --duplicates-name Prints duplicate files by file name -s, --singles-hex Prints non-duplicate files by MD5 hexdigest -n, --singles-name Prints non-duplicate files by file name
Slop
OptionParser est un peu verbeux quand il s’agit d’écrire les définitions de la ligne de commande. La gemme Slop est conçue pour vous permettre d’écrire votre propre analyse syntaxique de ligne de commande avec beaucoup moins d’efforts. Au lieu de vous faire fournir les blocs de code dans la définition du drapeau, Slop crée simplement un objet que vous pouvez interroger dans votre application pour voir si un drapeau a été donné et quelle(s) valeur(s) ont pu être fournies pour lui.
# Excerpt from https://github.com/leejarvis/slopopts = Slop.parse do |o| o.string '-h', '--host', 'a hostname' o.integer '--port', 'custom port', default: 80 o.bool '-v', '--verbose', 'enable verbose mode' o.bool '-q', '--quiet', 'suppress output (quiet mode)' o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host' o.on '--version', 'print the version' do puts Slop::VERSION exit endendARGV #=> -v --host 192.168.0.1 --check-ssl-certificateopts #=> 192.168.0.1opts.verbose? #=> trueopts.quiet? #=> falseopts.check_ssl_certificate? #=> trueopts.to_hash #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }
Cette simplicité peut aider à simplifier votre base de code et votre suite de tests, ainsi qu’à accélérer votre temps de développement.
Sortie (STDOUT)
Lorsque vous écrivez un outil de ligne de commande simple, vous voulez souvent qu’il sorte les résultats du travail effectué. Garder à l’esprit quelle est la cible de cet outil déterminera en grande partie comment vous voulez que la sortie se présente.
Vous pourriez formater les données de sortie comme une simple chaîne, une liste, un hachage, un tableau imbriqué, JSON, XML, ou toute autre forme de données à consommer. Si vos données sont destinées à être diffusées en continu sur une connexion réseau, alors vous voudrez emballer les données dans une chaîne de données serrée. Si elles sont destinées à être vues par les yeux d’un utilisateur, alors vous voudrez les développer d’une manière présentable.
De nombreux outils de ligne de commande Linux/Mac existants peuvent imprimer des détails par paires ou ensembles de valeurs. Les informations peuvent être séparées par un deux-points, des espaces, des tabulations et des blocs d’indentation. Lorsque vous n’êtes pas sûr de la façon exacte dont il peut être utilisé, optez pour la façon la plus simple et la plus présentable de présenter les données.
Un exemple de cible que vous pourriez avoir besoin de considérer est un outil de test d’API. De nombreuses API fournissent une réponse JSON et peuvent être accessibles avec un outil en ligne de commande comme curl
. Si la bande passante est un souci, alors utilisez la méthode to_json
de JSON, mais si c’est destiné au travail sur une machine locale, utilisez pretty_generate
.
x = {"hello" => "world", this: {"apple" => 4, tastes: "delicious"}}require 'json'puts x.to_json# {"hello":"world","this":{"apple":4,"tastes":"delicious"}}puts JSON.pretty_generate( x )# {# "hello": "world",# "this": {# "apple": 4,# "tastes": "delicious"# }# }
Vous pouvez de même utiliser YAML pour les données.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Si vous voulez avoir une sortie plus complexe joliment formatée, alors je recommande fortement d’utiliser la gemme awesome_print. Cela vous donnera un contrôle spécifique de votre présentation sur la sortie.
Erreur standard (STDERR)
C’est un autre type de sortie qui peut se produire à partir d’applications en ligne de commande. Lorsque quelque chose ne va pas ou a mal tourné, il est habituel que l’outil de ligne de commande écrive sur la sortie connue sous le nom de STDERR. Il apparaîtra comme une sortie régulière, mais d’autres outils peuvent vérifier que la commande n’a pas réussi.
STDERR.puts "Oops! You broke it!"
La pratique la plus courante est d’utiliser un Logger pour écrire des détails spécifiques sur les erreurs. Vous pouvez acheminer cela vers la sortie STDERR sur la ligne de commande ou éventuellement un fichier journal.
Interface utilisateur (STDIN et STDOUT)
Écrire une interface utilisateur peut être l’une des choses les plus gratifiantes à accomplir. Elle vous permet d’appliquer un certain design artistique et de fournir différentes façons pour un utilisateur d’interagir.
L’interface utilisateur minimale est un affichage d’un certain texte avec une invite attendant une entrée. Elle peut être aussi simple que :
Who is your favorite super hero /Batman/Wonder-Woman ?
Ce qui précède indique la question, les options et une option par défaut si l’utilisateur choisit d’appuyer sur la touche entrée sans rien saisir. En Ruby ordinaire, cela ressemblerait à :
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
C’est un code très grossier à utiliser pour l’entrée et la sortie, et si vous avez l’intention d’écrire une interface utilisateur, je recommande fortement d’essayer une gemme comme highline ou tty.
Avec la gemme highline, vous pourriez écrire ce qui précède comme:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Ici, highline montrera la question, montrera le défaut, et attrapera toute option incorrecte entrée, notifiant l’utilisateur qu’il n’a pas sélectionné une des options fournies.
Les deux gemmes highline et tty ont une vaste quantité de choses supplémentaires que vous pouvez faire pour un menu et une expérience utilisateur, avec des subtilités telles que l’ajout de couleurs à votre affichage. Mais encore une fois, vous devez prendre en considération qui est votre public cible.
Plus vous fournissez une expérience visuelle, plus vous devez prêter attention aux capacités multiplateformes de ces outils. Toutes les lignes de commande ne traitent pas les mêmes données de présentation de la même manière, ce qui crée une mauvaise expérience utilisateur.
Compatibilité multiplateforme
La bonne nouvelle est que Ruby possède une grande partie des solutions nécessaires à l’outillage sur différents systèmes d’exploitation. Lorsque Ruby est compilé pour un système d’exploitation spécifique, le fichier source de RbConfig est généré avec des valeurs absolues natives du système sur lequel il a été construit, enregistrées au format hash. C’est donc la clé pour détecter et utiliser les fonctionnalités du système d’exploitation.
Pour voir ce fichier avec votre éditeur de texte préféré, vous pouvez exécuter le code Ruby suivant:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Cela vous montrera tout d’une manière présentable, ce qui me semble mieux que de voir le hachage via RbConfig::CONFIG
. Ce hachage comprend des choses utiles comme les commandes utilisées pour manipuler le système de fichiers, la version de Ruby, l’architecture du système, où réside tout ce qui est important pour Ruby, et d’autres choses de ce genre.
Pour vérifier le système d’exploitation, vous pouvez faire :
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Pour d’autres valeurs spécifiques au système d’exploitation, vous pouvez regarder le code source de Gem::Platform.
Maintenant, les commandes spécifiques au système de fichiers stockées ici ne sont pas destinées à être utilisées à partir de cette ressource. Ruby a les classes Dir, File, et Pathname écrites pour cela.
Lorsque vous avez besoin de savoir si un exécutable existe dans le PATH du système, alors vous voudrez utiliser MakeMakefile.find_executable. Ruby supporte la construction d’extensions C et l’une des fonctionnalités sympathiques qu’ils ont ajouté pour cela est cette possibilité de savoir si l’exécutable existe pour l’appeler.
Mais cela va générer un fichier journal dans le répertoire courant de votre système à chaque fois que vous l’exécutez. Donc pour éviter d’écrire ce fichier journal, vous devrez faire ce qui suit:
require 'mkmf'MakeMakefile::Logging.instance_variable_set(:@log, File.open(File::NULL, 'w'))executable = MakeMakefile.find_executable('clear') # or whatever executable you're looking for
Lorsque vous dessinez un menu visuel fenêtré sur une ligne de commande, il est préférable que le menu reste dans une position fixe lorsque l’affichage se met à jour. La façon la plus simple de le faire est d’effacer la ligne de commande entre chaque écriture de l’affichage complet.
Dans l’exemple ci-dessus, j’ai cherché la commande clear
. Sous Windows, la commande shell pour effacer la ligne de commande est cls
, mais vous ne pourrez pas trouver d’exécutable pour celle-ci car elle fait partie du code interne de command.com
.
La bonne nouvelle est que saisir la sortie de chaîne de la commande clear
de Linux produit cette séquence de code d’échappement : \e[3J\e[H\e[2J
. Moi et d’autres personnes avons testé cela sur Windows, Mac et Linux, et cela fait exactement ce que nous voulons : effacer l’écran pour le redessiner dessus.
Cette chaîne d’échappement a trois actions différentes qu’elle effectue :
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Cependant, il est préférable d’utiliser une bibliothèque de ligne de commande plutôt que d’écrire les codes d’échappement vous-même. Mais celui-ci mérite bien d’être mentionné.
Tester STDIN/STDOUT/STDERR
Avec tout ce qui écrit dans quelque chose comme STDOUT ou STDERR, il serait plus sage de créer une variable interne dans votre objet Ruby UI qui peut être modifiée avec un paramètre optionnel à new
. Ainsi, lorsque votre programme s’exécute normalement, la valeur sera STDOUT
. Mais lorsque vous écrirez des tests, vous passerez StringIO.new
, contre laquelle vous pourrez facilement tester.
Lorsque vous essayez de lire l’IO d’une commande exécutée en dehors de Ruby, les tests sont un peu plus impliqués. Vous aurez probablement besoin de regarder dans Open3.popen3 pour gérer chaque flux IO STDIN, STDOUT, et STDERR.
Utiliser les commandes Bash
Bash est arrivé sur Windows (WSL) ! Cela signifie que vous avez accès aux commandes bash sur la grande majorité des systèmes d’exploitation utilisés dans le monde. Cela ouvre plus de possibilités pour votre propre outil de ligne de commande.
Il est typique de « pipe » les commandes les unes à travers les autres, en envoyant la sortie d’une commande comme un flux à la suivante. Sachant cela, vous pouvez envisager d’ajouter le support de l’entrée en flux, ou réfléchir à la façon de mieux formater votre sortie pour la consommation d’autres outils de ligne de commande.
Voici un exemple de substitution regex avec la commande sed
de Bash recevant son entrée canalisée depuis echo:
echo hello | sed "s/ll/ck n/g"
Cela sort heck no
. Plus vous apprenez sur Bash, plus vous serez en mesure d’accomplir des choses sur une ligne de commande et mieux vous serez équipé pour écrire de meilleurs outils de ligne de commande de votre propre.
Summary
Il y a beaucoup à apprendre quand il s’agit d’écrire vos propres outils de ligne de commande. Plus vous devez prendre en compte de situations, plus les choses deviennent complexes.
Prenez le temps d’apprendre les outils que vous avez à portée de main, et vous récolterez de nombreuses récompenses que vous ne saviez pas que vous manquiez. Je vous souhaite le meilleur !