Repository: reubeno/brush
Pull request #476 (2025-05-09 02:29:56)

chore: better log + connect unimpl functionality with GH issues
* Minor updates to readme
* Adds new unimplemented error that can track and report on associated GitHub issue
* Tie in unimplemented log messages with event pattern
* Allow disablement of unimplemented errors via `--disable-event=unimp`

github-actions[bot]: # Performance Benchmark Report
| Benchmark name                         | Baseline (μs) |  Test/PR (μs) |    Delta (μs) | Delta %         |
| -------------------------------------- | ------------- | ------------- | ------------- | --------------- |
| `clone_shell_object                  ` | `   18.15 μs` | `   18.13 μs` | `   -0.02 μs` | `⚪  Unchanged` |
| `eval_arithmetic                     ` | `    0.17 μs` | `    0.17 μs` | `    0.00 μs` | `⚪  Unchanged` |
| `expand_one_string                   ` | `    1.87 μs` | `    1.82 μs` | `   -0.06 μs` | `⚪  Unchanged` |
| `for_loop                            ` | `   23.05 μs` | `   23.01 μs` | `   -0.03 μs` | `⚪  Unchanged` |
| `function_call                       ` | `    2.34 μs` | `    2.41 μs` | `    0.07 μs` | `⚪  Unchanged` |
| `instantiate_shell                   ` | `   57.84 μs` | `   57.30 μs` | `   -0.54 μs` | `🟢 -0.94%    ` |
| `instantiate_shell_with_init_scripts ` | `23692.36 μs` | `23712.67 μs` | `   20.31 μs` | `⚪  Unchanged` |
| `parse_bash_completion               ` | ` 1657.73 μs` | ` 1670.81 μs` | `   13.07 μs` | `⚪  Unchanged` |
| `parse_sample_script                 ` | `    1.76 μs` | `    1.78 μs` | `    0.01 μs` | `🟠 +0.68%    ` |
| `run_echo_builtin_command            ` | `   15.62 μs` | `   14.87 μs` | `   -0.75 μs` | `⚪  Unchanged` |
| `run_one_external_command            ` | ` 2737.83 μs` | ` 2384.17 μs` | ` -353.66 μs` | `🟢 -12.92%   ` |
| `tokenize_sample_script              ` | `    2.83 μs` | `    2.83 μs` | `   -0.00 μs` | `⚪  Unchanged` |

# Code Coverage Report: Only Changed Files listed

| Package | Base Coverage | New Coverage | Difference |
| ------- | ------------- | ------------ | ---------- |
| brush-core/src/builtins/declare.rs | 🟢 84.72% | 🟢 85.67% | 🟢 0.95% |
| brush-core/src/builtins/factory.rs | 🟢 95.2% | 🟢 95.38% | 🟢 0.18% |
| brush-core/src/commands.rs | 🟢 86.46% | 🟢 85.13% | 🔴 -1.33% |
| brush-core/src/jobs.rs | 🔴 37.07% | 🔴 43.1% | 🟢 6.03% |
| brush-shell/src/args.rs | 🟠 59.57% | 🟠 58.33% | 🔴 -1.24% |
| brush-shell/src/events.rs | 🔴 37.23% | 🔴 36.61% | 🔴 -0.62% |
| brush-shell/src/main.rs | 🟢 84.02% | 🟢 84.26% | 🟢 0.24% |
| **Overall Coverage** | **🟢 78.5%** | **🟢 78.56%** | **🟢 0.06%** |

_Minimum allowed coverage is_ `70%`_, this run produced_ `78.56%`

# Test Summary: bash-completion test suite
| Outcome            | Count                   | Percentage |
| ------------------ | ----------------------: | ---------: |
| ✅ Pass            | 1304            | <span style='color:green'>61.83</span> |
| ❗️ Error           | 230           | <span style='color:red'>10.91</span> |
| ❌ Fail            | 186            | <span style='color:red'>8.82</span> |
| ⏩ Skip            | 374            | 17.73 |
| ❎ Expected Fail   | 15   | 0.71 |
| 📊 Total           | 2109           | 100.00 |

<!-- Sticky Pull Request Comment -->
github-actions[bot]: ## Test Results
    3 files     14 suites   3m 51s ⏱️
  733 tests   733 ✅ 0 💤 0 ❌
2 185 runs  2 185 ✅ 0 💤 0 ❌

