Modules
Similar to many other programming languages, rsh also has modules to organize your code. Each module is a "bag" containing a bunch of definitions (typically commands) that you can export (take out or the bag) and use in your current scope. Since rsh is also a shell, modules allow you to modify environment variables when importing them.
Quick Overview
There are three ways to define a module in rsh:
-
"inline"
module spam { ... }
-
from a file
- using a .rsh file as a module
-
from a directory
-
directory must contain
mod.rshfile (can be empty)
-
directory must contain
In rsh, creating a module and importing definitions from a
module are two different actions. The former is done using the
module keyword. The latter using the
use keyword. You can think of
module as "wrapping definitions into a
bag" and use as "opening a bag and taking
definitions from it". In most cases, calling
use will create the module implicitly, so you
typically don't need to use module that much.
You can define the following things inside a module:
- Commands* (
def) - Aliases (
alias) - Known externals* (
extern) - Submodules (
module) - Imported symbols from other modules (
use) - Environment setup (
export-env)
Only definitions marked with export are possible to
access from outside of the module ("take out of the
bag"). Definitions not marked with export are
allowed but are visible only inside the module (you could call
them private). (export-env is special and does not require
export.)
*These definitions can also be defined as
main (see below).
"Inline" modules
The simplest (and probably least useful) way to define a module is an "inline" module can be defined like this:
module greetings {
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
}
use greetings hello
hello "world"
You can paste the code into a file and run it with
rsh, or type into the REPL.
First, we create a module (put hello and
hi commands into a "bag" called
greetings), then we import the
hello command from the module (find a
"bag" called greetings and take
hello command from it) with use.
Modules from files
A .rsh file can be a module. Just take the contents of the
module block from the example above and save it to a file
greetings.rsh. The module name is automatically
inferred as the stem of the file ("greetings").
# greetings.rsh
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
then
> use greetings.rsh hello
> hello
The result should be similar as in the previous section.
Note that the
use greetings.rsh hellocall here first implicitly creates thegreetingsmodule, then takeshellofrom it. You could also write it asmodule greetings.rsh,use greetings hello. Usingmodulecan be useful if you're not interested in any definitions from the module but want to, e.g., re-export the module (export module greetings.rsh).
Modules from directories
Finally, a directory can be imported as a module. The only
condition is that it needs to contain a
mod.rsh file (even empty). The
mod.rsh file defines the root module. All .rsh
files inside the module directory are added as submodules (more
about submodules later). We could write our
greetings module like this:
In the following examples, / is used at the end
to denote that we're importing a directory but it is not
required.
# greetings/mod.rsh
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
then
> use greetings/ hello
> hello
The name of the module follows the same rule as module created
from a file: Stem of the directory name, i.e., the directory
name, is used as the module name. Again, you could do this as a
two-step action using module and
use separately, as explained in the previous
section.
TIP
You can define main command inside
mod.rsh to create a command named after the
module directory.
Import Pattern
Anything after the
use
keyword forms an import pattern which controls
how the definitions are imported. The import pattern has the
following structure use head members... where
head defines the module (name of an existing
module, file, or directory). The members are optional and
specify what exactly should be imported from the module.
Using our greetings example:
use greetings
imports all symbols prefixed with the
greetings namespace (can call
greetings hello and greetings hi).
use greetings hello
will import the hello command directly without any
prefix.
use greetings [hello, hi]
imports multiple definitions<> directly without any prefix.
use greetings *
will import all names directly without any prefix.
main
Exporting a command called main from a module
defines a command named as the module. Let's extend our
greetings example:
# greetings.rsh
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
export def main [] {
"greetings and salutations!"
}
then
> use greetings.rsh
> greetings
greetings and salutations!
> greetings hello world
hello world!
The main is exported only when
-
no import pattern members are used (
use greetings.rsh) - glob member is used (
use greetings.rsh *)
Importing definitions selectively (use greetings.rsh hello
or use greetings.rsh [hello hi]) does not define
the greetings command from main. You
can, however, selectively import main using
use greetings main (or [main]) which
defines only the greetings command without
pulling in hello or hi.
Apart from commands (def, def --env),
known externals (extern) can also be named
main.
Submodules and subcommands
Submodules are modules inside modules. They are automatically
created when you call use on a directory: Any .rsh
files inside the directory are implicitly added as submodules of
the main module. There are two more ways to add a submodule to a
module:
- Using
export module - Using
export use
The difference is that export module some-module
only adds the module as a submodule, while
export use some-module also re-exports the
submodule's definitions. Since definitions of submodules are
available when importing from a module,
export use some-module is typically redundant,
unless you want to re-export its definitions without the
namespace prefix.
Note
modulewithoutexportdefines only a local module, it does not export a submodule.
Let's illustrate this with an example. Assume three files:
# greetings.rsh
export def hello [name: string] {
$"hello ($name)!"
}
export def hi [where: string] {
$"hi ($where)!"
}
export def main [] {
"greetings and salutations!"
}
# animals.rsh
export def dog [] {
"haf"
}
export def cat [] {
"meow"
}
# voice.rsh
export use greetings.rsh *
export module animals.rsh
Then:
> use voice.rsh
> voice animals dog
haf
> voice animals cat
meow
> voice hello world
hello world
> voice hi there
hi there!
> voice greetings
greetings and salutations!
As you can see, defining the submodule structure also shapes the command line API. In rsh, namespaces directly folds into subcommands. This is true for all definitions: aliases, commands, known externals, modules.
Environment Variables
Modules can also define an environment using
export-env:
# greetings.rsh
export-env {
$env.MYNAME = "Arthur, King of the Britons"
}
export def hello [] {
$"hello ($env.MYNAME)"
}
When
use
is evaluated, it will run the code inside the
export-env
block and merge its environment into the current scope:
> use greetings.rsh
> $env.MYNAME
Arthur, King of the Britons
> greetings hello
hello Arthur, King of the Britons!
TIP
You can put a complex code defining your environment without polluting the namespace of the module, for example:
def tmp [] { "tmp" }
def other [] { "other" }
let len = (tmp | str length)
load-env {
OTHER_ENV: (other)
TMP_LEN: $len
}
}
Only $env.TMP_LEN and
$env.OTHER_ENV are preserved after evaluating the
export-env module.
Caveats
Like any programming language, rsh is also a product of a tradeoff and there are limitations to our module system.
export-env runs only when the use call
is evaluated
If you also want to keep your variables in separate modules and
export their environment, you could try to
export use
it:
# purpose.rsh
export-env {
$env.MYPURPOSE = "to build an empire."
}
export def greeting_purpose [] {
$"Hello ($env.MYNAME). My purpose is ($env.MYPURPOSE)"
}
and then use it
> use purpose.rsh
> purpose greeting_purpose
However, this won't work, because the code inside the module
is not evaluated, only parsed (only the
export-env block is evaluated when you call
use purpose.rsh). To export the environment of
greetings.rsh, you need to add it to the
export-env module:
# purpose.rsh
export-env {
use greetings.rsh
$env.MYPURPOSE = "to build an empire."
}
export def greeting_purpose [] {
$"Hello ($env.MYNAME). My purpose is ($env.MYPURPOSE)"
}
then
> use purpose.rsh
> purpose greeting_purpose
Hello Arthur, King of the Britons. My purpose is to build an empire.
Calling use purpose.rsh ran the
export-env block inside
purpose.rsh which in turn ran
use greetings.rsh which in turn ran the
export-env block inside greetings.rsh,
preserving the environment changes.
Module file / command cannot be named after parent module
-
Module directory cannot contain .rsh file named after the
directory (
spam/spam.rsh)-
Consider a
spamdirectory containing bothspam.rshandmod.rsh, callinguse spam *would create an ambiguous situation where thespammodule would be defined twice.
-
Consider a
-
Module cannot contain file named after the module
-
Same case as above: Module
spamcontaining bothmainandspamcommands would create an ambiguous situation when exported asuse spam *.
-
Same case as above: Module
Examples
This section describes some useful patterns using modules.
Local Definitions
Anything defined in a module without the
export
keyword will work only in the module's scope.
# greetings.rsh
use tools/utils.rsh generate-prefix # visible only locally (we assume the file exists)
export def hello [name: string] {
greetings-helper "hello" "world"
}
export def hi [where: string] {
greetings-helper "hi" "there"
}
def greetings-helper [greeting: string, subject: string] {
$"(generate-prefix)($greeting) ($subject)!"
}
then
> use greetings.rsh *
> hello "world"
hello world!
> hi "there"
hi there!
> greetings-helper "foo" "bar" # fails because 'greetings-helper' is not exported
> generate-prefix # fails because the command is imported only locally inside the module
Directory structure as subcommad tree
Since directories can be imported as submodules and submodules can naturally form subcommands it is easy to build even complex command line applications with a simple file structure.
Warning Work In Progress
Dumping files into directory
A common pattern in traditional shells is dumping and auto-sourcing files from a directory (for example, loading custom completions). In rsh, similar effect can be achieved via module directories.
Here we'll create a simple completion module with a submodule dedicated to some Git completions:
-
Create the completion directory
mkdir ($rsh.default-config-dir | path join completions) -
Create an empty
mod.rshfor ittouch ($rsh.default-config-dir | path join completions mod.rsh) -
Put the following snippet in
git.rshunder the completion directory
export extern main [
--version(-v)
-C: string
# ... etc.
]
export extern add [
--verbose(-v)
--dry-run(-n)
# ... etc.
]
export extern checkout [
branch: string@complete-git-branch
]
def complete-git-branch [] {
# ... code to list git branches
}
-
Add the parent of the completion directory to your
rsh_LIB_DIRS inside
env.rsh
$env.rsh_LIB_DIRS = [
...
$rsh.default-config-dir
]
-
import the completions to rsh in your
config.rshuse completions *Now you've set up a directory where you can put your completion files and you should have some Git completions the next time you start rsh
Note This will use the file name (in our example
gitfromgit.rsh) as the modules name. This means some completions might not work if the definition has the base command in it's name. For example a completion like thisexport extern 'git push' []in ourgit.rshwill result in a completion like thisgit git push. If you have this style of completion you must instead import like thisuse completions git *which will import the exported definitions of the submoudulegit. Or simply renamegit pushto justpush.
Setting environment + aliases (conda style)
def --env commands, export-env block
and aliases can be used to dynamically manipulate "virtual
environments" (a concept well known from Python).
We use it in our official virtualenv integration https://github.com/pypa/virtualenv/blob/main/src/virtualenv/activation/rsh/activate.rsh
Another example could be our unofficial Conda module: https://github.com/radhesh1/rsh_scripts/blob/f86a060c10f132407694e9ba0f536bfe3ee51efc/modules/virtual_environments/conda.rsh
Warning Work In Progress
Hiding
Any custom command or alias, imported from a module or not, can
be "hidden", restoring the previous definition. We do
this with the
hide
command:
> def foo [] { "foo" }
> foo
foo
> hide foo
> foo # error! command not found!
The
hide
command also accepts import patterns, just like
use. The import pattern is interpreted slightly differently,
though. It can be one of the following:
hide foo or hide greetings
- If the name is a custom command or an environment variable, hides it directly. Otherwise:
- If the name is a module name, hides all of its exports prefixed with the module name
hide greetings hello
- Hides only the prefixed command / environment variable
hide greetings [hello, hi]
- Hides only the prefixed commands / environment variables
hide greetings *
- Hides all of the module's exports, without the prefix
Note
hideis not a supported keyword at the root of the module (unlikedefetc.)