Browse Source

Clean multishell on shell exit

this commit introduces this feature for Bash, let's see how to support other shells
remotes/origin/clean-multishell-on-shell-exit
Gal Schlezinger 2 years ago
parent
commit
d1b3eed406
  1. 23
      e2e/__snapshots__/env.test.ts.snap
  2. 29
      e2e/env.test.ts
  3. 6
      e2e/shellcode/shells.ts
  4. 10
      e2e/shellcode/shells/cmdEnv.ts
  5. 17
      e2e/shellcode/shells/get-env-var.ts
  6. 1
      jest.config.cjs
  7. 10
      src/commands/env.rs
  8. 34
      src/shell/bash.rs
  9. 3
      src/shell/shell.rs

23
e2e/__snapshots__/env.test.ts.snap

@ -1,17 +1,40 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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`] = ` exports[`Bash outputs json: Bash 1`] = `
"set -e "set -e
fnm env --json > file.json" 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[`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`] = ` exports[`PowerShell outputs json: PowerShell 1`] = `
"$ErrorActionPreference = "Stop" "$ErrorActionPreference = "Stop"
fnm env --json | Out-File file.json -Encoding UTF8" 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`] = ` exports[`Zsh outputs json: Zsh 1`] = `
"set -e "set -e
fnm env --json > file.json" fnm env --json > file.json"

29
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 { join } from "node:path"
import { script } from "./shellcode/script.js" import { script } from "./shellcode/script.js"
import { Bash, Fish, PowerShell, WinCmd, Zsh } from "./shellcode/shells.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()) { if (shell.currentlySupported()) {
const file = await readFile(join(testCwd(), filename), "utf8") 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_ARCH: expect.any(String),
FNM_DIR: expect.any(String), FNM_DIR: expect.any(String),
FNM_LOGLEVEL: "info", 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/
)
}
})
}) })
} }

6
e2e/shellcode/shells.ts

@ -1,6 +1,7 @@
import { cmdCall } from "./shells/cmdCall.js" import { cmdCall } from "./shells/cmdCall.js"
import { cmdEnv } from "./shells/cmdEnv.js" import { cmdEnv } from "./shells/cmdEnv.js"
import { cmdExpectCommandOutput } from "./shells/expect-command-output.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 { cmdHasOutputContains } from "./shells/output-contains.js"
import { redirectOutput } from "./shells/redirect-output.js" import { redirectOutput } from "./shells/redirect-output.js"
import { cmdInSubShell } from "./shells/sub-shell.js" import { cmdInSubShell } from "./shells/sub-shell.js"
@ -21,6 +22,7 @@ export const Bash = {
...cmdExpectCommandOutput.bash, ...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash, ...cmdHasOutputContains.bash,
...cmdInSubShell.bash, ...cmdInSubShell.bash,
...getEnvVar.posix,
} }
export const Zsh = { export const Zsh = {
@ -38,6 +40,7 @@ export const Zsh = {
...cmdExpectCommandOutput.bash, ...cmdExpectCommandOutput.bash,
...cmdHasOutputContains.bash, ...cmdHasOutputContains.bash,
...cmdInSubShell.zsh, ...cmdInSubShell.zsh,
...getEnvVar.posix,
} }
export const Fish = { export const Fish = {
@ -55,6 +58,7 @@ export const Fish = {
...cmdExpectCommandOutput.fish, ...cmdExpectCommandOutput.fish,
...cmdHasOutputContains.fish, ...cmdHasOutputContains.fish,
...cmdInSubShell.fish, ...cmdInSubShell.fish,
...getEnvVar.posix,
} }
export const PowerShell = { export const PowerShell = {
@ -73,6 +77,7 @@ export const PowerShell = {
...cmdExpectCommandOutput.powershell, ...cmdExpectCommandOutput.powershell,
...cmdHasOutputContains.powershell, ...cmdHasOutputContains.powershell,
...cmdInSubShell.powershell, ...cmdInSubShell.powershell,
...getEnvVar.powershell,
} }
export const WinCmd = { export const WinCmd = {
@ -94,4 +99,5 @@ export const WinCmd = {
...cmdCall.all, ...cmdCall.all,
...cmdExpectCommandOutput.wincmd, ...cmdExpectCommandOutput.wincmd,
...redirectOutput.bash, ...redirectOutput.bash,
...getEnvVar.winCmd,
} }

10
e2e/shellcode/shells/cmdEnv.ts

@ -1,14 +1,14 @@
import { ScriptLine, define } from "./types.js" 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<EnvConfig>): ScriptLine } export type HasEnv = { env(cfg: Partial<EnvConfig>): ScriptLine }
function stringify(envConfig: Partial<EnvConfig> = {}) { function stringify(config: Partial<EnvConfig> = {}) {
const { useOnCd, logLevel } = envConfig
return [ return [
`fnm env`, `fnm env`,
useOnCd && "--use-on-cd", config.useOnCd && "--use-on-cd",
logLevel && `--log-level=${logLevel}`, config.logLevel && `--log-level=${config.logLevel}`,
...(config.args ?? []),
] ]
.filter(Boolean) .filter(Boolean)
.join(" ") .join(" ")

17
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<HasGetEnvVar>({
getEnvVar: (name) => `$${name}`,
}),
powershell: define<HasGetEnvVar>({
getEnvVar: (name) => `$env:${name}`,
}),
winCmd: define<HasGetEnvVar>({
getEnvVar: (name) => `%${name}%`,
}),
}

1
jest.config.cjs

@ -4,6 +4,7 @@ module.exports = {
testEnvironment: "node", testEnvironment: "node",
testTimeout: 120000, testTimeout: 120000,
extensionsToTreatAsEsm: [".ts"], extensionsToTreatAsEsm: [".ts"],
testPathIgnorePatterns: ["<rootDir>/target/", "<rootDir>/node_modules/"],
moduleNameMapper: { moduleNameMapper: {
"^(\\.{1,2}/.*)\\.js$": "$1", "^(\\.{1,2}/.*)\\.js$": "$1",
"#ansi-styles": "ansi-styles/index.js", "#ansi-styles": "ansi-styles/index.js",

10
src/commands/env.rs

@ -25,6 +25,10 @@ pub struct Env {
/// Print the script to change Node versions every directory change /// Print the script to change Node versions every directory change
#[clap(long)] #[clap(long)]
use_on_cd: bool, use_on_cd: bool,
/// EXPERIMENTAL: delete the multishell on shell exit
#[clap(long)]
delete_on_exit: bool,
} }
fn generate_symlink_path() -> String { fn generate_symlink_path() -> String {
@ -117,6 +121,12 @@ impl Command for Env {
println!("{}", v); println!("{}", v);
} }
if self.delete_on_exit {
if let Some(v) = shell.delete_on_exit(&multishell_path) {
println!("{}", v);
}
}
Ok(()) Ok(())
} }
} }

34
src/shell/bash.rs

@ -51,4 +51,38 @@ impl Shell for Bash {
autoload_hook = autoload_hook autoload_hook = autoload_hook
)) ))
} }
fn delete_on_exit(&self, fnm_multishell: &Path) -> Option<String> {
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()
))
}
} }

3
src/shell/shell.rs

@ -9,6 +9,9 @@ pub trait Shell: Debug {
None None
} }
fn to_clap_shell(&self) -> clap_complete::Shell; fn to_clap_shell(&self) -> clap_complete::Shell;
fn delete_on_exit(&self, _fnm_multishell: &Path) -> Option<String> {
None
}
} }
#[cfg(windows)] #[cfg(windows)]

Loading…
Cancel
Save