Skip to content

Commit

Permalink
Add fnm exec to run commands with the fnm environment (#194)
Browse files Browse the repository at this point in the history
Adds `fnm exec` to run a shell executable with the current node version (or a custom one provided):

```
fnm exec -- node -v # will print the current Node version in fnm
fnm exec --using 12 -- node -v # will print the version of the latest node 12 installed
fnm exec --using-file -- node -v # will print the version of the current directory's node version (based on `.nvmrc` or `.node-version`)
```
  • Loading branch information
Schniz committed Feb 27, 2020
1 parent 6f08f93 commit 50ad22c
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 39 deletions.
93 changes: 93 additions & 0 deletions executable/Exec.re
Original file line number Diff line number Diff line change
@@ -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(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"You passed both "
<Pastel color=Pastel.Cyan> "--using" </Pastel>
" and "
<Pastel color=Pastel.Cyan> "--using-file" </Pastel>
".\n"
"Please provide only one of them."
</Pastel>,
);
Lwt.return_error(1);
| System_Version_Not_Supported =>
Console.error(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"System version is not supported in "
<Pastel color=Pastel.Yellow> "`fnm exec`" </Pastel>
</Pastel>,
);
Lwt.return_error(1);
| LocalVersionResolver.Version_Not_Installed(versionName) =>
Console.error(
<Pastel color=Pastel.Red>
<Pastel bold=true> "Error: " </Pastel>
"Version "
<Pastel color=Pastel.Cyan> versionName </Pastel>
" is not installed."
</Pastel>,
);
Lwt.return_error(1);
| Dotfiles.Version_Not_Provided =>
Console.error(
<Pastel color=Pastel.Red>
"No .nvmrc or .node-version file was found in the current directory. Please provide a version number."
</Pastel>,
);
Lwt.return_error(1);
};
};
48 changes: 47 additions & 1 deletion executable/FnmApp.re
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
13 changes: 2 additions & 11 deletions executable/Uninstall.re
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
| [] =>
Expand Down
22 changes: 2 additions & 20 deletions executable/Use.re
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
<Pastel color=Pastel.Red>
Expand Down
12 changes: 12 additions & 0 deletions feature_tests/exec/run.sh
Original file line number Diff line number Diff line change
@@ -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"
38 changes: 38 additions & 0 deletions library/LocalVersionResolver.re
Original file line number Diff line number Diff line change
@@ -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);
};
1 change: 1 addition & 0 deletions test/TestFramework.re
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions test/TestListRemote.re
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions test/TestUninstall.re
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
Expand Down

0 comments on commit 50ad22c

Please sign in to comment.