From 1aaa3cd50a7d415db6ae61ece0371e06b3b3af8c Mon Sep 17 00:00:00 2001
From: Xavier L'Heureux <xavier.lheureux@icloud.com>
Date: Tue, 25 Jun 2019 08:42:34 -0400
Subject: [PATCH] Move the suspend builtin to the binary and complete moving
 man_pages

---
 members/builtins-proc/src/lib.rs |  12 +-
 src/binary/builtins.rs           |  59 +++++----
 src/binary/mod.rs                |  11 +-
 src/lib/builtins/command_info.rs |  30 +++--
 src/lib/builtins/is.rs           |   1 +
 src/lib/builtins/man_pages.rs    | 149 ----------------------
 src/lib/builtins/mod.rs          | 211 ++++++++++++++++++++-----------
 src/lib/lib.rs                   |   1 +
 src/main.rs                      |   7 +-
 tests/exists.out                 |   4 +-
 10 files changed, 213 insertions(+), 272 deletions(-)

diff --git a/members/builtins-proc/src/lib.rs b/members/builtins-proc/src/lib.rs
index e50cd9fe..1a453b39 100644
--- a/members/builtins-proc/src/lib.rs
+++ b/members/builtins-proc/src/lib.rs
@@ -12,6 +12,7 @@ pub fn builtin(attr: TokenStream, item: TokenStream) -> TokenStream {
     let syn::FnDecl { ref fn_token, ref inputs, ref output, .. } = **decl;
     let mut help = None;
     let mut short_description = None;
+    let mut names = None;
 
     let name = syn::Ident::new(&format!("builtin_{}", &ident), input.ident.span());
 
@@ -29,6 +30,12 @@ pub fn builtin(attr: TokenStream, item: TokenStream) -> TokenStream {
                 } else {
                     panic!("`desc` attribute should be a string variable");
                 }
+            } else if attr.ident == "names" {
+                if let syn::Lit::Str(h) = &attr.lit {
+                    names = Some(h.value());
+                } else {
+                    panic!("`desc` attribute should be a string variable");
+                }
             } else {
                 panic!("Only the `man` and `desc` attributes are allowed");
             }
@@ -40,8 +47,9 @@ pub fn builtin(attr: TokenStream, item: TokenStream) -> TokenStream {
     let help = help.trim();
     let short_description = short_description
         .expect("A short description is required! Please add an attribute with name `desc`");
-    let man = format!("NAME\n    {} - {}\n\n{}", ident, short_description, help);
-    let help = format!("{}\n\n```txt\n{}\n```", short_description, help);
+    let names = names.unwrap_or_else(|| ident.to_string());
+    let man = format!("NAME\n    {} - {}\n\n{}", names, short_description, help);
+    let help = format!("{} - {}\n\n```txt\n{}\n```", names, short_description, help);
 
     let result = quote! {
         #[doc = #help]
diff --git a/src/binary/builtins.rs b/src/binary/builtins.rs
index 854d3e06..14f0c2aa 100644
--- a/src/binary/builtins.rs
+++ b/src/binary/builtins.rs
@@ -1,40 +1,32 @@
-use ion_shell::{
-    builtins::{man_pages::check_help, Status},
-    types::Str,
-    Shell,
-};
+use ion_shell::{builtin, builtins::Status, types::Str, Shell};
 use libc::SIGTERM;
 use std::{error::Error, os::unix::process::CommandExt, process::Command};
 
-const MAN_EXEC: &str = r#"NAME
-    exec - Replace the shell with the given command.
-
+#[builtin(
+    desc = "suspend the current shell",
+    man = "
 SYNOPSIS
-    exec [-ch] [--help] [command [arguments ...]]
+    suspend
 
 DESCRIPTION
-    Execute <command>, replacing the shell with the specified program.
-    The <arguments> following the command become the arguments to
-    <command>.
-
-OPTIONS
-    -c  Execute command with an empty environment."#;
-
-pub const MAN_EXIT: &str = r#"NAME
-    exit - exit the shell
+    Suspends the current shell by sending it the SIGTSTP signal,
+    returning to the parent process. It can be resumed by sending it SIGCONT."
+)]
+pub fn suspend(args: &[Str], _shell: &mut Shell<'_>) -> Status {
+    let _ = unsafe { libc::kill(0, libc::SIGSTOP) };
+    Status::SUCCESS
+}
 
