Regex cron expression
The reference question's top-voted answer (80 votes) gives a regex that doesn't distinguish field position: a comment points out, with a concrete example, that it validates "* * 30 * * *" as a correct cron expression even though that sixth field has no business being there. Another answer (16 votes) does check each field's range, but a comment shows it rejects comma-separated lists like "0 3,15 * * *". Here's a pattern that combines per-position ranges AND lists/steps, compared against both of those attempts and a third - a community synthesis of earlier answers that nonetheless inherits the same position-blindness as the first.
/^(?:\*|[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]+)?)*$/passes 21/21 cases
Comparison of existing patterns
| Source | pattern | Score on the 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 | undetermined |
| 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 | safe |
| 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 | undetermined |
Implementation
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);Limits
- 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.