package fr.monlouyan.bakefile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Parseur de fichier Bakefile. * Cette classe est responsable de l'analyse syntaxique du fichier Bakefile * pour extraire les règles de build, les dépendances et les commandes associées. * * @author Moncef STITI, Yanis HAMOUDI, Louay DARDOURI * @version 1.0 */ public class BakefileParser { /** * Nom du fichier Bakefile à parser (donc Bakefile...). */ private String filename; /** * Regex pour détecter les targets et leurs dépendances. * Format : "nom1 nom2 nom3 : dépendance1 dépendance2" * La nouvelle regex assure que la ligne ne commence pas par une tabulation * et vérifie que le premier caractère non-espace n'est pas un symbole de commentaire */ private static final Pattern TARGET_PATTERN = Pattern.compile("^([^\\t:#][^:#]+?)\\s*:\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les lignes de commande associées à une target. * Format : " gcc -o program program.c" (ligne indentée) */ private static final Pattern COMMAND_PATTERN = Pattern.compile("^\\t(.+)$"); /** * Regex pour détecter les définitions de variables. * Format : "FLAGS = -ansi -pedantic" */ private static final Pattern VARIABLE_PATTERN = Pattern.compile("^(\\w+)\\s*=\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les déclarations .PHONY * Format : ".PHONY: clean all" */ private static final Pattern PHONY_PATTERN = Pattern.compile("^\\.PHONY:\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les lignes de continuation. * Format : " gcc -o program program.c \" */ private static final Pattern CONTINUATION_PATTERN = Pattern.compile("^(.*)\\\\\\s*$"); /** * Regex pour détecter les références de variables. * Format : "${VAR}" ou "$(VAR)" */ private static final Pattern VARIABLE_REFERENCE = Pattern.compile("\\$\\{(\\w+)\\}|\\$\\((\\w+)\\)"); /** * Première cible trouvée dans le fichier Bakefile. */ private static String firstTarget; /** * Stocke les variables définies dans le Bakefile. */ private Map variables = new HashMap<>(); /** * Constructeur de la classe BakefileParser. * @param filename Nom du fichier Bakefile à parser */ public BakefileParser(String filename) { this.filename = filename; firstTarget = null; } /** * Gérer les lignes de continuation. * @param lines Liste des lignes du fichier Bakefile * @param startIndex Index de la première ligne de continuation * @return La ligne combinée */ private String handleContinuationLines(List lines, int startIndex) { StringBuilder combinedLine = new StringBuilder(); int i = startIndex; while (i < lines.size()) { String line = lines.get(i); Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); if (contMatcher.matches()) { // Ajouter la ligne sans le backslash mais conserver le contenu entier // Ne pas ajouter d'espace après certains opérateurs comme && String content = contMatcher.group(1); combinedLine.append(content); // Si la ligne ne se termine pas déjà par un opérateur tel que &&, ajouter un espace if (!content.trim().endsWith("&&") && !content.trim().endsWith("|") && !content.trim().endsWith(";")) { combinedLine.append(" "); } else { // Si elle se termine par &&, |, ou ;, ajouter juste un espace après combinedLine.append(" "); } i++; } else { // Ajouter la dernière ligne et sortir combinedLine.append(line.trim()); break; } } return combinedLine.toString(); } /** * Remplacer les variables dans une chaîne. * Cette méthode remplace toutes les références de variables (${VAR} ou $(VAR)) * par leur valeur. Si une variable n'est pas définie, elle est remplacée par une chaîne vide. * * @param input Chaîne à traiter * @return Chaîne avec les variables remplacées */ private String replaceVariables(String input) { if (input == null) return null; String result = input; // Détecter et remplacer toutes les occurrences de variables Matcher matcher = VARIABLE_REFERENCE.matcher(result); StringBuffer sb = new StringBuffer(); while (matcher.find()) { String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); // Remplacer par la valeur de la variable si elle existe, sinon par une chaîne vide String replacement = variables.containsKey(varName) ? variables.get(varName) : ""; matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); } matcher.appendTail(sb); result = sb.toString(); // Vérifier les références imbriquées de variables et continuer à remplacer si nécessaire if (VARIABLE_REFERENCE.matcher(result).find()) { result = replaceVariables(result); // Appel récursif pour gérer les variables imbriquées } return result.trim(); } /** * Remplacer les variables dans une liste de chaînes. * @param items Liste de chaînes à traiter * @return Liste de chaînes avec les variables remplacées */ private List replaceVariablesInList(List items) { return items.stream() .map(this::replaceVariables) .collect(Collectors.toList()); } /** * Découper les dépendances en une liste de chaînes. * @param depStr Chaîne de dépendances * @return Liste de dépendances */ private List splitDependencies(String depStr) { if (depStr == null || depStr.trim().isEmpty()) { return new ArrayList<>(); } String resolvedStr = replaceVariables(depStr.trim()); return Arrays.stream(resolvedStr.split("\\s+")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } /** * Découper les cibles en une liste de chaînes. * @param targetStr Chaîne de cibles * @return Liste de cibles */ private List splitTargets(String targetStr) { if (targetStr == null || targetStr.trim().isEmpty()) { return new ArrayList<>(); } String resolvedStr = replaceVariables(targetStr.trim()); return Arrays.stream(resolvedStr.split("\\s+")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } /** * Analyser le fichier Bakefile pour extraire les règles de build. * @return Liste des règles extraites */ public List parse() { List rules = new ArrayList<>(); Set phonyTargets = new HashSet<>(); if (!Files.exists(Paths.get(filename))) { System.out.println("*** No targets specified and no bakefile found. Stop."); System.exit(2); } try { List lines = Files.readAllLines(Paths.get(filename)); List currentTargets = null; List dependencies = new ArrayList<>(); List commands = new ArrayList<>(); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); // Vérifier si la ligne a un caractère de continuation Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); if (contMatcher.matches()) { // Récupérer toute la définition multi-ligne line = handleContinuationLines(lines, i); // Ajuster i pour sauter les lignes traitées while (i + 1 < lines.size() && CONTINUATION_PATTERN.matcher(lines.get(i)).matches()) { i++; } } if (line.trim().isEmpty()) { continue; } if (line.matches("^ +.*$")) { System.err.println(filename + ":" + (i+1) + ": *** missing separator. Stop."); System.exit(2); } Matcher varMatcher = VARIABLE_PATTERN.matcher(line); Matcher targetMatcher = TARGET_PATTERN.matcher(line); Matcher commandMatcher = COMMAND_PATTERN.matcher(line); Matcher phonyMatcher = PHONY_PATTERN.matcher(line); if (phonyMatcher.matches()) { String[] phonies = phonyMatcher.group(1).trim().split("\\s+"); Collections.addAll(phonyTargets, phonies); continue; } if (varMatcher.matches()) { String varName = varMatcher.group(1); String varValue = varMatcher.group(2).trim(); // Évaluer les variables référencées dans la valeur varValue = replaceVariables(varValue); variables.put(varName, varValue); } else if (targetMatcher.matches()) { if (currentTargets != null) { // Créer une règle pour chaque cible avec les mêmes dépendances et commandes for (String target : currentTargets) { String resolvedTarget = replaceVariables(target.trim()); rules.add(new Rule( resolvedTarget, splitDependencies(dependencies.stream() .collect(Collectors.joining(" "))), replaceVariablesInList(commands), phonyTargets.contains(resolvedTarget) )); if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { firstTarget = resolvedTarget; } } } String targetStr = targetMatcher.group(1); currentTargets = splitTargets(targetStr); String depStr = targetMatcher.group(2); dependencies = splitDependencies(depStr); commands = new ArrayList<>(); } else if (commandMatcher.matches()) { commands.add(commandMatcher.group(1)); } } if (currentTargets != null) { // Créer une règle pour chaque cible avec les mêmes dépendances et commandes for (String target : currentTargets) { String resolvedTarget = replaceVariables(target.trim()); rules.add(new Rule( resolvedTarget, replaceVariablesInList(dependencies), replaceVariablesInList(commands), phonyTargets.contains(resolvedTarget) )); if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { firstTarget = resolvedTarget; } } } } catch (IOException e) { e.printStackTrace(); } return rules; } /** * Récupérer la première cible * @return String la première cible */ public static String getFirstTarget() { return firstTarget; } }