Repository: reubeno/brush (rust)

Issue:
set -- mistakenly outputs shell variables and shell functions
`set --` must clear positional parameters.

```console
$ brush --version
brush 0.2.14 (git:dd1d39c)

$ brush -c 'set --'
BASH=brush
BASH_VERSINFO=([0]="5" [1]="2" [2]="15" [3]="1" [4]="release" [5]="unknown")
BASH_VERSION=5.2.15(1)-release
COMP_WORDBREAKS='
  ︙

$ bash -c 'set --'
(nothing)
```

Pull request description:
fix(builtins): fix set builtin handling of - and --
Addresses issues with `set` builtin's handling of `-` and `--`. This required applying the same `--` parsing workaround to the `set` builtin implementation that was previously applied to `echo`.

Resolves #339.

Patch:
diff --git a/brush-core/src/builtins/set.rs b/brush-core/src/builtins/set.rs
index c28c758..5aa76ce 100644
--- a/brush-core/src/builtins/set.rs
+++ b/brush-core/src/builtins/set.rs
@@ -129,6 +129,7 @@ pub(crate) struct SetCommand {
     #[clap(flatten)]
     set_option: SetOption,
 
+    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
     positional_args: Vec<String>,
 }
 
@@ -137,6 +138,35 @@ impl builtins::Command for SetCommand {
         true
     }
 
+    /// Override the default [`builtins::Command::new`] function to handle clap's limitation related
+    /// to `--`. See [`builtins::parse_known`] for more information
+    /// TODO: we can safely remove this after the issue is resolved
+    fn new<I>(args: I) -> Result<Self, clap::Error>
+    where
+        I: IntoIterator<Item = String>,
+    {
+        //
+        // TODO: This is getting pretty messy; we need to see how to avoid this -- handling from
+        // leaking into too many commands' custom parsing.
+        //
+
+        // Apply the same workaround from the default implementation of Command::new to handle +
+        // args.
+        let args = args.into_iter().map(|arg| {
+            if arg.starts_with('+') {
+                format!("--{arg}")
+            } else {
+                arg
+            }
+        });
+
+        let (mut this, rest_args) = crate::builtins::try_parse_known::<SetCommand>(args)?;
+        if let Some(args) = rest_args {
+            this.positional_args.extend(args);
+        }
+        Ok(this)
+    }
+
     #[allow(clippy::too_many_lines)]
     async fn execute(
         &self,
@@ -308,16 +338,26 @@ impl builtins::Command for SetCommand {
             }
         }
 
-        for (i, arg) in self.positional_args.iter().enumerate() {
-            if arg == "-" && i == 0 {
-                continue;
+        let skip = match self.positional_args.first() {
+            Some(x) if x == "-" => {
+                if self.positional_args.len() > 1 {
+                    context.shell.positional_parameters.clear();
+                }
+                1
             }
-
-            if i < context.shell.positional_parameters.len() {
-                arg.clone_into(&mut context.shell.positional_parameters[i]);
-            } else {
-                context.shell.positional_parameters.push(arg.to_owned());
+            Some(x) if x == "--" => {
+                context.shell.positional_parameters.clear();
+                1
             }
+            Some(_) => {
+                context.shell.positional_parameters.clear();
+                0
+            }
+            None => 0,
+        };
+
+        for arg in self.positional_args.iter().skip(skip) {
+            context.shell.positional_parameters.push(arg.to_owned());
         }
 
         saw_option = saw_option || !self.positional_args.is_empty();


Test patch:
diff --git a/brush-shell/tests/cases/builtins/set.yaml b/brush-shell/tests/cases/builtins/set.yaml
index 74a54e1..9668b6f 100644
--- a/brush-shell/tests/cases/builtins/set.yaml
+++ b/brush-shell/tests/cases/builtins/set.yaml
@@ -1,6 +1,41 @@
 name: "Builtins: set"
 cases:
+  - name: "set with no args"
+    stdin: |
+      MYVARIABLE=VALUE
+      set > set-output.txt
+      grep MYVARIABLE set-output.txt
+
+      # Remove set-output.txt to avoid it being byte-for-byte compared.
+      rm set-output.txt
+
   - name: "Basic set usage"
     stdin: |
       set a b c d
       echo ${*}
+
+  - name: "set clearing args"
+    stdin: |
+      set a b c
+      echo ${*}
+      set a
+      echo ${*}
+
+  - name: "set with -"
+    stdin: |
+      set - a b c
+      echo "args: " ${*}
+      set -
+      echo "args: " ${*}
+
+  - name: "set with --"
+    stdin: |
+      set -- a b c
+      echo "args: " ${*}
+      set --
+      echo "args: " ${*}
+
+  - name: "set with option-looking tokens"
+    stdin: |
+      set a -v
+      echo ${*}