diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb2e296ef3..4ef347c512f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added - We introduced a settings parameters to manage citations' relations local storage time-to-live with a default value set to 30 days. [#11189](https://github.com/JabRef/jabref/issues/11189) +- We added support for Cygwin-file paths on a Windows Operating System. [#13274](https://github.com/JabRef/jabref/issues/13274) ### Changed diff --git a/jabkit/src/main/java/module-info.java b/jabkit/src/main/java/module-info.java index 68a32d97644..58897c8b573 100644 --- a/jabkit/src/main/java/module-info.java +++ b/jabkit/src/main/java/module-info.java @@ -3,6 +3,7 @@ requires info.picocli; opens org.jabref.cli; + opens org.jabref.cli.converter; requires transitive org.jspecify; requires java.prefs; @@ -22,7 +23,6 @@ requires org.tinylog.impl; requires java.xml; - // region: other libraries (alphabetically) requires io.github.adr; // endregion diff --git a/jabkit/src/main/java/org/jabref/cli/CheckConsistency.java b/jabkit/src/main/java/org/jabref/cli/CheckConsistency.java index 5afff87aa70..e0d0d364729 100644 --- a/jabkit/src/main/java/org/jabref/cli/CheckConsistency.java +++ b/jabkit/src/main/java/org/jabref/cli/CheckConsistency.java @@ -3,9 +3,11 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.file.Path; import java.util.List; import java.util.Optional; +import org.jabref.cli.converter.CygWinPathConverter; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.l10n.Localization; import org.jabref.logic.quality.consistency.BibliographyConsistencyCheck; @@ -33,8 +35,8 @@ class CheckConsistency implements Runnable { @Mixin private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions(); - @Option(names = {"--input"}, description = "Input BibTeX file", required = true) - private String inputFile; + @Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "Input BibTeX file", required = true) + private Path inputFile; @Option(names = {"--output-format"}, description = "Output format: txt or csv", defaultValue = "txt") private String outputFormat; diff --git a/jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java b/jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java index 8f4bef85c42..7186de34f56 100644 --- a/jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java +++ b/jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java @@ -1,7 +1,8 @@ package org.jabref.cli; -import java.io.File; +import java.nio.file.Path; +import org.jabref.cli.converter.CygWinPathConverter; import org.jabref.logic.l10n.Localization; import static picocli.CommandLine.Command; @@ -15,11 +16,11 @@ class CheckIntegrity implements Runnable { @Mixin private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions(); - @Parameters(index = "0", description = "BibTeX file to check", arity = "0..1") - private File inputFile; + @Parameters(index = "0", converter = CygWinPathConverter.class, description = "BibTeX file to check", arity = "0..1") + private Path inputFile; - @Option(names = {"--input"}, description = "Input BibTeX file") - private File inputOption; + @Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "Input BibTeX file") + private Path inputOption; @Option(names = {"--output-format"}, description = "Output format: txt or csv") private String outputFormat = "txt"; // FixMe: Default value? diff --git a/jabkit/src/main/java/org/jabref/cli/Convert.java b/jabkit/src/main/java/org/jabref/cli/Convert.java index 6071d5076c6..f2f2700462b 100644 --- a/jabkit/src/main/java/org/jabref/cli/Convert.java +++ b/jabkit/src/main/java/org/jabref/cli/Convert.java @@ -8,6 +8,7 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; +import org.jabref.cli.converter.CygWinPathConverter; import org.jabref.logic.exporter.Exporter; import org.jabref.logic.exporter.ExporterFactory; import org.jabref.logic.exporter.SaveException; @@ -36,13 +37,13 @@ public class Convert implements Runnable { @Mixin private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions(); - @Option(names = {"--input"}, description = "Input file", required = true) - private String inputFile; + @Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "Input file", required = true) + private Path inputFile; @Option(names = {"--input-format"}, description = "Input format") private String inputFormat; - @Option(names = {"--output"}, description = "Output file") + @Option(names = {"--output"}, converter = CygWinPathConverter.class, description = "Output file") private Path outputFile; @Option(names = {"--output-format"}, description = "Output format") diff --git a/jabkit/src/main/java/org/jabref/cli/Pseudonymize.java b/jabkit/src/main/java/org/jabref/cli/Pseudonymize.java index 22146798821..ffda758c648 100644 --- a/jabkit/src/main/java/org/jabref/cli/Pseudonymize.java +++ b/jabkit/src/main/java/org/jabref/cli/Pseudonymize.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.Optional; +import org.jabref.cli.converter.CygWinPathConverter; import org.jabref.logic.importer.ParserResult; import org.jabref.logic.l10n.Localization; import org.jabref.logic.pseudonymization.Pseudonymization; @@ -34,11 +35,11 @@ public class Pseudonymize implements Runnable { private ArgumentProcessor.SharedOptions sharedOptions = new ArgumentProcessor.SharedOptions(); @ADR(45) - @Option(names = {"--input"}, description = "BibTeX file to be pseudonymized", required = true) - private String inputFile; + @Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "BibTeX file to be pseudonymized", required = true) + private Path inputPath; - @Option(names = {"--output"}, description = "Output pseudo-bib file") - private String outputFile; + @Option(names = {"--output"}, converter = CygWinPathConverter.class, description = "Output pseudo-bib file") + private Path outputFile; @Option(names = {"--key"}, description = "Output pseudo-keys file") private String keyFile; @@ -48,24 +49,23 @@ public class Pseudonymize implements Runnable { @Override public void run() { - Path inputPath = Path.of(inputFile); - String fileName = FileUtil.getBaseName(inputFile); - Path pseudoBibPath = resolveOutputPath(outputFile, inputPath, fileName + PSEUDO_SUFFIX + BIB_EXTENSION); + String fileName = FileUtil.getBaseName(inputPath); + Path pseudoBibPath = resolveOutputPath(outputFile.toString(), inputPath, fileName + PSEUDO_SUFFIX + BIB_EXTENSION); Path pseudoKeyPath = resolveOutputPath(keyFile, inputPath, fileName + PSEUDO_SUFFIX + CSV_EXTENSION); Optional parserResult = ArgumentProcessor.importFile( - inputFile, + inputPath, "bibtex", argumentProcessor.cliPreferences, sharedOptions.porcelain); if (parserResult.isEmpty()) { - System.out.println(Localization.lang("Unable to open file '%0'.", inputFile)); + System.out.println(Localization.lang("Unable to open file '%0'.", inputPath)); return; } if (parserResult.get().isInvalid()) { - System.out.println(Localization.lang("Input file '%0' is invalid and could not be parsed.", inputFile)); + System.out.println(Localization.lang("Input file '%0' is invalid and could not be parsed.", inputPath)); return; } diff --git a/jabkit/src/main/java/org/jabref/cli/Search.java b/jabkit/src/main/java/org/jabref/cli/Search.java index 4be5fba1a02..1b77e4f7592 100644 --- a/jabkit/src/main/java/org/jabref/cli/Search.java +++ b/jabkit/src/main/java/org/jabref/cli/Search.java @@ -8,6 +8,7 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; +import org.jabref.cli.converter.CygWinPathConverter; import org.jabref.logic.exporter.Exporter; import org.jabref.logic.exporter.ExporterFactory; import org.jabref.logic.exporter.SaveException; @@ -46,10 +47,10 @@ class Search implements Runnable { @Option(names = {"--query"}, description = "Search query", required = true) private String query; - @Option(names = {"--input"}, description = "Input BibTeX file", required = true) - private String inputFile; + @Option(names = {"--input"}, converter = CygWinPathConverter.class, description = "Input BibTeX file", required = true) + private Path inputFile; - @Option(names = {"--output"}, description = "Output file") + @Option(names = {"--output"}, converter = CygWinPathConverter.class, description = "Output file") private Path outputFile; @Option(names = {"--output-format"}, description = "Output format: bib, txt, etc.") diff --git a/jabkit/src/main/java/org/jabref/cli/converter/CygWinPathConverter.java b/jabkit/src/main/java/org/jabref/cli/converter/CygWinPathConverter.java new file mode 100644 index 00000000000..5c242d75ec0 --- /dev/null +++ b/jabkit/src/main/java/org/jabref/cli/converter/CygWinPathConverter.java @@ -0,0 +1,16 @@ +package org.jabref.cli.converter; + +import java.nio.file.Path; + +import org.jabref.logic.util.io.FileUtil; + +import picocli.CommandLine; + +/// Converts Cygwin-style paths to Path objects using platform-specific formatting. +public class CygWinPathConverter implements CommandLine.ITypeConverter { + + @Override + public Path convert(String path) { + return FileUtil.convertCygwinPathToWindows(path); + } +} diff --git a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java index 9093acfe570..b2049156a61 100644 --- a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -25,6 +25,7 @@ import org.jabref.logic.FilePreferences; import org.jabref.logic.citationkeypattern.BracketedPattern; import org.jabref.logic.layout.format.RemoveLatexCommandsFormatter; +import org.jabref.logic.os.OS; import org.jabref.logic.util.StandardFileType; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; @@ -49,6 +50,9 @@ public class FileUtil { private static final String ELLIPSIS = "..."; private static final int ELLIPSIS_LENGTH = ELLIPSIS.length(); private static final RemoveLatexCommandsFormatter REMOVE_LATEX_COMMANDS_FORMATTER = new RemoveLatexCommandsFormatter(); + private static final String CYGDRIVE_PREFIX = "/cygdrive/"; + private static final String MNT_PREFIX = "/mnt/"; + private static final Pattern ROOT_DRIVE_PATTERN = Pattern.compile("^/[a-zA-Z]/.*"); /** * MUST ALWAYS BE A SORTED ARRAY because it is used in a binary search @@ -589,4 +593,47 @@ public static String shortenFileName(String fileName, Integer maxLength) { public static boolean isCharLegal(char c) { return Arrays.binarySearch(ILLEGAL_CHARS, c) < 0; } + + /// Converts a Cygwin-style file path to a Windows-style path if the operating system is Windows. + /// + /// Supported formats: + /// - /cygdrive/c/Users/... → C:\Users\... + /// - /mnt/c/Users/... → C:\Users\... + /// - /c/Users/... → C:\Users\... + /// + /// @param filePath the input file path + /// @return the converted path if running on Windows and path is in Cygwin format; otherwise, returns the original path + public static Path convertCygwinPathToWindows(String filePath) { + if (filePath == null) { + return null; + } + + if (!OS.WINDOWS) { + return Path.of(filePath); + } + + if (filePath.startsWith(MNT_PREFIX) && filePath.length() > 5) { + return buildWindowsPathWithDriveLetterIndex(filePath, 5); + } + + if (filePath.startsWith(CYGDRIVE_PREFIX) && filePath.length() > 10) { + return buildWindowsPathWithDriveLetterIndex(filePath, 10); + } + + if (ROOT_DRIVE_PATTERN.matcher(filePath).matches()) { + return buildWindowsPathWithDriveLetterIndex(filePath, 1); + } + + return Path.of(filePath); + } + + /// Builds a Windows-style path from a Cygwin-style path using a known prefix index. + /// @param path the input file path + /// @param letterIndex the index driver letter, zero-based indexing + /// @return a windows-style path + private static Path buildWindowsPathWithDriveLetterIndex(String path, int letterIndex) { + String driveLetter = path.substring(letterIndex, letterIndex + 1).toUpperCase(); + String windowsPath = path.substring(letterIndex + 1).replace("/", "\\\\"); + return Path.of(driveLetter + ":" + windowsPath); + } } diff --git a/jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java b/jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java index 6d63a6bd98f..ee3fd7873e5 100644 --- a/jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java +++ b/jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -518,4 +519,20 @@ void illegalPaths(String fileName) { void shortenFileName(String expected, String fileName, Integer maxLength) { assertEquals(expected, FileUtil.shortenFileName(fileName, maxLength)); } + + @EnabledOnOs(value = org.junit.jupiter.api.condition.OS.WINDOWS) + @ParameterizedTest + @ValueSource(strings = {"/c/Users/username/Downloads/test.bib", + "/cygdrive/c/Users/username/Downloads/test.bib", + "/mnt/c/Users/username/Downloads/test.bib"}) + void convertCygwinPathToWindowsShouldConvertToWindowsFormatWhenRunningOnWindows(String filePath) { + assertEquals(Path.of("C:\\\\Users\\\\username\\\\Downloads\\\\test.bib"), FileUtil.convertCygwinPathToWindows(filePath)); + } + + @DisabledOnOs(value = org.junit.jupiter.api.condition.OS.WINDOWS, disabledReason = "Test in others operational systems") + @ParameterizedTest + @ValueSource(strings = {"/home/username/Downloads/test.bib"}) + void convertCygwinPathToWindowsShouldReturnOriginalFilePathWhenRunningOnWindows(String filePath) { + assertEquals(Path.of(filePath), FileUtil.convertCygwinPathToWindows(filePath)); + } }