+#[builtin(
+    desc = "exit the shell",
+    man = "
 SYNOPSIS
     exit
 
 DESCRIPTION
-    Makes ion exit. The exit status will be that of the last command executed."#;
-
-/// Executes the givent commmand.
-
+    Makes ion exit. The exit status will be that of the last command executed."
+)]
 pub fn exit(args: &[Str], shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_EXIT) {
-        return Status::SUCCESS;
-    }
     // Kill all active background tasks before exiting the shell.
     shell.background_send(SIGTERM);
     let exit_code = args
@@ -44,10 +36,21 @@ pub fn exit(args: &[Str], shell: &mut Shell<'_>) -> Status {
     std::process::exit(exit_code);
 }
 
+#[builtin(
+    desc = "replace the shell with the given command",
+    man = "
+SYNOPSIS
+    exec [-ch] [--help] [command [arguments ...]]
+
+DESCRIPTION
+    Execute <command>, replacing the shell with the specified program.
+    The <arguments> following the command become the arguments to
+    <command>.
+
+OPTIONS
+    -c  Execute command with an empty environment."
+)]
 pub fn exec(args: &[Str], _shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_EXEC) {
-        return Status::SUCCESS;
-    }
     let mut clear_env = false;
     let mut idx = 1;
     for arg in args.iter().skip(1) {
diff --git a/src/binary/mod.rs b/src/binary/mod.rs
index 065fd606..651c2c02 100644
--- a/src/binary/mod.rs
+++ b/src/binary/mod.rs
@@ -169,11 +169,12 @@ impl<'a> InteractiveBinary<'a> {
         // change the lifetime to allow adding local builtins
         let InteractiveBinary { context, shell } = self;
         let mut shell = shell.into_inner();
-        let builtins = shell.builtins_mut();
-        builtins.add("history", history, "Display a log of all commands previously executed");
-        builtins.add("keybindings", keybindings, "Change the keybindings");
-        builtins.add("exit", exit, "Exits the current session");
-        builtins.add("exec", exec, "Replace the shell with the given command.");
+        shell
+            .builtins_mut()
+            .add("history", history, "Display a log of all commands previously executed")
+            .add("keybindings", keybindings, "Change the keybindings")
+            .add("exit", exit, "Exits the current session")
+            .add("exec", exec, "Replace the shell with the given command.");
 
         Self::exec_init_file(&mut shell);
 
diff --git a/src/lib/builtins/command_info.rs b/src/lib/builtins/command_info.rs
index cf6e800e..5b09f02c 100644
--- a/src/lib/builtins/command_info.rs
+++ b/src/lib/builtins/command_info.rs
@@ -1,19 +1,26 @@
-use super::{check_help, man_pages::MAN_WHICH, Status};
+use super::Status;
+use crate as ion_shell;
 use crate::{
     shell::{Shell, Value},
     types,
 };
+use builtins_proc::builtin;
 
 use std::{borrow::Cow, env};
 
-pub fn which(args: &[types::Str], shell: &mut Shell<'_>) -> Result<Status, ()> {
-    if check_help(args, MAN_WHICH) {
-        return Ok(Status::SUCCESS);
-    }
+#[builtin(
+    desc = "locate a program file in the current user's path",
+    man = "
+SYNOPSIS
+    which PROGRAM
 
+DESCRIPTION
+    The which utility takes a list of command names and searches for the
+    alias/builtin/function/executable that would be executed if you ran that command."
+)]
+pub fn which(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     if args.len() == 1 {
-        eprintln!("which: Expected at least 1 args, got only 0");
-        return Err(());
+        return Status::bad_argument("which: Expected at least 1 args, got only 0");
     }
 
     let mut result = Status::SUCCESS;
@@ -32,14 +39,13 @@ pub fn which(args: &[types::Str], shell: &mut Shell<'_>) -> Result<Status, ()> {
             Err(_) => result = Status::from_exit_code(1),
         }
     }
-    Ok(result)
+    result
 }
 
