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 gère plusieurs cibles séparées par des espaces */ private static final Pattern TARGET_PATTERN = Pattern.compile("^([A-Za-z0-9_.\\-\\$\\(\\)\\{\\}]+(?:\\s+[A-Za-z0-9_.\\-\\$\\(\\)\\{\\}]+)*)\\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 Tableau contenant la commande combinée, les lignes brutes et le nombre de lignes traitées */ private Object[] handleContinuationLines(List lines, int startIndex) { StringBuilder combinedLine = new StringBuilder(); List rawLines = new ArrayList<>(); int i = startIndex; // Ajouter la première ligne avec son backslash String firstLine = lines.get(i); rawLines.add(firstLine); // Garder la ligne telle quelle Matcher contMatcher = CONTINUATION_PATTERN.matcher(firstLine); if (contMatcher.matches()) { combinedLine.append(contMatcher.group(1).trim()).append(" "); i++; } // Traiter les lignes suivantes while (i < lines.size()) { String line = lines.get(i); rawLines.add(line); // Garder la ligne telle quelle contMatcher = CONTINUATION_PATTERN.matcher(line); if (contMatcher.matches()) { // Ajouter sans le backslash à la commande combinée combinedLine.append(contMatcher.group(1).trim()).append(" "); i++; } else { // Dernière ligne de la séquence (sans backslash) combinedLine.append(line.trim()); i++; break; } } if (BakeCLI.isDebug()) { System.out.println("Debug: Combined " + (i - startIndex) + " lines into: [" + combinedLine.toString().trim() + "]"); System.out.println("Debug: Raw lines preserved: " + rawLines.size()); } // Retourner la commande combinée, les lignes brutes et le nombre de lignes return new Object[] { combinedLine.toString().trim(), // Commande combinée pour exécution rawLines, // Lignes brutes pour affichage i - startIndex // Nombre de lignes traitées }; } /** * Remplacer les variables dans une chaîne. * @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; Set processedVars = new HashSet<>(); boolean changed; do { changed = false; 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); // Modification ici: remplacer par la valeur de la variable ou par une chaîne vide si elle n'existe pas String replacement = ""; if (variables.containsKey(varName)) { replacement = variables.get(varName); } else if (BakeCLI.isDebug()) { System.out.println("Debug: Variable '" + varName + "' not defined, replacing with empty string"); } matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); changed = true; processedVars.add(varName); } matcher.appendTail(sb); result = sb.toString(); // Si aucun changement n'a été fait dans ce passage, arrêter if (!changed) { break; } // Réinitialiser processedVars pour le prochain passage si nécessaire processedVars.clear(); } while (changed); 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 makefile found. Stop."); System.exit(2); } try { List lines = Files.readAllLines(Paths.get(filename)); List currentTargets = null; List dependencies = new ArrayList<>(); List commands = new ArrayList<>(); List> displayCommands = new ArrayList<>(); // Variable pour suivre si la ligne précédente était une continuation boolean previousLineContinues = false; for (int i = 0; i < lines.size(); i++) { String line = lines.get(i).replace("\r", ""); // Ignorer les lignes vides if (line.trim().isEmpty()) { previousLineContinues = false; // Réinitialiser le flag de continuation continue; } // Gérer les erreurs de format (espaces au lieu de tabulations) // Mais uniquement si ce n'est pas une continuation de variable if (line.matches("^ +.*$") && !previousLineContinues) { System.err.println(filename + ":" + (i+1) + ": *** missing separator. Stop."); System.exit(2); } // Vérifier si cette ligne se termine par un backslash (continuation) previousLineContinues = line.trim().endsWith("\\"); // Matcher pour les déclarations .PHONY Matcher phonyMatcher = PHONY_PATTERN.matcher(line); if (phonyMatcher.matches()) { String[] phonies = phonyMatcher.group(1).trim().split("\\s+"); Collections.addAll(phonyTargets, phonies); continue; } // Matcher pour les déclarations de variables Matcher varMatcher = VARIABLE_PATTERN.matcher(line); if (varMatcher.matches()) { String varName = varMatcher.group(1); String varValue = varMatcher.group(2).trim(); // Vérifier si la ligne se termine par un backslash (continuation) if (varValue.endsWith("\\")) { StringBuilder fullValue = new StringBuilder(varValue.substring(0, varValue.length() - 1).trim()); int j = i + 1; while (j < lines.size()) { String nextLine = lines.get(j).trim(); if (nextLine.endsWith("\\")) { fullValue.append(" ").append(nextLine.substring(0, nextLine.length() - 1).trim()); j++; } else { fullValue.append(" ").append(nextLine); i = j; // Mettre à jour l'indice principal pour sauter les lignes traitées break; } } varValue = fullValue.toString(); } // Évaluer les variables référencées dans la valeur varValue = replaceVariables(varValue); variables.put(varName, varValue); if (BakeCLI.isDebug()) { System.out.println("Debug: Variable defined: " + varName + " = " + varValue); } continue; } // Matcher pour les cibles et dépendances Matcher targetMatcher = TARGET_PATTERN.matcher(line); if (targetMatcher.matches()) { // Si nous avions des cibles précédentes, créons les règles correspondantes 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), displayCommands, phonyTargets.contains(resolvedTarget) )); if (firstTarget == null) { firstTarget = resolvedTarget; } } } // Configuration pour les nouvelles cibles String targetStr = targetMatcher.group(1); if (BakeCLI.isDebug()) { System.out.println("Debug: Raw target(s): " + targetStr); } currentTargets = splitTargets(targetStr); if (BakeCLI.isDebug()) { System.out.println("Debug: Resolved targets: " + currentTargets); } String depStr = targetMatcher.group(2); dependencies = splitDependencies(depStr); commands = new ArrayList<>(); displayCommands = new ArrayList<>(); continue; } // Matcher pour les lignes de commande Matcher commandMatcher = COMMAND_PATTERN.matcher(line); if (commandMatcher.matches()) { String command = commandMatcher.group(1); // Gérer la continuation de ligne if (command.endsWith("\\")) { // Traiter la séquence complète de continuation Object[] result = handleContinuationLines(lines, i); String fullCommand = (String)result[0]; @SuppressWarnings("unchecked") List rawLines = (List)result[1]; int linesUsed = (Integer)result[2]; // Ajouter la commande complète pour l'exécution commands.add(fullCommand); // Ajouter les lignes brutes pour l'affichage displayCommands.add(rawLines); // Ajuster i pour sauter les lignes traitées (moins 1 car la boucle for incrémente i) i += linesUsed - 1; } else { String executableCommand = command; commands.add(executableCommand); // Pour l'affichage, préserver le formatage mais remplacer les variables String displayLine = line; // Ne pas modifier les lignes qui commencent par @ (silencieuses) if (command.startsWith("@")) { displayLine = line; // Garder le formatage complet pour les commandes silencieuses } else { // Remplacer les variables dans la partie de la commande uniquement (après la tabulation) Matcher cmdMatcher = COMMAND_PATTERN.matcher(line); if (cmdMatcher.matches()) { String cmdPart = cmdMatcher.group(1); String cmdWithVars = replaceVariables(cmdPart); displayLine = "\t" + cmdWithVars; } } List singleLineDisplay = new ArrayList<>(); singleLineDisplay.add(displayLine); displayCommands.add(singleLineDisplay); } continue; } } // Traiter les dernières cibles 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), displayCommands, phonyTargets.contains(resolvedTarget) )); if (firstTarget == null) { firstTarget = resolvedTarget; } } } if (BakeCLI.isDebug()) { System.out.println("Debug: First target is: " + firstTarget); System.out.println("Debug: Parsed " + rules.size() + " rules."); for (Rule rule : rules) { System.out.println("Debug: Rule: " + rule.getName()); System.out.println("Debug: Commands: " + rule.getCommands().size()); for (String cmd : rule.getCommands()) { System.out.println("Debug: [" + cmd + "]"); } } } } 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; } }