Creating Modules
Important
When working through the examples below, it is recommended that you start a new shell before importing an updated version of each module or command. This will help reduce any confusion caused by definitions from previous imports.
Overview
Modules (and Submodules, to be covered below) are created in one of two ways:
- Most commonly, by creating a file with a series of
export
statements of definitions to be exported from the module. - For submodules inside a module, using the
module
command
Tips
While it's possible to use the module
command to create a module directly at the commandline, it's far more useful and common to store the module definitions in a file for reusability.
The module file can be either:
- A file named
mod.nu
, in which case its directory becomes the module name - Any other
<module_name>.nu
file, in which case the filename becomes the module name
Simple Module Example
Create a file named inc.nu
with the following:
export def increment []: int -> int {
$in + 1
}
This is a module! We can now import it and use the increment
command:
use inc.nu *
5 | increment
# => 6
Of course, you can easily distribute a file like this so that others can make use of the module as well.
Exports
We covered the types of definitions that are available in modules briefly in the main Modules Overview above. While this might be enough explanation for an end-user, module authors will need to know how to create the export definitions for:
- Commands (
export def
) - Aliases (
export alias
) - Constants (
export const
) - Known externals (
export extern
) - Submodules (
export module
) - Imported symbols from other modules (
export use
) - Environment setup (
export-env
)
Tips
Only definitions marked with export
(or export-env
for environment variables) are accessible when the module is imported. Definitions not marked with export
are only visible from inside the module. In some languages, these would be called "private" or "local" definitions. An example can be found below in Additional Examples.
main
Exports
Important
An export cannot have the same name as that of the module itself.
In the Basic Example above, we had a module named inc
with a command named increment
. However, if we rename that file to increment.nu
, it will fail to import.
mv inc.nu increment.nu
use increment.nu *
# => Error: nu::parser::named_as_module
# => ...
# => help: Module increment can't export command named
# => the same as the module. Either change the module
# => name, or export `main` command.
As helpfully mentioned in the error message, you can simply rename the export main
, in which case it will take on the name of the module when imported. Edit the increment.nu
file:
export def main []: int -> int {
$in + 1
}
Now it works as expected:
use ./increment.nu
2024 | increment
# => 2025
Note
main
can be used for both export def
and export extern
definitions.
Tips
main
definitions are imported in the following cases:
- The entire module is imported with
use <module>
oruse <module.nu>
- The
*
glob is used to import all of the modules definitions (e.g.,use <module> *
, etc.) - The
main
definition is explicitly imported withuse <module> main
,use <module> [main]
, etc.)
Conversely, the following forms do not import the main
definition:
use <module> <other_definition>
# or
use <module> [ <other_definitions> ]
Note
Additionally, main
has special behavior if used in a script file, regardless of whether it is exported or not. See the Scripts chapter for more details.
Module Files
As mentioned briefly in the Overview above, modules can be created either as:
<module_name>.nu
: "File-form" - Useful for simple modules<module_name>/mod.nu
: "Directory-form" - Useful for organizing larger module projects where submodules can easily map to subdirectories of the main module
The increment.nu
example above is clearly an example of (1) the file-form. Let's try converting it to the directory-form:
mkdir increment
mv increment.nu increment/mod.nu
use increment *
41 | increment
# => 42
Notice that the behavior of the module once imported is identical regardless of whether the file-form or directory-form is used; only its path changes.
Note
Technically, you can import this either using the directory form above or explicitly with use increment/mod.nu *
, but the directory shorthand is preferred when using a mod.nu
.
Subcommands
As covered in Custom Commands, subcommands allow us to group commands logically. Using modules, this can be done in one of two ways:
- As with any custom command, the command can be defined as
"<command> <subcommand>"
, using a space inside quotes. Let's add anincrement by
subcommand to theincrement
module we defined above:
export def main []: int -> int {
$in + 1
}
export def "increment by" [amount: int]: int -> int {
$in + $amount
}
It can then be imported with use increment *
to load both the increment
command and increment by
subcommand.
- Alternatively, we can define the subcommand simply using the name
by
, since importing the entireincrement
module will result in the same commands:
export def main []: int -> int {
$in + 1
}
export def by [amount: int]: int -> int {
$in + $amount
}
This module is imported using use increment
(without the glob *
) and results in the same increment
command and increment by
subcommand.
Note
We'll continue to use this version for further examples below, so notice that the import pattern has changed to use increment
(rather than use increment *
) below.
Submodules
Submodules are modules that are exported from another module. There are two ways to add a submodule to a module:
- With
export module
: Exports (a) the submodule and (b) its definitions as members of the submodule - With
export use
: Exports (a) the submodule and (b) its definitions as members of the parent module
To demonstrate the difference, let's create a new my-utils
module, with our increment
example as a submodule. Additionally, we'll create a new range-into-list
command in its own submodule.
Create a directory for the new
my-utils
and move theincrement.nu
into itmkdir my-utils # Adjust the following as needed mv increment/mod.nu my-utils/increment.nu rm increment cd my-utils
In the
my-utils
directory, create arange-into-list.nu
file with the following:export def main []: range -> list { # It looks odd, yes, but the following is just # a simple way to convert ranges to lists each {||} }
Test it:
use range-into-list.nu 1..5 | range-into-list | describe # => list<int> (stream)
We should now have a
my-utils
directory with the:increment.nu
modulerange-into-list.nu
module
The following examples show how to create a module with submodules.
Example: Submodule with export module
The most common form for a submodule definition is with export module
.
Create a new module named
my-utils
. Since we're in themy-utils
directory, we will create amod.nu
to define it. This version ofmy-utils/mod.nu
will contain:export module ./increment.nu export module ./range-into-list.nu
We now have a module
my-init
with the two submodules. Try it out:# Go to the parent directory of my-utils cd .. use my-utils * 5 | increment by 4 # => 9 let file_indices = 0..2..<10 | range-into-list ls | select ...$file_indices # => Returns the 1st, 3rd, 5th, 7th, and 9th file in the directory
Before proceeding to the next section, run scope modules
and look for the my-utils
module. Notice that it has no commands of its own; just the two submodules.
Example: Submodule with export use
Alternatively, we can (re)export the definitions from other modules. This is slightly different from the first form, in that the commands (and other definitions, if they were present) from increment
and range-into-list
become members of the my-utils
module itself. We'll be able to see the difference in the output of the scope modules
command.
Let's change my-utils/mod.nu
to:
export use ./increment.nu
export use ./range-into-list.nu
Try it out using the same commands as above:
# Go to the parent directory of my-utils
cd ..
use my-utils *
5 | increment by 4
# => 9
let file_indices = 0..2..<10 | range-into-list
ls / | sort-by modified | select ...$file_indices
# => Returns the 1st, 3rd, 5th, 7th, and 9th file in the directory, oldest-to-newest
Run scope modules
again and notice that all of the commands from the submodules are re-exported into the my-utils
module.
Tips
While export module
is the recommended and most common form, there is one module-design scenario in which export use
is required -- export use
can be used to selectively export definitions from the submodule, something export module
cannot do. See Additional Examples - Selective Export for an example.
Note
module
without export
defines only a local module; it does not export a submodule.
Documenting Modules
As with custom commands, modules can include documentation that can be viewed with help <module_name>
. The documentation is simply a series of commented lines at the beginning of the module file. Let's document the my-utils
module:
# A collection of helpful utility functions
export use ./increment.nu
export use ./range-into-list.nu
Now examine the help:
use my-utils *
help my-utils
# => A collection of helpful utility functions
Also notice that, because the commands from increment
and range-into-list
are re-exported with export use ...
, those commands show up in the help for the main module as well.
Environment Variables
Modules can define an environment using export-env
. Let's extend our my-utils
module with an environment variable export for a common directory where we'll place our modules in the future. This directory is (by default) in the $env.NU_LIB_DIRS
search path discussed in Using Modules - Module Path.
# A collection of helpful utility functions
export use ./increment.nu
export use ./range-into-list.nu
export-env {
$env.NU_MODULES_DIR = ($nu.default-config-dir | path join "scripts")
}
When this module is imported with use
, the code inside the export-env
block is run and the its environment merged into the current scope:
use my-utils
$env.NU_MODULES_DIR
# => Returns the directory name
cd $env.NU_MODULES_DIR
Tips
As with any command defined without --env
, commands and other definitions in the module use their own scope for environment. This allows changes to be made internal to the module without them bleeding into the user's scope. Add the following to the bottom of my-utils/mod.nu
:
export def examine-config-dir [] {
# Changes the PWD environment variable
cd $nu.default-config-dir
ls
}
Running this command changes the directory locally in the module, but the changes are not propagated to the parent scope.
Caveats
export-env
runs only when the use
call is evaluated
Note
This scenario is commonly encountered when creating a module that uses std/log
.
Attempting to import a module's environment within another environment may not work as expected. Let's create a new module go.nu
that creates "shortcuts" to common directories. One of these will be the $env.NU_MODULES_DIR
defined above in my-utils
.
We might try:
# go.nu, in the parent directory of my-utils
use my-utils
export def --env home [] {
cd ~
}
export def --env modules [] {
cd $env.NU_MODULES_DIR
}
And then import it:
use go.nu
go home
# => Works
go modules
# => Error: $env.NU_MODULES_DIR is not found
This doesn't work because my-utils
isn't evaluated in this case; it is only parsed when the go.nu
module is imported. While this brings all of the other exports into scope, it does not run the export-env
block.
Important
As mentioned at the start of this chapter, trying this while my-utils
(and its $env.NU_MODULES_DIR
) is still in scope from a previous import will not fail as expected. Test in a new shell session to see the "normal" failure.
To bring my-utils
exported environment into scope for the go.nu
module, there are two options:
Import the module in each command where it is needed
By placing
use my-utils
in thego home
command itself, itsexport-env
will be evaludated when the command is. For example:# go.nu export def --env home [] { cd ~ } export def --env modules [] { use my-utils cd $env.NU_MODULES_DIR }
Import the
my-utils
environment inside anexport-env
block in thego.nu
moduleuse my-utils export-env { use my-utils [] } export def --env home [] { cd ~ } export def --env modules [] { cd $env.NU_MODULES_DIR }
In the example above,
go.nu
importsmy-utils
twice:- The first
use my-utils
imports the module and its definitions (except for the environment) into the module scope. - The second
use my-utils []
imports nothing but the environment intogo.nu
's exported environment block. Because theexport-env
ofgo.nu
is executed when the module is first imported, theuse my-utils []
is also evaluated.
- The first
Note that the first method keeps my-utils
environment inside the go.nu
module's scope. The second, on the other hand, re-exports my-utils
environment into the user scope.
Module files and commands cannot be named after parent module
A .nu
file cannot have the same name as its module directory (e.g., spam/spam.nu
) as this would create an ambiguous condition with the name being defined twice. This is similar to the situation described above where a command cannot have the same name as its parent.
Windows Path Syntax
Important
Nushell on Windows supports both forward-slashes and back-slashes as the path separator. However, to ensure that they work on all platforms, using only the forward-slash /
in your modules is highly recommended.
Additional Examples
Local Definitions
As mentioned above, definitions in a module without the export
keyword are only accessible in the module's scope.
To demonstrate, create a new module is-alphanumeric.nu
. Inside this module, we'll create a str is-alphanumeric
command. If any of the characters in the string are not alpha-numeric, it returns false
:
# is-alphanumeric.nu
def alpha-num-range [] {
[
...(seq char 'a' 'z')
...(seq char 'A' 'Z')
...(seq 0 9 | each { into string })
]
}
export def "str is-alphanumeric" []: string -> bool {
if ($in == '') {
false
} else {
let chars = (split chars)
$chars | all {|char| $char in (alpha-num-range)}
}
}
Notice that we have two definitions in this module -- alpha-num-range
and str is-alphanumeric
, but only the second is exported.
use is-alphanumeric.nu *
'Word' | str is-alphanumeric
# => true
'Some punctuation?!' | str is-alphanumeric
# => false
'a' in (alpha-num-range)
# => Error:
# => help: `alpha-num-range` is neither a Nushell built-in or a known external command
Selective Export from a Submodule
Note
While the following is a rare use-case, this technique is used by the Standard Library to make the dirs
commands and its aliases available separately.
As mentioned in the Submodules section above, only export use
can selectively export definitions from a submodule.
To demonstrate, let's add a modified form of the go.nu
module example above to my-utils
:
# go.nu, in the my-utils directory
export def --env home [] {
cd ~
}
export def --env modules [] {
cd ($nu.default-config-dir | path join "scripts")
}
export alias h = home
export alias m = modules
This go.nu
includes the following changes from the original:
- It doesn't rely on the
my-utils
mod since it will now be a submodule ofmy-utils
instead - It adds "shortcut" aliases:
h
: Goes to the home directory (alias ofgo home
)m
: Goes to the modules directory (alias ofgo modules
)
A user could import just the aliases with:
use my-utils/go.nu [h, m]
However, let's say we want to have go.nu
be a submodule of my-utils
. When a user imports my-utils
, they should only get the commands, but not the aliases. Edit my-utils/mod.nu
and add:
export use ./go.nu [home, modules]
That almost works -- It selectively exports home
and modules
, but not the aliases. However, it does so without the go
prefix. For example:
use my-utils *
home
# => works
go home
# => Error: command not found
To export them as go home
and go modules
, make the following change to my-utils/mod.nu
:
# Replace the existing `export use` with ...
export module go {
export use ./go.nu [home, modules]
}
This creates a new, exported submodule go
in my-utils
with the selectively (re)exported definitions for go home
and go modules
.
use my-utils *
# => As expected:
go home
# => works
home
# => Error: command not found