Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7cf83da
#1788: Created a commandlet to simulate the behaviour of ln -s
KarimALotfy Apr 23, 2026
818f7a4
Merge 'main' into 'feature/1788-add-ln-commandlet-to-create-link'
KarimALotfy Apr 24, 2026
d9d1101
#1788: Refactored LnCommandlet
KarimALotfy Apr 27, 2026
e1182c0
#1788: Merge with upstream main
KarimALotfy Apr 27, 2026
0e14ba4
#1788: Fix bug in order of val in Help.pro
KarimALotfy Apr 27, 2026
61a560d
#1788: Made -s flag optional, enabling both options of symbolic or ha…
KarimALotfy Apr 28, 2026
d0e5bf9
Merge branch "main" into "feature/1788-add-ln-commandlet-to-create-li…
KarimALotfy Apr 28, 2026
46236f2
#1788: Fix change in Help.properties
KarimALotfy Apr 28, 2026
3116b1c
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy Apr 28, 2026
ecabdb9
#1788: Change naming from target to source
KarimALotfy Apr 30, 2026
4d9db9a
Merge branch 'feature/1788-add-ln-commandlet-to-create-links' of http…
KarimALotfy Apr 30, 2026
8da6d74
Merge branch 'main' of https://github.com/devonfw/IDEasy into feature…
KarimALotfy Apr 30, 2026
aa91650
#1788: Refactored link by making the overriding functionality optional
KarimALotfy May 7, 2026
7f733f8
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
hohwille May 8, 2026
1221f3c
#1788: Replaced force flag with the global force option
KarimALotfy May 11, 2026
568d3ed
Merge banch "main" into "feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 11, 2026
88df80b
Merge branch 'feature/1788-add-ln-commandlet-to-create-links' of http…
KarimALotfy May 11, 2026
cf7dc41
#1788 : Remove unneeded test, since the -f option is irrelevant to sy…
KarimALotfy May 11, 2026
00fb7df
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
hohwille May 11, 2026
bd3a739
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 11, 2026
a031666
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 12, 2026
197fc07
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 18, 2026
fdac6b9
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
hohwille May 18, 2026
77b001d
#1788 : Refactored LnCommandlet
KarimALotfy May 19, 2026
dfe6211
Merge branch 'feature/1788-add-ln-commandlet-to-create-links' of http…
KarimALotfy May 19, 2026
2503d34
Merge branch 'main' of https://github.com/devonfw/IDEasy into feature…
KarimALotfy May 19, 2026
1140604
#1788 : fix
KarimALotfy May 19, 2026
010935d
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 20, 2026
56c577b
Merge branch 'main' into feature/1788-add-ln-commandlet-to-create-links
KarimALotfy May 22, 2026
9a4a38a
#1788: Refactor LnCommandlet
KarimALotfy May 22, 2026
5e23c49
Merge branch 'feature/1788-add-ln-commandlet-to-create-links' of http…
KarimALotfy May 22, 2026
177aa64
Merge branch 'main' of https://github.com/devonfw/IDEasy into feature…
KarimALotfy May 22, 2026
cdadf35
#1788: Simplify createHardLink
KarimALotfy May 22, 2026
2f60435
#1788: Fix test
KarimALotfy May 22, 2026
9e342cd
#1788: Fix test
KarimALotfy May 22, 2026
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
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ij_wrap_on_typing = false

[*.properties]
ij_properties_align_group_field_declarations = false
ij_properties_keep_blank_lines = true
ij_properties_keep_blank_lines = false
ij_properties_key_value_delimiter = equals
ij_properties_spaces_around_key_value_delimiter = false

