From d1b3eed4061a668be49815a27a1ad0dc69f23816 Mon Sep 17 00:00:00 2001 From: Gal Schlezinger Date: Sat, 19 Nov 2022 12:55:21 +0200 Subject: [PATCH] Clean multishell on shell exit this commit introduces this feature for Bash, let's see how to support other shells --- e2e/__snapshots__/env.test.ts.snap | 23 +++++++++++++++++++ e2e/env.test.ts | 29 ++++++++++++++++++++++-- e2e/shellcode/shells.ts | 6 +++++ e2e/shellcode/shells/cmdEnv.ts | 10 ++++----- e2e/shellcode/shells/get-env-var.ts | 17 +++++++++++++++ jest.config.cjs | 1 + src/commands/env.rs | 10 +++++++++ src/shell/bash.rs | 34 +++++++++++++++++++++++++++++ src/shell/shell.rs | 3 +++ 9 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 e2e/shellcode/shells/get-env-var.ts diff --git a/e2e/__snapshots__/env.test.ts.snap b/e2e/__snapshots__/env.test.ts.snap index 1ad8405..cd9ae70 100644 --- a/e2e/__snapshots__/env.test.ts.snap +++ b/e2e/__snapshots__/env.test.ts.snap @@ -1,17 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Bash deletes the multishell upon shell exit: Bash 1`] = ` +"set -e +eval "$(fnm env --delete-on-exit)" +echo $FNM_MULTISHELL_PATH > multishell_path" +`; + exports[`Bash outputs json: Bash 1`] = ` "set -e fnm env --json > file.json" `; +exports[`Fish deletes the multishell upon shell exit: Fish 1`] = ` +"fnm env --delete-on-exit | source +echo $FNM_MULTISHELL_PATH > multishell_path" +`; + exports[`Fish outputs json: Fish 1`] = `"fnm env --json > file.json"`; +exports[`PowerShell deletes the multishell upon shell exit: PowerShell 1`] = ` +"$ErrorActionPreference = "Stop" +fnm env --delete-on-exit | Out-String | Invoke-Expression +echo $env:FNM_MULTISHELL_PATH | Out-File multishell_path -Encoding UTF8" +`; + exports[`PowerShell outputs json: PowerShell 1`] = ` "$ErrorActionPreference = "Stop" fnm env --json | Out-File file.json -Encoding UTF8" `; +exports[`Zsh deletes the multishell upon shell exit: Zsh 1`] = ` +"set -e +eval "$(fnm env --delete-on-exit)" +echo $FNM_MULTISHELL_PATH > multishell_path" +`; + exports[`Zsh outputs json: Zsh 1`] = ` "set -e fnm env --json > file.json" diff --git a/e2e/env.test.ts b/e2e/env.test.ts index d07ad6a..f33d067 100644 --- a/e2e/env.test.ts +++ b/e2e/env.test.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises" +import { lstat, readFile, realpath } from "node:fs/promises" import { join } from "node:path" import { script } from "./shellcode/script.js" import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.js" @@ -20,7 +20,8 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) { if (shell.currentlySupported()) { const file = await readFile(join(testCwd(), filename), "utf8") - expect(JSON.parse(file)).toEqual({ + const json = JSON.parse(file) + expect(json).toEqual({ FNM_ARCH: expect.any(String), FNM_DIR: expect.any(String), FNM_LOGLEVEL: "info", @@ -30,5 +31,29 @@ for (const shell of [Bash, Zsh, Fish, PowerShell, WinCmd]) { }) } }) + + test(`deletes the multishell upon shell exit`, async () => { + const filename = `multishell_path` + await script(shell) + .then(shell.env({ args: ["--delete-on-exit"] })) + .then( + shell.redirectOutput( + shell.call("echo", [shell.getEnvVar("FNM_MULTISHELL_PATH")]), + { output: filename } + ) + ) + .takeSnapshot(shell) + .execute(shell) + + if (shell.currentlySupported()) { + const multishell = await readFile( + join(testCwd(), filename), + "utf8" + ).then((x) => x.trim()) + await expect(lstat(multishell)).rejects.toThrowError( + /no such file or directory/ + ) + } + }) }) } diff --git a/e2e/shellcode/shells.ts b/e2e/shellcode/shells.ts index 10fd0af..4214bbf 100644 --- a/e2e/shellcode/shells.ts +++ b/e2e/shellcode/shells.ts @@ -1,6 +1,7 @@ import { cmdCall } from "./shells/cmdCall.js" import { cmdEnv } from "./shells/cmdEnv.js" import { cmdExpectCommandOutput } from "./shells/expect-command-output.js" +import { getEnvVar } from "./shells/get-env-var.js" import { cmdHasOutputContains } from "./shells/output-contains.js" import { redirectOutput } from "./shells/redirect-output.js" import { cmdInSubShell } from "./shells/sub-shell.js" @@ -21,6 +22,7 @@ export const Bash = { ...cmdExpectCommandOutput.bash, ...cmdHasOutputContains.bash, ...cmdInSubShell.bash, + ...getEnvVar.posix, } export const Zsh = { @@ -38,6 +40,7 @@ export const Zsh = { ...cmdExpectCommandOutput.bash, ...cmdHasOutputContains.bash, ...cmdInSubShell.zsh, + ...getEnvVar.posix, } export const Fish = { @@ -55,6 +58,7 @@ export const Fish = { ...cmdExpectCommandOutput.fish, ...cmdHasOutputContains.fish, ...cmdInSubShell.fish, + ...getEnvVar.posix, } export const PowerShell = { @@ -73,6 +77,7 @@ export const PowerShell = { ...cmdExpectCommandOutput.powershell, ...cmdHasOutputContains.powershell, ...cmdInSubShell.powershell, + ...getEnvVar.powershell, } export const WinCmd = { @@ -94,4 +99,5 @@ export const WinCmd = { ...cmdCall.all, ...cmdExpectCommandOutput.wincmd, ...redirectOutput.bash, + ...getEnvVar.winCmd, } diff --git a/e2e/shellcode/shells/cmdEnv.ts b/e2e/shellcode/shells/cmdEnv.ts index 581965f..0e50afc 100644 --- a/e2e/shellcode/shells/cmdEnv.ts +++ b/e2e/shellcode/shells/cmdEnv.ts @@ -1,14 +1,14 @@ import { ScriptLine, define } from "./types.js" -type EnvConfig = { useOnCd: boolean; logLevel: string } +type EnvConfig = { useOnCd: boolean; logLevel: string; args: string[] } export type HasEnv = { env(cfg: Partial): ScriptLine } -function stringify(envConfig: Partial = {}) { - const { useOnCd, logLevel } = envConfig +function stringify(config: Partial = {}) { return [ `fnm env`, - useOnCd && "--use-on-cd", - logLevel && `--log-level=${logLevel}`, + config.useOnCd && "--use-on-cd", + config.logLevel && `--log-level=${config.logLevel}`, + ...(config.args ?? []), ] .filter(Boolean) .join(" ") diff --git a/e2e/shellcode/shells/get-env-var.ts b/e2e/shellcode/shells/get-env-var.ts new file mode 100644 index 0000000..5b1e0ff --- /dev/null +++ b/e2e/shellcode/shells/get-env-var.ts @@ -0,0 +1,17 @@ +import { define } from "./types.js" + +export type HasGetEnvVar = { + getEnvVar(name: string): string +} + +export const getEnvVar = { + posix: define({ + getEnvVar: (name) => `$${name}`, + }), + powershell: define({ + getEnvVar: (name) => `$env:${name}`, + }), + winCmd: define({ + getEnvVar: (name) => `%${name}%`, + }), +} diff --git a/jest.config.cjs b/jest.config.cjs index 10e45aa..276abf6 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -4,6 +4,7 @@ module.exports = { testEnvironment: "node", testTimeout: 120000, extensionsToTreatAsEsm: [".ts"], + testPathIgnorePatterns: ["/target/", "/node_modules/"], moduleNameMapper: { "^(\\.{1,2}/.*)\\.js$": "$1", "#ansi-styles": "ansi-styles/index.js", diff --git a/src/commands/env.rs b/src/commands/env.rs index 6bb4427..a820a7f 100644 --- a/src/commands/env.rs +++ b/src/commands/env.rs @@ -25,6 +25,10 @@ pub struct Env { /// Print the script to change Node versions every directory change #[clap(long)] use_on_cd: bool, + + /// EXPERIMENTAL: delete the multishell on shell exit + #[clap(long)] + delete_on_exit: bool, } fn generate_symlink_path() -> String { @@ -117,6 +121,12 @@ impl Command for Env { println!("{}", v); } + if self.delete_on_exit { + if let Some(v) = shell.delete_on_exit(&multishell_path) { + println!("{}", v); + } + } + Ok(()) } } diff --git a/src/shell/bash.rs b/src/shell/bash.rs index 64982a1..ce13f6c 100644 --- a/src/shell/bash.rs +++ b/src/shell/bash.rs @@ -51,4 +51,38 @@ impl Shell for Bash { autoload_hook = autoload_hook )) } + + fn delete_on_exit(&self, fnm_multishell: &Path) -> Option { + Some(indoc::formatdoc!( + r#" + # appends a command to a trap + # + # - 1st arg: code to add + # - remaining args: names of traps to modify + # + trap_add() {{ + trap_add_cmd=$1; shift || fatal "${{FUNCNAME}} usage error" + for trap_add_name in "$@"; do + trap -- "$( + # helper fn to get existing trap command from output + # of trap -p + extract_trap_cmd() {{ printf '%s\n' "$3"; }} + # print existing trap command with newline + eval "extract_trap_cmd $(trap -p "${{trap_add_name}}")" + # print the new trap command + printf '%s\n' "${{trap_add_cmd}}" + )" "${{trap_add_name}}" \ + || fatal "unable to add to trap ${{trap_add_name}}" + done + }} + # set the trace attribute for the above function. this is + # required to modify DEBUG or RETURN traps because functions don't + # inherit them unless the trace attribute is set + declare -f -t trap_add + + trap_add 'echo hi; rm "{fnm_multishell}"' EXIT + "#, + fnm_multishell = fnm_multishell.display() + )) + } } diff --git a/src/shell/shell.rs b/src/shell/shell.rs index 45488fa..bad4a7c 100644 --- a/src/shell/shell.rs +++ b/src/shell/shell.rs @@ -9,6 +9,9 @@ pub trait Shell: Debug { None } fn to_clap_shell(&self) -> clap_complete::Shell; + fn delete_on_exit(&self, _fnm_multishell: &Path) -> Option { + None + } } #[cfg(windows)]