Skip to content
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

JDK-8236128: Allow jpackage create installers for services #7793

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
7116d42
Linux initial commit ready
alexeysemenyukoracle Nov 19, 2021
c5e28df
Tests added. Linux implementation done
alexeysemenyukoracle Nov 30, 2021
093d0d3
bugfix
alexeysemenyukoracle Nov 30, 2021
51c1ead
AppImageFile unit test fixed
alexeysemenyukoracle Nov 30, 2021
7675cf0
Debian packaging minor adjustments
alexeysemenyukoracle Dec 1, 2021
9fdffaa
Reduce code duplication
alexeysemenyukoracle Dec 1, 2021
ffd4d8a
Works on Windows
alexeysemenyukoracle Dec 2, 2021
77a28f6
Bugfix
alexeysemenyukoracle Dec 2, 2021
95b3eed
Works on Windows (using NSSM service manager)!
alexeysemenyukoracle Dec 3, 2021
26c8e90
Linux initial commit ready
alexeysemenyukoracle Nov 19, 2021
4e39dd4
Tests added. Linux implementation done
alexeysemenyukoracle Nov 30, 2021
ec85581
bugfix
alexeysemenyukoracle Nov 30, 2021
9c3efef
AppImageFile unit test fixed
alexeysemenyukoracle Nov 30, 2021
2d353f1
Debian packaging minor adjustments
alexeysemenyukoracle Dec 1, 2021
a16117a
Reduce code duplication
alexeysemenyukoracle Dec 1, 2021
f48495e
Works on Windows
alexeysemenyukoracle Dec 2, 2021
28ea813
Bugfix
alexeysemenyukoracle Dec 2, 2021
7436196
Works on Windows (using NSSM service manager)!
alexeysemenyukoracle Dec 3, 2021
f0506c5
Wrong imports removed
alexeysemenyukoracle Dec 3, 2021
b5b1447
Bugfix & allow to run test on Windows even without service intaller i…
alexeysemenyukoracle Dec 6, 2021
4d27aab
Merge
alexeysemenyukoracle Jan 18, 2022
16f4ba7
Apply stashed changes properly
alexeysemenyukoracle Jan 18, 2022
882e50c
Minor clean up
alexeysemenyukoracle Feb 5, 2022
e7e1e15
Minor clean up
alexeysemenyukoracle Feb 6, 2022
cee68f2
Comments improved
alexeysemenyukoracle Feb 7, 2022
6a06d74
Can run on Windows without %COMSPEC% env variable set. LauncherAsServ…
alexeysemenyukoracle Feb 7, 2022
e63c1e2
Launchers as services work on macOS. Better test coverage for launche…
alexeysemenyukoracle Feb 9, 2022
c3ca928
Bugfix
alexeysemenyukoracle Feb 9, 2022
e336309
Fully functional on macOS
alexeysemenyukoracle Feb 10, 2022
0a9c311
Cleanup and copyright year updates
alexeysemenyukoracle Feb 10, 2022
f59104b
bugfix
alexeysemenyukoracle Feb 10, 2022
d39b990
Added mode to create small runtime to speed up test run
alexeysemenyukoracle Feb 10, 2022
0c5b194
Delete file added by accident
alexeysemenyukoracle Feb 10, 2022
8b8d912
Add "purge" package test action to delete packages produced by jpackage.
alexeysemenyukoracle Feb 10, 2022
154af2a
Bugfix
alexeysemenyukoracle Feb 10, 2022
13e7e0a
More robust configuring of test actions
alexeysemenyukoracle Feb 10, 2022
97f4cd3
Minor
alexeysemenyukoracle Feb 10, 2022
04231b5
Remove obsolete script
alexeysemenyukoracle Feb 10, 2022
2640f92
More robust rpm uninstall - it doesn't fail if the package is not ins…
alexeysemenyukoracle Feb 10, 2022
76ed2b2
Better handling of purge action.
alexeysemenyukoracle Feb 10, 2022
7d8f234
Bugfix
alexeysemenyukoracle Feb 10, 2022
5d05ce8
Gracefully handle request for RPM uninstall if the package is not ins…
alexeysemenyukoracle Feb 10, 2022
6044503
Fix issue with missing replacement data for scripts when jpackage is …
alexeysemenyukoracle Feb 10, 2022
07e53ba
Bugfix
alexeysemenyukoracle Feb 10, 2022
9a34812
Bugfix
alexeysemenyukoracle Feb 10, 2022
dd5eb53
Make it possible to run install and unpacking in a single test run
alexeysemenyukoracle Feb 10, 2022
263922f
Use Optional a bit
alexeysemenyukoracle Feb 11, 2022
33133c8
Minor
alexeysemenyukoracle Feb 11, 2022
ab32bde
Minor: Replace Files.walk(path, 1) with Files.List(path)
alexeysemenyukoracle Feb 11, 2022
6bda7b3
Add modes to set `jpackage.test.action` system property
alexeysemenyukoracle Feb 11, 2022
0df1447
Got rid of PackageTest.addLauncherName(). It is error prone to explic…
alexeysemenyukoracle Feb 12, 2022
401f6f7
Windows automated tests fixed
alexeysemenyukoracle Feb 12, 2022
d7d5434
Shorten package name to prevent unpack failures of msiexec because ab…
alexeysemenyukoracle Feb 12, 2022
a26a2a3
Comment typo fix
alexeysemenyukoracle Feb 12, 2022
4a81b56
ServiceTest bugfix on Windows.
alexeysemenyukoracle Feb 14, 2022
73d8430
Bugfix
alexeysemenyukoracle Feb 14, 2022
0ac9e32
Bugfix
alexeysemenyukoracle Feb 14, 2022
c46e8e0
Linux initial commit ready
alexeysemenyukoracle Nov 19, 2021
362671b
Tests added. Linux implementation done
alexeysemenyukoracle Nov 30, 2021
52f7b3c
bugfix
alexeysemenyukoracle Nov 30, 2021
8acf347
AppImageFile unit test fixed
alexeysemenyukoracle Nov 30, 2021
3f7a7be
Debian packaging minor adjustments
alexeysemenyukoracle Dec 1, 2021
35d35ec
Reduce code duplication
alexeysemenyukoracle Dec 1, 2021
54831ba
Works on Windows
alexeysemenyukoracle Dec 2, 2021
b56e77a
Bugfix
alexeysemenyukoracle Dec 2, 2021
6def3bd
Works on Windows (using NSSM service manager)!
alexeysemenyukoracle Dec 3, 2021
0a7b8b5
Wrong imports removed
alexeysemenyukoracle Dec 3, 2021
f6f8ff5
Bugfix & allow to run test on Windows even without service intaller i…
alexeysemenyukoracle Dec 6, 2021
1faae38
Linux initial commit ready
alexeysemenyukoracle Nov 19, 2021
286e138
Tests added. Linux implementation done
alexeysemenyukoracle Nov 30, 2021
798ead9
bugfix
alexeysemenyukoracle Nov 30, 2021
752a164
AppImageFile unit test fixed
alexeysemenyukoracle Nov 30, 2021
f5aecc2
Debian packaging minor adjustments
alexeysemenyukoracle Dec 1, 2021
9f463c3
Reduce code duplication
alexeysemenyukoracle Dec 1, 2021
437bf53
Works on Windows
alexeysemenyukoracle Dec 2, 2021
cd108db
Bugfix
alexeysemenyukoracle Dec 2, 2021
47522c6
Works on Windows (using NSSM service manager)!
alexeysemenyukoracle Dec 3, 2021
dc99162
Apply stashed changes properly
alexeysemenyukoracle Jan 18, 2022
2ad68b6
Minor clean up
alexeysemenyukoracle Feb 5, 2022
ea3572b
Minor clean up
alexeysemenyukoracle Feb 6, 2022
155a835
Comments improved
alexeysemenyukoracle Feb 7, 2022
217c53e
Can run on Windows without %COMSPEC% env variable set. LauncherAsServ…
alexeysemenyukoracle Feb 7, 2022
ac47446
Launchers as services work on macOS. Better test coverage for launche…
alexeysemenyukoracle Feb 9, 2022
1264b26
Bugfix
alexeysemenyukoracle Feb 9, 2022
e1eeee7
Fully functional on macOS
alexeysemenyukoracle Feb 10, 2022
dcc5620
Cleanup and copyright year updates
alexeysemenyukoracle Feb 10, 2022
4aa6ec6
bugfix
alexeysemenyukoracle Feb 10, 2022
0f44229
Added mode to create small runtime to speed up test run
alexeysemenyukoracle Feb 10, 2022
b79c98f
Add "purge" package test action to delete packages produced by jpackage.
alexeysemenyukoracle Feb 10, 2022
e18cd78
Bugfix
alexeysemenyukoracle Feb 10, 2022
7580b27
More robust configuring of test actions
alexeysemenyukoracle Feb 10, 2022
8e69789
Minor
alexeysemenyukoracle Feb 10, 2022
b94a763
Remove obsolete script
alexeysemenyukoracle Feb 10, 2022
39fcb69
More robust rpm uninstall - it doesn't fail if the package is not ins…
alexeysemenyukoracle Feb 10, 2022
a776c2e
Better handling of purge action.
alexeysemenyukoracle Feb 10, 2022
67948bc
Bugfix
alexeysemenyukoracle Feb 10, 2022
6ab80e2
Gracefully handle request for RPM uninstall if the package is not ins…
alexeysemenyukoracle Feb 10, 2022
4684cae
Fix issue with missing replacement data for scripts when jpackage is …
alexeysemenyukoracle Feb 10, 2022
3c4b237
Bugfix
alexeysemenyukoracle Feb 10, 2022
7f45bab
Bugfix
alexeysemenyukoracle Feb 10, 2022
d171d52
Make it possible to run install and unpacking in a single test run
alexeysemenyukoracle Feb 10, 2022
283e1cc
Use Optional a bit
alexeysemenyukoracle Feb 11, 2022
3561d60
Minor
alexeysemenyukoracle Feb 11, 2022
e82da8d
Minor: Replace Files.walk(path, 1) with Files.List(path)
alexeysemenyukoracle Feb 11, 2022
3779b28
Add modes to set `jpackage.test.action` system property
alexeysemenyukoracle Feb 11, 2022
e530e8e
Got rid of PackageTest.addLauncherName(). It is error prone to explic…
alexeysemenyukoracle Feb 12, 2022
d256fd1
Windows automated tests fixed
alexeysemenyukoracle Feb 12, 2022
ab0bccc
Comment typo fix
alexeysemenyukoracle Feb 12, 2022
c21873c
ServiceTest bugfix on Windows.
alexeysemenyukoracle Feb 14, 2022
37eab34
Bugfix
alexeysemenyukoracle Feb 14, 2022
9b6b129
Bugfix
alexeysemenyukoracle Feb 14, 2022
70c641d
Merge branch 'JDK-8236128' of https://github.com/alexeysemenyukoracle…
alexeysemenyukoracle Feb 17, 2022
90f5a1a
Merge branch 'master' into JDK-8236128
alexeysemenyukoracle Feb 17, 2022
f33c732
Merge fixed
alexeysemenyukoracle Feb 17, 2022
2a49ffd
Merge fix
alexeysemenyukoracle Feb 17, 2022
6325427
Minor formatting fix
alexeysemenyukoracle Feb 17, 2022
2ac1bd7
Sync l10n files. Fix copyright year
alexeysemenyukoracle Mar 11, 2022
a70d08a
JDK-8236128: Allow jpackage create installers for services
alexeysemenyukoracle Mar 11, 2022
42ff27e
Whitespace cleanup
alexeysemenyukoracle Mar 11, 2022
0ff8e9a
Whitespace cleanup
alexeysemenyukoracle Mar 11, 2022
1a3d7cf
Merge branch 'master' into JDK-8236128
alexeysemenyukoracle Apr 12, 2022
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
2 changes: 1 addition & 1 deletion make/modules/jdk.jpackage/Java.gmk
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@

COPY += .gif .png .txt .spec .script .prerm .preinst \
.postrm .postinst .list .sh .desktop .copyright .control .plist .template \
.icns .scpt .wxs .wxl .wxi .ico .bmp .tiff
.icns .scpt .wxs .wxl .wxi .ico .bmp .tiff .service

CLEAN += .properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand All @@ -25,11 +25,8 @@
package jdk.jpackage.internal;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
Expand All @@ -39,8 +36,8 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.imageio.ImageIO;
Expand All @@ -56,16 +53,19 @@
import static jdk.jpackage.internal.StandardBundlerParam.FILE_ASSOCIATIONS;
import static jdk.jpackage.internal.StandardBundlerParam.ICON;
import static jdk.jpackage.internal.StandardBundlerParam.PREDEFINED_APP_IMAGE;
import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT;;
import static jdk.jpackage.internal.StandardBundlerParam.SHORTCUT_HINT;

/**
* Helper to create files for desktop integration.
*/
final class DesktopIntegration {
final class DesktopIntegration extends ShellCustomAction {

static final String DESKTOP_COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
static final String DESKTOP_COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
static final String UTILITY_SCRIPTS = "UTILITY_SCRIPTS";
private static final String COMMANDS_INSTALL = "DESKTOP_COMMANDS_INSTALL";
private static final String COMMANDS_UNINSTALL = "DESKTOP_COMMANDS_UNINSTALL";
private static final String SCRIPTS = "DESKTOP_SCRIPTS";

private static final List<String> REPLACEMENT_STRING_IDS = List.of(
COMMANDS_INSTALL, COMMANDS_UNINSTALL, SCRIPTS);

private DesktopIntegration(PlatformPackage thePackage,
Map<String, ? super Object> params,
Expand Down Expand Up @@ -171,21 +171,28 @@ private DesktopIntegration(PlatformPackage thePackage,
}
}

static DesktopIntegration create(PlatformPackage thePackage,
static ShellCustomAction create(PlatformPackage thePackage,
Map<String, ? super Object> params) throws IOException {
if (StandardBundlerParam.isRuntimeInstaller(params)) {
return null;
return ShellCustomAction.nop(REPLACEMENT_STRING_IDS);
}
return new DesktopIntegration(thePackage, params, null);
}

@Override
List<String> requiredPackages() {
return Stream.of(List.of(this), nestedIntegrations).flatMap(
List::stream).map(DesktopIntegration::requiredPackagesSelf).flatMap(
List::stream).distinct().toList();
}

Map<String, String> create() throws IOException {
@Override
protected List<String> replacementStringIds() {
return REPLACEMENT_STRING_IDS;
}

@Override
protected Map<String, String> createImpl() throws IOException {
associations.forEach(assoc -> assoc.data.verify());

if (iconFile != null) {
Expand Down Expand Up @@ -230,36 +237,26 @@ Map<String, String> create() throws IOException {
// of the additional launchers and append them to the corresponding
// commands of the main launcher.
List<String> installShellCmds = new ArrayList<>(Arrays.asList(
data.get(DESKTOP_COMMANDS_INSTALL)));
data.get(COMMANDS_INSTALL)));
List<String> uninstallShellCmds = new ArrayList<>(Arrays.asList(
data.get(DESKTOP_COMMANDS_UNINSTALL)));
data.get(COMMANDS_UNINSTALL)));
for (var integration: nestedIntegrations) {
if (!integration.associations.isEmpty()) {
needCleanupScripts = true;
}

Map<String, String> launcherData = integration.create();

installShellCmds.add(launcherData.get(DESKTOP_COMMANDS_INSTALL));
uninstallShellCmds.add(launcherData.get(
DESKTOP_COMMANDS_UNINSTALL));
installShellCmds.add(launcherData.get(COMMANDS_INSTALL));
uninstallShellCmds.add(launcherData.get(COMMANDS_UNINSTALL));
}

data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(
installShellCmds));
data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(
uninstallShellCmds));
data.put(COMMANDS_INSTALL, stringifyShellCommands(installShellCmds));
data.put(COMMANDS_UNINSTALL, stringifyShellCommands(uninstallShellCmds));

if (needCleanupScripts) {
// Pull in utils.sh scrips library.
try (InputStream is = OverridableResource.readDefault("utils.sh");
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr)) {
data.put(UTILITY_SCRIPTS, reader.lines().collect(
Collectors.joining(System.lineSeparator())));
}
} else {
data.put(UTILITY_SCRIPTS, "");
// Pull in desktop_utils.sh scrips library.
data.put(SCRIPTS, stringifyTextFile("desktop_utils.sh"));
}

return data;
Expand All @@ -277,17 +274,12 @@ private Map<String, String> createDataForDesktopFile(
Map<String, String> data = new HashMap<>();
data.put("APPLICATION_NAME", APP_NAME.fetchFrom(params));
data.put("APPLICATION_DESCRIPTION", DESCRIPTION.fetchFrom(params));
data.put("APPLICATION_ICON",
iconFile != null ? iconFile.installPath().toString() : null);
data.put("APPLICATION_ICON", Optional.ofNullable(iconFile).map(
f -> f.installPath().toString()).orElse(null));
data.put("DEPLOY_BUNDLE_CATEGORY", MENU_GROUP.fetchFrom(params));

String appLauncher = thePackage.installedApplicationLayout().launchersDirectory().resolve(
LinuxAppImageBuilder.getLauncherName(params)).toString();
if (Pattern.compile("\\s").matcher(appLauncher).find()) {
// Path contains whitespace(s). Enclose in double quotes.
appLauncher = "\"" + appLauncher + "\"";
}
data.put("APPLICATION_LAUNCHER", appLauncher);
data.put("APPLICATION_LAUNCHER", Enquoter.forPropertyValues().applyTo(
thePackage.installedApplicationLayout().launchersDirectory().resolve(
LinuxAppImageBuilder.getLauncherName(params)).toString()));

return data;
}
Expand Down Expand Up @@ -356,13 +348,13 @@ void applyTo(Map<String, String> data) {
cmds.add(registerDesktopFileCmd);
cmds.add(registerFileAssociationsCmd);
cmds.addAll(registerIconCmds);
data.put(DESKTOP_COMMANDS_INSTALL, stringifyShellCommands(cmds));
data.put(COMMANDS_INSTALL, stringifyShellCommands(cmds));

cmds.clear();
cmds.add(unregisterDesktopFileCmd);
cmds.add(unregisterFileAssociationsCmd);
cmds.addAll(unregisterIconCmds);
data.put(DESKTOP_COMMANDS_UNINSTALL, stringifyShellCommands(cmds));
data.put(COMMANDS_UNINSTALL, stringifyShellCommands(cmds));
}

private String registerDesktopFileCmd;
Expand All @@ -385,24 +377,25 @@ void applyTo(Map<String, String> data) {
private class DesktopFile {

DesktopFile(String fileName) {
installPath = thePackage
var installPath = thePackage
.installedApplicationLayout()
.destktopIntegrationDirectory().resolve(fileName);
srcPath = thePackage
var srcPath = thePackage
.sourceApplicationLayout()
.destktopIntegrationDirectory().resolve(fileName);
}

private final Path installPath;
private final Path srcPath;
impl = new InstallableFile(srcPath, installPath);
}

Path installPath() {
return installPath;
return impl.installPath();
}

Path srcPath() {
return srcPath;
return impl.srcPath();
}

private final InstallableFile impl;
}

private void appendFileAssociation(XMLStreamWriter xml,
Expand Down Expand Up @@ -526,15 +519,6 @@ private static int normalizeIconSize(int iconSize) {
return commonIconSize;
}

private static String stringifyShellCommands(String... commands) {
return stringifyShellCommands(Arrays.asList(commands));
}

private static String stringifyShellCommands(List<String> commands) {
return String.join(System.lineSeparator(), commands.stream().filter(
s -> s != null && !s.isEmpty()).toList());
}

private static class LinuxFileAssociation {
LinuxFileAssociation(FileAssociation fa) {
this.data = fa;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.jpackage.internal;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static jdk.jpackage.internal.OverridableResource.createResource;

/**
* Helper to install launchers as services using "systemd".
*/
public final class LinuxLaunchersAsServices extends UnixLaunchersAsServices {

private LinuxLaunchersAsServices(PlatformPackage thePackage,
Map<String, Object> params) throws IOException {
super(thePackage, REQUIRED_PACKAGES, params, li -> {
return new Launcher(thePackage, li.getName(), params);
});
}

static ShellCustomAction create(PlatformPackage thePackage,
Map<String, Object> params) throws IOException {
if (StandardBundlerParam.isRuntimeInstaller(params)) {
return ShellCustomAction.nop(REPLACEMENT_STRING_IDS);
}
return new LinuxLaunchersAsServices(thePackage, params);
}

public static Path getServiceUnitFileName(String packageName,
String launcherName) {
String baseName = launcherName.replaceAll("[\\s]", "_");
return Path.of(packageName + "-" + baseName + ".service");
}

private static class Launcher extends UnixLauncherAsService {

Launcher(PlatformPackage thePackage, String name,
Map<String, Object> mainParams) {
super(name, mainParams, createResource("unit-template.service",
mainParams).setCategory(I18N.getString(
"resource.systemd-unit-file")));

unitFilename = getServiceUnitFileName(thePackage.name(), getName());

getResource()
.setPublicName(unitFilename)
.addSubstitutionDataEntry("APPLICATION_LAUNCHER",
Enquoter.forPropertyValues().applyTo(
thePackage.installedApplicationLayout().launchersDirectory().resolve(
getName()).toString()));
}

@Override
Path descriptorFilePath(Path root) {
return root.resolve("lib/systemd/system").resolve(unitFilename);
}

private final Path unitFilename;
}

private final static List<String> REQUIRED_PACKAGES = List.of("systemd",
"coreutils" /* /usr/bin/wc */, "grep");
}
Loading