Hooks
Hooks allow you to run a code snippet at some predefined situations. They are only available in the interactive mode (REPL), they do not work if you run a Nushell with a script (nu script.nu
) or commands (nu -c "print foo"
) arguments.
Currently, we support these types of hooks:
pre_prompt
: Triggered before the prompt is drawnpre_execution
: Triggered before the line input starts executingenv_change
: Triggered when an environment variable changesdisplay_output
: A block that the output is passed to (experimental).command_not_found
: Triggered when a command is not found.
To make it clearer, we can break down Nushell's execution cycle. The steps to evaluate one line in the REPL mode are as follows:
- Check for
pre_prompt
hooks and run them - Check for
env_change
hooks and run them - Display prompt and wait for user input
- After user typed something and pressed "Enter": Check for
pre_execution
hooks and run them - Parse and evaluate user input
- If a command is not found: Run the
command_not_found
hook. If it returns a string, show it. - If
display_output
is defined, use it to print command output - Return to 1.
Basic Hooks
To enable hooks, define them in your config:
$env.config = {
# ...other config...
hooks: {
pre_prompt: { print "pre prompt hook" }
pre_execution: { print "pre exec hook" }
env_change: {
PWD: {|before, after| print $"changing directory from ($before) to ($after)" }
}
}
}
Try putting the above to your config, running Nushell and moving around your filesystem. When you change a directory, the PWD
environment variable changes and the change triggers the hook with the previous and the current values stored in before
and after
variables, respectively.
Instead of defining just a single hook per trigger, it is possible to define a list of hooks which will run in sequence:
$env.config = {
...other config...
hooks: {
pre_prompt: [
{ print "pre prompt hook" }
{ print "pre prompt hook2" }
]
pre_execution: [
{ print "pre exec hook" }
{ print "pre exec hook2" }
]
env_change: {
PWD: [
{|before, after| print $"changing directory from ($before) to ($after)" }
{|before, after| print $"changing directory from ($before) to ($after) 2" }
]
}
}
}
Also, it might be more practical to update the existing config with new hooks, instead of defining the whole config from scratch:
$env.config = ($env.config | upsert hooks {
pre_prompt: ...
pre_execution: ...
env_change: {
PWD: ...
}
})
Changing Environment
One feature of the hooks is that they preserve the environment. Environment variables defined inside the hook block will be preserved in a similar way as def --env
. You can test it with the following example:
> $env.config = ($env.config | upsert hooks {
pre_prompt: { $env.SPAM = "eggs" }
})
> $env.SPAM
eggs
The hook blocks otherwise follow the general scoping rules, i.e., commands, aliases, etc. defined within the block will be thrown away once the block ends.
Conditional Hooks
One thing you might be tempted to do is to activate an environment whenever you enter a directory:
$env.config = ($env.config | upsert hooks {
env_change: {
PWD: [
{|before, after|
if $after == /some/path/to/directory {
load-env { SPAM: eggs }
}
}
]
}
})
This won't work because the environment will be active only within the if
block. In this case, you could easily rewrite it as load-env (if $after == ... { ... } else { {} })
but this pattern is fairly common and later we'll see that not all cases can be rewritten like this.
To deal with the above problem, we introduce another way to define a hook -- a record:
$env.config = ($env.config | upsert hooks {
env_change: {
PWD: [
{
condition: {|before, after| $after == /some/path/to/directory }
code: {|before, after| load-env { SPAM: eggs } }
}
]
}
})
When the hook triggers, it evaluates the condition
block. If it returns true
, the code
block will be evaluated. If it returns false
, nothing will happen. If it returns something else, an error will be thrown. The condition
field can also be omitted altogether in which case the hook will always evaluate.
The pre_prompt
and pre_execution
hook types also support the conditional hooks but they don't accept the before
and after
parameters.
Hooks as Strings
So far a hook was defined as a block that preserves only the environment, but nothing else. To be able to define commands or aliases, it is possible to define the code
field as a string. You can think of it as if you typed the string into the REPL and hit Enter. So, the hook from the previous section can be also written as
> $env.config = ($env.config | upsert hooks {
pre_prompt: '$env.SPAM = "eggs"'
})
> $env.SPAM
eggs
This feature can be used, for example, to conditionally bring in definitions based on the current directory:
$env.config = ($env.config | upsert hooks {
env_change: {
PWD: [
{
condition: {|_, after| $after == /some/path/to/directory }
code: 'def foo [] { print "foo" }'
}
{
condition: {|before, _| $before == /some/path/to/directory }
code: 'hide foo'
}
]
}
})
When defining a hook as a string, the $before
and $after
variables are set to the previous and current environment variable value, respectively, similarly to the previous examples:
$env.config = ($env.config | upsert hooks {
env_change: {
PWD: {
code: 'print $"changing directory from ($before) to ($after)"'
}
}
}
Examples
Adding a Single Hook to Existing Config
An example for PWD env change hook:
$env.config = ($env.config | upsert hooks.env_change.PWD {|config|
let val = ($config | get -i hooks.env_change.PWD)
if $val == null {
$val | append {|before, after| print $"changing directory from ($before) to ($after)" }
} else {
[
{|before, after| print $"changing directory from ($before) to ($after)" }
]
}
})
Automatically Activating an Environment when Entering a Directory
This one looks for test-env.nu
in a directory
$env.config = ($env.config | upsert hooks.env_change.PWD {
[
{
condition: {|_, after|
($after == '/path/to/target/dir'
and ($after | path join test-env.nu | path exists))
}
code: "overlay use test-env.nu"
}
{
condition: {|before, after|
('/path/to/target/dir' not-in $after
and '/path/to/target/dir' in $before
and 'test-env' in (overlay list))
}
code: "overlay hide test-env --keep-env [ PWD ]"
}
]
})
Filtering or Diverting Command Output
You can use the display_output
hook to redirect the output of commands. You should define a block that works on all value types. The output of external commands is not filtered through display_output
.
This hook can display the output in a separate window, perhaps as rich HTML text. Here is the basic idea of how to do that:
$env.config = ($env.config | upsert hooks {
display_output: { to html --partial --no-color | save --raw /tmp/nu-output.html }
})
You can view the result by opening file:https:///tmp/nu-output.html
in a web browser. Of course this isn't very convenient unless you use a browser that automatically reloads when the file changes. Instead of the save
command, you would normally customize this to send the HTML output to a desired window.
Changing how Output is Displayed
You can change to default behavior of how output is displayed by using the display_output
hook. Here is an example that changes the default display behavior to show a table 1 layer deep if the terminal is wide enough, or collapse otherwise:
$env.config = ($env.config | upsert hooks {
display_output: {if (term size).columns >= 100 { table -ed 1 } else { table }}
})
command_not_found
Hook in Arch Linux
The following hook uses the pkgfile
command, to find which packages commands belong to in Arch Linux.
$env.config = {
...other config...
hooks: {
...other hooks...
command_not_found: {
|cmd_name| (
try {
let pkgs = (pkgfile --binaries --verbose $cmd_name)
if ($pkgs | is-empty) {
return null
}
(
$"(ansi $env.config.color_config.shape_external)($cmd_name)(ansi reset) " +
$"may be found in the following packages:\n($pkgs)"
)
}
)
}
}
}
command_not_found
Hook in Windows
The following hook uses the ftype
command, to find program paths in Windows that might be relevant to the user for alias
-ing.
$env.config = {
...other config...
hooks: {
...other hooks...
command_not_found: {
|cmd_name| (
try {
let attrs = (
ftype | find $cmd_name | to text | lines | reduce -f [] { |line, acc|
$line | parse "{type}={path}" | append $acc
} | group-by path | transpose key value | each { |row|
{ path: $row.key, types: ($row.value | get type | str join ", ") }
}
)
let len = ($attrs | length)
if $len == 0 {
return null
} else {
return ($attrs | table --collapse)
}
}
)
}
}
}