-pub fn find_type(args: &[types::Str], shell: &mut Shell<'_>) -> Result<Status, ()> {
+pub fn builtin_type(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     // Type does not accept help flags, aka "--help".
     if args.len() == 1 {
-        eprintln!("type: Expected at least 1 args, got only 0");
-        return Err(());
+        return Status::bad_argument("type: Expected at least 1 args, got only 0");
     }
 
     let mut result = Status::SUCCESS;
@@ -61,7 +67,7 @@ pub fn find_type(args: &[types::Str], shell: &mut Shell<'_>) -> Result<Status, (
             Err(_) => result = Status::error(format!("type: {}: not found", command)),
         }
     }
-    Ok(result)
+    result
 }
 
 fn get_command_info<'a>(command: &str, shell: &mut Shell<'_>) -> Result<Cow<'a, str>, ()> {
diff --git a/src/lib/builtins/is.rs b/src/lib/builtins/is.rs
index 0abd3b10..ae30fbcc 100644
--- a/src/lib/builtins/is.rs
+++ b/src/lib/builtins/is.rs
@@ -5,6 +5,7 @@ use builtins_proc::builtin;
 
 // TODO: Add support for multiple name in builtins man
 #[builtin(
+    names = "eq, is",
     desc = "checks if two arguments are the same",
     man = "
 SYNOPSIS
diff --git a/src/lib/builtins/man_pages.rs b/src/lib/builtins/man_pages.rs
index cc38ebf1..f0ed8ff9 100644
--- a/src/lib/builtins/man_pages.rs
+++ b/src/lib/builtins/man_pages.rs
@@ -10,28 +10,6 @@ pub fn check_help(args: &[types::Str], man_page: &'static str) -> bool {
     false
 }
 
-pub const MAN_IS: &str = r#"NAME
-    is - Checks if two arguments are the same
-
-SYNOPSIS
-    is [ -h | --help ] [not]
-
-DESCRIPTION
-    Returns 0 if the two arguments are equal
-
-OPTIONS
-    not
-        returns 0 if the two arguments are not equal."#;
-
-pub const MAN_ISATTY: &str = r#"
-    isatty - Checks if argument is a file descriptor
-
-SYNOPSIS
-    isatty [FD]
-
-DESCRIPTION
-    Returns 0 exit status if the supplied file descriptor is a tty."#;
-
 // pub const MAN_FN: &str = r#"NAME
 // fn - print a list of all functions or create a function
 //
@@ -59,130 +37,3 @@ DESCRIPTION
 //
 // example 1
 //"#;
