Sortir Java de sa coquille : découvrir jshell
Une REPL en Java ?
Quand on a besoin de tester ou d’expérimenter rapidement un bout de code il est intéressant d’avoir une REPL (Read-Eval-Print Loop) : un shell interactif dédié à un langage dans lequel on peut saisir et évaluer du code pour avoir un feedback immédiat. De nombreux langages et environnements de développement en proposent une comme par exemple Ruby, Python ou encore Groovy, Scala et Clojure pour des langages de l’écosystème Java.
D’ailleurs cela remonte au premier temps de l’informatique numérique (fin de la décennie 1950) avec LISP qui se distinguait de langages comme Fortran ou COBOL par cet aspect justement. Ainsi en Clojure — un dialecte moderne de LISP que l’on trouve sur la JVM — ces afficionados promeuvent le “REPL Driven Development”.
Il est des situations où l’on souhaite tester rapidement un petit bout de code sans pour autant initialiser un projet complet dans son IDE préféré. Une REPL peut alors être un outil pratique dans ces cas-là.
Le JDK propose une REPL depuis sa version 9 — prénommée jshell vous vous en doutiez — même si elle semble assez peu connue et utilisée (du moins parmi les développeurs Java que je côtoie).
L’objet de cet article est de présenter les principales fonctionnalités de cet outil afin de pouvoir l’utiliser rapidement en cas de besoin. L’idée est de fournir un petit aide-mémoire sur les options et les commandes de base.
TLDR : le strict minimum à savoir
L’outil jshell
fait partie des binaires du JDK et peut être lancé depuis un shell ou une invite de commande juste en tapant jshell
.
Pour l’aide, il faut lancer jshell
avec les options--help
, -h
ou -?
, par exemple jshell -?
.
Une fois dans jshell
, les commandes spécifiques à jshell
commencent par un /
. Les 2 commandes à connaître au minimum sont /help
pour avoir des informations sur les commandes et /exit
pour pouvoir sortir proprement d’une session.
Et comme c’est une REPL, vous pouvez en plus des commandes jshell
, saisir du code Java qui sera interprété.
C’est vraiment le minimum à savoir pour pouvoir commencer à l’utiliser si vous êtes pressés. Dans la suite de l’article, on prend le temps de présenter jshell
plus en détail.
Lancement
L’outil jshell
fait partie des binaires distribués dans votre JDK et devrait être dans le PATH si java
et javac
le sont. Dans la copie d’écran ci-après, on peut voir que l’exécutable est dans le répertoire bin
de votre distribution java (GraalVM ici) comme javac
et java
.
Pour le lancer il suffit donc normalement de saisir jshell
dans une invite de commandes ou un shell, si vous avez un JDK correctement installé dont les binaires sont bien dans le PATH.
L’outil fonctionne de la même manière sur MacOs, Windows ou Linux, dans un shell de commandes. Je l’utilise pour ma part dans un environnement Windows.
Connaître la version de jshell
Pour connaitre la version de jshell
, vous avez les options --version
qui ne fait qu'afficher la version et --show-version
qui affiche la version et lance jshell
Afficher de l’aide
Vous avez bien sûr les classiques options pour afficher les options de l’outil avec --help
, -h
ou -?
, par exemple jshell -?
.
Préciser un classpath
Il est possible de préciser des bibliothèques à inclure dans le classpath avec l’option --class-path <path>
. Par exemple si je veux pouvoir utiliser des classes de la bibliothèque Apache Common Math, le jar
correspondant étant dans le répertoire C:\tmp\javalib\
: je pourrais lancer jshell
de la manière suivante
jshell --class-path C:\tmp\javalib\commons-math3-3.6.1.jar
import org.apache.commons.math3.util.MathUtils;
MathUtils.PI_SQUARED;
Si vous souhaitez inclure plusieurs bibliothèques ou répertoires dans le classpath
, il faut les renseigner les uns à la suite des autres avec le séparateur de path
du système (;
sous Windows et :
sous Unix).
Ainsi si je souhaite importer Apache Common Math et Apache Common Lang, les jar
correspondant étant dans le répertoire C:\tmp\javalib\
:
jshell --class-path C:\tmp\javalib\commons-math3-3.6.1.jar;C:\tmp\javalib\commons-lang3-3.12.0.jar
import org.apache.commons.math3.util.MathUtils;
import org.apache.commons.lang3.RandomUtils;
MathUtils.PI_SQUARED;
RandomUtils.nextInt();
Imports et scripts de démarrage
Il est possible de préciser un script de démarrage avec l’option --startup <script démarrage>
. Cela peut être un fichier local que vous avez écrit ou un des scripts prédéfinis que l'on précise par une valeur spécifique. On peut bien sûr associer cette option avec la précédente relative au classpath
. Ainsi si je souhaite avoir un script qui importe MathUtils
et RandomUtils
dès le démarrage je peux procéder de la manière décrite ci-après.
J’ai un script my-import.jsh
avec les lignes présentées ci-dessous sous C:\tmp\
:
import org.apache.commons.math3.util.MathUtils;
import org.apache.commons.lang3.RandomUtils;
Je lance jshell
avec la ligne suivante, les jar
d’Apache Common Math et d’Apache Common Lang étant dans le répertoire C:\tmp\javalib\
:
jshell --class-path C:\tmp\javalib\commons-math3-3.6.1.jar;C:\tmp\javalib\commons-lang3-3.12.0.jar --startup my-import.jsh
Une fois jshell
lancé, si vous tapez la commande /imports
, qui permet de visualiser les imports de la session — elle est détaillée plus loin — , vous pourrez vérifier que les classes sont bien importées.
Les valeurs pour les scripts prédéfinis sont les suivantes :
DEFAULT
: importe les bibliothèques standards de Java les plus communément utilisées.JAVASE
: importe l'ensemble des bibliothèques standard de Java.PRINTING
: définitprint
,println
etprintf
comme des fonctions directement utilisables dansjshell
.
Ses scripts sont joués lors du démarrage mais également lors de redémarrages de session qui peuvent être provoquées par certaines commandes que nous verrons par la suite.
L’option DEFAULT
est particulière car les imports que ce script fait sont effectués par défaut si on ne précise pas l’option --startup
, comme vous pouvez le constater dans la copie d’écran suivante qui utilise à nouveau la commande /imports
pour visualiser les imports.
On notera qu’il n’y a pas de différence aux niveaux des imports réalisés par défaut entre jshell
et jshell --startup DEFAULT
.
Par contre si on utilise l’option --startup
et que l’on souhaite avoir les imports normalement effectués par DEFAULT
, il faut penser à l’ajouter.
On voit que si on ne précise pas DEFAULT
avec my-import.jsh
aucuns des imports par défaut ne sont effectués.
Si on lance jshell
avec l'option --startup PRINTING
, on n'a plus les imports par défaut. Par contre, avec PRINTING
on note qu'on peut utiliser directement print
.
Pour effectuer le chargement de plusieurs scripts au démarrage on peut les mettre les uns à la suite des autres, séparés par des espaces (voir les exemples précédents) mais l’on peut également renseigner plusieurs fois l’option comme dans l’exemple ci-après.
Il faut noter qu’avec la commande open
décrite plus loin, on peut également lancer ces scripts prédéfinis si on n’a pas pu ou pas voulu les lancer au démarrage de jshell
.
Utiliser des fonctionnalités en preview dans le JDK
Si vous voulez pouvoir tester avec jshell
les fonctionnalités en preview de votre JDK, il suffit de le lancer avec l'option --enable-preview
: jshell --enable-preview
.
Par exemple, avec la version de jshell
que j’utilise (Java 17), je veux essayer le pattern matching dans les switchs (c’est en preview en Java 17) en exécutant le script switch-example.jsh suivant :
double getDoubleUsingSwitch(Object o) {
return switch (o) {
case Integer i -> i.doubleValue();
case Float f -> f.doubleValue();
case String s -> Double.parseDouble(s);
default -> 0d;
};
}
System.out.println(getDoubleUsingSwitch("56.6"));
System.out.println(getDoubleUsingSwitch(5.4f));
System.out.println(getDoubleUsingSwitch(5));
System.out.println(getDoubleUsingSwitch(new Object()));
Toutes les options ne sont pas sur la table
Je n’ai présenté qu’une partie des options qui me semblaient les plus pertinentes et utiles. Beaucoup d’autres options sont utilisables au lancement de jshell.
Commandes
Une fois dans jshell
, vous pouvez saisir du code Java directement.
Les commandes de jshell
(par exemple ouvrir un fichier) sont introduites par le caractère /
.
Il y a des nombreuses commandes possibles pouvant être paramétrées avec des options. Comme pour les options de lancement, on se concentre ci-après sur les plus utiles (de mon point de vue) dans une utilisation quotidienne.
Sortir de jshell
Pour sortir de jshell
, la commande est simplement /exit
.
Obtenir de l’aide
/help
pour afficher la liste des commandes et ce qui est nommé subjects (voir les copies d’écran) dans jshell
.
Il est possible de passer à la suite de help
un nom de commande (sans oublier le /
devant) ou un nom de subject. Par exemple
/help /exit
/help intro
Complétion de commande
La complétion de commande fonctionne avec TAB
après avoir commencé à saisir le caractère /
.
Ouvrir un fichier
Pour ouvrir et faire exécuter un fichier de code java : /open <nom-fichier>
.
Par exemple avec l’invite de commande depuis laquelle jshell
a été ouverte positionnée dans le répertoire dans lequel le fichier perfectNumber.jsh
se trouve.
/open perfectNumber.jsh
Il est de bon ton d’utiliser l’extension jsh
pour vos fichiers de scripts jshell
mais ce n'est pas une obligation. Ils peuvent avoir l'extension java
ou n'importe quelle autre valeur. A noter cependant que du code java valide pour jshell
ne le sera pas forcément pour votre IDE ou votre éditeur de texte.
A toutes fins utiles, le script utilisé dans cet exemple.
Vous pouvez également utiliser la commande /open
pour ouvrir les mêmes scripts par défaut que ceux qu'on peut lancer au démarrage de jshell
:
DEFAULT
: importe les bibliothèques standards de Java les plus communément utilisées.JAVASE
: importer l'ensemble des bibliothèques standard de Java.PRINTING
: définitprint
,println
etprintf
comme des fonctions directement utilisables dansjshell
.
Lister les imports
Pour lister les imports la commande est /imports
. Cela vous donne la liste des imports disponibles directement dans jshell.
Dans la copie d’écran, on voit l’effet de la commande suite au lancement sans options de jshell
, avec les imports par défaut. Suite à l'ouverture du script perfectNumber.jsh
qui réalise 2 imports supplémentaires, on voit apparaître ces derniers dans la liste suite à l'utilisation de la commande imports
.
Lister les snippets
Un snippet est un morceau de code saisi, chargé et exécuté lors d’une session avec jshell
. Il y a plusieurs opérations sur les snippets tel que les lister, les sauvegarder ou les supprimer. Ainsi il est possible de transformer du code que vous avez rédigé lors de votre session pour le consolider en un script exécutable par jshell
.
Pour avoir la liste des snippets saisis :/list
Le code devant le snippet est un identifiant du snippet. Cet identifiant peut être utilisé dans certaines commandes pour le référencer comme/drop
ou /save
qui permettent respectivement de supprimer et de sauver des snippets.
Avec /list -all
on liste tous les snippets y compris ceux qui ont échoués et ceux exécutés au lancement de jshell
(comme les imports effectués par défaut que l'on voit dans la copie d'écran ci-dessus).
On notera que les identifiants des snippets exécutés au lancement et ceux en erreur, commence respectivement pas un s
et un e
.
Sauvegarder les snippets
On peut sauvegarder un ou des snippets en précisant un identifiant /save <id1> <nom fichier>
ou une plage d'identifiants /save <id1>-<id2> <nom fichier>
. Il faut alors l'utiliser en conjonction avec la commande/list
.
Le contenu du fichier obtenu dans l’exemple en copie d’écran est le suivant :
Il est également possible de sauvegarder l’historique des commandes avec les snippets avec save -history <nom fichier>
ou tous les snippets avec save -all <nom fichier>
L’exemple précédent donne respectivement les 2 fichiers suivants :
On notera qu’avec le /save -all
on sauvegarde tous les snippets mais pas les commandes.
Lister les types, les variables et les méthodes
Il y a 3 commandes pour lister les types (classes, énumérations, interfaces, annotations), les variables et les méthodes définies dans la sessions jshell
courante : /types
, /vars
et /methods
. Pour chacune de ces 3 commandes vous pouvez utiliser l'option--all
pour avoir également ce qui a été chargé au démarrage (entre autre chose).
Manipuler les variables d’environnements de la session
La commande /env
permet de visualiser et manipuler la configuration relative à l’environnement de la session, c’est-à-dire essentiellement les chemins à inclure pour récupérer les classes (class path) et les modules (module path).
Lancer sans options /env
affiche les chemins définis pour les classes et les modules pour la session courante.
Ainsi, si je lance jshell
sans préciser de class path ou de module path, /env
ne retourne rien. Si je précise un class path au lancement, sa valeur est indiquée par /env
.
Cette commande vous permet également de définir de nouvelles valeurs pour le class path ou le module path pour la session courante, sans avoir à recréer une nouvelle session.
Ainsi, si je lance une session jshell
, sans donner de class path mais que je souhaite finalement préciser ce dernier en cours de session, je vais pouvoir le faire avec /env
, dans l’exemple ci-après avec /env -class-path C:\tmp\javalib\commons-math3-3.6.1.jar
.
Manipuler l’historique des commandes
On peut se déplacer dans l’historique des commandes d’une session avec les flèches haut et bas. La commande/history
permet d'avoir la liste des commandes effectuées.
Vous pouvez rechercher dans l’historique avec la combinaison de touches Ctrl + R
.
Intégration dans les outils de développement
jshell
s’utilise bien sûr dans une invite de commande mais il y a des intégrations dans les IDE, les éditeurs de texte et même les outils de build.
IntelliJ IDEA fournit directement la possibilité d’ouvrir une session jshell
liée au projet courant.
Le classpath de la session jshell pourra être celui du projet courant.
Par contre les commandes jshell
ne sont pas utilisables et j’ai pu constater que par exemple pour les méthodes définies lors d’une session, il faut les rendre static
par rapport à une version faite directement pour jshell
.
Il est agréable d’avoir le formatage, de l’auto-complétion avancée et la gestion des imports mais on ne peut pas ouvrir juste un script indépendant seul, il faut forcément utiliser l’outil dans le cadre d’un projet.
Eclipse ne propose pas directement l’intégration de jshell
. Cependant il existe le plugin QuickShell qui offre plus de possibilités une fois installé que ce que propose IntelliJ IDEA : on peut ouvrir une session jshell
similaire à ce qu’on a dans une invite de commande, il est possible de faire exécuter directement un bout de code dans jshell
via le menu contextuel. N’étant plus utilisateur d’Eclipse depuis pas mal de temps, je préfère vous renvoyer vers la page GitHub de QuickShell qui donne toutes les informations pertinentes.
Il y a également un support de jshell
dans VS Code via un plugin, JShell Easy, qui interprète en direct le code saisi.
Enfin, il existe un plugin pour maven qui permet de lancer une session jshell
via maven (avec un mvn jshell:run
). L’intérêt ici est de pouvoir lancer cette session avec le classpath défini par les dépendances du POM (il existe également un plugin pour Gradle mais je ne l’ai pas testé).
Utilisation de jshell
jshell est très intéressant pour tester ou expérimenter un petit bout de code.
Par exemple pour mettre au point une expression un peu compliquée avec des streams ou une expression régulière, jshell
pourra être un outil pertinent.
Pour apprendre la syntaxe de Java ou l’enseigner à quelqu’un, cela peut être intéressant pour s’éviter la lourdeur d’un IDE et de Java lui-même pour faire des choses simples : penser au code nécessaire et aux concepts à expliquer à un débutant en informatique pour afficher un “Hello World” en Java !
Et dans un cadre qui rejoint à la fois l’expérimentation et la pédagogie, je trouve que c’est parfois un outil intéressant pour montrer des exemples de code dans des billets de blog ou des articles.
En effet, il est assez aisé de faire un script auto-porteur qui tiendra en relativement peu de lignes et qui sera facile à tester pour le lecteur avec un outil disponible dans le JDK. Cela ne remplace pas des exemples plus complets intégrés dans un vrai projet Java mais cela apporte un complément qui permet de mettre le focus sur ce qui est essentiel.
Conclusion
C’était une présentation non-exhaustive (j’ai laissé certaines commandes et options de côté) d’un outil du JDK méconnu mais utile.
C’est un outil intéressant pour expérimenter rapidement une idée ou tester un petit bout de code ; on aurait tord de se priver de l’utiliser.
Pour aller plus loin, la documentation d’Oracle est déjà très complète :
Il y a également un chapitre complet dans l’exhaustif et indispensable “Développons en Java” de Jean-Michel Doudoux.
Remerciement
Je remercie Baptiste, Damien, Thomas et Farid pour leur relecture attentive et leurs remarques.
Note
Une première version de ce billet a été publiée sur mon blog personnel.