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
exportstatements of definitions to be exported from the module. - For submodules inside a module, using the
modulecommand
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>.nufile, 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
# => 6Of 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
# => 2025Note
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
maindefinition 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
# => 42Notice 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 bysubcommand to theincrementmodule 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 entireincrementmodule 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-utilsand move theincrement.nuinto itmkdir my-utils # Adjust the following as needed mv increment/mod.nu my-utils/increment.nu rm increment cd my-utilsIn the
my-utilsdirectory, create arange-into-list.nufile 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-utilsdirectory with the:increment.numodulerange-into-list.numodule
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-utilsdirectory, we will create amod.nuto define it. This version ofmy-utils/mod.nuwill contain:export module ./increment.nu export module ./range-into-list.nuWe now have a module
my-utilswith 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.nuTry 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-newestRun 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.nuNow examine the help:
use my-utils *
help my-utils
# => A collection of helpful utility functionsAlso 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_DIRTips
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 foundThis 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-utilsin thego homecommand itself, itsexport-envwill be evaluated 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-utilsenvironment inside anexport-envblock in thego.numoduleuse 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.nuimportsmy-utilstwice:- The first
use my-utilsimports 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-envofgo.nuis 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 commandSelective 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 = modulesThis go.nu includes the following changes from the original:
- It doesn't rely on the
my-utilsmod since it will now be a submodule ofmy-utilsinstead - 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 foundTo 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