-
-pub const MAN_RANDOM: &str = r#"NAME
-    random - generate a random number
-
-SYNOPSIS
-    random
-    random START END
-
-DESCRIPTION
-    random generates a pseudo-random integer. IT IS NOT SECURE.
-    The range depends on what arguments you pass. If no arguments are given the range is [0, 32767].
-    If two arguments are given the range is [START, END]."#;
-
-pub const MAN_FG: &str = r#"NAME
-    fg - bring job to foreground
-
-SYNOPSIS
-    fg PID
-
-DESCRIPTION
-    fg brings the specified job to foreground resuming it if it has stopped."#;
-
-pub const MAN_SUSPEND: &str = r#"NAME
-    suspend - suspend the current shell
-
-SYNOPSIS
-    suspend
-
-DESCRIPTION
-    Suspends the current shell by sending it the SIGTSTP signal,
-    returning to the parent process. It can be resumed by sending it SIGCONT."#;
-
-pub const MAN_DISOWN: &str = r#"NAME
-    disown - Disown processes
-
-SYNOPSIS
-    disown [ --help | -r | -h | -a ][PID...]
-
-DESCRIPTION
-    Disowning a process removes that process from the shell's background process table.
-
-OPTIONS
-    -r  Remove all running jobs from the background process list.
-    -h  Specifies that each job supplied will not receive the SIGHUP signal when the shell receives a SIGHUP.
-    -a  If no job IDs were supplied, remove all jobs from the background process list."#;
-
-pub const MAN_MATCHES: &str = r#"NAME
-    matches - checks if the second argument contains any portion of the first.
-
-SYNOPSIS
-    matches VALUE VALUE
-
-DESCRIPTION
-    Makes the exit status equal 0 if the first argument contains the second.
-    Otherwise matches makes the exit status equal 1.
-
-EXAMPLES
-    Returns true:
-        matches xs x
-    Returns false:
-        matches x xs"#;
-
-pub const MAN_EXISTS: &str = r#"NAME
-    exists - check whether items exist
-
-SYNOPSIS
-    exists [EXPRESSION]
-
-DESCRIPTION
-    Checks whether the given item exists and returns an exit status of 0 if it does, else 1.
-
-OPTIONS
-    -a ARRAY
-        array var is not empty
-
-    -b BINARY
-        binary is in PATH
-
-    -d PATH
-        path is a directory
-        This is the same as test -d
-
-    -f PATH
-        path is a file
-        This is the same as test -f
-
-    --fn FUNCTION
-        function is defined
-
-    -s STRING
-        string var is not empty
-
-    STRING
-        string is not empty
-        This is the same as test -n
-
-EXAMPLES
-    Test if the file exists:
-        exists -f FILE && echo "The FILE exists" || echo "The FILE does not exist"
-
-    Test if some-command exists in the path and is executable:
-        exists -b some-command && echo "some-command exists" || echo "some-command does not exist"
-
-    Test if variable exists AND is not empty
-        exists -s myVar && echo "myVar exists: $myVar" || echo "myVar does not exist or is empty"
-        NOTE: Don't use the '$' sigil, but only the name of the variable to check
-
-    Test if array exists and is not empty
-        exists -a myArr && echo "myArr exists: @myArr" || echo "myArr does not exist or is empty"
-        NOTE: Don't use the '@' sigil, but only the name of the array to check
-
-    Test if a function named 'myFunc' exists
-        exists --fn myFunc && myFunc || echo "No function with name myFunc found"
-
-AUTHOR
-    Written by Fabian Würfl.
-    Heavily based on implementation of the test builtin, which was written by Michael Murph."#;
-
-pub const MAN_WHICH: &str = r#"NAME
-    which - locate a program file in the current user's path
-
-SYNOPSIS
-    which PROGRAM
-
-DESCRIPTION
-    The which utility takes a list of command names and searches for the
-    alias/builtin/function/executable that would be executed if you ran that command."#;
diff --git a/src/lib/builtins/mod.rs b/src/lib/builtins/mod.rs
index e1b9712e..0e1c8280 100644
--- a/src/lib/builtins/mod.rs
+++ b/src/lib/builtins/mod.rs
@@ -16,21 +16,21 @@ mod status;
 mod test;
 mod variables;
 
