diff --git a/discovery.go b/discovery.go index 7cc1fd5..a251864 100644 --- a/discovery.go +++ b/discovery.go @@ -8,11 +8,12 @@ import ( ) type discoverer struct { - maxDepth int - onError func(error) - executor ExecutorFunc - contracts []Contract - cache Cache + maxDepth int + onError func(error) + executor ExecutorFunc + contracts []Contract + cache Cache + moduleMetadataFilename string } type DiscoveryContext interface { @@ -25,11 +26,12 @@ type DiscoveryContext interface { func (d *discoverer) Next() DiscoveryContext { return &discoverer{ - maxDepth: d.MaxDepth() - 1, - onError: d.onError, - executor: d.executor, - contracts: d.contracts, - cache: d.cache, + maxDepth: d.MaxDepth() - 1, + onError: d.onError, + executor: d.executor, + contracts: d.contracts, + cache: d.cache, + moduleMetadataFilename: d.moduleMetadataFilename, } } @@ -38,11 +40,12 @@ var _ DiscoveryContext = &discoverer{} func (e *Entrypoint) discoverIn(paths []string) Commands { all := Commands{} d := &discoverer{ - onError: e.onError, - executor: e.executor, - maxDepth: e.maxDepth, - contracts: e.contracts, - cache: e.cache, + onError: e.onError, + executor: e.executor, + maxDepth: e.maxDepth, + contracts: e.contracts, + cache: e.cache, + moduleMetadataFilename: e.moduleMetadataFilename, } for _, path := range paths { cmds, _ := d.DiscoverIn(path, e) @@ -75,6 +78,12 @@ func (d *discoverer) DiscoverIn(path string, parent Command) (Commands, []error) } for _, file := range files { + // The metadata file describes a module; it is never itself a command, + // even if it is executable. + if d.moduleMetadataFilename != "" && file.Name() == d.moduleMetadataFilename { + continue + } + if cmd, err := d.buildCommand(path, parent, file); err != nil { if d.onError != nil { d.onError(err) diff --git a/discovery_test.go b/discovery_test.go index 068406e..025f661 100644 --- a/discovery_test.go +++ b/discovery_test.go @@ -112,6 +112,35 @@ func TestDiscoverInWithMaxDepth(t *testing.T) { } } +func TestDiscoverInSkipsModuleMetadataFiles(t *testing.T) { + // A module's metadata file describes the module; it is not a command. + // Packages sometimes ship the file with the executable bit set, which + // would otherwise match a contract and get probed as a command. + dir := t.TempDir() + module := filepath.Join(dir, "mymod") + assert.NoError(t, os.Mkdir(module, 0755)) + assert.NoError(t, os.WriteFile(filepath.Join(module, ".exoskeleton"), []byte("# SUMMARY: my module\n"), 0755)) + assert.NoError(t, os.WriteFile(filepath.Join(module, "hello"), []byte("#!/bin/sh\n# SUMMARY: says hello\n"), 0755)) + + d := discoverer{ + maxDepth: -1, + executor: defaultExecutor, + contracts: defaultContracts(), + moduleMetadataFilename: ".exoskeleton", + } + cmds, errs := d.DiscoverIn(dir, nil) + assert.Empty(t, errs) + + all, flattenErrs := cmds.Flatten() + assert.Empty(t, flattenErrs) + + var names []string + for _, cmd := range all { + names = append(names, Usage(cmd)) + } + assert.Equal(t, []string{"mymod", "mymod hello"}, names) +} + func TestDiscovererBuildsCommand(t *testing.T) { var parent Command = &builtinCommand{}