Regex expression cron
La réponse la plus votée (80 votes) sur la question de référence donne une regex qui ne distingue pas la position des champs : un commentaire signale, exemple à l'appui, qu'elle valide « * * 30 * * * » comme une expression cron correcte alors que ce sixième champ n'a rien à faire là. Une autre réponse (16 votes) vérifie bien la plage de chaque champ, mais un commentaire montre qu'elle rejette les listes séparées par des virgules comme « 0 3,15 * * * ». Voici un motif qui combine plages par position ET listes/pas, comparé à ces deux tentatives et à une troisième, une synthèse communautaire des réponses précédentes qui hérite pourtant du même défaut de position que la première.
/^(?:\*|[0-5]?[0-9](?:-[0-5]?[0-9])?)(?:/[0-9]+)?(?:,(?:\*|[0-5]?[0-9](?:-[0-5]?[0-9])?)(?:/[0-9]+)?)* (?:\*|(?:[01]?[0-9]|2[0-3])(?:-(?:[01]?[0-9]|2[0-3]))?)(?:/[0-9]+)?(?:,(?:\*|(?:[01]?[0-9]|2[0-3])(?:-(?:[01]?[0-9]|2[0-3]))?)(?:/[0-9]+)?)* (?:\*|(?:[1-9]|[12][0-9]|3[01])(?:-(?:[1-9]|[12][0-9]|3[01]))?)(?:/[0-9]+)?(?:,(?:\*|(?:[1-9]|[12][0-9]|3[01])(?:-(?:[1-9]|[12][0-9]|3[01]))?)(?:/[0-9]+)?)* (?:\*|(?:[1-9]|1[0-2])(?:-(?:[1-9]|1[0-2]))?)(?:/[0-9]+)?(?:,(?:\*|(?:[1-9]|1[0-2])(?:-(?:[1-9]|1[0-2]))?)(?:/[0-9]+)?)* (?:\*|[0-6](?:-[0-6])?)(?:/[0-9]+)?(?:,(?:\*|[0-6](?:-[0-6])?)(?:/[0-9]+)?)*$/passe 21/21 cas
Comparatif des patterns existants
| Source | pattern | Score sur le corpus | ReDoS |
|---|---|---|---|
| Stack Overflow — réponse la plus votée (80 votes), ne distingue pas la position des champs (accepte « * * 30 * * * », bug signalé en commentaire) | ^(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}$ | 13 / 21 | indéterminé |
| Stack Overflow — réponse à 16 votes, plages correctes par position mais rejette les listes séparées par virgules (bug signalé en commentaire sur « 0 3,15 * * * ») | ^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$ | 18 / 21 | sûr |
| GitHub Gist — synthèse communautaire des réponses du même fil (« credit to Juarez and Slava »), hérite du même défaut de position que la réponse à 80 votes | ^((((\d+,)+\d+|(\d+(\/|-|#)\d+)|\d+L?|\*(\/\d+)?|L(-\d+)?|\?|[A-Z]{3}(-[A-Z]{3})?) ?){5,7})$ | 14 / 21 | indéterminé |
Implémentation
const re = new RegExp("^(?:\\*|[0-5]?[0-9](?:-[0-5]?[0-9])?)(?:/[0-9]+)?(?:,(?:\\*|[0-5]?[0-9](?:-[0-5]?[0-9])?)(?:/[0-9]+)?)* (?:\\*|(?:[01]?[0-9]|2[0-3])(?:-(?:[01]?[0-9]|2[0-3]))?)(?:/[0-9]+)?(?:,(?:\\*|(?:[01]?[0-9]|2[0-3])(?:-(?:[01]?[0-9]|2[0-3]))?)(?:/[0-9]+)?)* (?:\\*|(?:[1-9]|[12][0-9]|3[01])(?:-(?:[1-9]|[12][0-9]|3[01]))?)(?:/[0-9]+)?(?:,(?:\\*|(?:[1-9]|[12][0-9]|3[01])(?:-(?:[1-9]|[12][0-9]|3[01]))?)(?:/[0-9]+)?)* (?:\\*|(?:[1-9]|1[0-2])(?:-(?:[1-9]|1[0-2]))?)(?:/[0-9]+)?(?:,(?:\\*|(?:[1-9]|1[0-2])(?:-(?:[1-9]|1[0-2]))?)(?:/[0-9]+)?)* (?:\\*|[0-6](?:-[0-6])?)(?:/[0-9]+)?(?:,(?:\\*|[0-6](?:-[0-6])?)(?:/[0-9]+)?)*$", "");
re.test(input);Limites
- Ne couvre que la forme classique à 5 champs (minute heure jour-du-mois mois jour-de-semaine) : ni le champ « secondes » optionnel de certains ordonnanceurs (Quartz, Spring), ni le champ « année » final, ne sont pris en charge.
- Ne reconnaît pas les noms de mois (JAN-DEC) ni de jours (SUN-SAT), acceptés par de nombreuses implémentations réelles de cron en plus des valeurs numériques : seule la forme numérique est couverte ici.
- Ne reconnaît pas les macros (« @daily », « @hourly »...) ni la syntaxe d'intervalle (« @every 5m ») que certains ordonnanceurs (notamment Go/Kubernetes CronJob) acceptent en remplacement d'une expression à 5 champs.
- N'impose pas la cohérence entre jour-du-mois et jour-de-semaine (les deux champs sont interprétés en OU logique par cron quand ils sont tous deux restreints, une subtilité sémantique qu'une regex de forme ne peut pas exprimer), et ne vérifie pas qu'une date comme le 30 février ne peut jamais se produire.