-use self::{
-    command_info::{find_type, which},
+pub use self::{
+    command_info::{builtin_type, builtin_which},
     conditionals::{contains, ends_with, starts_with},
     echo::builtin_echo,
     exists::exists,
     functions::print_functions,
+    helpers::Status,
     is::builtin_is,
-    man_pages::*,
+    man_pages::check_help,
     set::builtin_set,
     source::builtin_source,
     status::builtin_status,
     test::test,
     variables::{alias, drop_alias, drop_array, drop_variable},
 };
-pub use self::{helpers::Status, man_pages::check_help};
 use crate as ion_shell;
 use crate::{
     shell::{sys, Capture, Shell, Value},
@@ -202,8 +202,8 @@ impl<'a> BuiltinMap<'a> {
             .add("calc", &builtin_calc, "Calculate a mathematical expression")
             .add("eq", &builtin_is, "Simple alternative to == and !=")
             .add("is", &builtin_is, "Simple alternative to == and !=")
-            .add("true", &builtin_tru, "Do nothing, successfully")
-            .add("false", &builtin_fals, "Do nothing, unsuccessfully")
+            .add("true", &builtin_true_, "Do nothing, successfully")
+            .add("false", &builtin_false_, "Do nothing, unsuccessfully")
             .add(
                 "starts-with",
                 &starts_with,
@@ -234,17 +234,15 @@ impl<'a> BuiltinMap<'a> {
             .add("type", &builtin_type, "indicates how a command would be interpreted")
     }
 
-    /// Utilities specific for a shell, that should probably not be included in an embedded context
+    /// Utilities that may be a security risk. Not included by default
     ///
-    /// Contains `eval`, `exec`, `exit`, `set`, `suspend`
-    pub fn with_shell_unsafe(&mut self) -> &mut Self {
-        self.add("eval", &builtin_eval, "Evaluates the evaluated expression")
-            .add(
-                "set",
-                &builtin_set,
-                "Set or unset values of shell options and positional parameters.",
-            )
-            .add("suspend", &builtin_suspend, "Suspends the shell with a SIGTSTOP signal")
+    /// Contains `eval`, `set`
+    pub fn with_unsafe(&mut self) -> &mut Self {
+        self.add("eval", &builtin_eval, "Evaluates the evaluated expression").add(
+            "set",
+            &builtin_set,
+            "Set or unset values of shell options and positional parameters.",
+        )
     }
 }
 
@@ -667,18 +665,28 @@ pub fn builtin_calc(args: &[types::Str], _: &mut Shell<'_>) -> Status {
     }
 }
 
-pub fn builtin_random(args: &[types::Str], _: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_RANDOM) {
-        return Status::SUCCESS;
-    }
+#[builtin(
+    desc = "generate a random number",
+    man = "
+SYNOPSIS
+    random
+    random START END
+
+DESCRIPTION
+    random generates a pseudo-random integer. IT IS NOT SECURE.
+    The range depends on what arguments you pass. If no arguments are given the range is [0, \
+           32767].
+    If two arguments are given the range is [START, END]."
+)]
+pub fn random(args: &[types::Str], _: &mut Shell<'_>) -> Status {
     match random::random(&args[1..]) {
         Ok(()) => Status::SUCCESS,
         Err(why) => Status::error(why),
     }
 }
 
-// TODO: Find a workaround
 #[builtin(
+    names = "true",
     desc = "does nothing sucessfully",
     man = "
 SYNOPSIS
@@ -687,11 +695,10 @@ SYNOPSIS
 DESCRIPTION
     Sets the exit status to 0."
 )]
-pub fn tru(args: &[types::Str], _: &mut Shell<'_>) -> Status {
-    Status::SUCCESS
-}
+pub fn true_(args: &[types::Str], _: &mut Shell<'_>) -> Status { Status::SUCCESS }
 
 #[builtin(
+    names = "false",
     desc = "does nothing unsuccessfully",
     man = "
 SYNOPSIS
@@ -700,9 +707,7 @@ SYNOPSIS
 DESCRIPTION
     Sets the exit status to 1."
 )]
