Skip to content

Common options passed to parent not accessible to subcommands. #606

@lucasshiva

Description

@lucasshiva

When using class inheritance to share common options between commands, options passed to parent commands are not accessible to subcommands, even though both commands inherit from the same base class with shared option definitions. The same issue is present with other ways of sharing options.

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option

class CommonOptions : OptionGroup("Common options") {
    val verbose by option("-v", "--verbose")
        .flag(default = false, defaultForHelp = "false")
        .help("Show verbose output")
}

open class MyCli : CliktCommand() {
    override val invokeWithoutSubcommand: Boolean = true
    val commonOptions by CommonOptions()

    override fun run() {
        echo("Root verbose: ${commonOptions.verbose}")
        // Not checking `currentContext.invokedSubcommand == null` on purpose to always print `commonOptions`
    }
}

class Count : MyCli() {
    override fun run() {
        echo("Count verbose: ${commonOptions.verbose}")
    }
}

fun main(args: Array<String>) {
    MyCli().subcommands(Count()).main(args)
}

Current output:

$ mycli -v count
Root verbose: true
Count verbose: false    # ❌ Should be true

$ mycli count -v  
Root verbose: false
Count verbose: true     # ✅ Works as expected

Expected output:

$ mycli -v count
Root verbose: true
Count verbose: true     # ✅ Should inherit parent's value

$ mycli count -v
Root verbose: false     # ✅ OK - subcommand option doesn't affect parent, though I wouldn't mind if it did.
Count verbose: true     # ✅ Works as expected

Comparison with other languages:

  • Go (Cobra):
// Same variable reference shared across all commands
var verbose bool

var rootCmd = &cobra.Command{
    Use:   "root",
    Short: "My root command for a basic Cobra CLI.",
}

var countCmd = &cobra.Command{
	Use:   "count",
	Short: "Count command for a basic Cobra CLI.",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Is verbose: ", verbose)
	},
}

func main() {
    rootCmd.AddCommand(countCmd)
    rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
}
  • C# (DotMake)
[CliCommand(ShortFormAutoGenerate = false)]
public class RootCommand
{
    [CliOption(Description = "Whether to show verbose output", Recursive = true, Aliases = ["-v"])]
    public bool Verbose { get; set; }
}

[CliCommand(Parent = typeof(RootCommand))]
public class CountCommand
{
    // RootCommand is injected automatically, so we can access all its options. 
    public required RootCommand RootCommand { get; set; }
    
    public void Run()
    {
        // This feels weird to use, but at least it works.
        Console.WriteLine($"Verbose: {RootCommand.Verbose}");
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions