Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1165[#1165]: Fix automatic project import for Eclipse
* https://github.com/devonfw/IDEasy/issues/2040[#2040]: Fixed buggy workspace selection in the GUI
* https://github.com/devonfw/IDEasy/issues/2253[#2253]: Fix structure and log documentation
* https://github.com/devonfw/IDEasy/issues/2219[#2219]: Add unpack commandlet

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/49?closed=1[milestone 2026.08.002].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public CommandletManagerImpl(IdeContext context) {
add(new UninstallPluginCommandlet(context));
add(new UpgradeCommandlet(context));
add(new TruststoreCommandlet(context));
add(new UnpackCommandlet(context));
add(new Gh(context));
add(new Helm(context));
add(new Java(context));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.devonfw.tools.ide.commandlet;

import java.nio.file.Path;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.property.PathProperty;
import com.devonfw.tools.ide.util.FilenameUtil;

/**
* {@link Commandlet} to extract an archive file to a target directory.
* <p>
* Supports ZIP, TAR, TAR.GZ, TAR.BZ2, 7Z, JAR archives (cross-platform), as well as MSI (Windows) and DMG/PKG (Mac).
* </p>
*/
public final class UnpackCommandlet extends Commandlet {

private static final Logger LOG = LoggerFactory.getLogger(UnpackCommandlet.class);

/** The archive file to extract. */
public final PathProperty archive;

/** The target directory to extract into. If not specified, defaults to {@code <cwd>/<archive_name_without_extension>}. */
public final PathProperty target;

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

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

this.archive = add(new PathProperty("", true, "archive", true));
this.target = add(new PathProperty("", false, "target", false));
}

@Override
public String getName() {

return "unpack";
}

@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 archivePath = this.archive.getValue();
if (!archivePath.isAbsolute()) {
archivePath = cwd.resolve(archivePath).normalize();
}

Path targetDir = this.target.getValue();
if (targetDir == null) {
// Derive default target from archive filename without extension
String targetName = FilenameUtil.getFilenameWithoutExtension(archivePath);
targetDir = cwd.resolve(targetName);
}
if (!targetDir.isAbsolute()) {
targetDir = cwd.resolve(targetDir).normalize();
}

LOG.info("Extracting {} to {}", archivePath, targetDir);
this.context.getFileAccess().extract(archivePath, targetDir);
LOG.info("Extraction completed successfully.");
}
}
4 changes: 4 additions & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ cmd.uninstall=Uninstall selected tool(s).
cmd.uninstall-plugin=Uninstall the selected plugin for the selected tool.
cmd.uninstall-plugin.detail=Plugins can be only installed or uninstalled for tools that support such. Using the command "ide install-plugin", an uninstalled plugin can be restored.
cmd.uninstall.detail=Can be used to uninstall selected tool(s). E.g. to uninstall java simply call 'ide uninstall java'. To uninstall IDEasy itself, run 'ide uninstall' without further arguments. To fully delete the selected tool from your machine, use force mode. E.g. 'ide --force uninstall java'.
cmd.unpack=Unpack the selected archive.
cmd.unpack.detail=Can be used to unpack an archive file. E.g. to unpack an archive call 'ide unpack archive.zip'.
cmd.update=Pull your settings and apply updates (software, configuration and repositories).
cmd.update.detail=To update your IDE (if instructed by your ide-admin), you only need to run the following command: 'ide update'.
cmd.upgrade=Upgrade the version of IDEasy to the latest version available.
Expand Down Expand Up @@ -203,6 +205,7 @@ options.global=Global options:
options.local=Local options:
toolcommandlets=Available tool commandlets:
usage=Usage:
val.archive=The archive file to unpack.
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).
Expand All @@ -211,6 +214,7 @@ 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.target=The target directory to extract the archive into.
val.tool=The tool commandlet to select.
val.version=The tool version.
values=Values:
Expand Down
4 changes: 4 additions & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ cmd.uninstall=Deinstalliert ausgewählte Werkzeug(e).
cmd.uninstall-plugin=Deinstalliert die selektierte Erweiterung für das selektierte Werkzeug.
cmd.uninstall-plugin.detail=Erweiterung können nur für Werkzeuge installiert und deinstalliert werden die diese unterstützen. Mit dem Befehl "ide install-plugin" kann die Erweiterung wieder hergestellt werden.
cmd.uninstall.detail=Wird dazu verwendet um ausgewählte Werkzeuge zu deinstallieren. Um z.B. Java zu deinstallieren, dient der Befehl 'ide uninstall java'. Um IDEasy selbst zu installieren, dient der Befehl 'ide uninstall' ohne weitere Parameter. Um ein aktuelles Werzeug vollständig von der Maschine zu löschen, wird der Force-Modus verwendet. Z.B. 'ide --force uninstall java'.
cmd.unpack=Entpackt das ausgewählte Archiv.
cmd.unpack.detail=Dies wird das ausgewählte Archiv entpacken. Um ein Archiv zu entpacken, dient der Befehl 'ide unpack datei.zip'.
cmd.update=Updatet die Settings, Software und Repositories.
cmd.update.detail=Um die IDE auf den neuesten Stand zu bringen (falls von Ihrem Admin angewiesen) geben Sie einfach 'ide update' in die Konsole ein.
cmd.upgrade=Aktualisiere IDEasy auf die neueste Version.
Expand Down Expand Up @@ -203,6 +205,7 @@ options.global=Globale Optionen:
options.local=Lokale Optionen:
toolcommandlets=Verfügbare Werkzeug Kommandos:
usage=Verwendung:
val.archive=Die zu entpackende Archivdatei.
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).
Expand All @@ -211,6 +214,7 @@ 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.target=Ziel des zu extrahierenden Archives (neuer Pfad).
val.tool=Das zu selektierende Werkzeug Kommando.
val.version=Die Werkzeug Version.
values=Werte:
Expand Down
Comment thread
majesteSil marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package com.devonfw.tools.ide.commandlet;