-pub fn fals(args: &[types::Str], _: &mut Shell<'_>) -> Status {
-    Status::error("")
-}
+pub fn false_(args: &[types::Str], _: &mut Shell<'_>) -> Status { Status::error("") }
 
 // TODO create a manpage
 pub fn builtin_wait(_: &[types::Str], shell: &mut Shell<'_>) -> Status {
@@ -737,28 +742,35 @@ pub fn bg(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     job_control::bg(shell, &args[1..])
 }
 
-pub fn builtin_fg(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_FG) {
-        return Status::SUCCESS;
-    }
+#[builtin(
+    desc = "bring job to the foreground",
+    man = "
+SYNOPSIS
+    fg PID
+
+DESCRIPTION
+    fg brings the specified job to foreground resuming it if it has stopped."
+)]
+pub fn fg(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     job_control::fg(shell, &args[1..])
 }
 
-pub fn builtin_suspend(args: &[types::Str], _shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_SUSPEND) {
-        return Status::SUCCESS;
-    }
-    let _ = unsafe { libc::kill(0, libc::SIGSTOP) };
-    Status::SUCCESS
-}
+#[builtin(
+    desc = "disown processes",
+    man = "
+SYNOPSIS
+    disown [ --help | -r | -h | -a ][PID...]
 
-pub fn builtin_disown(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
-    for arg in args {
-        if *arg == "--help" {
-            println!("{}", MAN_DISOWN);
-            return Status::SUCCESS;
-        }
-    }
+DESCRIPTION
+    Disowning a process removes that process from the shell's background process table.
+
+OPTIONS
+    -r  Remove all running jobs from the background process list.
+    -h  Specifies that each job supplied will not receive the SIGHUP signal when the shell \
+           receives a SIGHUP.
+    -a  If no job IDs were supplied, remove all jobs from the background process list."
+)]
+pub fn disown(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     match job_control::disown(shell, &args[1..]) {
         Ok(()) => Status::SUCCESS,
         Err(err) => Status::error(format!("ion: disown: {}", err)),
@@ -779,10 +791,23 @@ pub fn builtin_help(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
 }
 
 use regex::Regex;
-pub fn builtin_matches(args: &[types::Str], _: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_MATCHES) {
-        return Status::SUCCESS;
-    }
+#[builtin(
+    desc = "checks if the second argument contains any proportion of the first",
+    man = "
+SYNOPSIS
+    matches VALUE VALUE
+
+DESCRIPTION
+    Makes the exit status equal 0 if the first argument contains the second.
+    Otherwise matches makes the exit status equal 1.
+
+EXAMPLES
+    Returns true:
+        matches xs x
+    Returns false:
+        matches x xs"
+)]
+pub fn matches(args: &[types::Str], _: &mut Shell<'_>) -> Status {
     if args[1..].len() != 2 {
         return Status::bad_argument("match takes two arguments");
     }
@@ -801,10 +826,63 @@ pub fn builtin_matches(args: &[types::Str], _: &mut Shell<'_>) -> Status {
     }
 }
 
