Repository: jacobdeichert/mask (language: rust)

Issue:
Support alternative way to define command headings so that ancestor commands are not required to be present
## Current behavior:

```
## foo

    ```bash
    do something
    ```
```

`mask foo`

## Desired behavior

```
## foo

### bar

    ```bash
    do something
    ```
```

`mask foo.bar`


Discussion / hints:
Hey! So nested headings 3+ should already be supported.

See the subcommands section here: https://github.com/jakedeichert/mask#subcommands

> Nested command structures can easily be created since they are simply defined by the level of markdown heading. H2 (##) is where you define your top-level commands. Every level after that is a subcommand. **The only requirement is that subcommands must have all ancestor commands present in their heading.**

Try this:

~~~md
## foo

Make sure you add "foo" before "bar" since that's the parent command.

### foo bar

```bash
echo "do something"
```

#### foo bar bat

Level 4 should also work.

```bash
echo "do something else"
```
~~~


```sh
mask foo bar
mask foo bar bat
```


The section naming requirement is too strict.

It would be nice if an existing markdown file could be used (i.e. Readme.md) and any subsection within it could be executed.

This would allow a readme to operate like a makefile.
@jgroom33 i'll consider this. I kind of like the strict schema for headings right now, but I'm open to changing my mind in the future. 

The reason I enforced this constraint is because I personally believe it improves readability when someone is reading a maskfile. Some markdown renderers don't provide enough of a distinction between different levels of headings. And when reading raw markdown source, it's a little easier to know which level of heading you're at when the ancestor commands are present... also making it easier for someone to copy/paste into their terminal.

I actually would like to hold this decision for awhile though. I want to see how mask evolves slowly over the next few months. For me, readability is the largest concern. Though, both patterns can be simultaneously supported.

> This would allow a readme to operate like a makefile.

Good news is that #19 will be merged shortly and will allow you to use any markdown file you want :)

Though the requirement _ancestor commands present in their heading_ will still exist for now. 
@jakedeichert For additional consideration. implementing #17 would alleviate the requirement for this: https://gitlab.com/gitlab-org/gitlab-ce/issues/43289
I've rethought this and am on board now. Typing out the parent commands each time makes the raw markdown too cluttered which makes it annoying to read.

I think we should support both ways even if they are mixed within the same file.

```md
## build

### build client
~~~bash
echo client
~~~

### build server
~~~bash
echo server
~~~

#### build server production
~~~bash
echo "server for production"
~~~
```


```md
## build

### client
~~~bash
echo client
~~~

### server
~~~bash
echo server
~~~

#### production
~~~bash
echo "server for production"
~~~
```


Fix patch:
diff --git a/src/parser.rs b/src/parser.rs
index b150e25..dfce78c 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -53,9 +53,8 @@ pub fn build_command_structure(maskfile_contents: String) -> Command {
                 text = "".to_string();
             }
             End(tag) => match tag {
-                Tag::Header(heading_level) => {
-                    let (name, required_args) =
-                        parse_command_name_and_required_args(heading_level, text.clone());
+                Tag::Header(_) => {
+                    let (name, required_args) = parse_command_name_and_required_args(text.clone());
                     current_command.name = name;
                     current_command.required_args = required_args;
                 }
@@ -170,29 +169,35 @@ fn treeify_commands(commands: Vec<Command>) -> Vec<Command> {
     let num_commands = commands.len();
 
     for i in 0..num_commands {
-        let c = &commands[i];
-        let is_last_cmd = i == num_commands - 1;
+        let mut c = commands[i].clone();
 
         // This must be a subcommand
         if c.cmd_level > current_command.cmd_level {
-            current_command.subcommands.push(c.clone());
+            if c.name.starts_with(&current_command.name) {
+                // remove parent command name prefixes from subcommand
+                c.name = c
+                    .name
+                    .strip_prefix(&current_command.name)
+                    .unwrap()
+                    .trim()
+                    .to_string();
+            }
+            current_command.subcommands.push(c);
         }
         // This must be a sibling command
         else if c.cmd_level == current_command.cmd_level {
             // Make sure the initial command doesn't skip itself before it finds children
             if i > 0 {
                 // Found a sibling, so the current command has found all children.
-                command_tree.push(current_command.clone());
-                current_command = c.clone();
+                command_tree.push(current_command);
+                current_command = c;
             }
         }
-
-        // The last command needs to be added regardless, since there's not another iteration of the loop to do so
-        if is_last_cmd && c.cmd_level >= current_command.cmd_level {
-            command_tree.push(current_command.clone());
-        }
     }
 
+    // Adding last command which was not added in the above loop
+    command_tree.push(current_command);
+
     // Treeify all subcommands recursively
     for c in &mut command_tree {
         if !c.subcommands.is_empty() {
@@ -207,29 +212,9 @@ fn treeify_commands(commands: Vec<Command>) -> Vec<Command> {
     command_tree
 }
 
-fn parse_command_name_and_required_args(
-    heading_level: i32,
-    text: String,
-) -> (String, Vec<RequiredArg>) {
-    // Why heading_level > 2? Because level 1 is the root command title (unused)
-    // and level 2 can't be a subcommand so no need to split.
-    let name = if heading_level > 2 {
-        // Takes a subcommand name like this:
-        // "#### db flush postgres (required_arg_name)"
-        // and returns "postgres (required_arg_name)" as the actual name
-        text.clone()
-            .split(" ")
-            .collect::<Vec<&str>>()
-            // Get subcommand after the parent command name
-            .split_at(heading_level as usize - 2)
-            .1
-            .join(" ")
-    } else {
-        text.clone()
-    };
-
+fn parse_command_name_and_required_args(text: String) -> (String, Vec<RequiredArg>) {
     // Find any required arguments. They look like this: (required_arg_name)
-    let name_and_args: Vec<&str> = name.split(|c| c == '(' || c == ')').collect();
+    let name_and_args: Vec<&str> = text.split(|c| c == '(' || c == ')').collect();
     let (name, args) = name_and_args.split_at(1);
     let name = name.join(" ").trim().to_string();
     let mut required_args: Vec<RequiredArg> = vec![];


Test patch: