diff --git a/executable/Exec.re b/executable/Exec.re
new file mode 100644
index 000000000..6a2667c0d
--- /dev/null
+++ b/executable/Exec.re
@@ -0,0 +1,93 @@
+open Fnm;
+
+exception System_Version_Not_Supported;
+exception Ambiguous_Arguments;
+
+let startsWith = (~prefix, str) =>
+ Base.String.prefix(str, String.length(prefix)) != prefix;
+
+let unsafeRun = (~cmd, ~version as maybeVersion, ~useFileVersion) => {
+ let%lwt version =
+ switch (maybeVersion, useFileVersion) {
+ | (None, false) => Lwt.return_none
+ | (Some(_), true) => Lwt.fail(Ambiguous_Arguments)
+ | (None, true) => Fnm.Dotfiles.getVersion() |> Lwt.map(x => Some(x))
+ | (Some(version), false) => Lwt.return_some(version)
+ };
+ let%lwt currentVersion =
+ switch (version) {
+ | None => Lwt.return(Directories.currentVersion)
+ | Some(version) =>
+ let%lwt matchingVersion = LocalVersionResolver.getVersion(version);
+ let matchingVersionPath =
+ switch (matchingVersion) {
+ | Alias(path) => Versions.Aliases.toDirectory(path)
+ | Local(path) => Versions.Local.toDirectory(path)
+ | System => raise(System_Version_Not_Supported)
+ };
+ Lwt.return(matchingVersionPath);
+ };
+ let fnmPath = Filename.concat(currentVersion, "bin");
+ let path = Opt.(Sys.getenv_opt("PATH") or "");
+ let pathEnv = Printf.sprintf("PATH=%s:%s", fnmPath, path);
+ let cmd = cmd |> Array.copy |> Array.append([|"env", pathEnv|]);
+ let%lwt exitCode =
+ Lwt_process.exec(
+ ~stdin=`Keep,
+ ~stdout=`Keep,
+ ~stderr=`Keep,
+ ~env=Unix.environment(),
+ ("", cmd),
+ );
+
+ switch (exitCode) {
+ | Unix.WEXITED(0) => Lwt.return_ok()
+ | Unix.WEXITED(x)
+ | Unix.WSTOPPED(x)
+ | Unix.WSIGNALED(x) => Lwt.return_error(x)
+ };
+};
+
+let run = (~cmd, ~version, ~useFileVersion) => {
+ try%lwt(unsafeRun(~cmd, ~version, ~useFileVersion)) {
+ | Ambiguous_Arguments =>
+ Console.error(
+
+ "Error: "
+ "You passed both "
+ "--using"
+ " and "
+ "--using-file"
+ ".\n"
+ "Please provide only one of them."
+ ,
+ );
+ Lwt.return_error(1);
+ | System_Version_Not_Supported =>
+ Console.error(
+
+ "Error: "
+ "System version is not supported in "
+ "`fnm exec`"
+ ,
+ );
+ Lwt.return_error(1);
+ | LocalVersionResolver.Version_Not_Installed(versionName) =>
+ Console.error(
+
+ "Error: "
+ "Version "
+ versionName
+ " is not installed."
+ ,
+ );
+ Lwt.return_error(1);
+ | Dotfiles.Version_Not_Provided =>
+ Console.error(
+
+ "No .nvmrc or .node-version file was found in the current directory. Please provide a version number."
+ ,
+ );
+ Lwt.return_error(1);
+ };
+};
diff --git a/executable/FnmApp.re b/executable/FnmApp.re
index c6281563f..dc477105f 100644
--- a/executable/FnmApp.re
+++ b/executable/FnmApp.re
@@ -11,6 +11,8 @@ let runCmd = lwt => {
};
module Commands = {
+ let exec = (version, useFileVersion, cmd) =>
+ Exec.run(~cmd=Array.of_list(cmd), ~version, ~useFileVersion) |> runCmd;
let use = (version, quiet) => Use.run(~version, ~quiet) |> runCmd;
let alias = (version, name) => Alias.run(~name, ~version) |> runCmd;
let default = version => Alias.run(~name="default", ~version) |> runCmd;
@@ -233,6 +235,40 @@ let alias = {
);
};
+let exec = {
+ let doc = "Execute a binary with the current Node.js in the PATH";
+ let man = help_secs;
+ let sdocs = Manpage.s_common_options;
+
+ let usingVersion = {
+ let doc = "Use a specific $(docv)";
+ Arg.(value & opt(some(string), None) & info(["using"], ~doc));
+ };
+
+ let usingFileVersion = {
+ let doc = "Use a version from a version file";
+ Arg.(value & flag & info(["using-file"], ~doc));
+ };
+
+ let command = {
+ let doc = "The $(docv) to execute";
+ Arg.(non_empty & pos_all(string, []) & info([], ~docv="COMMAND", ~doc));
+ };
+
+ (
+ Term.(const(Commands.exec) $ usingVersion $ usingFileVersion $ command),
+ Term.info(
+ "exec",
+ ~envs,
+ ~version,
+ ~doc,
+ ~exits=Term.default_exits,
+ ~man,
+ ~sdocs,
+ ),
+ );
+};
+
let default = {
let doc = "Alias a version as default";
let man = help_secs;
@@ -378,7 +414,17 @@ let argv =
let _ =
Term.eval_choice(
defaultCmd,
- [install, uninstall, use, alias, default, listLocal, listRemote, env],
+ [
+ install,
+ uninstall,
+ use,
+ alias,
+ default,
+ listLocal,
+ listRemote,
+ env,
+ exec,
+ ],
~argv,
)
|> Term.exit;
diff --git a/executable/Uninstall.re b/executable/Uninstall.re
index f61da514e..f4468450f 100644
--- a/executable/Uninstall.re
+++ b/executable/Uninstall.re
@@ -2,17 +2,8 @@ open Fnm;
open Lwt.Infix;
let run = (~version) => {
- let%lwt installedVersions = Versions.getInstalledVersions();
-
- let formattedVersionName = Versions.format(version);
- let matchingLocalVersions =
- installedVersions
- |> Versions.(
- List.filter(v =>
- isVersionFitsPrefix(formattedVersionName, Local.(v.name))
- || v.name == formattedVersionName
- )
- );
+ let%lwt matchingLocalVersions =
+ LocalVersionResolver.getMatchingLocalVersions(version);
switch (matchingLocalVersions) {
| [] =>
diff --git a/executable/Use.re b/executable/Use.re
index 25e46806a..38dee1ea8 100644
--- a/executable/Use.re
+++ b/executable/Use.re
@@ -2,8 +2,6 @@ open Fnm;
let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());
-exception Version_Not_Installed(string);
-
let info = (~quiet, arg) =>
if (!quiet) {
Logger.info(arg);
@@ -19,26 +17,10 @@ let error = (~quiet, arg) =>
Logger.error(arg);
};
-let getVersion = version => {
- let%lwt parsed = Versions.parse(version);
- let%lwt resultWithLts =
- switch (parsed) {
- | Ok(x) => Lwt.return_ok(x)
- | Error("latest-*") =>
- switch%lwt (VersionListingLts.getLatest()) {
- | Error(_) => Lwt.return_error(Version_Not_Installed(version))
- | Ok({VersionListingLts.lts, _}) =>
- Versions.Alias("latest-" ++ lts) |> Lwt.return_ok
- }
- | _ => Version_Not_Installed(version) |> Lwt.return_error
- };
- resultWithLts |> Result.fold(Lwt.fail, Lwt.return);
-};
-
let switchVersion = (~version, ~quiet) => {
let info = info(~quiet);
let debug = debug(~quiet);
- let%lwt parsedVersion = getVersion(version);
+ let%lwt parsedVersion = LocalVersionResolver.getVersion(version);
let%lwt versionPath =
switch (parsedVersion) {
@@ -120,7 +102,7 @@ let rec askIfInstall = (~version, ~quiet, retry) => {
let rec run = (~version, ~quiet) =>
try%lwt(main(~version, ~quiet)) {
- | Version_Not_Installed(versionString) =>
+ | LocalVersionResolver.Version_Not_Installed(versionString) =>
error(
~quiet,
diff --git a/feature_tests/exec/run.sh b/feature_tests/exec/run.sh
new file mode 100755
index 000000000..f5a388893
--- /dev/null
+++ b/feature_tests/exec/run.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+set -e
+
+fnm install v6.10.0
+fnm install v8.10.0
+fnm install v10.10.0
+fnm use v8.10.0
+
+fnm exec -- node -v | grep "v8.10.0"
+fnm exec --using 6 -- node -v | grep "v6.10.0"
+fnm exec --using 10 -- node -v | grep "v10.10.0"
diff --git a/library/LocalVersionResolver.re b/library/LocalVersionResolver.re
new file mode 100644
index 000000000..f848efba4
--- /dev/null
+++ b/library/LocalVersionResolver.re
@@ -0,0 +1,38 @@
+exception Version_Not_Installed(string);
+
+/** Parse a local version, including lts and aliases */
+let getVersion = version => {
+ let%lwt parsed = Versions.parse(version);
+ let%lwt resultWithLts =
+ switch (parsed) {
+ | Ok(x) => Lwt.return_ok(x)
+ | Error("latest-*") =>
+ switch%lwt (VersionListingLts.getLatest()) {
+ | Error(_) => Lwt.return_error(Version_Not_Installed(version))
+ | Ok({VersionListingLts.lts, _}) =>
+ Versions.Alias("latest-" ++ lts) |> Lwt.return_ok
+ }
+ | _ => Version_Not_Installed(version) |> Lwt.return_error
+ };
+ resultWithLts |> Result.fold(Lwt.fail, Lwt.return);
+};
+
+/**
+ * Get matches for all versions that match a semver partial
+ */
+let getMatchingLocalVersions = version => {
+ open Versions.Local;
+
+ let%lwt installedVersions = Versions.getInstalledVersions();
+ let formattedVersionName = Versions.format(version);
+
+ let matchingVersions =
+ installedVersions
+ |> List.filter(v =>
+ Versions.isVersionFitsPrefix(formattedVersionName, v.name)
+ || v.name == formattedVersionName
+ )
+ |> List.sort((a, b) => - compare(a.name, b.name));
+
+ Lwt.return(matchingVersions);
+};
diff --git a/test/TestFramework.re b/test/TestFramework.re
index 191dd93bc..a5b607809 100644
--- a/test/TestFramework.re
+++ b/test/TestFramework.re
@@ -21,6 +21,7 @@ let run = args => {
Unix.environment()
|> Array.append([|
Printf.sprintf("%s=%s", Fnm.Config.FNM_DIR.name, tmpDir),
+ "FORCE_COLOR=false",
|]);
let result =
Lwt_process.pread_chars(~env, ("", arguments)) |> Lwt_stream.to_string;
diff --git a/test/TestListRemote.re b/test/TestListRemote.re
index ee2759086..b1f9cda99 100644
--- a/test/TestListRemote.re
+++ b/test/TestListRemote.re
@@ -57,14 +57,14 @@ let allVersions6_11 = [
"v6.11.5",
];
-describe("List Remote", ({test}) => {
- let versionRegExp = Str.regexp(".*[0-9]+\.[0-9]+\.[0-9]+\|.*latest-*");
+describe("List Remote", ({test, _}) => {
+ let versionRegExp = Str.regexp(".*[0-9]+\\.[0-9]+\\.[0-9]+\\|.*latest-*");
let filterVersionNumbers = response =>
response
|> String.split_on_char('\n')
|> List.filter(s => Str.string_match(versionRegExp, s, 0))
- |> List.map(s => Str.replace_first(Str.regexp("\*"), "", s))
+ |> List.map(s => Str.replace_first(Str.regexp("\\*"), "", s))
|> List.map(String.trim);
let runAndFilterVersionNumbers = args => run(args) |> filterVersionNumbers;
diff --git a/test/TestUninstall.re b/test/TestUninstall.re
index e3e35fdda..85eeb1b93 100644
--- a/test/TestUninstall.re
+++ b/test/TestUninstall.re
@@ -8,7 +8,7 @@ let isVersionInstalled = version =>
|> String.split_on_char('\n')
|> List.exists(v => v == "* v" ++ version);
-describe("Uninstall", ({test}) => {
+describe("Uninstall", ({test, _}) => {
test("Should be possible to uninstall a specific version", ({expect, _}) => {
let version = "6.0.0";
let _ = installVersion(version);
@@ -29,9 +29,9 @@ describe("Uninstall", ({test}) => {
uninstallVersion("6")
|> String.split_on_char('\n')
|> String.concat(" ");
- expect.string(response).toMatch(
- ".*multiple versions.*" ++ v1 ++ ".*" ++ v2 ++ ".*",
- );
+ expect.string(response).toMatch("multiple versions");
+ expect.string(response).toMatch(" v" ++ v1 ++ " ");
+ expect.string(response).toMatch(" v" ++ v2 ++ " ");
expect.bool(isVersionInstalled(v1)).toBeTrue();
expect.bool(isVersionInstalled(v2)).toBeTrue();
clearTmpDir();