Results for commit c977f15c.

[test-results]:data:application/gzip;base64,H4sIAIJsHWgC/13MSw7CIBSF4a00jB3wLMXNGLxcEmJbDIWRce9S0mrr8PtPcl7EhxEXcu3EpSNLCbmBySpXks0hztVcsBrqltdVC7HrthSA//QIz5roN3gbxlPAlGLaSirz+snZoDbtn6f0+2w+XDYfHyFOU8gVBIzWnikAM0i0PXUK0CkjHZOokRt57xXlTpP3B/9S75cIAQAA


Patch:
diff --git a/README.md b/README.md
index 7e8dd6c..f29d439 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ implemented in Rust. It's built and tested on Linux and macOS, with experimental
 ![screenshot](https://github.com/user-attachments/assets/0e64d1b9-7e4e-43be-8593-6c1b9607ac52)
 
 `brush` is functional for interactive use as a daily driver! It can execute most `sh` and `bash` scripts we've
-encountered. Known limitations are tracked with filed issues. Out of an abundance of caution, 
+encountered. Known limitations are tracked with filed issues. Out of an abundance of caution,
 we wouldn't recommend using it yet in _production_ scenarios in case it doesn't behave identically
 to your existing stable shell. (If you do find any behavioral differences, though, please report them with an
 issue!)
@@ -33,9 +33,11 @@ attitude :).
 
 Available for use and distribution under the [MIT license](LICENSE).
 
-### Try it out!
+### Try it out
 
-We don't publish binary releases of `brush`, but if you have a working `rust` toolchain installed you can simply run:
+#### Building from sources
+
+We don't (yet) publish binary releases of `brush` but will Real Soon Now. In the meantime, if you have a working `rust` toolchain installed, then you can simply run:
 
 ```bash
 cargo install --locked brush-shell
@@ -46,15 +48,19 @@ greatest bits, you can clone this repo and execute `cargo run`.
 
 If you don't have `rust` installed, we recommend installing it via [`rustup`](https://rustup.rs/).
 
-(If you *are* interested in having a binary release, then please let us know in the 'Discussions' area of this
+(If you _are_ interested in having a binary release, then please let us know in the 'Discussions' area of this
 project or by filing a feature request in 'Issues'.)
 
+#### Installing on Nix
+
 If you are a Nix user, you can also use the registered version.
 
 ```bash
 nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#brush' -- --version
 ```
 
+#### Installing on Arch Linux
+
 Arch Linux users can install `brush` [from the AUR](https://aur.archlinux.org/packages/brush) with their [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) of choice, e.g.
 
 ```bash
@@ -72,14 +78,14 @@ There are some known gaps in compatibility. Most notably:
 * **Some `set` and `shopt` options.**
   The `set` builtin is implemented, as is `set -x` and many frequently used `set`/`shopt` options, but a number aren't fully implemented. For example, `set -e` will execute but its semantics aren't applied across execution.
 
-* **Anything tagged with a `TODO` comment or where `error::unimp()` is used to return a "not implemented" error**.
-  These aren't all tracked with GitHub issues right now, but there's a number of these scattered throughout the code base. Some are indicative of missing functionality that may be straightforward to implement; others may be more complicated.
+* **The `history` builtin and support for programmatically manipulating command history.**
+  This is something we're actively working on, with promises for supporting shell extensions like [atuin](https://atuin.sh/).
 
 If you feel so inclined, we'd love contributions toward any of the above, with broadening test coverage, deeper compatibility evaluation, or really any other opportunities you can find to help us make this project better.
 
 ## Testing strategy
 
