diff --git a/.ci/install.sh b/.ci/install.sh index 9a719b0a4..bf21a5e6b 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -97,12 +97,12 @@ setup_shell() { echo "" echo ' # fnm' echo ' export PATH=$HOME/.fnm:$PATH' - echo ' eval `fnm env --multi`' + echo ' eval "`fnm env --multi`"' echo '' >> $CONF_FILE echo '# fnm' >> $CONF_FILE echo 'export PATH=$HOME/.fnm:$PATH' >> $CONF_FILE - echo 'eval `fnm env --multi`' >> $CONF_FILE + echo 'eval "`fnm env --multi`"' >> $CONF_FILE elif [ "$CURRENT_SHELL" == "fish" ]; then CONF_FILE=$HOME/.config/fish/config.fish @@ -110,12 +110,12 @@ setup_shell() { echo "" echo ' # fnm' echo ' set PATH $HOME/.fnm $PATH' - echo ' eval (fnm env --multi --fish)' + echo ' fnm env --multi | source' echo '' >> $CONF_FILE echo '# fnm' >> $CONF_FILE echo 'set PATH $HOME/.fnm $PATH' >> $CONF_FILE - echo 'eval (fnm env --multi --fish)' >> $CONF_FILE + echo 'fnm env --multi | source"' >> $CONF_FILE elif [ "$CURRENT_SHELL" == "bash" ]; then if [ "$OS" == "Darwin" ]; then @@ -127,12 +127,12 @@ setup_shell() { echo "" echo ' # fnm' echo ' export PATH=$HOME/.fnm:$PATH' - echo ' eval `fnm env --multi`' + echo ' eval "`fnm env --multi`"' echo '' >> $CONF_FILE echo '# fnm' >> $CONF_FILE echo 'export PATH=$HOME/.fnm:$PATH' >> $CONF_FILE - echo 'eval `fnm env --multi`' >> $CONF_FILE + echo 'eval "`fnm env --multi`"' >> $CONF_FILE else echo "Could not infer shell type. Please set up manually." diff --git a/README.md b/README.md index 10ce82a28..f52778d87 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ curl https://raw.githubusercontent.com/Schniz/fnm/master/.ci/install.sh | bash - - Add the following line to your `.bashrc`/`.zshrc` file: ```bash - eval `fnm env --multi` + eval "`fnm env --multi`" ``` If you are using [fish shell](https://fishshell.com/), add this line to your `config.fish` file: ```fish - eval (fnm env --multi --fish) + fnm env --multi | source ``` ## Usage @@ -80,13 +80,14 @@ Lists the installed Node versions. Lists the Node versions available to download remotely. -### `fnm env [--multi] [--fish] [--node-dist-mirror=URI] [--base-dir=DIR]` +### `fnm env [--multi] [--shell=fish|bash|zsh] [--node-dist-mirror=URI] [--use-on-cd] [--base-dir=DIR]` Prints the required shell commands in order to configure your shell, Bash compliant by default. - Providing `--multi` will output the multishell support, allowing a different current Node version per shell -- Providing `--fish` will output the Fish-compliant version. +- Providing `--shell=fish` will output the Fish-compliant version. Omitting it and `fnm` will try to infer the current shell based on the process tree - Providing `--node-dist-mirror="https://npm.taobao.org/dist"` will use the Chinese mirror of Node.js +- Providing `--use-on-cd` will also output a script that will automatically change the node version if a `.nvmrc`/`.node-version` file is found - Providing `--base-dir="/tmp/fnm"` will install and use versions in `/tmp/fnm` directory ## Future Plans diff --git a/executable/Env.re b/executable/Env.re index fb7d75cac..6b25cc98c 100644 --- a/executable/Env.re +++ b/executable/Env.re @@ -1,10 +1,9 @@ open Fnm; -let symlinkExists = path => { +let symlinkExists = path => try%lwt (Lwt_unix.lstat(path) |> Lwt.map(_ => true)) { | _ => Lwt.return(false) }; -}; let rec makeTemporarySymlink = () => { let suggestedName = @@ -25,8 +24,66 @@ let rec makeTemporarySymlink = () => { }; }; -let run = (~shell, ~multishell, ~nodeDistMirror, ~fnmDir) => { +let rec printUseOnCd = (~shell) => + switch (shell) { + | System.Shell.Bash => {| + __fnmcd () { + cd $@ + + if [[ -f .node-version && .node-version ]]; then + echo "fnm: Found .node-version" + fnm use + elif [[ -f .nvmrc && .nvmrc ]]; then + echo "fnm: Found .nvmrc" + fnm use + fi + } + + alias cd=__fnmcd + |} + | Fish => {| + function _fnm_autoload_hook --on-variable PWD --description 'Change Node version on directory change' + status --is-command-substitution; and return + if test -f .node-version + echo "fnm: Found .node-version" + fnm use + else if test -f .nvmrc + echo "fnm: Found .nvmrc" + fnm use + end + end + |} + | Zsh => {| + autoload -U add-zsh-hook + _fnm_autoload_hook () { + if [[ -f .node-version && -r .node-version ]]; then + echo "fnm: Found .node-version" + fnm use + elsif + elif [[ -f .nvmrc && -r .nvmrc ]]; then + echo "fnm: Found .nvmrc" + fnm use + fi + } + + add-zsh-hook chpwd _fnm_autoload_hook \ + && _fnm_autoload_hook + |} + }; + +let run = (~forceShell, ~multishell, ~nodeDistMirror, ~fnmDir, ~useOnCd) => { open Lwt; + open System.Shell; + + let%lwt shell = + switch (forceShell) { + | None => + switch%lwt (System.Shell.infer()) { + | None => Lwt.return(Bash) + | Some(shell) => Lwt.return(shell) + } + | Some(shell) => Lwt.return(shell) + }; Random.self_init(); @@ -35,7 +92,8 @@ let run = (~shell, ~multishell, ~nodeDistMirror, ~fnmDir) => { ? makeTemporarySymlink() : Lwt.return(Directories.globalCurrentVersion); switch (shell) { - | System.Shell.Bash => + | Bash + | Zsh => Printf.sprintf("export PATH=%s/bin:$PATH", path) |> Console.log; Printf.sprintf("export %s=%s", Config.FNM_MULTISHELL_PATH.name, path) |> Console.log; @@ -46,7 +104,7 @@ let run = (~shell, ~multishell, ~nodeDistMirror, ~fnmDir) => { nodeDistMirror, ) |> Console.log; - | System.Shell.Fish => + | Fish => Printf.sprintf("set PATH %s/bin $PATH;", path) |> Console.log; Printf.sprintf("set %s %s;", Config.FNM_MULTISHELL_PATH.name, path) |> Console.log; @@ -59,5 +117,9 @@ let run = (~shell, ~multishell, ~nodeDistMirror, ~fnmDir) => { |> Console.log; }; + if (useOnCd) { + printUseOnCd(~shell) |> Console.log; + }; + Lwt.return(); }; diff --git a/executable/FnmApp.re b/executable/FnmApp.re index 62102fe55..eb33f0b21 100644 --- a/executable/FnmApp.re +++ b/executable/FnmApp.re @@ -6,13 +6,15 @@ module Commands = { let listRemote = () => Lwt_main.run(ListRemote.run()); let listLocal = () => Lwt_main.run(ListLocal.run()); let install = version => Lwt_main.run(Install.run(~version)); - let env = (isFishShell, isMultishell, nodeDistMirror, fnmDir) => + let env = + (isFishShell, isMultishell, nodeDistMirror, fnmDir, shell, useOnCd) => Lwt_main.run( Env.run( - ~shell=Fnm.System.Shell.(isFishShell ? Fish : Bash), + ~forceShell=Fnm.System.Shell.(isFishShell ? Some(Fish) : shell), ~multishell=isMultishell, ~nodeDistMirror, ~fnmDir, + ~useOnCd, ), ); }; @@ -149,6 +151,14 @@ let env = { Arg.(value & flag & info(["fish"], ~doc)); }; + let shell = { + open Fnm.System.Shell; + let doc = "Specifies a specific shell type. If omitted, it will be inferred based on the process tree. $(docv)"; + let shellChoices = + Arg.enum([("fish", Fish), ("bash", Bash), ("zsh", Zsh)]); + Arg.(value & opt(some(shellChoices), None) & info(["shell"], ~doc)); + }; + let nodeDistMirror = { let doc = "https://nodejs.org/dist mirror"; Arg.( @@ -172,6 +182,11 @@ let env = { Arg.(value & flag & info(["multi"], ~doc)); }; + let useOnCd = { + let doc = "Hook into the shell `cd` and automatically use the specified version for the project"; + Arg.(value & flag & info(["use-on-cd"], ~doc)); + }; + ( Term.( const(Commands.env) @@ -179,6 +194,8 @@ let env = { $ isMultishell $ nodeDistMirror $ fnmDir + $ shell + $ useOnCd ), Term.info("env", ~version, ~doc, ~exits=Term.default_exits, ~man, ~sdocs), ); diff --git a/feature_tests/fish/run.fish b/feature_tests/fish/run.fish index 4765c919b..8234133ff 100644 --- a/feature_tests/fish/run.fish +++ b/feature_tests/fish/run.fish @@ -1,6 +1,6 @@ #!/usr/bin/env fish -eval (fnm env --fish) +fnm env --fish | source fnm install v8.11.3 fnm use v8.11.3 diff --git a/feature_tests/use_on_cd/app/.nvmrc b/feature_tests/use_on_cd/app/.nvmrc new file mode 100644 index 000000000..dba04c1e1 --- /dev/null +++ b/feature_tests/use_on_cd/app/.nvmrc @@ -0,0 +1 @@ +8.11.3 diff --git a/feature_tests/use_on_cd/run.sh b/feature_tests/use_on_cd/run.sh new file mode 100644 index 000000000..9bf5932a5 --- /dev/null +++ b/feature_tests/use_on_cd/run.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e + +DIRECTORY=`dirname $0` + +eval "`fnm env --multi`" +fnm install 6.11.3 +fnm install 8.11.3 +fnm use 6.11.3 + +if hash zsh 2>/dev/null; then + echo ' > Running test on Zsh' + + zsh -c ' + set -e + + eval "`fnm env --multi --use-on-cd`" + + fnm use 6.11.3 + + NODE_VERSION=$(node -v) + if [ "$NODE_VERSION" != "v6.11.3" ]; then + echo "Failed: Node version ($NODE_VERSION) is not v6.11.3" + exit 1 + fi + + cd app + + NODE_VERSION=$(node -v) + if [ "$NODE_VERSION" != "v8.11.3" ]; then + echo "Failed: Node version ($NODE_VERSION) is not v8.11.3" + exit 1 + fi + ' +else + echo "Skipping zsh test: \`zsh\` is not installed" +fi + +if hash fish 2>/dev/null; then + echo ' > Running test on Fish' + + fish -c ' + fnm env --multi --use-on-cd | source + + fnm use 6.11.3 + + set NODE_VERSION (node -v) + if test "$NODE_VERSION" != "v6.11.3" + echo "Failed: Node version ($NODE_VERSION) is not v6.11.3" + exit 1 + end + + cd app + + set NODE_VERSION (node -v) + if test "$NODE_VERSION" != "v8.11.3" + echo "Failed: Node version ($NODE_VERSION) is not v8.11.3" + exit 1 + end + ' +else + echo "Skipping fish test: \`zsh\` is not installed" +fi + +echo " > Running test on Bash..." +bash -c ' + shopt -s expand_aliases + eval "`fnm env --multi --use-on-cd`" + fnm use 6.11.3 + NODE_VERSION=$(node -v) + if [ "$NODE_VERSION" != "v6.11.3" ]; then + echo "Failed: Node version ($NODE_VERSION) is not v6.11.3" + exit 1 + fi + cd app + NODE_VERSION=$(node -v) + if [ "$NODE_VERSION" != "v8.11.3" ]; then + echo "Failed: Node version ($NODE_VERSION) is not v8.11.3" + exit 1 + fi +' diff --git a/library/System.re b/library/System.re index dca724933..243a89e5a 100644 --- a/library/System.re +++ b/library/System.re @@ -11,7 +11,40 @@ let mkdirp = destination => module Shell = { type t = | Bash + | Zsh | Fish; + + let infer = () => { + let processInfo = pid => { + switch%lwt (unix_exec("ps", ~args=[|"-o", "ppid,comm", pid|])) { + | [] => Lwt.return_none + | [_headers, line, ..._otherLines] => + let psResult = String.split_on_char(' ', line |> String.trim); + let parentPid = List.nth(psResult, 0); + let executable = List.nth(psResult, 1) |> Filename.basename; + Lwt.return_some((parentPid, executable)); + | [_, ...xs] => Lwt.return_none + }; + }; + + let rec getShell = (~level=0, pid) => { + switch%lwt (processInfo(pid)) { + | Some((_, "sh")) + | Some((_, "-sh")) + | Some((_, "-bash")) + | Some((_, "bash")) => Lwt.return_some(Bash) + | Some((_, "-zsh")) + | Some((_, "zsh")) => Lwt.return_some(Zsh) + | Some((_, "fish")) + | Some((_, "-fish")) => Lwt.return_some(Fish) + | Some((ppid, _)) when level < 10 => getShell(~level=level + 1, ppid) + | Some(_) + | None => Lwt.return_none + }; + }; + + getShell(Unix.getpid() |> string_of_int); + }; }; module NodeArch = {