Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commands)!: add support for flags #12288

Draft
wants to merge 38 commits into
base: master
Choose a base branch
from

Conversation

RoloEdits
Copy link
Contributor

@RoloEdits RoloEdits commented Dec 18, 2024

TODO:

  • How would we go about documentation to make sure that the flags are discoverable?

    • Is the prompt enough?
    • Should docgen add another column? If so, how should we showcase when there are lots of flags?
      Current look using the same methodology as before:
      image
  • Should prompt completions be added for the flags? If so, how would we handle competing completions?


Adds the ability to parse out flags for typeable commands.

The implimentation tries to be as straight forward as possible, using no new dependencies, with only one helper method on Args, flag, to assist some boilerplate, and a trait in shellwords.rs.

flag takes &mut self, flags: impl IntoFlags and returns an Option<&str>. This is done with the expected use of :command [<flags>] [<args>]. This advances the Args iterator along if matches are found, so that once all flags are gotten, only arguments remain.

NOTE:
Currently only known flags would be returned, so any flag not associated would get treated like an arg. This might lead to confusion on a flag typo, but nothing is coming to mind to better handle this in a generic way where many commands will be using this.

Manual Flag Termination

Flags can be terminated with a -- to indicate to the checker that all flags that are expected to be yielded have been. Once -- is encountered, flag returns None. The -- itself is eaten in this process..

:write --no-fmt -- --no-fmt

Dev Experience

Basic

A basic example of adding flags to a command first involves adding the names and description. This can be done using the flags! macro:

    TypableCommand {
        name: "sort",
        aliases: aliases![],
        flags: flags![
            {
                long: "reverse",
                short: "r",
                desc: "sort ranges in selection in reverse order"
            },
        ],
        accepts: None,
        doc: "Sort ranges in selection.",
        fun: sort,
        signature: CommandSignature::none(),
    },

These can be chained for as many flags as needed. long and desc are required, with an optional short for shorthand aliasing, and an accepts, used to indicate if a flag accepts an argument. long flags are passed in with --LONG. If no short is provided, then the only calling convention available would be to use the long name. short can be called with a single dash: -SHORT. flags! supports empty arguments too: flags![], indicating no flags.

This alone automatically provides formatted docs:
image

From there, its just a matter of checking for the flags in the callback function body, and checking which ones were matched. flags! provides a pattern to reduce boilerplate:

fn sort(
    cx: &mut compositor::Context,
    mut args: Args,
    flags: Flags,
    event: PromptEvent,
) -> anyhow::Result<()> {
    if event != PromptEvent::Validate {
        return Ok(());
    }

    // Flags
    let mut reverse = false;

    flags! {
        for flags, args => {
            "reverse" | "r" => reverse = true,
        }
    }

    // Expands to:
    // while let Some(flag) = args.flag(&flags) {
    //     match flag {
    //         "reverse" | "r" => reverse = true,
    //         _ => {
    //             anyhow::bail!(
    //                 "unhandled command flag `{}`, implementation failed to cover all flags.",
    //                 flag
    //             );
    //         }
    //     }
    // }

    sort_impl(cx, reverse);

    Ok(())
}

All callback functions inject any flags that were declared on the command. Passing to Args::flag checks each argument that would be yielded via a next, and if there is a match, it returns the flag name, stripped of any dashes, and advances the iterator. If no matches are found, then the iterator is not advanced and None is returned. This convention supports both if let Some and while let Some.

flag only ever returns Some(flag) if a match was found, so the catch all message is there to indicate unhandled flag implementations. This should only ever happen if flags were provided on the command, but there is no branch covering its implimentation.

Flag with parameters

For a more complex example, with flags that take arguments, here is a yank command, which incorporates other yank command functionality into one command, using the flags to dictate the behavior:

fn yank(
    cx: &mut compositor::Context,
    mut args: Args,
    flags: Flags,
    event: PromptEvent,
) -> anyhow::Result<()> {
    if event != PromptEvent::Validate {
        return Ok(());
    }

    // Flags
    let mut join = false;
    let mut register: Option<char> = cx.editor.selected_register;
    let mut primary = false;
    let mut diagnostic = false;

    flags! {
        for flags, args => {
            "join" | "j" =>  join = true,
            "primary" | "p" => primary = true,
            "clipboard" | "c" => register = Some('*'),
            "system" | "s" => register = Some('+'),
            "diagnostic" | "d" => diagnostic = true,
            "register" | "r" => {
                 if let Some(arg) = args.next() {
                     let mut reg = arg.chars();
                     ensure!(reg.clone().count() == 1, "invalid register was provided");
                     register = reg.next();
                 } else {
                     bail!("`register` flag was used, but nothing was passed to it");
                 }
            },
        }
    }

    if diagnostic {
        let (view, doc) = current_ref!(cx.editor);
        let primary = doc.selection(view.id).primary();

        // Look only for diagnostics that intersect with the primary selection
        let diag: Vec<_> = doc
            .diagnostics()
            .iter()
            .filter(|d| primary.overlaps(&helix_core::Range::new(d.range.start, d.range.end)))
            .map(|d| d.message.clone())
            .collect();
        let n = diag.len();
        if n == 0 {
            bail!("No diagnostics under primary selection");
        }

        let register = register.unwrap_or('"');

        cx.editor.registers.write(register, diag)?;
        cx.editor.set_status(format!(
            "Yanked {n} diagnostic{} to register {register}",
            if n == 1 { "" } else { "s" }
        ));

        return Ok(());
    }

    if primary && join {
        bail!("`primary` and `join` were both passed as flags, but are mutually exclusive");
    }

    if primary {
        yank_primary_selection_impl(cx.editor, register.unwrap_or('"'));
        return Ok(());
    }

    if join {
        yank_joined_impl(cx.editor, args.rest(), register.unwrap_or('"'));
        return Ok(());
    }

    yank_impl(cx.editor, register.unwrap_or('"'));

    Ok(())
}

Prompt

Added an accepts: Option<&'static str> field to TypeableCommand and to Flags.

    TypableCommand {
        name: "yank",
        aliases: aliases![],
        flags: flags![
            {
                long: "join",
                short: "j",
                desc: "joins selection with optional separator",
                accepts: "<separator>"
            },
            {
                long: "primary",
                short: "p",
                desc: "yanks only the primary selection",
            },
            {
                long: "register",
                short: "r",
                desc: "specify the register to yank to",
                accepts: "<register>",
            },
            {
                long: "clipboard",
                short: "c",
                desc: "yanks to the primary (*) clipboard",
            },
            {
                long: "system",
                short: "s",
                desc: "yanks to the system (+) clipboard",
            },
            {
                long: "diagnostic",
                short: "d",
                desc: "yanks the diagnostic under the primary cursor",
            },
        ],
        accepts: None,
        doc: "yank selection to clipboard.",
        fun: yank,
        signature: CommandSignature::none(),
    },

This exposes more information in the prompt:
image
This is dynamically added, and skipped when None.

Aliasing

Added Aliases and Alias structs, as well as an aliases! macro. These allow the ability to define aliases in a way that they can map to flags and all point to the same command.

This means that currently separate commands, where behavior can be defined with flags, can be collapsed into one.

Dev Experience

Adding aliases is done through the aliases! macro like so:

    TypableCommand {
        name: "write",
        aliases: aliases![
            "w",
            "u" => "write --update",
            "x" => "write --quit",
            "wq" => "write --quit",
            "x!" => "write --quit --force",
            "wq!" => "write --quit --force",
            "w!" => "write --force",
            "wa" => "write --all",
            "wa!" => "write --all --force",
            "waq" => "write --all --quit",
            "xa" => "write --all --quit",
            "waq!" => "write --all --force --quit",
            "xa!" => "write --all --force --quit",
            "wbc" => "write --close-buffer",
            "wbc!" => "write --force --close-buffer",
        ],
        flags: flags![
            {
                long: "no-format",
                desc: "skips formatting step when writing",
            },
            {
                long: "all",
                short: "a",
                desc: "writes all buffers",
            },
            {
                long: "quit",
                short: "q",
                desc: "quite helix after write",
            },
            {
                long: "force",
                short: "f",
                desc: "forcefully write changes, creating subdirectories as needed",
            },
            {
                long: "update",
                short: "u",
                desc: "write changes only if the buffer has been modified",
            },
            {
                long: "close-buffer",
                desc: "close buffer after writing",
            },
        ],
        accepts: Some("<path>"),
        doc: "write changes to disk",
        fun: write,
        signature: CommandSignature::positional(&[completers::filename]),
    },

The pattern can be a standalone string literal, implicitly mapping to the base functionality of the command, and a mapping pattern, which maps the alias to the base command plus some combination of flags.

Prompt

Any added aliases are automatically added to the prompt:
image

Commands

The expectation is that this can be merged with only the few commands changed in this PR, and any other commands can be done in follow-up PRs, using the implementations done here as reference.

Breaking!

  • rsort is removed, use :sort --reverse or :sort -r instead
  • clear-register no longer clears all registers if no arguments are provided. For the same effect use :clear-register --all or :clear-register -a instead. This change also means that an argument of some kind MUST be provided.
  • write-*: all write related commands have been collapsed into a single write command.
    • The behaviors of the previous standalone commands can be accessed through flags or through aliases.
    • Config usage with these commands will have to be updated.
    • If only interaction done was through the aliases, then the behavior is as before.

Depends: #11149
Closes: #5828

refactor: no longer special case for bracket lists

refactor: no longer special case end space

This was a hold over from before the `raw` function was added to `Args`.

perf: remove `bytes` field to save 16 bytes

From 56 bytes to 40, saving 16 bytes.

perf: move `in_quotes` field to local variable

perf: move `quote` field to local variable

refactor: remove `is_finished` state from `Args`

test: change example command to `read`

`yank-join` now uses `raw` and thus would not be parsed with the `next`
function so no longer applicable.

refactor: remove unneeded range end for index

refactor: remove backtracking escape check

Instead, it can be tracked as the parser scans through the first time.

refactor: clean up code and add more comments

feat(shellwords): add test for empty values
refactor(commands): ignore unit pattern match

refactor(commands): use `if let` over `matches!`

refactor(commands): ignore unit pattern match

perf(commands):remove unnecessary ref for a &str

refactor(commands): change `MappableCommand` `args` from `Vec<String>` to String

refactor(commands): use `Args::raw` over `fold`ing

refactor: use `rest` in `set_option` command

refactor: use `rest` in `toggle_option` command

chore(dap): add TODOs to switch to `Args`

perf: change `ok_or` to `ok_or_else` to make fn call lazy

feat(shellwords): add `args_count` to `Args`

fix(commands): default to `line_ending` for `yank_join` when args are empty
@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 18, 2024

@the-mikedavis If this looks promising, then I would be willing to see it to merger. If not, then I can wait to get some more feedback before I would think of fully picking this up.

This diff includes changes from the dependent tree. For this pr, there is the flag.rs file, Args::flags in shellwords.rs and then the propagation change in the callback function to take a flags: &[Flag].

The sort command function has the working example implimentation.

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 18, 2024

One friction point I found was that Args is in helix-core where as the Flag struct is in helix-term. This is the reason for:

    let flags = flags
        .iter()
        .map(|flag| flag.long)
        .chain(flags.iter().filter_map(|flag| flag.short));

as Flag cannot be passed into args.flags directly.

edit: Added a trait IntoFlags in shellwords.rs and than implemented t for Flags. This adds more things to shellwords.rs but the ergonomics should be worth it.

@RoloEdits
Copy link
Contributor Author

The prompt logic was changed to deal with indicating optional flags and potentially to indicate if a command takes arguments or not.

        let shellwords = Shellwords::from(input);

        if let Some(typed::TypableCommand {
            name,
            aliases,
            flags,
            doc,
            ..
        }) = typed::TYPABLE_COMMAND_MAP.get(shellwords.command())
        {
            // EXAMPLE:
            // write [<flags>] - write the current buffer to its file.
            // aliases: w
            // flags:
            //     --no-format    exclude formatting operation when saving.
            let mut prompt = String::new();

            prompt.push_str(name);

            if !flags.is_empty() {
                prompt.push_str(" [<flags>]");
            }

            writeln!(prompt, " - {doc}").unwrap();

            if !aliases.is_empty() {
                writeln!(prompt, "aliases: {}", aliases.join(", ")).unwrap();
            }

            if !flags.is_empty() {
                prompt.push_str("flags:\n");

                for flag in *flags {
                    write!(prompt, "    --{}", flag.long).unwrap();

                    if let Some(short) = flag.short {
                        write!(prompt, ", -{short}").unwrap();
                    }

                    // TODO: Need to add if the flag takes any arguments?
                    // --all, -a   <arg>    will save all

                    writeln!(prompt, "    {}", flag.desc).unwrap();
                }
            }

            return Some(prompt.into());
        }

        None

Currently it does not show if the command itself takes arguments:

write [<flags>] - write the current buffer to its file.
aliases: w
flags:
    --no-format    exclude formatting operation when saving.

But there could be something like this added:

write [<flags>] ... - write the current buffer to its file.
                ^ ?
aliases: w
flags:
    --no-format ...    exclude formatting operation when saving.

Where then the docs can explain further.

Another option would be to add something like a Takes enum:

enum Takes {
    None,
    One(&str),
    Many(&str),
}

Where there could be something like "[PATH]" or "[PATHS]" added to One and Many respectively, which can be conditionally added like the rest of the prompt parts.

This would entail adding to both the TypeableCommand and Flag struct.

@RoloEdits RoloEdits changed the title feat(commands): add support for flags feat(commands)!: add support for flags Dec 18, 2024
@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 18, 2024

Should also mention up front that no new dependencies where brought in for this. Its unknown if this remains the case going forward.

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 18, 2024

As brought up here, :write --no-fmt could have an issue of not being able to write a file named --no-fmt, but given how much is exposed to the user when implementing, this could be covered by accounting for if a flag was already encountered:

    let flags = flags
        .iter()
        .map(|flag| flag.long)
        .chain(flags.iter().filter_map(|flag| flag.short));

    let mut encountered = false;

    if let Some(flag) = args.flags(flags) {
        match flag {
            "reverse" | "r" if !encountered => {
                encountered = true;
                sort_impl(cx, true)
            }
            _ => {
                bail!("unhandled command flag `{flag}`, implementation failed to cover all flags.")
            }
        }
    } else {
        sort_impl(cx, false);
    }

The issue of trying to handle it so that all flags can only be given once is that not all commands might want to adhere to this restriction. If Flag was provided, or if the iterator was a (&str, bool) instead, then perhaps something can be built from the knowledge, but as an iterator is lazy, there would be no easy way to skip over already encountered ones and be able to control it externally, as there would need to be bool variables in the calling code anyways.

One other way to handle this was, as said in the comment, to check for --. In this case, the flags method would have this as a check, and if encountered, return None. Added this functionality.

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 18, 2024

One other limitation is that to keep iterating over the potential flags, the iterator must be cloned, as each iteration all flags must be checked for. This is the main mechanism that allows random order flags. But considering this is dealing with &'static str there shouldn't be any real performance hit.

while let Some(flag) = args.flags(flags.clone()) {}

edit:
Added a trait to shellwords.rs that handles both the iterator boilerplate and the clone handing.

    if let Some(flag) = args.flag(&flags) {
        match flag {
            "reverse" | "r" => sort_impl(cx, true),
            _ => {
                bail!("unhandled command flag `{flag}`, implementation failed to cover all flags.")
            }
        }
    } else {
        sort_impl(cx, false);
    }

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 19, 2024