Expand Down Expand Up @@ -354,4 +354,4 @@ ij_yaml_keep_line_breaks = true
ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true
ij_yaml_spaces_within_brackets = true
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
!.anyedit.properties
!.ide.properties
!.templateengine
!.github
target/
eclipse-target/
generated/
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1834[#1834]: Fix fix-vpn-tls-problem NullPointerException
* https://github.com/devonfw/IDEasy/issues/1724[#1724]: Add gui commandlet
* https://github.com/devonfw/IDEasy/issues/1853[#1853]: Add ARM releases for VSCode on Mac
* https://github.com/devonfw/IDEasy/issues/1788[#1788]: Add Commandlet to create links
* https://github.com/devonfw/IDEasy/issues/797[#797]: Use system unzip on macOS to preserve symlinks in ZIP extraction
* https://github.com/devonfw/IDEasy/issues/1723[#1723]: Add commandlet for GitHub Copilot CLI
* https://github.com/devonfw/IDEasy/issues/1880[#1880]: Reinstall all plugins for IDE in force mode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public CommandletManagerImpl(IdeContext context) {
add(new StatusCommandlet(context));
add(new RepositoryCommandlet(context));
add(new UninstallCommandlet(context));
add(new LnCommandlet(context));
add(new UpdateCommandlet(context));
add(new UpgradeSettingsCommandlet(context));
add(new CreateCommandlet(context));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.devonfw.tools.ide.commandlet;

import java.nio.file.Files;
import java.nio.file.Path;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.io.PathLinkType;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.property.PathProperty;

/**
* // * Link creation {@link Commandlet} similar to {@code ln -s}.
* <p>
* It tries to create a true symbolic link first. On Windows, symlink creation may be restricted due to missing privileges. In that case, IDEasy will create a
* hard link as an alternative (file-only, same volume) to avoid the Git-Bash behavior of silently copying files.
*/
public final class LnCommandlet extends Commandlet {

/** Grammar token {@code -s} (optional). */
public final FlagProperty symbolic;

/** Grammar token {@code -r} (optional). */
public final FlagProperty relative;

/** The source path to link to. */
public final PathProperty source;

/** The target path (the created link). */
public final PathProperty link;

/**
* The constructor.
*
* @param context the {@link IdeContext}.
*/
public LnCommandlet(IdeContext context) {

super(context);
addKeyword(getName());

this.symbolic = add(new FlagProperty("--symbolic", false, "-s"));
this.relative = add(new FlagProperty("--relative", false, "-r"));
this.source = add(new PathProperty("", true, "source", true));
this.link = add(new PathProperty("", true, "link", false));
}

@Override
public String getName() {

return "ln";
}

@Override
public boolean isIdeRootRequired() {

return false;
}

@Override
public boolean isIdeHomeRequired() {

return false;
}

@Override
public boolean isWriteLogFile() {

return false;
}

@Override
protected void doRun() {

Path cwd = this.context.getCwd();
if (cwd == null) {
throw new CliException("Missing current working directory!");
}

Path sourcePath = cwd.resolve(this.source.getValue()).normalize();
Path linkPath = cwd.resolve(this.link.getValue()).normalize();
boolean relative = this.relative.isTrue();

if (!Files.exists(sourcePath)) {
throw new CliException("Source does not exist: " + sourcePath);
}

PathLinkType linkType = this.symbolic.isTrue() ? PathLinkType.SYMBOLIC_LINK : PathLinkType.HARD_LINK;
this.context.getFileAccess().link(sourcePath, linkPath, relative, linkType);
}
}
4 changes: 2 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ default void hardlink(Path source, Path link) {
}

/**
* Creates a link. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will be replaced. In case of missing privileges,
* Windows mklink may be used as fallback, which must point to absolute paths. In such case the {@code relative} flag will be ignored.
* Creates a link. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will be replaced. In case of missing
* privileges, Windows mklink may be used as fallback, which must point to absolute paths. In such case the {@code relative} flag will be ignored.
*
* @param source the source {@link Path} to link to, may be relative or absolute.
* @param link the destination {@link Path} where the link shall be created pointing to {@code source}.
Expand Down
63 changes: 40 additions & 23 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -424,24 +424,22 @@ private void copyRecursive(Path source, Path target, FileCopyMode mode, PathCopy
}

/**
* Deletes the given {@link Path} if it is a symbolic link or a Windows junction. And throws an {@link IllegalStateException} if there is a file at the given
* {@link Path} that is neither a symbolic link nor a Windows junction.
* Deletes the given {@link Path} if it is a symbolic link or a Windows junction or hard link. And throws an {@link IllegalStateException} if it fails to
* delete the file at the given {@link Path}.
*
* @param path the {@link Path} to delete.
*/
private void deleteLinkIfExists(Path path) {

boolean isJunction = isJunction(path); // since broken junctions are not detected by Files.exists()
boolean isSymlink = Files.exists(path, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(path);
boolean exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);

assert !(isSymlink && isJunction);

if (isJunction || isSymlink) {
LOG.info("Deleting previous " + (isJunction ? "junction" : "symlink") + " at " + path);
if (isJunction || exists) {
LOG.info("Deleting previous link or file at " + path);
try {
Files.delete(path);
} catch (IOException e) {
throw new IllegalStateException("Failed to delete link at " + path, e);
throw new IllegalStateException("Failed to delete link or file at " + path, e);
}
}
}
Expand Down Expand Up @@ -545,7 +543,7 @@ public void link(Path source, Path link, boolean relative, PathLinkType type) {
if (type == PathLinkType.SYMBOLIC_LINK) {
Files.createSymbolicLink(finalLink, finalSource);
} else if (type == PathLinkType.HARD_LINK) {
Files.createLink(finalLink, finalSource);
Comment thread
hohwille marked this conversation as resolved.
createHardLink(finalSource, finalLink);
} else {
throw new IllegalStateException("" + type);
}
Expand All @@ -555,7 +553,13 @@ public void link(Path source, Path link, boolean relative, PathLinkType type) {
"Due to lack of permissions, Microsoft's mklink with junction had to be used to create a Symlink. See\n"
+ "https://github.com/devonfw/IDEasy/blob/main/documentation/symlink.adoc for further details. Error was: "
+ e.getMessage());
mklinkOnWindows(finalSource, absoluteSource, finalLink, type, relative);

try {
mklinkOnWindows(finalSource, absoluteSource, finalLink, type, relative);
} catch (IllegalStateException mkEx) {
LOG.info("Creating a hard link as a fallback for the failed mklink attempt.");
createHardLink(absoluteSource, finalLink);
}
Comment thread
hohwille marked this conversation as resolved.
} else {
throw new RuntimeException(e);
}
Expand All @@ -564,6 +568,22 @@ public void link(Path source, Path link, boolean relative, PathLinkType type) {
}
}


/**
* Creates a hard link at {@code link} pointing to {@code source}.
*
* @param source the {@link Path} the hard link will point to.
* @param link the {@link Path} where to create the hard link.
*/
void createHardLink(Path source, Path link) {
try {
Files.createLink(link, source);
LOG.trace("Created hard link at {} pointing to {}", link, source);
} catch (IOException e) {
throw new RuntimeException("Failed to create a hardlink for " + source + " at " + link, e);
}
}

@Override
public Path getPathStart(Path path, int nameEnd) {

Expand Down Expand Up @@ -748,13 +768,12 @@ public void extractZip(Path file, Path targetDir) {
/**
* Extracts a ZIP archive to the given target directory using Java (commons-compress {@link ZipFile}).
* <p>
* Symlinks are handled in a two-phase approach: all regular files and directories are written first, and symlinks are
* collected and created afterwards. This ensures every symlink target already exists on disk before the link is made.
* Symlinks are handled in a two-phase approach: all regular files and directories are written first, and symlinks are collected and created afterwards. This
* ensures every symlink target already exists on disk before the link is made.
* <p>
* {@link ZipFile} is used instead of {@link org.apache.commons.compress.archivers.zip.ZipArchiveInputStream} because
* Unix file attributes (needed for symlink detection and permission restoration) are stored in the ZIP central
* directory at the end of the file. A sequential stream only sees the local file headers, where external attributes
* are always zero. {@link ZipFile} reads the central directory via random access, so attributes are always correct.
* {@link ZipFile} is used instead of {@link org.apache.commons.compress.archivers.zip.ZipArchiveInputStream} because Unix file attributes (needed for symlink
* detection and permission restoration) are stored in the ZIP central directory at the end of the file. A sequential stream only sees the local file headers,
* where external attributes are always zero. {@link ZipFile} reads the central directory via random access, so attributes are always correct.
*
* @param file the ZIP archive to extract.
* @param targetDir the directory to extract into.
Expand Down Expand Up @@ -807,8 +826,8 @@ private void extractZipWithJava(Path file, Path targetDir) {
/**
* Returns the Unix file mode stored in the external file attributes of the given ZIP entry.
* <p>
* The ZIP specification stores platform-specific metadata in a 32-bit external-attributes field. When the archive was
* created on a Unix system the layout is:
* The ZIP specification stores platform-specific metadata in a 32-bit external-attributes field. When the archive was created on a Unix system the layout
* is:
* <ul>
* <li>bits 31–16 (upper 16 bits): Unix file mode (same bit layout as {@code stat.st_mode})</li>
* <li>bits 15–0 (lower 16 bits): MS-DOS attributes (read-only, hidden, …)</li>
Expand All @@ -828,8 +847,7 @@ private static int getZipUnixMode(ZipArchiveEntry entry) {
/**
* Returns {@code true} if the given ZIP entry represents a symbolic link.
* <p>
* Unix file modes encode the file type in the top 4 bits (bits 15–12) of the mode word. The possible file-type
* values are defined in {@code <sys/stat.h>}:
* Unix file modes encode the file type in the top 4 bits (bits 15–12) of the mode word. The possible file-type values are defined in {@code <sys/stat.h>}:
* <ul>
* <li>{@code 0x8000} – regular file ({@code S_IFREG})</li>
* <li>{@code 0x4000} – directory ({@code S_IFDIR})</li>
Expand All @@ -849,8 +867,8 @@ private static boolean isZipSymlink(ZipArchiveEntry entry) {
/**
* Applies Unix file permissions stored in a ZIP entry to the extracted file.
* <p>
* Permissions are only applied on non-Windows systems because POSIX permission bits have no equivalent on Windows.
* If the entry carries no Unix attributes (mode is {@code 0}), the call is a no-op.
* Permissions are only applied on non-Windows systems because POSIX permission bits have no equivalent on Windows. If the entry carries no Unix attributes
* (mode is {@code 0}), the call is a no-op.
*
* @param entry the source ZIP entry carrying the Unix mode.
* @param target the extracted file whose permissions should be updated.
Expand Down Expand Up @@ -1329,7 +1347,6 @@ private long getFileSize(Path file) {
}



@Override
public Path findExistingFile(String fileName, List<Path> searchDirs) {

Expand Down
6 changes: 6 additions & 0 deletions cli/src/main/resources/nls/Help.properties
Comment thread
KarimALotfy marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ cmd.list-editions=List the available editions of the selected tool.
cmd.list-editions.detail=To list all available editions of e.g. 'intellij' simply type: 'ide list-editions intellij'.
cmd.list-versions=List the available versions of the selected tool.
cmd.list-versions.detail=To list all available versions of e.g. 'intellij' simply type: 'ide list-versions intellij'.
cmd.ln=Create a symbolic or hard link.
cmd.ln.detail=Creates a link similar to the command "ln" but working cross-platform (unlike in git-bash on Windows that typically only creates a copy).
cmd.mvn=Tool commandlet for Maven (Build-Tool).
cmd.mvn.detail=Apache Maven is a build automation and dependency management tool for Java projects. Detailed documentation can be found at https://maven.apache.org/guides/index.html
cmd.nest=Tool commandlet for Nest CLI.
Expand Down Expand Up @@ -167,9 +169,11 @@ opt.--no-colors=disable colored log messages.
opt.--offline=enable offline mode (skip updates or git pull, fail downloads or git clone).
opt.--privacy=enable GDPR-compliant console output.
opt.--quiet=disable info logging (only log success, warning or error).
opt.--relative=use relative paths.
opt.--skip-repositories=skip the setup of repositories.
opt.--skip-tools=skip the installation/update of tools.
opt.--skip-updates=disables tool updates if the configured versions match the installed versions.
opt.--symbolic=Create a symbolic link instead of a hard link (default).
opt.--trace=enable trace logging.
opt.--version=Print the IDE version and exit.
options.global=Global options:
Expand All @@ -180,8 +184,10 @@ val.args=The commandline arguments to pass to the tool.
val.cfg=Selection of the configuration file (settings | home | conf | workspace).
val.commandlet=The selected commandlet (use 'ide help' to list all commandlets).
val.edition=The tool edition.
val.link=The path where the link is created.
val.plugin=The plugin to select
val.settingsRepository=The settings git repository with the IDEasy configuration for the project.
val.source=The source path the link points to (existing file or directory).
val.tool=The tool commandlet to select.
val.version=The tool version.
values=Values:
Expand Down
6 changes: 6 additions & 0 deletions cli/src/main/resources/nls/Help_de.properties
Comment thread
KarimALotfy marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ cmd.list-editions=Listet die verfügbaren Editionen des selektierten Werkzeugs a
cmd.list-editions.detail=Um alle verfügbaren Editionen von z.B. 'intellij' aufzulisten, geben Sie einfach 'ide list-editions intellij' in die Konsole ein.
cmd.list-versions=Listet die verfügbaren Versionen des selektierten Werkzeugs auf.
cmd.list-versions.detail=Um alle verfügbaren Versionen von z.B. 'intellij' aufzulisten, geben Sie einfach 'ide list-versions intellij' in die Konsole ein.
cmd.ln=Erstellt einen symbolischen oder harten Link
cmd.ln.detail=Erstellt einen Link wie das Kommando "ln" jedoch plattform-übergreifend (anders als in Git-Bash unter Windows wo typischerweise nur eine Kopie erstellt wird).
cmd.mvn=Werkzeug Kommando für Maven (Build-Werkzeug).
cmd.mvn.detail=Apache Maven ist ein Build-Automatisierungs- und Abhängigkeitsverwaltungstool für Java-Projekte. Detaillierte Dokumentation ist zu finden unter https://maven.apache.org/guides/index.html
cmd.nest=Werkzeug Kommando für Nest CLI.
Expand Down Expand Up @@ -167,9 +169,11 @@ opt.--no-colors=Deaktiviert farbige Log-Meldungen.
opt.--offline=Aktiviert den Offline-Modus (Überspringt Aktualisierungen oder git pull, schlägt fehl bei Downloads or git clone).
opt.--privacy=Aktiviert DSGVO konforme Konsolenausgaben.
opt.--quiet=Deaktiviert Info Logging ( nur success, warning und error).
opt.--relative=Relative Pfade verwenden.
opt.--skip-repositories=Überspringt die Einrichtung der Repositories.
opt.--skip-tools=Überspringt die Installation/Aktualisierung der Tools.
opt.--skip-updates=Deaktiviert Aktualisierungen von Tools wenn die installierten Versionen mit den konfigurierten Versionen übereinstimmen.
opt.--symbolic=Erstellt einen symbolischen Link anstelle eines Hardlinks (Standard).
opt.--trace=Aktiviert Trace-Ausgaben (detaillierte Fehleranalyse).
opt.--version=Zeigt die IDE Version an und beendet das Programm.
options.global=Globale Optionen:
Expand All @@ -180,8 +184,10 @@ val.args=Die Kommandozeilen-Argumente zur Übergabe an das Werkzeug.
val.cfg=Auswahl der Konfigurationsdatei (settings | home | conf | workspace).
val.commandlet=Das ausgewählte Commandlet ("ide help" verwenden, um alle Commandlets aufzulisten).
val.edition=Die Werkzeug Edition.
val.link=Pfad des zu erstellenden Links.
val.plugin=Die zu selektierende Erweiterung.
val.settingsRepository=Das settings git Repository mit den IDEasy Einstellungen für das Projekt.
val.source=Ziel des Links (existierender Pfad).
val.tool=Das zu selektierende Werkzeug Kommando.
val.version=Die Werkzeug Version.
values=Werte:
Expand Down
Loading
Loading