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.rsh
file (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 hello
call here first implicitly creates thegreetings
module, then takeshello
from it. You could also write it asmodule greetings.rsh
,use greetings hello
. Usingmodule
can 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
module
withoutexport
defines 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
spam
directory containing bothspam.rsh
andmod.rsh
, callinguse spam *
would create an ambiguous situation where thespam
module would be defined twice.
-
Consider a
-
Module cannot contain file named after the module
-
Same case as above: Module
spam
containing bothmain
andspam
commands 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.rsh
for ittouch ($rsh.default-config-dir | path join completions mod.rsh)
-
Put the following snippet in
git.rsh
under 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.rsh
use 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
git
fromgit.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.rsh
will 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 push
to 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
hide
is not a supported keyword at the root of the module (unlikedef
etc.)