Changed

flags: &[flag!{...}]

to

flags: flags![]

And can be provided with flags using

        flags: flags![
            {
                long: "reverse", // required
                short: "r", // optional
                desc: "sort ranges in selection in reverse order" // required
            }
        ],

@the-mikedavis
Copy link
Member

The changes look pretty good to me so far. I think we will try to get #11149 merged right after the next release (so it has time to be thoroughly tested in master) and then we can hopefully take a proper look at this within the same release cycle.

The code for flags will probably take some design/discussion. &[Flag] is very straightforward and nicely doesn't need fancy stuff like traits but ideally I'd like the flags to be a little more ergonomic to access and the parsing to less verbose (maybe done within Args somehow). What I'm imagining is something closer to how Clap would expose args or the args.rs in helix-term, so for :sort --reverse for example you might define:

#[derive(Default)]
struct SortFlags {
    reverse: bool,
}

and maybe within the implementation of sort you can ask for that type out of args:

let (flags, args) = args.split_flags::<SortFlags>()?; // returns an Err for unknown flags
let sort_reverse = flags.reverse;

Most typable commands would have a sort of no-op type for this (struct NoFlags;). To do this we would probably need a new trait and a macro to help define it. In contrast to Clap we should really try to avoid a proc macro if possible - I think it should be possible to do something like this with a regular declarative macro. So maybe the actual definition of SortFlags might look like...

command_flags! {
    SortFlags;
    reverse: bool, "r", "sort ranges in selection in reverse order";
}

