Skip to content

Fixes #13274: Allow cygwin-paths on Windows #13297

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
382071f
feat: add method to convert Cygwin-style paths to Windows format
kvitorr Jun 10, 2025
93087c9
feat: apply the convertCygwinPathToWinds on the path to prevent the I…
kvitorr Jun 10, 2025
568bb34
Merge branch 'main' into fix-for-issue-13274
kvitorr Jun 10, 2025
2f87d70
fix: remove FileUtil method
kvitorr Jun 10, 2025
0f5cce4
Merge branch 'fix-for-issue-13274' of https://github.com/kvitorr/jabr…
kvitorr Jun 10, 2025
c343086
feat: add Cygwin file path /mtn/
kvitorr Jun 10, 2025
773a4eb
feat: add test to Cygwin file path /mtn/
kvitorr Jun 10, 2025
2334c9e
feat: add entry on CHANGELOG.md and weave the method into JabKit.
kvitorr Jun 10, 2025
37c6203
feat: remove convertCygwinPathToWindows
kvitorr Jun 12, 2025
19d92f3
feat: create converters and add new package to java modules
kvitorr Jun 12, 2025
e6aea3b
feat: apply converters to --inputs and --outputs
kvitorr Jun 12, 2025
025a2eb
style: remove blank space
kvitorr Jun 12, 2025
70cc464
Merge branch 'JabRef:main' into fix-for-issue-13274
kvitorr Jun 12, 2025
249f38f
feat: change the DataType from String and File to Path
kvitorr Jun 14, 2025
5241f3c
feat: remove code duplication
kvitorr Jun 14, 2025
385ab81
fix: correct the style
kvitorr Jun 14, 2025
2c40476
feat: create a new method to remove code duplication, create constant…
kvitorr Jun 14, 2025
fde43a6
feat: correct the unit tests to the changes made in the class
kvitorr Jun 14, 2025
35a84ec
Merge branch 'fix-for-issue-13274' of https://github.com/kvitorr/jabr…
kvitorr Jun 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion jabkit/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

requires info.picocli;
opens org.jabref.cli;
opens org.jabref.cli.converter;

requires transitive org.jspecify;
requires java.prefs;
Expand All @@ -22,7 +23,6 @@
requires org.tinylog.impl;

requires java.xml;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not modify lines of code not related to the PR if not 90% sure

Here, the empty line does make sence to separate blocks. Please undo this change.

// region: other libraries (alphabetically)
requires io.github.adr;
// endregion
Expand Down
6 changes: 4 additions & 2 deletions jabkit/src/main/java/org/jabref/cli/CheckConsistency.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
11 changes: 6 additions & 5 deletions jabkit/src/main/java/org/jabref/cli/CheckIntegrity.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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?
Expand Down
7 changes: 4 additions & 3 deletions jabkit/src/main/java/org/jabref/cli/Convert.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand Down
20 changes: 10 additions & 10 deletions jabkit/src/main/java/org/jabref/cli/Pseudonymize.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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> 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;
}

Expand Down
7 changes: 4 additions & 3 deletions jabkit/src/main/java/org/jabref/cli/Search.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.jabref.cli.converter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For naming, I think, this should be with "s", because there will be more than one converters in the future.

pacakge org.jabref.cli.converters.


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<Path> {

@Override
public Path convert(String path) {
return FileUtil.convertCygwinPathToWindows(path);
}
}
47 changes: 47 additions & 0 deletions jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add something like "plainly converted if on non-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) {
Copy link
Member

@koppor koppor Jun 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method also works on linux - proposal for another name:

Suggested change
public static Path convertCygwinPathToWindows(String filePath) {
public static Path convertStringToPathRespectingCygwin(String filePath) {

if (filePath == null) {
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New public methods should not return null. The method should return Optional instead to better handle the null case and follow Java best practices.

}

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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triple-slash comments are used instead of proper JavaDoc format. Additionally, the comment is trivial and doesn't add new information beyond what the code shows.

/// @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);
}
}
17 changes: 17 additions & 0 deletions jablib/src/test/java/org/jabref/logic/util/io/FileUtilTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
}
Loading