-pub fn builtin_exists(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_EXISTS) {
-        return Status::SUCCESS;
-    }
+#[builtin(
+    desc = "check whether items exist",
+    man = r#"
+SYNOPSIS
+    exists [EXPRESSION]
+
+DESCRIPTION
+    Checks whether the given item exists and returns an exit status of 0 if it does, else 1.
+
+OPTIONS
+    -a ARRAY
+        array var is not empty
+
+    -b BINARY
+        binary is in PATH
+
+    -d PATH
+        path is a directory
+        This is the same as test -d
+
+    -f PATH
+        path is a file
+        This is the same as test -f
+
+    --fn FUNCTION
+        function is defined
+
+    -s STRING
+        string var is not empty
+
+    STRING
+        string is not empty
+        This is the same as test -n
+
+EXAMPLES
+    Test if the file exists:
+        exists -f FILE && echo "The FILE exists" || echo "The FILE does not exist"
+
+    Test if some-command exists in the path and is executable:
+        exists -b some-command && echo "some-command exists" || echo "some-command does not exist"
+
+    Test if variable exists AND is not empty
+        exists -s myVar && echo "myVar exists: $myVar" || echo "myVar does not exist or is empty"
+        NOTE: Don't use the '$' sigil, but only the name of the variable to check
+
+    Test if array exists and is not empty
+        exists -a myArr && echo "myArr exists: @myArr" || echo "myArr does not exist or is empty"
+        NOTE: Don't use the '@' sigil, but only the name of the array to check
+
+    Test if a function named 'myFunc' exists
+        exists --fn myFunc && myFunc || echo "No function with name myFunc found"
+
+AUTHOR
+    Written by Fabian Würfl.
+    Heavily based on implementation of the test builtin, which was written by Michael Murphy."#
+)]
+pub fn exists(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     match exists(args, shell) {
         Ok(true) => Status::SUCCESS,
         Ok(false) => Status::error(""),
@@ -812,25 +890,16 @@ pub fn builtin_exists(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
     }
 }
 
-pub fn builtin_which(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
-    match which(args, shell) {
-        Ok(result) => result,
-        Err(()) => Status::error(""),
-    }
-}
-
-pub fn builtin_type(args: &[types::Str], shell: &mut Shell<'_>) -> Status {
-    match find_type(args, shell) {
-        Ok(result) => result,
-        Err(()) => Status::error(""),
-    }
-}
-
-pub fn builtin_isatty(args: &[types::Str], _: &mut Shell<'_>) -> Status {
-    if check_help(args, MAN_ISATTY) {
-        return Status::SUCCESS;
-    }
+#[builtin(
+    desc = "checks if the provided file descriptor is a tty",
+    man = "
+SYNOPSIS
+    isatty [FD]
 
+DESCRIPTION
+    Returns 0 exit status if the supplied file descriptor is a tty."
+)]
+pub fn isatty(args: &[types::Str], _: &mut Shell<'_>) -> Status {
     if args.len() > 1 {
         // sys::isatty expects a usize if compiled for redox but otherwise a i32.
         #[cfg(target_os = "redox")]
diff --git a/src/lib/lib.rs b/src/lib/lib.rs
index 9590dc19..9f6d1663 100644
--- a/src/lib/lib.rs
+++ b/src/lib/lib.rs
@@ -82,3 +82,4 @@ pub use crate::{
     builtins::{BuiltinFunction, BuiltinMap},
     shell::*,
 };
+pub use builtins_proc::builtin;
diff --git a/src/main.rs b/src/main.rs
index 86d216d9..d2628a8d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -135,9 +135,10 @@ fn main() {
 
     let mut builtins = BuiltinMap::default();
     builtins
-        .with_shell_unsafe()
-        .add("exec", &builtins::exec, "Replace the shell with the given command.")
-        .add("exit", &builtins::exit, "Exits the current session");
+        .with_unsafe()
+        .add("exec", &builtins::builtin_exec, "Replace the shell with the given command.")
+        .add("exit", &builtins::builtin_exit, "Exits the current session")
+        .add("suspend", &builtins::builtin_suspend, "Suspends the shell with a SIGTSTOP signal");
 
     let stdin_is_a_tty = atty::is(Stream::Stdin);
     let mut shell = Shell::with_builtins(builtins);
diff --git a/tests/exists.out b/tests/exists.out
index 5685604c..f6b66ea1 100644
--- a/tests/exists.out
+++ b/tests/exists.out
@@ -54,7 +54,7 @@ EXAMPLES
 
 AUTHOR
     Written by Fabian Würfl.
-    Heavily based on implementation of the test builtin, which was written by Michael Murph.
+    Heavily based on implementation of the test builtin, which was written by Michael Murphy.
 0
 NAME
     exists - check whether items exist
@@ -110,7 +110,7 @@ EXAMPLES
 
 AUTHOR
     Written by Fabian Würfl.
-    Heavily based on implementation of the test builtin, which was written by Michael Murph.
+    Heavily based on implementation of the test builtin, which was written by Michael Murphy.
 0
 1
 0
-- 
GitLab