diff --git a/installer/src/main/java/ca/weblite/jdeploy/installer/cli/UnixPathManager.java b/installer/src/main/java/ca/weblite/jdeploy/installer/cli/UnixPathManager.java index 339173eb..e9b3900c 100644 --- a/installer/src/main/java/ca/weblite/jdeploy/installer/cli/UnixPathManager.java +++ b/installer/src/main/java/ca/weblite/jdeploy/installer/cli/UnixPathManager.java @@ -187,10 +187,15 @@ private static boolean addPathToConfigFile(File configFile, File binDir, File ho } // Append PATH export to the config file (always at the end) + // Read current content to ensure clean trailing whitespace before appending DebugLogger.log("Writing PATH export to: " + configFile.getAbsolutePath()); - String pathExport = "\n# Added by jDeploy installer\nexport PATH=\"" + pathExportString + ":$PATH\"\n"; - try (FileOutputStream fos = new FileOutputStream(configFile, true)) { - fos.write(pathExport.getBytes(StandardCharsets.UTF_8)); + String currentContent = IOUtil.readToString(new FileInputStream(configFile)); + String trimmedContent = currentContent.replaceAll("\\s+$", ""); + // Use a blank line separator only when there's existing content + String separator = trimmedContent.isEmpty() ? "" : "\n\n"; + String pathExport = separator + "# Added by jDeploy installer\nexport PATH=\"" + pathExportString + ":$PATH\"\n"; + try (FileOutputStream fos = new FileOutputStream(configFile)) { + fos.write((trimmedContent + pathExport).getBytes(StandardCharsets.UTF_8)); } System.out.println("Added " + displayPath + " to PATH in " + configFile.getName()); @@ -299,13 +304,22 @@ public static boolean removePathFromConfigFile(File configFile, File binDir, Fil // Look ahead to see if the next line is an export for our binDir if (i + 1 < lines.length) { String nextLine = lines[i + 1].trim(); - if (nextLine.startsWith("export PATH=\"") && + if (nextLine.startsWith("export PATH=\"") && (nextLine.contains(pathExportString) || nextLine.contains(absolutePath))) { // Skip this comment line and mark to skip the export line skipNextExport = true; removed = true; continue; } + // Orphaned comment: not followed by any export PATH line + if (!nextLine.startsWith("export PATH=\"")) { + removed = true; + continue; + } + } else { + // Comment at end of file with no following line - orphaned + removed = true; + continue; } } @@ -335,8 +349,12 @@ public static boolean removePathFromConfigFile(File configFile, File binDir, Fil if (removed) { // Write back the modified content - // Preserve trailing newline if original had one String newContent = result.toString(); + // Collapse multiple consecutive blank lines into at most one + newContent = newContent.replaceAll("\n{3,}", "\n\n"); + // Trim trailing blank lines, keeping at most one trailing newline + newContent = newContent.replaceAll("\n{2,}$", "\n"); + // Preserve trailing newline if original had one if (content.endsWith("\n") && !newContent.endsWith("\n")) { newContent += "\n"; } diff --git a/installer/src/test/java/ca/weblite/jdeploy/installer/cli/UnixPathManagerTest.java b/installer/src/test/java/ca/weblite/jdeploy/installer/cli/UnixPathManagerTest.java index c8612034..4af1a451 100644 --- a/installer/src/test/java/ca/weblite/jdeploy/installer/cli/UnixPathManagerTest.java +++ b/installer/src/test/java/ca/weblite/jdeploy/installer/cli/UnixPathManagerTest.java @@ -1092,4 +1092,113 @@ public void testAddToPathOnLinuxRespectsNoAutoPathInProfile() throws IOException assertTrue(bashrcContent.contains(binDir.getAbsolutePath()), ".bashrc should contain the PATH export"); } + + // ==================== Blank Line Cleanup Tests ==================== + + @Test + public void testRemovePathCollapsesConsecutiveBlankLines() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Simulate a file with blank lines around jDeploy entries (from repeated install/uninstall) + String originalContent = "# Some config\nexport FOO=bar\n\n\n\n\n# Added by jDeploy installer\nexport PATH=\"" + existingPath + ":$PATH\"\n\n\n\n\nexport BAZ=qux\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + boolean removed = UnixPathManager.removePathFromConfigFile(bashrc, binDir, homeDir); + + assertTrue(removed, "Should return true when entry was removed"); + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains(existingPath), "PATH entry should be removed"); + assertFalse(content.contains("\n\n\n"), "Should not have more than one consecutive blank line"); + assertTrue(content.contains("export FOO=bar"), "Other content should be preserved"); + assertTrue(content.contains("export BAZ=qux"), "Other content should be preserved"); + } + + @Test + public void testRemovePathRemovesOrphanedComments() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Simulate orphaned comments (comment not followed by export PATH line) + String originalContent = "# Some config\nexport FOO=bar\n# Added by jDeploy installer\n\n# Added by jDeploy installer\n# Added by jDeploy installer\nexport PATH=\"" + existingPath + ":$PATH\"\nexport BAZ=qux\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + boolean removed = UnixPathManager.removePathFromConfigFile(bashrc, binDir, homeDir); + + assertTrue(removed, "Should return true when entries were removed"); + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains("# Added by jDeploy installer"), "All jDeploy comments should be removed"); + assertFalse(content.contains(existingPath), "PATH entry should be removed"); + assertTrue(content.contains("export FOO=bar"), "Other content should be preserved"); + assertTrue(content.contains("export BAZ=qux"), "Other content should be preserved"); + } + + @Test + public void testRemovePathRemovesOrphanedCommentAtEndOfFile() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Orphaned comment at end of file + String originalContent = "# Some config\nexport FOO=bar\n# Added by jDeploy installer\nexport PATH=\"" + existingPath + ":$PATH\"\n# Added by jDeploy installer\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + boolean removed = UnixPathManager.removePathFromConfigFile(bashrc, binDir, homeDir); + + assertTrue(removed, "Should return true when entries were removed"); + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains("# Added by jDeploy installer"), "All jDeploy comments should be removed"); + assertFalse(content.contains(existingPath), "PATH entry should be removed"); + assertTrue(content.contains("export FOO=bar"), "Other content should be preserved"); + } + + @Test + public void testRemovePathPreservesCommentsForOtherApps() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Comment+export for a different app should be preserved + String originalContent = "# Some config\n# Added by jDeploy installer\nexport PATH=\"" + existingPath + ":$PATH\"\n# Added by jDeploy installer\nexport PATH=\"/some/other/app:$PATH\"\nexport FOO=bar\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + boolean removed = UnixPathManager.removePathFromConfigFile(bashrc, binDir, homeDir); + + assertTrue(removed, "Should return true when entry was removed"); + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains(existingPath), "Our PATH entry should be removed"); + assertTrue(content.contains("# Added by jDeploy installer"), "Comment for other app should be preserved"); + assertTrue(content.contains("/some/other/app"), "Other app's PATH should be preserved"); + assertTrue(content.contains("export FOO=bar"), "Other content should be preserved"); + } + + @Test + public void testRemovePathTrimsTrailingBlankLines() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Entry at end of file followed by blank lines + String originalContent = "# Some config\nexport FOO=bar\n# Added by jDeploy installer\nexport PATH=\"" + existingPath + ":$PATH\"\n\n\n\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + boolean removed = UnixPathManager.removePathFromConfigFile(bashrc, binDir, homeDir); + + assertTrue(removed, "Should return true when entry was removed"); + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains(existingPath), "PATH entry should be removed"); + assertTrue(content.endsWith("\n"), "Should end with a single newline"); + assertFalse(content.endsWith("\n\n"), "Should not end with multiple newlines"); + } + + @Test + public void testAddPathDoesNotAccumulateBlankLines() throws IOException { + File bashrc = new File(homeDir, ".bashrc"); + String existingPath = binDir.getAbsolutePath(); + // Simulate existing content with trailing blank lines + String originalContent = "# Some config\nexport FOO=bar\n\n\n\n"; + Files.write(bashrc.toPath(), originalContent.getBytes(StandardCharsets.UTF_8)); + + // Add path entry + String shell = "/bin/bash"; + String pathEnv = "/usr/bin:/bin"; + UnixPathManager.addToPath(binDir, shell, pathEnv, homeDir); + + String content = IOUtil.readToString(new FileInputStream(bashrc)); + assertFalse(content.contains("\n\n\n"), "Should not have more than one consecutive blank line"); + assertTrue(content.contains("# Added by jDeploy installer"), "Should have jDeploy comment"); + assertTrue(content.contains(existingPath), "Should have PATH entry"); + } }