Browse Source

Add `fnm exec` to run commands with the fnm environment (#194)

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`)
```
remotes/origin/add-simple-redirecting-site
Gal Schlezinger 5 years ago committed by GitHub
parent
commit
50ad22c432
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 93
      executable/Exec.re
  2. 48
      executable/FnmApp.re
  3. 13
      executable/Uninstall.re
  4. 22
      executable/Use.re
  5. 12
      feature_tests/exec/run.sh
  6. 38
      library/LocalVersionResolver.re
  7. 1
      test/TestFramework.re
  8. 6
      test/TestListRemote.re
  9. 8
      test/TestUninstall.re

93
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(
<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
executable/FnmApp.re

@ -11,6 +11,8 @@ let runCmd = lwt => {
}; };
module Commands = { 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 use = (version, quiet) => Use.run(~version, ~quiet) |> runCmd;
let alias = (version, name) => Alias.run(~name, ~version) |> runCmd; let alias = (version, name) => Alias.run(~name, ~version) |> runCmd;
let default = version => Alias.run(~name="default", ~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 default = {
let doc = "Alias a version as default"; let doc = "Alias a version as default";
let man = help_secs; let man = help_secs;
@ -378,7 +414,17 @@ let argv =
let _ = let _ =
Term.eval_choice( Term.eval_choice(
defaultCmd, defaultCmd,
[install, uninstall, use, alias, default, listLocal, listRemote, env], [
install,
uninstall,
use,
alias,
default,
listLocal,
listRemote,
env,
exec,
],
~argv, ~argv,
) )
|> Term.exit; |> Term.exit;

13
executable/Uninstall.re

@ -2,17 +2,8 @@ open Fnm;
open Lwt.Infix; open Lwt.Infix;
let run = (~version) => { let run = (~version) => {
let%lwt installedVersions = Versions.getInstalledVersions(); let%lwt matchingLocalVersions =
LocalVersionResolver.getMatchingLocalVersions(version);
let formattedVersionName = Versions.format(version);
let matchingLocalVersions =
installedVersions
|> Versions.(
List.filter(v =>
isVersionFitsPrefix(formattedVersionName, Local.(v.name))
|| v.name == formattedVersionName
)
);
switch (matchingLocalVersions) { switch (matchingLocalVersions) {
| [] => | [] =>

22
executable/Use.re

@ -2,8 +2,6 @@ open Fnm;
let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return()); let lwtIgnore = lwt => Lwt.catch(() => lwt, _ => Lwt.return());
exception Version_Not_Installed(string);
let info = (~quiet, arg) => let info = (~quiet, arg) =>
if (!quiet) { if (!quiet) {
Logger.info(arg); Logger.info(arg);
@ -19,26 +17,10 @@ let error = (~quiet, arg) =>
Logger.error(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 switchVersion = (~version, ~quiet) => {
let info = info(~quiet); let info = info(~quiet);
let debug = debug(~quiet); let debug = debug(~quiet);
let%lwt parsedVersion = getVersion(version); let%lwt parsedVersion = LocalVersionResolver.getVersion(version);
let%lwt versionPath = let%lwt versionPath =
switch (parsedVersion) { switch (parsedVersion) {
@ -120,7 +102,7 @@ let rec askIfInstall = (~version, ~quiet, retry) => {
let rec run = (~version, ~quiet) => let rec run = (~version, ~quiet) =>
try%lwt(main(~version, ~quiet)) { try%lwt(main(~version, ~quiet)) {
| Version_Not_Installed(versionString) => | LocalVersionResolver.Version_Not_Installed(versionString) =>
error( error(
~quiet, ~quiet,
<Pastel color=Pastel.Red> <Pastel color=Pastel.Red>

12
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"

38
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);
};

1
test/TestFramework.re

@ -21,6 +21,7 @@ let run = args => {
Unix.environment() Unix.environment()
|> Array.append([| |> Array.append([|
Printf.sprintf("%s=%s", Fnm.Config.FNM_DIR.name, tmpDir), Printf.sprintf("%s=%s", Fnm.Config.FNM_DIR.name, tmpDir),
"FORCE_COLOR=false",
|]); |]);
let result = let result =
Lwt_process.pread_chars(~env, ("", arguments)) |> Lwt_stream.to_string; Lwt_process.pread_chars(~env, ("", arguments)) |> Lwt_stream.to_string;

6
test/TestListRemote.re

@ -57,14 +57,14 @@ let allVersions6_11 = [
"v6.11.5", "v6.11.5",
]; ];
describe("List Remote", ({test}) => { describe("List Remote", ({test, _}) => {
let versionRegExp = Str.regexp(".*[0-9]+\.[0-9]+\.[0-9]+\|.*latest-*"); let versionRegExp = Str.regexp(".*[0-9]+\\.[0-9]+\\.[0-9]+\\|.*latest-*");
let filterVersionNumbers = response => let filterVersionNumbers = response =>
response response
|> String.split_on_char('\n') |> String.split_on_char('\n')
|> List.filter(s => Str.string_match(versionRegExp, s, 0)) |> 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); |> List.map(String.trim);
let runAndFilterVersionNumbers = args => run(args) |> filterVersionNumbers; let runAndFilterVersionNumbers = args => run(args) |> filterVersionNumbers;

8
test/TestUninstall.re

@ -8,7 +8,7 @@ let isVersionInstalled = version =>
|> String.split_on_char('\n') |> String.split_on_char('\n')
|> List.exists(v => v == "* v" ++ version); |> List.exists(v => v == "* v" ++ version);
describe("Uninstall", ({test}) => { describe("Uninstall", ({test, _}) => {
test("Should be possible to uninstall a specific version", ({expect, _}) => { test("Should be possible to uninstall a specific version", ({expect, _}) => {
let version = "6.0.0"; let version = "6.0.0";
let _ = installVersion(version); let _ = installVersion(version);
@ -29,9 +29,9 @@ describe("Uninstall", ({test}) => {
uninstallVersion("6") uninstallVersion("6")
|> String.split_on_char('\n') |> String.split_on_char('\n')
|> String.concat(" "); |> String.concat(" ");
expect.string(response).toMatch( expect.string(response).toMatch("multiple versions");
".*multiple versions.*" ++ v1 ++ ".*" ++ v2 ++ ".*", expect.string(response).toMatch(" v" ++ v1 ++ " ");
); expect.string(response).toMatch(" v" ++ v2 ++ " ");
expect.bool(isVersionInstalled(v1)).toBeTrue(); expect.bool(isVersionInstalled(v1)).toBeTrue();
expect.bool(isVersionInstalled(v2)).toBeTrue(); expect.bool(isVersionInstalled(v2)).toBeTrue();
clearTmpDir(); clearTmpDir();

Loading…
Cancel
Save