(or something, that's just rough pseudocode)

And the declarative macro would have enough information to implement a few trait functions that could list the flags, shorthands and descriptions, and Args or whatever could use that information to parse them from the shellwords, show flags in the command_mode popup, etc..

@RoloEdits
Copy link
Contributor Author

The changes look pretty good to me so far. I think we will try to get #11149 merged right after the next release (so it has time to be thoroughly tested in master) and then we can hopefully take a proper look at this within the same release cycle.

Sounds good to me. I know there are still a few things for that pr that are still unclear for me, as far as expected parsing rules, so testing is a definite must.

As the for the requirements here, I had also thought a typed flags approach was something worth discussing. I agree that the boilerplate currently leaves some to be desired, but wasn't sure how to cover potentially needed bespoke use cases.

For example, here:

let (flags, args) = args.split_flags::<SortFlags>()?; // returns an Err for unknown flags
let sort_reverse = flags.reverse;

In this alone, we have an Args iterator that gets consumed up to the split and then returns a self or &mut self, so args would need to be mut args in the output. The split also seems like it would only ever support an only-flags-first approach. Meaning yank * --join would never work as a valid syntax. And returning on unknown flags means that -- would have to be passed-in in more instances to force a flag-like input to pass this check: :read -- --format.md.

To solve these, perhaps there could be a Args::try_extract_flags<T: Flag>(&self) that would clone &self so that the injected args: Args can be used the same as before. Then we would have:

let flags = args.try_extract_flags::<SortFlags>()?; // Doesn't return error on unknown flags, but tries to populate all known flags.
if flags.reverse { ... }

In this case though, if you want to pass in an argument that has the same name as a flag then you would need to escape it with something not --, as it would go through all arguments to check if there is a match. Though I imagine this collision wont happen often, something would need to be thought up. Another thought point with this approach means that args would need to progress past the flags after the fact, as the this one is not progressed. So a decision would need to be made on where to compromise. Where perhaps it does consume the arg, but only returns the flag struct. This way no need to reassign args. This would restrict the syntax, but might be better for the rest of the experience.

The error returned would then be if the parse function fails if, for example, a flag is expected to itself take an input, and there was none provided. Otherwise, it just returns the default implimentation for the fields. Doing it this way, though, could bring us back to the unknown flags issue.

As for implimentation details, I can imagine something like:

    // impl Args
    fn try_extract_flags<F: Flag>(&self) -> anyhow::Result<F> {
        let mut flag = F::default();

        let mut args = self.clone();

        while let Some(arg) = args.next() {
            flag.extract(arg, &mut args)?;
        }

        Ok(flag)
    }

With the trait something like:

pub trait Flag: Default {
    fn extract<'a, Args: Iterator<Item = &'a str>>(
        &mut self,
        arg: &'a str,
        mut args: &mut Args,
    ) -> anyhow::Result<()> {
        if arg == "--" {
            return Ok(());
        }

        match arg.trim_start_matches("--").trim_start_matches('-') {
                // Would match the field name: join
            "join" => self.NAME = true,
            // Optional short
            "register" | "r" => {
                self.NAME = args
                    .next()
                    // In the Flag implementing struct, could be an `accepts: Option<&'static str>` so that context can
                    // be provided?
                    .context("`NAME` was not not provided a `ACCEPTS`")?
            }
            _ => {}
        }

        Ok(())
    }
}

If the flag takes multiple arguments then would need to handle that as well. Would also decide if -- would ever be considered a valid argument for a flag or if encountered on an expected argument would mean that one wasn't provided.

As for the prompt, this generative change would significantly increase the complexity for its generation, but its probably something that would be best done regardless. For some smaller refactors, making a prompt method on TypeableCommand could clean up some of the other code where its generated now. The callback could then just take command.prompt.

And lastly, for the macros approach as a whole, contributing to changes and general maintenance in this part of the code would become much more complex. I also think there could be some future large changes to the commands themselves to open up specific use cases, with this potentially including generating commands itself, so wonder how much we want to take into account these potential changes now, with this implimentation; keeping a simpler approach or not.

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 19, 2024

Before I forget, another middle ground approach, if we moved the checks to the try_extract_flags:

    pub fn try_extract_flags<F: Flag>(&mut self) -> anyhow::Result<F> {
        let mut flag = F::default();

        while let Some(arg) = self.next() {
            if arg == "--" {
                return Ok(flag);
            }

            let arg = arg.trim_start_matches("--").trim_start_matches('-');
            
            flag.extract(arg, self)?;
        }

        Ok(flag)
    }

This would only support flag-first-only syntax, but would advance args forward, not needing to do so again after the fact.

With the trait now as:

pub trait Flag: Default {
    fn extract<'a, Args: Iterator<Item = &'a str>>(
        &mut self,
        arg: &'a str,
        args: &mut Args,
    ) -> anyhow::Result<()>;

    fn prompt(&self) -> Option<&'static str> { None }
}

Then when you implement it you would match on the flags like now, but offers a bit more support for bespoke handling.

The prompt part could be implemented here as well, with a convention to follow, but no hard constraints being done for the implementer. It puts more onus on new flag implementors, but id say in one of the most straightforward ways; they can decide if -- is acceptable for their flag now.

In the prompt building phase it would just be a:

if let Some(flags) = command.flags.prompt() { ... }

With the prompt building code moved to TypableCommand::prompt(&self) -> String.

It all comes back to what the dev experience should be like for both implementing, as well as maintaining, the code.

@RoloEdits
Copy link
Contributor Author

Starting to look into what the implimentation would be like.

To start with, needing to store each type on TypeableCommand in the flags field is going to prove tricky. The Flags trait is not object safe, as Default is a supertrait. But even if we only needed to store it so we can prompt, and make a new trait just for this and have an Box<dyn Prompt>, we run into the issue that the struct needs to be instantiated, as associated functions are also not object safe. And to do this we would need to have a unit type with an implimentation of Prompt. But now we have two different types we are working with.

If the flag! was used to define the structs in a separate file, for organization, then the implementer of the flag would need to pull in a differently named struct than what was defined. I'm not sure about having like:

    TypableCommand {
        name: "read",
        aliases: &["r"],
        flags: flags!{
         // ...
        },
        accepts: Some("<path>"),
        doc: "load a file into buffer",
        fun: read,
        signature: CommandSignature::positional(&[completers::filename]),
    },

I don't know if the struct defined here would be accessible outside of where it was defined in the macro.

We also cant just do a $long: <$type as Default>::default() in a per struct impl, as the try_extract_field function takes Flags to instantiate default values. So it doesn't know about per struct impl's.

I'm sure there are some other things here, but even if we can get them to work, this is still a Box for each command. Don't really like that if at all possible.

On the other side of this, using concrete types with generics seems like a non-starter as we have a slice and a hashmap of TypableCommand, which must all have the same type.

Perhaps I'm just not familiar enough with Rust to know the obvious solution, but it seems to me like without a major refactor around TypableCommand, this cannot be done.

@RoloEdits
Copy link
Contributor Author

RoloEdits commented Dec 20, 2024

Well, I guess before all that, the const TYPABLE_COMMAND_LIST: &[TypableCommand] isn't able to refer to interior mutability. Would have to make it a static. And even if I were to use a LazyLock or something, i'm pretty sure it would need to be make a new static for each flag to refer to it. Which, even if there is a way to do this in a nice way, would leak memory for all the Box allocations. This is fine in theory, but if there is ever a real memory leak elsewhere, will complicate the process of tracking down.

@RoloEdits
Copy link
Contributor Author

Pursuing the current implimentation a bit more, as the other way is getting out of hand with "magic", I thought about seeing how I could reduce the boilerplate instead.

Now, along with being able to define the flags right next to the command, flags! handles boilerplate:

    // Flags
    let mut join = false;
    let mut register = String::new();

    flags! {
        for flags, args => {
            "join" | "j" => join = true,
            "register" | "r" => {
                if let Some(reg) = args.next() {
                    register.push_str(reg);
                } else {
                    // Handle if no args were provided to flag
                }
            }
        }
    }

This is something that feels much better than before. The state is still free floating, but its all local still. Building off of what I feel is a strong suit of the current impl, all while being compact:

#[macro_export]
macro_rules! flags {
    // Empty case
    [] => {
        $crate::commands::flag::Flags::empty()
    };
    // Multiple flags case
    [$({ long: $long:expr, $(short: $short:expr,)? desc: $desc:expr $(, accepts: $accepts:expr)? $(,)?}),* $(,)?] => {
        {
            const FLAGS: &[$crate::commands::flag::Flag] = &[
                $(
                    $crate::commands::flag::Flag {
                        long: $long,
                        short: {
                            #[allow(unused_mut, unused_assignments)]
                            let mut short: Option<&'static str> = None;
                            $(short = Some($short);)?
                            short
                        },
                        desc: $desc,
                        accepts: {
                            #[allow(unused_mut, unused_assignments)]
                            let mut accepts: Option<&'static str> = None;
                            $(accepts = Some($accepts);)?
                            accepts
                        },
                    }
                ),*
            ];
            $crate::commands::flag::Flags::new(FLAGS)
        }
    };
    // Extract flags boilerplate
    (for $flags:expr, $args:expr => { $($flag:pat => $action:expr),* $(,)? }) => {
        while let Some(flag) = $args.flag(&$flags) {
            match flag {
                $(
                    $flag => {
                        $action;                    }
                )*
                _ => {
                    anyhow::bail!("unhandled command flag `{}`, implementation failed to cover all flags.", flag);
                }
            }
        }
    };
}

This is in contrast to the monstrosity that was developing with the parsing impl.

@RoloEdits RoloEdits force-pushed the arg-flags branch 3 times, most recently from 284eb1c to 8e83de8 Compare December 21, 2024 16:06
The `--` gets consumed, with the remaining iterator to yield only
command aruguments.
No longer clears if args are empty. Either a register or flag must be
provided.
Moved the prompt building to it.
This allows seperate commands to be collapsed into a single one and then
have their aliases to map to flag operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Switches for typable commands
2 participants