import java.io.IOException;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;

import com.devonfw.tools.ide.cli.CliArguments;
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeTestContext;

/**
* Test of {@link UnpackCommandlet}.
*/
class UnpackCommandletTest extends AbstractIdeContextTest {

/** Base filename of the test archive without extension. */
private static final String TEST_ARCHIVE_BASENAME = "executable_and_non_executable";

/** Path to the test archive directory. */
private static final Path TEST_ARCHIVE_DIR = Path.of("src/test/resources/com/devonfw/tools/ide/io");

/** Test ZIP archive. */
private static final Path TEST_ARCHIVE_ZIP = TEST_ARCHIVE_DIR.resolve(TEST_ARCHIVE_BASENAME + ".zip");

/** Test TAR.GZ archive. */
private static final Path TEST_ARCHIVE_TAR_GZ = TEST_ARCHIVE_DIR.resolve(TEST_ARCHIVE_BASENAME + ".tar.gz");

/** Test 7Z archive. */
private static final Path TEST_ARCHIVE_7Z = TEST_ARCHIVE_DIR.resolve(TEST_ARCHIVE_BASENAME + ".7z");

/**
* Tests extraction of a ZIP archive to the default target directory derived from the archive filename.
*/
@Test
void testUnpackZipWithDefaultTarget() throws IOException {

IdeTestContext context = newContext(PROJECT_BASIC);

Path archive = TEST_ARCHIVE_ZIP.toAbsolutePath();
UnpackCommandlet cmd = new UnpackCommandlet(context);
cmd.archive.setValue(archive);

cmd.run();

Path expectedTarget = context.getCwd().resolve(TEST_ARCHIVE_BASENAME);
assertThat(expectedTarget).isDirectory();
assertThat(expectedTarget.resolve("executableFile.txt")).isRegularFile();
assertThat(expectedTarget.resolve("nonExecutableFile.txt")).isRegularFile();
}

/**
* Tests extraction of a ZIP archive to an explicit target directory via --target.
*/
@Test
void testUnpackZipWithExplicitTarget() throws IOException {

IdeTestContext context = newContext(PROJECT_BASIC);

Path testDir = context.getWorkspacePath().resolve("unpack-test");
context.getFileAccess().mkdirs(testDir);
context.setCwd(testDir, context.getWorkspaceName(), context.getIdeHome());

Path archive = TEST_ARCHIVE_ZIP.toAbsolutePath();
Path target = testDir.resolve("my-extraction");

UnpackCommandlet cmd = new UnpackCommandlet(context);
cmd.archive.setValue(archive);
cmd.target.setValue(target);

cmd.run();

assertThat(target).isDirectory();
assertThat(target.resolve("executableFile.txt")).isRegularFile();
assertThat(target.resolve("nonExecutableFile.txt")).isRegularFile();
}

/**
* Tests that extracting a non-existing archive fails with an appropriate error.
*/
@Test
void testUnpackNonExistingArchiveFails() {

IdeTestContext context = newContext(PROJECT_BASIC);

UnpackCommandlet cmd = new UnpackCommandlet(context);
cmd.archive.setValue(Path.of("does_not_exist.zip"));

assertThatExceptionOfType(IllegalStateException.class)
.isThrownBy(cmd::run)
.withMessageContaining("does_not_exist.zip")
.withMessageContaining("Failed to extract");
}

/**
* Tests extraction of a tar.gz archive with default target directory.
*/
@Test
void testUnpackTarGzWithDefaultTarget() throws IOException {

IdeTestContext context = newContext(PROJECT_BASIC);

Path archive = TEST_ARCHIVE_TAR_GZ.toAbsolutePath();
UnpackCommandlet cmd = new UnpackCommandlet(context);
cmd.archive.setValue(archive);

cmd.run();

Path expectedTarget = context.getCwd().resolve(TEST_ARCHIVE_BASENAME);
assertThat(expectedTarget).isDirectory();
}

/**
* Tests extraction of a 7z archive with default target directory.
*/
@Test
void testUnpack7zWithDefaultTarget() throws IOException {

IdeTestContext context = newContext(PROJECT_BASIC);

Path archive = TEST_ARCHIVE_7Z.toAbsolutePath();
UnpackCommandlet cmd = new UnpackCommandlet(context);
cmd.archive.setValue(archive);

cmd.run();

Path expectedTarget = context.getCwd().resolve(TEST_ARCHIVE_BASENAME);
assertThat(expectedTarget).isDirectory();
}

/**
* End-to-end test that a positional {@code target} argument on the command line ({@code ide unpack <archive> <target>})
* is bound to the target property and the archive is extracted into the given directory.
* <p>
* The {@code target} property has no long option (it is a positional value argument); this guards that the positional
* binding works end-to-end, which the property-level tests above (which set the property directly) cannot verify.
* </p>
*/
@Test
void testUnpackWithPositionalTarget() throws IOException {

IdeTestContext context = newContext(PROJECT_BASIC);

Path archive = TEST_ARCHIVE_ZIP.toAbsolutePath();
Path target = context.getCwd().resolve("e2e-unpack-target");
CliArguments args = new CliArguments("unpack", archive.toString(), target.toString());
args.next();

int exitCode = context.run(args);

assertThat(exitCode).isEqualTo(0);
assertThat(context).logAtError().hasNoMessageContaining("Unknown command");
assertThat(context).logAtError().hasNoMessageContaining("Invalid option");
assertThat(context).logAtError().hasNoMessageContaining("No matching property");
assertThat(target).isDirectory();
assertThat(target.resolve("executableFile.txt")).isRegularFile();
assertThat(target.resolve("nonExecutableFile.txt")).isRegularFile();
}
}