Extensions¶
An extension is code written in Rhai that runs inside the shell. It can register hooks (e.g., run when the directory changes, or before each prompt), Tab completion for commands, and commands of its own. As it runs in the shell’s own process, it can reading files and other data without starting a process.
Writing an extension¶
Here is a simple extension that adds a builtin called hello:
// ~/.config/luish/plugins/hello.rhai
sh::builtin("hello", |argv| {
let who = if argv.len() > 1 { argv[1] } else { "world" };
print(`Hello, ${who}!`);
});
$ plugin load hello
$ hello there
Hello, there!
$ hello
Hello, world!
The top level of the extension runs once, when its plugin is loaded (in each shell that loads it: extensions are not
cached with the startup files). It registers functions with the sh module; they stay registered
until the plugin is unloaded (plugin unload) or loaded again. Loading the first extension costs about a millisecond,
once (see Commands in Rhai).
Strings, arrays and maps have no size limit. If a call into an extension raises the shell’s peak memory use by more than 256 MiB, luish warns, naming the plugin; deep recursion is an error.
sh::plugin_dir() gives the plugin’s directory at any time, also in hooks. For a single-file plugin it is the
directory of the file. sh::plugin_options() gives the options the plugin was loaded with (see Plugin
options); a plugin loaded with -c has none.
Code without a file: plugin load -c¶
plugin load -c CODE NAME loads Rhai code given on the command line as the plugin NAME, as if it were the file
NAME.rhai:
$ plugin load -c 'sh::hook("chpwd", |from, to| print(`now in ${to}`))' where
$ cd /tmp
now in /tmp
Its directory, for import and sh::plugin_dir(), is the current directory when it is loaded. Loading it again
with -c replaces it, and plugin unload NAME removes it. It doesn’t replace a plugin of the same name loaded from a
file (unload that one first), but a plugin loaded from a file replaces it, with a warning.
Running Rhai code once: plugin run¶
To run Rhai code without loading a plugin, as a script or to try something out, use plugin run with a file or
with -c and the code:
$ plugin run -c 'print(sh::which("git"))'
/usr/bin/git
$ cat count.rhai
print(`${argv.len() - 1} arguments`);
argv.len() > 1
$ plugin run ./count.rhai a b; echo $?
2 arguments
0
The code has the sh, fs and vcs modules, as an extension
does, and argv: the file (or -c) and the arguments. Its last value (or return’s) is the exit status if it is an
integer or a boolean, as for a command. As it isn’t part of a plugin, it can’t
register hooks, completers or commands (use plugin load -c for that), and sh::plugin_dir() and
sh::plugin_options() are errors; it leaves
the loaded plugins alone, even one with the same name as the file. import is relative to the file, or with -c, to
the current directory, and its modules are read again on each run.
Rhai’s eval, which compiles and runs a string as code, is disabled in
extensions. If you need very flexible code, use a shell file instead.
Hooks¶
sh::hook(kind, fn) registers a function to be called on an event: chpwd, precmd, preexec, exit, post-rc,
prompt-vars or prompt-rewrite. These hooks are luish’s way of running code on such events: unlike zsh, luish
doesn’t call shell functions with special names (chpwd, precmd, zshexit …). A hook can call a shell function
with sh::run.
Running code when the directory changes: chpwd¶
// ~/.config/luish/plugins/dirs.rhai
// Named functions can't see the extension's variables, but closures can.
let visits = 0;
sh::hook("chpwd", |from, to| {
visits += 1;
// Show what is in a project directory when entering it.
if sh::run("test -f Makefile") == 0 {
sh::run("ls");
}
sh::setvar("LAST_DIR", from);
});
chpwd hooks are called after each successful cd, pushd or popd, with the old and the new directory. $? is the same after
the hooks as before them. If a hook fails, the error is printed and the other hooks still run. A chpwd hook that
itself runs cd does not trigger chpwd again.
Before each prompt and each command: precmd and preexec¶
// ~/.config/luish/plugins/timing.rhai
// Report the commands that took more than 10 seconds.
let started = ();
sh::hook("preexec", |line| { started = timestamp(); });
sh::hook("precmd", |status| {
if started != () && started.elapsed > 10.0 {
sh::write(2, "took " + started.elapsed.to_int() + "s (status " + status + ")\n");
}
started = ();
});
In interactive shells, precmd hooks run before each prompt (but not before PS2), before the prompt is built (so
before prompt-vars), with the exit status of the last command as an integer. preexec hooks run after a command
is read and added to the history, before it runs, with its text as typed (before alias expansion, without the final
newline, and with all the lines of a command that spans several). A line with several commands (a; b) gives one
preexec call, with the whole line.
$? is the same after the hooks as before them. A hook that fails is reported, and the other hooks still run. A hook
that runs exit (through sh::run) exits the shell. Neither hook runs in scripts or in -c commands, even with
-i.
When the shell exits: exit¶
// Remove this shell's scratch directory, made when the extension was loaded.
let scratch = sh::capture(["mktemp", "-d"]).out;
sh::setvar("SCRATCH", scratch);
sh::hook("exit", |status| { sh::capture(["rm", "-rf", scratch]); });
exit hooks run when the shell exits, with exit or at the end of its input,
after the EXIT trap, with the exit status as an integer (also $?). They run
in scripts too (check sh::interactive() if that isn’t wanted), but not when a
subshell exits, nor when the shell is replaced by exec or killed by a signal.
A shell with exit hooks doesn’t replace itself with the last command of a
script or of -c, as it otherwise does. A hook that runs exit N changes the
status the shell exits with, and the hooks after it don’t run.
After the startup files: post-rc¶
post-rc hooks run after the startup files in rc.d, after the post-rc.lsh files of the plugins (see
After the startup files for the order). So an extension can read the
variables that you set in rc.d to configure it, even though its plugin was loaded before rc.d:
// extension.rhai
sh::hook("post-rc", || {
let style = sh::getvar("MYPROMPT_STYLE") ?? "plain";
// ...
});
As post-rc.lsh, they run only in interactive shells, but unlike what post-rc.lsh does, which is cached with
rc.d, they run in every shell. A plugin loaded later (in luishrc, or at the prompt) runs its post-rc hooks right
after it is loaded.
Variables for the prompt: prompt-vars¶
A prompt-vars hook returns a map of variables, which PS1 can then use:
// ~/.config/luish/plugins/branch.rhai
sh::hook("prompt-vars", || {
let i = vcs::info();
#{
git_branch: if i == () { "" } else { ` (${i.branch ?? "detached"})` },
last_failed: sh::last_status() != 0,
}
});
# ~/.config/luish/luishrc
plugin load branch
PS1='$PWD$git_branch\$ '
Before each prompt, the prompt-vars hooks run with the prompt-vars.lsh
files of the plugins, plugin by plugin in the order they were loaded, and PS1
is expanded with their variables, which are then put back as they were (see
Customizing the prompt).
A hook’s map gives each variable a string, a number or a boolean (which become
text, such as 42 or true), or () to unset the variable while the prompt
is built. A hook can also return () to set nothing. A bad entry (not a valid
name, a readonly variable, another type of value) is reported and skipped, and
a hook that fails is reported; the other hooks and files still run.
Rewriting the prompt: prompt-rewrite¶
Warning
prompt-rewrite is an advanced feature, and easy to get wrong: the hooks run in a fixed order, each one can replace
what the others did, and none of it goes through PS1 unless a hook asks for it. For most prompts, set PS1 and
give it variables with prompt-vars instead.
A prompt-rewrite hook returns the whole prompt, which is used instead of PS1:
// ~/.config/luish/plugins/prompt.rhai
sh::hook("prompt-rewrite", || {
let status = sh::last_status();
let mark = if status == 0 { "" } else { "%F{red}[" + status + "]%f " };
let branch = vcs::info()?.branch;
let branch = if branch == () { "" } else { " %F{yellow}(" + branch + ")%f" };
"%F{blue}%~%f" + branch + " " + mark + "%# "
});
The order of operations matters:
The
prompt-varshooks and files run first (see above), so aprompt-rewritehook sees their variables, withsh::getvar.The
prompt-rewritehooks run, from the one registered last, until one returns a string: that is the prompt. A hook that returns()leaves the prompt to the hooks registered before it, and then toPS1, so an extension can give the prompt only in some directories, for example. A hook that fails (or returns something other than a string or()) is reported, and the next one is tried.The prompt a hook returns doesn’t go through parameter expansion, so
$xin it stays as it is. It does go through%expansion if theprompt.percentoption is on (setopt prompt.percent, see Usage), as in the example.The variables set by
prompt-varsare put back. What aprompt-rewritehook itself changes, withsh::setvarorsh::run, stays.
$? is the same after the hooks as before them, and each hook sees that of the
last command (but sh::run changes it for the rest of the hook, so read
sh::last_status() first).
A hook that takes a parameter is given the previous prompt: the one the hooks registered before it give, or else
PS1 (after parameter expansion, with the prompt-vars variables, but before % expansion, which is done on the
prompt the hook returns). So an extension can add to the prompt of another one, or to PS1, instead of replacing it:
// ~/.config/luish/plugins/status.rhai
sh::hook("prompt-rewrite", |prev| {
let status = sh::last_status();
if status == 0 { prev } else { `%F{red}[${status}]%f ${prev}` }
});
Completing a command’s arguments¶
Extensions can register completers for specific commands. A completer is called
when Tab is pressed on an argument of its command (also after sudo, env and
the like, and through an alias: with alias g='git -C ~/src', g calls the
completer for git with the words git, -C and ~/src first).
A completer receives the words of the command, unquoted, starting with the command name, and the index of the word being completed. That word ends at the cursor, and may be empty; the words after it (if the cursor is not at the end of the command) come after it in the array.
A completer should return an array of candidates or ().
A candidate is a string, or a map with a value and optionally a desc, shown
next to it in the completion menu, and a suffix, added after the value when
it is the only match (a space by default; "" for none).
luish keeps the candidates that start with the word typed, and quotes what it adds. So a completer can simply return everything that could come next.
To complete only the end of the word, such as what follows = in --format=,
a completer returns a map with the candidates and the start of the word they
leave alone, which must be a prefix of the word:
if words[i].starts_with("--format=") {
return #{prefix: "--format=", candidates: ["json", "yaml"]};
}
A completer can also hand the line over to the completer of another command,
for a command that runs another (git annex runs git-annex; uv run CMD) or
that is better completed as another: it returns a map with delegate, the
words of the line to complete, starting with that command’s name, and index,
the word being completed in them, which must be the same word. That command’s
completer (or the -default- one, or else luish’s filenames) answers instead,
within the same time limit. With dir, a directory (relative to the current
one, and taken as it is: no ~), it runs there, as for git -C DIR; its ()
then gives the files of that directory. Handing the line back to a completer
already in the chain is an error.
// `git annex ...` is completed as `git-annex ...`.
if words[1] == "annex" {
return #{delegate: ["git-annex"] + words.extract(2), index: i - 1};
}
The completer registered for -default- (as in zsh’s compdef) is called for
the commands that have no completer of their own and whose arguments luish
doesn’t complete itself.
To see what Tab offers for a command line without typing it, for example while
writing a completer or in a test, use __luish_internal complete LINE, which
prints each match on a line of its own (the text that replaces the word, then a
tab and the description), in any shell:
$ __luish_internal complete 'git sw'
switch Switch branches
__luish_internal list-completions lists the commands that have completers,
each with the plugin that provides it.
Commands that a completer runs are not jobs: like those of $(...), they can’t
be stopped with Ctrl-Z, and Ctrl-C does not reach them (the terminal is in the
line editor’s mode). Their output goes to the terminal, over the command
line, so use sh::capture or redirect it.
// ~/.config/luish/plugins/git.rhai
sh::completer("git", |words, i| {
if i == 1 {
return [
#{value: "add", desc: "Add file contents to the index"},
#{value: "commit", desc: "Record changes to the repository"},
#{value: "switch", desc: "Switch branches"},
#{value: "--git-dir=", suffix: ""},
];
}
if words[1] == "switch" {
let r = sh::capture(["git", "branch", "--format=%(refname:short)"]);
return if r.status == 0 { r.out.split("\n") } else { [] };
}
() // the default: filenames
});
Commands written in Rhai: sh::builtin¶
sh::builtin(name, fn) adds a command that functions like a new shell builtin.
It is called with the command’s words as an array of strings, its name first
(as argv[0], so the arguments are argv[1], argv[2], …)
// ~/.config/luish/plugins/urlencode.rhai
// urlencode TEXT...: each TEXT percent-encoded, one per line.
sh::builtin("urlencode", |argv| {
if argv.len() < 2 { throw "usage: urlencode TEXT..."; }
for text in argv.extract(1) {
let out = "";
for c in text.chars() {
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "._~-".contains(c) {
out += c;
} else {
let h = c.to_int().to_hex().to_upper();
out += if h.len() < 2 { `%0${h}` } else { `%${h}` };
}
}
print(out);
}
});
Exit status: what the function returns:
()(nothing) is 0,true0 andfalse1, and an integer is taken modulo 256, asreturndoes. An error gives 1: a string thrown withthrowis printed as the command’s error message (urlencode: usage: urlencode TEXT...), and other errors with the extension’s file and position.Output goes to standard output with
print, orsh::write.Input:
sh::read_line()reads a line of standard input, without its newline, or gives()at the end. Like thereadbuilt-in, it reads a byte at a time, so the rest is left for the commands after it. To read a whole file,fs::read_fileis faster.Lookup: it ranks as a regular built-in, so a shell function with the same name comes first, and
builtin NAMEorcommand NAMEstill run it.typeshows it as a shell builtin from its plugin. The names of luish’s own built-ins can’t be taken. Registering a name again replaces the command, and it goes away when its plugin is unloaded.Ctrl-C stops it with status 130, as it stops any extension code.
Loops, arithmetic and string processing run two to three times as fast in rhai as in shell code (see Performance).
Splitting an extension into modules: import¶
An extension can use Rhai modules, other .rhai files of the plugin: import "NAME" as m; reads NAME.rhai, runs its top level once, and makes its
functions available as m::f(). NAME is relative to the directory of the
file that has the import, so extension.rhai finds its modules in the
plugin’s directory, and a module in a
subdirectory (import "hts/samtools") finds its own neighbours there (import "common" in hts/samtools.rhai reads hts/common.rhai). This holds wherever
the code runs: in a function or closure of the module, even when an extension
calls it. NAME can also be an absolute path, without .rhai.
A plugin can use the modules of another plugin: import "@SOURCE/PLUGIN/MODULE" reads MODULE.rhai in the
directory of the plugin SOURCE/PLUGIN, such as import "@std/completion/lib" for std’s completion engine (see
Reusing std’s completion engine). PLUGIN is the plugin’s path in its collection
(@extra/completion/bio/kinds is kinds.rhai in extra/completion/bio). That plugin must be loaded, so list it in the
[dependencies] of your plugin.toml (here
std.completion = "*"), which loads it first. It is found by its name (see Names), or, for a plugin
whose name has no source (one of the plugin directory, or loaded by path), by PLUGIN alone. The module’s own imports
are relative to its files, as above. Within your own source, prefer relative imports (import "../lib/util"): the
user names the source.
luish keeps each module once it is compiled, until a plugin is loaded again. An import inside a function (a
completer, for instance) reads the module only when the function first runs, so a large plugin loads quickly.
The sh module¶
Extensions reach the shell through the sh module:
Function |
Description |
|---|---|
|
Register a hook: |
|
Register a completer for a command’s arguments ( |
|
Register a command, called with its words (the name first); see Commands written in Rhai |
|
A line of standard input without its newline, or |
|
The variable’s value ( |
|
The variable’s elements ( |
|
An associative array as a map, or |
|
Set a shell variable to a string, to an array of strings (as |
|
Export or unset a variable |
|
The current directory (as |
|
The plugin’s directory |
|
The plugin’s options, as a map of strings, integers and booleans (see Plugin options) |
|
|
|
Whether the shell is interactive |
|
The file that running |
|
The names of the executables in |
|
Run shell code in the current shell, as |
|
Run a program, |
|
What |
|
A copy of the value that |
|
Keeps |
|
Run shell code in a subshell, as |
|
|
|
Write text, unbuffered, to fd 1 or 2 |
|
Whether |
|
The paths that the shell pattern |
|
|
Rhai’s print(text) and debug(text) write a line to standard output and standard error.
The fs module¶
The fs module answers common questions about files without starting a process, which matters for code that runs
at every prompt or directory change. Relative paths are relative to the current directory. A question about a file
that doesn’t exist gives false or () (use ?? for a default), not an error.
Function |
Description |
|---|---|
|
Whether the file exists (following symbolic links, as |
|
Whether it is a regular file or a directory (following symbolic links) |
|
Whether it is a symbolic link (even one that points nowhere) |
|
|
|
Access checks, as |
|
The size in bytes, or |
|
The modification time, in seconds since 1970, or |
|
Whether |
|
The file’s contents, or |
|
The names in a directory, sorted, without |
|
Where a symbolic link points, or |
|
The absolute path of the file, with every symbolic link (in the file’s name and in its directories), |
|
The path of the nearest |
// Rebuild a cache only when its source changed.
if fs::newer("aliases.txt", `${sh::getvar("HOME")}/.cache/aliases`) {
sh::run("make-alias-cache");
}
Integers in Rhai are 64-bit, as in shell arithmetic. Floating-point numbers are available too, for example to time
things with timestamp() and .elapsed.
The vcs module¶
The vcs module tells an extension about the version-control repository around
a directory, as zsh’s vcs_info does. Only git is supported for now.
vcs::info() (or vcs::info(dir)) finds the repository from the current
directory (or dir) by reading the repository’s files, without running git, so
it is cheap enough to call often. It returns () outside a repository, and
otherwise a map:
Field |
Description |
|
|---|---|---|
|
|
|
|
The top directory of the work tree |
|
|
The last component of |
|
|
Where the directory is under |
|
|
The git directory (for a worktree or a submodule, the one its |
|
|
The current branch (while rebasing, the branch being rebased), or |
|
|
The commit |
|
|
The operation in progress, or |
|
|
For a rebase or |
|
|
The number of stash entries |
vcs::status() (or vcs::status(dir)) runs git status, for what needs the index and the work tree, so it costs
more (as check-for-changes does in vcs_info). It returns () outside a repository or if git fails, and otherwise
a map:
Field |
Description |
|---|---|
|
The number of files with changes in the index ( |
|
The number of files with changes in the work tree that are not in the index ( |
|
The number of untracked files |
|
The number of files with merge conflicts |
|
Whether all of the above are 0 |
|
The upstream branch (such as |
|
The number of commits the branch is ahead of and behind its upstream |
git runs with --no-optional-locks, so that a prompt doesn’t get in the way of git commands running at the same
time.
Example: the branch in the prompt¶
// ~/.config/luish/plugins/vcs.rhai
sh::hook("prompt-vars", || {
let i = vcs::info();
if i == () {
return #{branch_info: ""}; // outside a repository
}
let branch = i.branch ?? i.head?.sub_string(0, 7) ?? "?";
let action = if i.action == () { "" } else { `|${i.action}` };
let dirty = if vcs::status()?.clean ?? true { "" } else { "*" };
#{branch_info: `(${i.vcs})-[${branch}${action}${dirty}] `}
});
PS1='$branch_info$PWD\$ '
vcs::info is cheap enough to call before every prompt; vcs::status runs git, which can be slow in a large
repository.
Text and bytes¶
Shell data (variables, paths) are bytes, while Rhai strings hold UTF-8 text. Text that is valid UTF-8 is passed unchanged. Each byte that is not part of valid UTF-8 becomes one of the characters U+10FF80 to U+10FFFF, and turns back into that byte when the string goes back to the shell. So a directory name that isn’t valid UTF-8 survives a round trip through an extension. A string containing a NUL character can’t be stored in a shell variable.
Reusing std’s completion engine¶
std.completion completes its commands from specs, maps that describe a
command’s options, their values, its subcommands and its arguments, with an
engine that other plugins can use for their own commands: lib::complete(spec, words, i), from import "@std/completion/lib", completes the word words[i]
of the command words, as a completer does.
The spec format, the kinds and lib::complete are part of luish’s interface:
std is tied to the version of luish, and later versions keep accepting the
specs of earlier ones.
Specs¶
A spec has these fields, all optional:
Field |
Meaning |
|---|---|
|
The options, as a table in the style of |
|
What the values of options complete to, by one of the option’s names: a kind or an array of candidates. By default, filenames. |
|
The kinds of the arguments that aren’t options, in order; the last one repeats. By default, |
|
The |
|
The subcommands ( |
|
The specs of the subcommands, by their first name (by default, a spec with no options and files as arguments). |
|
For the subcommands not in |
|
Options that are also valid after the subcommand, in any subcommand (and in theirs), as a table like |
|
Specs by the name of an option that chooses an operation ( |
|
True for a command whose long options start with a single |
|
True for a command whose long options take their value after |
|
True to complete the values of the options that have none in |
|
Prefixes of words that are neither options nor arguments, such as |
The values of a command are also those of its subcommands and modes, unless they have their own.
An options table has one line per option: its names (-a, --all), then at least two spaces and its description.
--name=ARG, --name ARG, --name <ARG> or -n ARG means that the option takes a value (with all its names), and
--name[=ARG] that its long name can take one after =:
let opts = `
-a, --all do not ignore entries starting with .
--color[=WHEN] colorize the output
-I, --ignore=PATTERN do not list entries matching PATTERN
-w, --width COLS set the output width
`;
Kinds¶
A kind says what a value or an argument completes to:
Kind |
Candidates |
|---|---|
|
filenames (luish’s own) |
|
directories |
|
nothing: a value to type, such as a number |
|
the users and groups in |
|
|
|
file modes (chmod) |
|
signal names |
|
the names and the IDs of the running processes |
|
ssh’s hosts ( |
|
files, or |
|
manual pages (in the section given before, if any) and sections |
|
make’s targets, from the makefile |
|
the files in the archive of |
|
dd’s operands ( |
|
the commands in |
|
|
An array is its own candidates.
For a kind of files by name, std’s kinds module (import "@std/completion/kinds" as kinds;) has
kinds::files_matching(cur, pattern): the files of the directory of cur that the shell pattern pattern matches,
and the directories, as the files kind gives them (files that start with . only after a .).
kinds::ending_with(suffixes) makes such a pattern from an array of suffixes:
kinds::files_matching(cur, kinds::ending_with([".fa", ".fasta"])).
They list the directory with sh::glob (above).
A plugin’s own kinds and subcommand specs are functions of its modules, named @SOURCE/PLUGIN/MODULE:NAME (or by
the module’s absolute path, without .rhai: sh::plugin_dir() + "/kinds:NAME", which doesn’t depend on the name
the user gives the source; a
MODULE:NAME without @ is one of std’s own modules). MODULE::kind(NAME, cur, words) completes the word cur (the
value, without an option before it) of the command line words, which starts with the command even in a
subcommand, and returns what a completer returns: candidates, a map with a prefix, or () for filenames.
MODULE::sub_spec(NAME, SUBCOMMAND) returns the spec of a subcommand, or (). A module can use its own functions
and import its neighbours in these functions, as anywhere else.
Example¶
A plugin bio, in a collection enabled as extra in config.toml, completes samtools:
bio/
├── plugin.toml
├── extension.rhai
├── kinds.rhai
└── specs/
└── samtools.rhai
# bio/plugin.toml
description = "Completion for bioinformatics tools"
[dependencies]
std.completion = "*"
The extension only registers the completer; the modules are compiled on the first Tab:
// bio/extension.rhai
sh::completer("samtools", |words, i| {
import "@std/completion/lib" as lib;
import "specs/samtools" as samtools;
lib::complete(samtools::spec(), words, i)
});
// bio/specs/samtools.rhai
fn spec() {
#{
commands: `
view view and convert SAM, BAM and CRAM files
faidx index a FASTA file, or extract regions from it
index index a BAM or CRAM file
`,
sub_spec: "@extra/bio/specs/samtools:sub",
}
}
fn sub_spec(name, sub) {
switch sub {
"view" => #{
opts: `
-b, --bam output BAM
-h, --with-header include the header
-H, --header-only print only the header
-o, --output FILE write to FILE
-T, --reference FILE the reference FASTA
`,
values: #{"-T": "@extra/bio/kinds:fasta"},
args: ["@extra/bio/kinds:alignments", "none"],
},
"faidx" => #{
opts: "-o, --output FILE write to FILE",
args: ["@extra/bio/kinds:fasta", "@extra/bio/kinds:regions"],
},
"index" => #{args: ["@extra/bio/kinds:alignments"]},
_ => (),
}
}
// bio/kinds.rhai
// The files in the directory of `cur` whose names end in one of `suffixes`,
// and the directories.
fn with_suffix(cur, suffixes) {
let parts = cur.split_rev("/", 2); // ["name", "dir"] or ["name"]
let name = parts[0];
let dir = if parts.len() == 2 { parts[1] + "/" } else { "" };
let out = [];
for n in fs::list_dir(if dir == "" { "." } else { dir }) ?? [] {
if n.starts_with(".") && !name.starts_with(".") {
continue;
}
if fs::is_dir(dir + n) {
out.push(#{value: dir + n + "/", suffix: ""});
} else if suffixes.some(|s| n.ends_with(s)) {
out.push(dir + n);
}
}
out
}
// The names of the sequences of the FASTA file before, from its index.
fn regions(words) {
for w in words {
if w.ends_with(".fa") || w.ends_with(".fasta") {
let fai = fs::read_file(w + ".fai") ?? "";
return fai.split("\n").filter(|l| l != "").map(|l| l.split("\t")[0]);
}
}
[]
}
fn kind(name, cur, words) {
switch name {
"fasta" => with_suffix(cur, [".fa", ".fasta", ".fa.gz", ".fasta.gz"]),
"alignments" => with_suffix(cur, [".sam", ".bam", ".cram"]),
"regions" => regions(words),
_ => throw `unknown kind: ${name}`,
}
}
@extra/bio/... finds the plugin by its name, bio, however the user named its source; extra is for the reader.