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; public class BakefileParser { /** * Nom du fichier Bakefile à parser (donc Bakefile...). */ private String filename; /** * Regex pour détecter les targets et leurs dépendances. * Format : "nom : dépendance1 dépendance2" */ private static final Pattern TARGET_PATTERN = Pattern.compile("^(\\S+)\\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*(?:#.*)?$"); /** * 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<>(); public BakefileParser(String filename) { this.filename = filename; firstTarget = null; } private List splitDependencies(String depStr) { if (depStr == null || depStr.trim().isEmpty()) { return new ArrayList<>(); } // Remplacer les variables avant de split String resolvedStr = replaceVariables(depStr.trim()); // Split sur un ou plusieurs espaces et filtrer les chaînes vides return Arrays.stream(resolvedStr.split("\\s+")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } 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(1); } try { List lines = Files.readAllLines(Paths.get(filename)); String currentTarget = null; List dependencies = new ArrayList<>(); List commands = new ArrayList<>(); int lineNumber = 0; for (String line : lines) { lineNumber++; Matcher varMatcher = VARIABLE_PATTERN.matcher(line); Matcher targetMatcher = TARGET_PATTERN.matcher(line); Matcher commandMatcher = COMMAND_PATTERN.matcher(line); if (line.trim().isEmpty()) { continue; } if (line.matches("^ +.*$")) { System.err.println(filename + ":" + lineNumber + ": *** missing separator. Stop."); System.exit(1); } if (varMatcher.matches()) { // Stocke la variable en enlevant les espaces en début et fin variables.put(varMatcher.group(1), varMatcher.group(2).trim()); } else if (targetMatcher.matches()) { if (currentTarget != null) { rules.add(new Rule( replaceVariables(currentTarget.trim()), splitDependencies(dependencies.stream() .collect(Collectors.joining(" "))), replaceVariablesInList(commands), phonyTargets.contains(currentTarget.trim()) )); } currentTarget = targetMatcher.group(1); String depStr = targetMatcher.group(2); dependencies = splitDependencies(depStr); if (firstTarget == null) { firstTarget = replaceVariables(currentTarget.trim()); } if (currentTarget.equals("clean")) { phonyTargets.add(currentTarget); } commands = new ArrayList<>(); } else if (commandMatcher.matches()) { commands.add(commandMatcher.group(1)); } } if (currentTarget != null) { rules.add(new Rule( replaceVariables(currentTarget.trim()), replaceVariablesInList(dependencies), replaceVariablesInList(commands), phonyTargets.contains(currentTarget.trim()) )); } } catch (IOException e) { e.printStackTrace(); } return rules; } private String replaceVariables(String input) { if (input == null) return null; String result = input; boolean replaced; do { replaced = false; for (Map.Entry entry : variables.entrySet()) { String key = "$(" + entry.getKey() + ")"; if (result.contains(key)) { // Préserver les espaces dans les valeurs des variables result = result.replace(key, entry.getValue()); replaced = true; } } } while (replaced); return result.trim(); } private List replaceVariablesInList(List items) { List resolved = new ArrayList<>(); for (String item : items) { resolved.add(replaceVariables(item)); } return resolved; } /** * Permet de récupérer la première cible trouvée dans le fichier Bakefile. * @return La première cible trouvée */ public static String getFirstTarget() { return firstTarget; } }