-This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [550+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
+This project is primarily tested by comparing its behavior with other existing shells, leveraging the latter as test oracles. The integration tests implemented in this repo include [600+ test cases](brush-shell/tests/cases) run on both this shell and an oracle, comparing standard output and exit codes.
 
 For more details, please consult the [reference documentation on integration testing](docs/reference/integration-testing.md).
 
@@ -106,5 +112,6 @@ There are a number of other POSIX-ish shells implemented in a non-C/C++ implemen
 * [`nushell`](https://www.nushell.sh/) - modern Rust-implemented shell (which also provides the `reedline` crate we use!)
 * [`rusty_bash`](https://github.com/shellgei/rusty_bash)
 * [`mvdan/sh`](https://github.com/mvdan/sh)
+* [`Oils`](https://github.com/oils-for-unix/oils)
 
 We're sure there are plenty more; we're happy to include links to them as well.
diff --git a/brush-core/src/builtins/bind.rs b/brush-core/src/builtins/bind.rs
index 33850fd..397ecbc 100644
--- a/brush-core/src/builtins/bind.rs
+++ b/brush-core/src/builtins/bind.rs
@@ -1,7 +1,7 @@
 use clap::{Parser, ValueEnum};
 use std::io::Write;
 
-use crate::{builtins, commands, error};
+use crate::{builtins, commands, error, trace_categories};
 
 /// Identifier for a keymap
 #[derive(Clone, ValueEnum)]
@@ -67,33 +67,35 @@ pub(crate) struct BindCommand {
     key_sequence: Option<String>,
 }
 
+const BIND_FEATURE_ISSUE_ID: u32 = 380;
+
 impl builtins::Command for BindCommand {
     async fn execute(
         &self,
         context: commands::ExecutionContext<'_>,
     ) -> Result<crate::builtins::ExitCode, crate::error::Error> {
         if self.list_funcs {
-            return error::unimp("bind -l is not yet implemented");
+            return error::unimp_with_issue("bind -l", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_funcs_and_bindings {
-            return error::unimp("bind -P is not yet implemented");
+            return error::unimp_with_issue("bind -P", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_funcs_and_bindings_reusable {
-            return error::unimp("bind -p is not yet implemented");
+            return error::unimp_with_issue("bind -p", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_key_seqs_that_invoke_macros {
-            return error::unimp("bind -S is not yet implemented");
+            return error::unimp_with_issue("bind -S", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_key_seqs_that_invoke_macros_reusable {
-            return error::unimp("bind -s is not yet implemented");
+            return error::unimp_with_issue("bind -s", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_vars {
-            return error::unimp("bind -V is not yet implemented");
+            return error::unimp_with_issue("bind -V", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_vars_reusable {
@@ -103,34 +105,33 @@ impl builtins::Command for BindCommand {
         }
 
         if self.query_func_bindings.is_some() {
-            return error::unimp("bind -q is not yet implemented");
+            return error::unimp_with_issue("bind -q", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.remove_func_bindings.is_some() {
-            return error::unimp("bind -u is not yet implemented");
+            return error::unimp_with_issue("bind -u", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.remove_key_seq_binding.is_some() {
-            return error::unimp("bind -r is not yet implemented");
+            return error::unimp_with_issue("bind -r", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.bindings_file.is_some() {
-            return error::unimp("bind -f is not yet implemented");
+            return error::unimp_with_issue("bind -f", BIND_FEATURE_ISSUE_ID);
         }
 
         if !self.key_seq_bindings.is_empty() {
-            return error::unimp("bind -x is not yet implemented");
+            return error::unimp_with_issue("bind -x", BIND_FEATURE_ISSUE_ID);
         }
 
         if self.list_key_seq_bindings {
-            return error::unimp("bind -X is not yet implemented");
+            return error::unimp_with_issue("bind -X", BIND_FEATURE_ISSUE_ID);
         }
 
         if let Some(key_sequence) = &self.key_sequence {
-            writeln!(
-                context.stderr(),
+            tracing::warn!(target: trace_categories::UNIMPLEMENTED,
                 "bind: key seq not implemented: {key_sequence}"
-            )?;
+            );
             return Ok(builtins::ExitCode::Unimplemented);
         }
 
diff --git a/brush-core/src/builtins/declare.rs b/brush-core/src/builtins/declare.rs
index 4c43778..64defb6 100644
--- a/brush-core/src/builtins/declare.rs
+++ b/brush-core/src/builtins/declare.rs
@@ -125,13 +125,8 @@ impl builtins::Command for DeclareCommand {
             _ => DeclareVerb::Declare,
         };
 
-        // TODO: implement declare -I
         if self.locals_inherit_from_prev_scope {
-            writeln!(
-                context.stderr(),
-                "UNIMPLEMENTED: declare -I: locals inherit from previous scope"
-            )?;
-            return Ok(builtins::ExitCode::Unimplemented);
+            return error::unimp("declare -I");
         }
 
         let mut result = builtins::ExitCode::Success;
diff --git a/brush-core/src/builtins/factory.rs b/brush-core/src/builtins/factory.rs
index d105a86..ee16151 100644
--- a/brush-core/src/builtins/factory.rs
+++ b/brush-core/src/builtins/factory.rs
@@ -267,8 +267,10 @@ pub(crate) fn get_default_builtins(
     m.insert("unalias".into(), builtin::<unalias::UnaliasCommand>());
     m.insert("wait".into(), builtin::<wait::WaitCommand>());
 
-    // TODO: Unimplemented non-special builtins
+    // TODO: implement fc builtin; should be done after history.
     m.insert("fc".into(), builtin::<unimp::UnimplementedCommand>());
+
+    // TODO(#473): implement ulimit builtin
     m.insert("ulimit".into(), builtin::<unimp::UnimplementedCommand>());
 
     if !options.sh_mode {
@@ -303,11 +305,19 @@ pub(crate) fn get_default_builtins(
         // Input configuration builtins
         m.insert("bind".into(), builtin::<bind::BindCommand>());
 
-        // TODO: Unimplemented builtins
+        // TODO(#469): implement history builtin
+        m.insert("history".into(), builtin::<unimp::UnimplementedCommand>());
+
+        // TODO: implement caller builtin
         m.insert("caller".into(), builtin::<unimp::UnimplementedCommand>());
+
+        // TODO: implement disown builtin
         m.insert("disown".into(), builtin::<unimp::UnimplementedCommand>());
-        m.insert("history".into(), builtin::<unimp::UnimplementedCommand>());
+
+        // TODO: implement logout builtin
         m.insert("logout".into(), builtin::<unimp::UnimplementedCommand>());
+
+        // TODO(#474): implement readarray builtin
         m.insert("readarray".into(), builtin::<unimp::UnimplementedCommand>());
     }
 
diff --git a/brush-core/src/builtins/read.rs b/brush-core/src/builtins/read.rs
index 8e1a476..44d6f45 100644
--- a/brush-core/src/builtins/read.rs
+++ b/brush-core/src/builtins/read.rs
@@ -75,7 +75,7 @@ impl builtins::Command for ReadCommand {
             tracing::debug!("read -r is not implemented");
         }
         if self.timeout_in_seconds.is_some() {
-            return error::unimp("read -t");
+            return error::unimp_with_issue("read -t", 227);
         }
 
         // Find the input stream to use.
diff --git a/brush-core/src/builtins/unimp.rs b/brush-core/src/builtins/unimp.rs
index 1fa445f..5745709 100644
--- a/brush-core/src/builtins/unimp.rs
+++ b/brush-core/src/builtins/unimp.rs
@@ -1,5 +1,4 @@
-use crate::{builtins, commands};
-use std::io::Write;
+use crate::{builtins, commands, trace_categories};
 
 use clap::Parser;
 
@@ -18,17 +17,11 @@ impl builtins::Command for UnimplementedCommand {
         &self,
         context: commands::ExecutionContext<'_>,
     ) -> Result<crate::builtins::ExitCode, crate::error::Error> {
-        writeln!(
-            context.stderr(),
-            "UNIMPLEMENTED: {}: built-in unimplemented: {} {}",
-            context
-                .shell
-                .shell_name
-                .as_ref()
-                .map_or("(unknown shell)", |sn| sn),
+        tracing::warn!(target: trace_categories::UNIMPLEMENTED,
+            "unimplemented built-in: {} {}",
             context.command_name,
             self.args.join(" ")
-        )?;
+        );
         Ok(builtins::ExitCode::Unimplemented)
     }
 }
diff --git a/brush-core/src/commands.rs b/brush-core/src/commands.rs
index b747ca8..34ce765 100644
--- a/brush-core/src/commands.rs
+++ b/brush-core/src/commands.rs
@@ -468,7 +468,7 @@ pub(crate) fn execute_external_command(
                 sys::terminal::move_self_to_foreground()?;
             }
 
-            tracing::error!("error: {}", e);
+            tracing::error!("{e}");
             Ok(CommandSpawnResult::ImmediateExit(126))
         }
     }
@@ -502,8 +502,16 @@ async fn execute_builtin_command(
                 return Ok(CommandSpawnResult::ContinueLoop(count))
             }
         },
+        Err(e @ error::Error::Unimplemented(..)) => {
+            tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
+            1
+        }
+        Err(e @ error::Error::UnimplementedAndTracked(..)) => {
+            tracing::warn!(target: trace_categories::UNIMPLEMENTED, "{e}");
+            1
+        }
         Err(e) => {
-            tracing::error!("error: {}", e);
+            tracing::error!("{e}");
             1
         }
     };
diff --git a/brush-core/src/completion.rs b/brush-core/src/completion.rs
index 4a32daf..4c5a1fc 100644
--- a/brush-core/src/completion.rs
+++ b/brush-core/src/completion.rs
@@ -360,7 +360,7 @@ impl Spec {
                 // It's not clear what exactly this means, though. From basic testing, it doesn't
                 // seem to include basic file and directory name completion.
                 //
-                tracing::debug!(target: trace_categories::COMPLETION, "UNIMPLEMENTED: complete -o bashdefault");
+                tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -o bashdefault");
             }
             if options.default || options.dir_names {
                 // N.B. We approximate "default" readline completion behavior by getting file and
@@ -408,7 +408,7 @@ impl Spec {
                     }
                 }
                 CompleteAction::Binding => {
-                    tracing::debug!(target: trace_categories::COMPLETION, "UNIMPLEMENTED: complete -A binding");
+                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A binding");
                 }
                 CompleteAction::Builtin => {
                     for name in shell.builtins.keys() {
@@ -507,7 +507,7 @@ impl Spec {
                     }
                 }
                 CompleteAction::Service => {
-                    tracing::debug!(target: trace_categories::COMPLETION, "UNIMPLEMENTED: complete -A service");
+                    tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: complete -A service");
                 }
                 CompleteAction::SetOpt => {
                     for (name, _) in namedoptions::SET_O_OPTIONS.iter() {
diff --git a/brush-core/src/error.rs b/brush-core/src/error.rs
index 38a1c20..7276b35 100644
--- a/brush-core/src/error.rs
+++ b/brush-core/src/error.rs
@@ -48,9 +48,13 @@ pub enum Error {
     CommandNotFound(String),
 
     /// The requested functionality has not yet been implemented in this shell.
-    #[error("UNIMPLEMENTED: {0}")]
+    #[error("not yet implemented: {0}")]
     Unimplemented(&'static str),
 
+    /// The requested functionality has not yet been implemented in this shell; it is tracked in a GitHub issue.
+    #[error("not yet implemented: {0}; see https://github.com/reubeno/brush/issues/{1}")]
+    UnimplementedAndTracked(&'static str, u32),
+
     /// An expected environment scope could not be found.
     #[error("missing scope")]
     MissingScope,
@@ -206,3 +210,12 @@ pub enum Error {
 pub(crate) fn unimp<T>(msg: &'static str) -> Result<T, Error> {
     Err(Error::Unimplemented(msg))
 }
+
+/// Convenience function for returning an error for *tracked*, unimplemented functionality.
+///
+/// # Arguments
+///
+/// * `msg` - The message to include in the error
+pub(crate) fn unimp_with_issue<T>(msg: &'static str, project_issue_id: u32) -> Result<T, Error> {
+    Err(Error::UnimplementedAndTracked(msg, project_issue_id))
+}
diff --git a/brush-core/src/interp.rs b/brush-core/src/interp.rs
index 8aa2a39..7785b37 100644
--- a/brush-core/src/interp.rs
+++ b/brush-core/src/interp.rs
@@ -1008,7 +1008,7 @@ impl ExecuteInPipeline for ast::SimpleCommand {
                             if let Some(alias_value) = context.shell.aliases.get(cmd_name.as_str())
                             {
                                 //
-                                // TODO: This is a total hack; aliases are supposed to be handled
+                                // TODO(#57): This is a total hack; aliases are supposed to be handled
                                 // much earlier in the process.
                                 //
                                 let mut alias_pieces: Vec<_> = alias_value
diff --git a/brush-core/src/jobs.rs b/brush-core/src/jobs.rs
index cce243d..19bcbab 100644
--- a/brush-core/src/jobs.rs
+++ b/brush-core/src/jobs.rs
@@ -141,7 +141,7 @@ impl JobManager {
                 self.jobs.iter_mut().find(|j| j.id == id)
             }
             _ => {
-                tracing::warn!("UNIMPLEMENTED: job spec naming command: '{job_spec}'");
+                tracing::warn!(target: trace_categories::UNIMPLEMENTED, "unimplemented: job spec naming command: '{job_spec}'");
                 None
             }
         }
diff --git a/brush-core/src/regex.rs b/brush-core/src/regex.rs
index a1b8e50..ca11614 100644
--- a/brush-core/src/regex.rs
+++ b/brush-core/src/regex.rs
@@ -77,7 +77,6 @@ impl Regex {
             .map(|piece| piece.to_regex_str())
             .collect();
 
-        // TODO: Evaluate how compatible the `fancy_regex` crate is with POSIX EREs.
         let re = compile_regex(regex_pattern, self.case_insensitive, self.multiline)?;
 
         Ok(re.captures(value)?.map(|captures| {
diff --git a/brush-core/src/sys/stubs/terminal.rs b/brush-core/src/sys/stubs/terminal.rs
index 9bb748d..4abd5e5 100644
--- a/brush-core/src/sys/stubs/terminal.rs
+++ b/brush-core/src/sys/stubs/terminal.rs
@@ -4,17 +4,9 @@ use crate::{error, sys};
 pub(crate) struct TerminalSettings {}
 
 impl TerminalSettings {
-    pub fn set_canonical(&mut self, _value: bool) {
-        // TODO: implement
-    }
-
-    pub fn set_echo(&mut self, _value: bool) {
-        // TODO: implement
-    }
-
-    pub fn set_int_signal(&mut self, _value: bool) {
-        // TODO: implement
-    }
+    pub fn set_canonical(&mut self, _value: bool) {}
+    pub fn set_echo(&mut self, _value: bool) {}
+    pub fn set_int_signal(&mut self, _value: bool) {}
 }
 
 pub(crate) fn get_term_attr<Fd>(_fd: Fd) -> Result<TerminalSettings, error::Error> {
diff --git a/brush-core/src/sys/unix/users.rs b/brush-core/src/sys/unix/users.rs
index 61b2600..c0857a5 100644
--- a/brush-core/src/sys/unix/users.rs
+++ b/brush-core/src/sys/unix/users.rs
@@ -1,4 +1,4 @@
-use crate::error;
+use crate::{error, trace_categories};
 use std::path::PathBuf;
 
 use uzers::os::unix::UserExt;
@@ -49,14 +49,14 @@ pub(crate) fn get_user_group_ids() -> Result<Vec<u32>, error::Error> {
 
 #[allow(clippy::unnecessary_wraps)]
 pub(crate) fn get_all_users() -> Result<Vec<String>, error::Error> {
-    // TODO: uzers::all_users() is available but unsafe
-    tracing::debug!("UNIMPLEMENTED: get_all_users");
+    // TODO(#475): uzers::all_users() is available but unsafe
+    tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_users");
     Ok(vec![])
 }
 
 #[allow(clippy::unnecessary_wraps)]
 pub(crate) fn get_all_groups() -> Result<Vec<String>, error::Error> {
-    // TODO: uzers::all_groups() is available but unsafe
-    tracing::debug!("UNIMPLEMENTED: get_all_groups");
+    // TODO(#475): uzers::all_groups() is available but unsafe
+    tracing::debug!(target: trace_categories::UNIMPLEMENTED, "get_all_groups");
     Ok(vec![])
 }
diff --git a/brush-core/src/trace_categories.rs b/brush-core/src/trace_categories.rs
index 8f37999..4532d1b 100644
--- a/brush-core/src/trace_categories.rs
+++ b/brush-core/src/trace_categories.rs
@@ -5,3 +5,4 @@ pub(crate) const JOBS: &str = "jobs";
 pub(crate) const PARSE: &str = "parse";
 pub(crate) const PATTERN: &str = "pattern";
 pub(crate) const FUNCTIONS: &str = "functions";
+pub(crate) const UNIMPLEMENTED: &str = "unimplemented";
diff --git a/brush-interactive/src/completion.rs b/brush-interactive/src/completion.rs
index 5f9dced..d3ae43a 100644
--- a/brush-interactive/src/completion.rs
+++ b/brush-interactive/src/completion.rs
@@ -72,7 +72,7 @@ fn postprocess_completion_candidate(
         }
     }
     if options.no_autoquote_filenames {
-        tracing::debug!(target: trace_categories::COMPLETION, "UNIMPLEMENTED: don't autoquote filenames");
+        tracing::debug!(target: trace_categories::COMPLETION, "unimplemented: don't autoquote filenames");
     }
     if completing_end_of_line && !options.no_trailing_space_at_end_of_line {
         if !options.treat_as_filenames || !candidate.ends_with(std::path::MAIN_SEPARATOR) {
diff --git a/brush-shell/src/args.rs b/brush-shell/src/args.rs
index d2de5a7..7f478f9 100644
--- a/brush-shell/src/args.rs
+++ b/brush-shell/src/args.rs
@@ -135,8 +135,12 @@ pub struct CommandLineArgs {
     pub input_backend: Option<InputBackend>,
 
     /// Enable debug logging for classes of tracing events.
-    #[clap(long = "log-enable", value_name = "EVENT")]
-    pub enabled_log_events: Vec<events::TraceEvent>,
+    #[clap(long = "debug", alias = "log-enable", value_name = "EVENT")]
+    pub enabled_debug_events: Vec<events::TraceEvent>,
+
+    /// Disable logging for classes of tracing events.
+    #[clap(long = "disable-event", alias = "log-disable", value_name = "EVENT")]
+    pub disabled_events: Vec<events::TraceEvent>,
 
     /// Path to script to execute.
     // allow any string as command_name similar to sh
diff --git a/brush-shell/src/brushctl.rs b/brush-shell/src/brushctl.rs
index efd5e24..5de5d7e 100644
--- a/brush-shell/src/brushctl.rs
+++ b/brush-shell/src/brushctl.rs
@@ -74,8 +74,8 @@ impl EventsCommand {
                                                                         // errors
                     }
                 }
-                EventsCommand::Enable { event } => event_config.enable(event)?,
-                EventsCommand::Disable { event } => event_config.disable(event)?,
+                EventsCommand::Enable { event } => event_config.enable(*event)?,
+                EventsCommand::Disable { event } => event_config.disable(*event)?,
             }
 
             Ok(brush_core::builtins::ExitCode::Success)
diff --git a/brush-shell/src/events.rs b/brush-shell/src/events.rs
index 84bd526..a89bbb1 100644
--- a/brush-shell/src/events.rs
+++ b/brush-shell/src/events.rs
@@ -6,7 +6,7 @@ use tracing_subscriber::{
 };
 
 /// Type of event to trace.
-#[derive(Clone, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, clap::ValueEnum)]
 pub enum TraceEvent {
     /// Traces parsing and evaluation of arithmetic expressions.
     #[clap(name = "arithmetic")]
@@ -35,6 +35,9 @@ pub enum TraceEvent {
     /// Traces the process of tokenizing input text.
     #[clap(name = "tokenize")]
     Tokenize,
+    /// Traces usage of unimplemented functionality.
+    #[clap(name = "unimplemented", alias = "unimp")]
+    Unimplemented,
 }
 
 impl Display for TraceEvent {
@@ -49,23 +52,30 @@ impl Display for TraceEvent {
             TraceEvent::Parse => write!(f, "parse"),
             TraceEvent::Pattern => write!(f, "pattern"),
             TraceEvent::Tokenize => write!(f, "tokenize"),
+            TraceEvent::Unimplemented => write!(f, "unimplemented"),
         }
     }
 }
 
 #[derive(Default)]
 pub(crate) struct TraceEventConfig {
-    enabled_trace_events: HashSet<TraceEvent>,
+    enabled_debug_events: HashSet<TraceEvent>,
+    disabled_events: HashSet<TraceEvent>,
     handle: Option<Handle<Targets, Registry>>,
 }
 
 impl TraceEventConfig {
-    pub fn init(enabled_log_events: &[TraceEvent]) -> TraceEventConfig {
-        let enabled_trace_events: HashSet<TraceEvent> =
-            enabled_log_events.iter().cloned().collect();
+    pub fn init(
+        enabled_debug_events: &[TraceEvent],
+        disabled_events: &[TraceEvent],
+    ) -> TraceEventConfig {
+        let enabled_debug_events: HashSet<TraceEvent> =
+            enabled_debug_events.iter().copied().collect();
+        let disabled_events: HashSet<TraceEvent> = disabled_events.iter().copied().collect();
 
         let mut config = TraceEventConfig {
-            enabled_trace_events,
+            enabled_debug_events,
+            disabled_events,
             ..Default::default()
         };
 
@@ -98,19 +108,8 @@ impl TraceEventConfig {
         let mut filter = tracing_subscriber::filter::Targets::new()
             .with_default(tracing_subscriber::filter::LevelFilter::INFO);
 
-        for event in &self.enabled_trace_events {
-            let targets = match event {
-                TraceEvent::Arithmetic => vec!["arithmetic"],
-                TraceEvent::Commands => vec!["commands"],
-                TraceEvent::Complete => vec!["completion"],
-                TraceEvent::Expand => vec!["expansion"],
-                TraceEvent::Functions => vec!["functions"],
-                TraceEvent::Jobs => vec!["jobs"],
-                TraceEvent::Parse => vec!["parse"],
-                TraceEvent::Pattern => vec!["pattern"],
-                TraceEvent::Tokenize => vec!["tokenize"],
-            };
-
+        for event in &self.enabled_debug_events {
+            let targets = Self::event_to_tracing_targets(event);
             filter = filter.with_targets(
                 targets
                     .into_iter()
@@ -118,25 +117,49 @@ impl TraceEventConfig {
             );
         }
 
+        for event in &self.disabled_events {
+            let targets = Self::event_to_tracing_targets(event);
+            filter = filter.with_targets(
+                targets
+                    .into_iter()
+                    .map(|target| (target, tracing::level_filters::LevelFilter::OFF)),
+            );
+        }
+
         filter
     }
 
+    fn event_to_tracing_targets(event: &TraceEvent) -> Vec<&str> {
+        match event {
+            TraceEvent::Arithmetic => vec!["arithmetic"],
+            TraceEvent::Commands => vec!["commands"],
+            TraceEvent::Complete => vec!["completion"],
+            TraceEvent::Expand => vec!["expansion"],
+            TraceEvent::Functions => vec!["functions"],
+            TraceEvent::Jobs => vec!["jobs"],
+            TraceEvent::Parse => vec!["parse"],
+            TraceEvent::Pattern => vec!["pattern"],
+            TraceEvent::Tokenize => vec!["tokenize"],
+            TraceEvent::Unimplemented => vec!["unimplemented"],
+        }
+    }
+
     pub fn get_enabled_events(&self) -> &HashSet<TraceEvent> {
-        &self.enabled_trace_events
+        &self.enabled_debug_events
     }
 
-    pub fn enable(&mut self, event: &TraceEvent) -> Result<(), Error> {
+    pub fn enable(&mut self, event: TraceEvent) -> Result<(), Error> {
         // Don't bother to reload config if nothing has changed.
-        if !self.enabled_trace_events.insert(event.to_owned()) {
+        if !self.enabled_debug_events.insert(event) {
             return Ok(());
         }
 
         self.reload_filter()
     }
 
-    pub fn disable(&mut self, event: &TraceEvent) -> Result<(), Error> {
+    pub fn disable(&mut self, event: TraceEvent) -> Result<(), Error> {
         // Don't bother to reload config if nothing has changed.
-        if !self.enabled_trace_events.remove(event) {
+        if !self.enabled_debug_events.remove(&event) {
             return Ok(());
         }
 
diff --git a/brush-shell/src/main.rs b/brush-shell/src/main.rs
index 27d00cb..95557aa 100644
--- a/brush-shell/src/main.rs
+++ b/brush-shell/src/main.rs
@@ -174,7 +174,10 @@ async fn run_impl(
 ) -> Result<u8, brush_interactive::ShellError> {
     // Initializing tracing.
     let mut event_config = TRACE_EVENT_CONFIG.try_lock().unwrap();
-    *event_config = Some(events::TraceEventConfig::init(&args.enabled_log_events));
+    *event_config = Some(events::TraceEventConfig::init(
+        &args.enabled_debug_events,
+        &args.disabled_events,
+    ));
     drop(event_config);
 
     // Instantiate an appropriately configured shell.


Test patch:
diff --git a/brush-shell/tests/cases/builtins/alias.yaml b/brush-shell/tests/cases/builtins/alias.yaml
index 03a54cd..d7f7283 100644
--- a/brush-shell/tests/cases/builtins/alias.yaml
+++ b/brush-shell/tests/cases/builtins/alias.yaml
@@ -8,7 +8,7 @@ cases:
       myalias 'hello'
 
   - name: "Alias with trailing space"
-    known_failure: true
+    known_failure: true # Issue #57
     stdin: |
       shopt -s expand_aliases
       alias cmd='echo '
@@ -26,7 +26,7 @@ cases:
       outeralias 'hello'
 
   - name: "Alias to keywords"
-    known_failure: true
+    known_failure: true # Issue #286
     stdin: |
       shopt -s expand_aliases
       alias myalias=if