Skip to content
Open
Show file tree
Hide file tree
Changes from 29 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 @@ -108,6 +108,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", true));
Comment thread
KarimALotfy marked this conversation as resolved.
Outdated
}

@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, this.context.isForceMode());
}
}
19 changes: 17 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 @@ -132,13 +132,28 @@ default void hardlink(Path source, Path link) {
* @param relative - {@code true} if the link shall be relative, {@code false} if it shall be absolute.
* @param type the {@link PathLinkType}.
*/
void link(Path source, Path link, boolean relative, PathLinkType type);
default void link(Path source, Path link, boolean relative, PathLinkType type) {
link(source, link, relative, type, true);
}

/**
* Creates a link. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will be replaced if {@code override} is
* {@code true}. 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}.
* @param relative - {@code true} if the link shall be relative, {@code false} if it shall be absolute.
* @param type the {@link PathLinkType}.
* @param override - {@code true} to override existing links, {@code false} otherwise.
*/
void link(Path source, Path link, boolean relative, PathLinkType type, boolean override);

/**
* @param link the {@link PathLink} to {@link #link(Path, Path, boolean, PathLinkType) create}.
*/
default void link(PathLink link) {
link(link.source(), link.link(), true, link.type());
link(link.source(), link.link(), true, link.type(), true);
}

/**
Expand Down
36 changes: 32 additions & 4 deletions cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ private boolean runMklink(Path source, Path link, Path cwd, String option) {
}

@Override
public void link(Path source, Path link, boolean relative, PathLinkType type) {
public void link(Path source, Path link, boolean relative, PathLinkType type, boolean override) {

Path finalLink = link.toAbsolutePath().normalize();
Path finalSource;
Expand All @@ -539,13 +539,15 @@ public void link(Path source, Path link, boolean relative, PathLinkType type) {
Path absoluteSource = finalSource.isAbsolute() ? finalSource : finalLink.getParent().resolve(finalSource).normalize();
String relativeOrAbsolute = relative ? "relative" : "absolute";
LOG.debug("Creating {} {} at {} pointing to {}", relativeOrAbsolute, type, finalLink, finalSource);
deleteLinkIfExists(finalLink);
if (override) {
deleteLinkIfExists(finalLink);
}
try {
// Attention: JavaDoc and position of path arguments can be very confusing - see comment in #1736
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, override);
} else {
throw new IllegalStateException("" + type);
}
Expand All @@ -555,7 +557,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, override);
}
Comment thread
hohwille marked this conversation as resolved.
} else {
throw new RuntimeException(e);
}
Expand All @@ -564,6 +572,26 @@ 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.
* @param override - {@code true} to override existing links, {@code false} otherwise.
*/
void createHardLink(Path source, Path link, boolean override) {
if (override && Files.exists(link, LinkOption.NOFOLLOW_LINKS)) {
delete(link);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would still expect this to be done also for symbolic links.
If we have an override flag added to link(Path source, Path link, boolean relative, PathLinkType type, boolean override) and JavaDoc says {@code true} to override existing links, {@code false} otherwise. then this looks inconsistent to me. Will Files.createSymbolicLink be idempotent? I guess not and mklink is not for sure.
So IMHO the implementation is not doing what you specified in the JavaDoc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestions, implemented the suggested changes !

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
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 @@ -83,6 +83,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 @@ -165,9 +167,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 @@ -178,8 +182,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 @@ -83,6 +83,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 @@ -165,9 +167,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 @@ -178,8 +182,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