Add multi-module release launcher example
This commit is contained in:
247
cmd/releasekit/multi_module_example_test.go
Normal file
247
cmd/releasekit/multi_module_example_test.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiModuleSelectorMapsSelectorsAndTags(t *testing.T) {
|
||||
example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module")
|
||||
helper := filepath.Join(example, "select-module.sh")
|
||||
for _, test := range []struct {
|
||||
mode string
|
||||
value string
|
||||
want string
|
||||
}{
|
||||
{mode: "selector", value: "root", want: "root\tv[01].*\t.\treleasekit-root.json"},
|
||||
{mode: "selector", value: "ckconfig", want: "ckconfig\tintegrations/ckconfig/v*\tintegrations/ckconfig\treleasekit-ckconfig.json"},
|
||||
{mode: "tag", value: "integrations/ckconfig/v1.2.3", want: "ckconfig\tintegrations/ckconfig/v*\tintegrations/ckconfig\treleasekit-ckconfig.json"},
|
||||
{mode: "tag", value: "integrations/core/v2.3.4", want: "core-v2\tintegrations/core/v2.*\tintegrations/core/v2\treleasekit-core-v2.json"},
|
||||
{mode: "tag", value: "v1.2.3", want: "root\tv[01].*\t.\treleasekit-root.json"},
|
||||
} {
|
||||
t.Run(test.mode+" "+test.value, func(t *testing.T) {
|
||||
got := runModuleSelector(t, helper, test.mode, test.value, "")
|
||||
if strings.TrimSpace(got) != test.want {
|
||||
t.Fatalf("selector output = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
mapPath := filepath.Join(t.TempDir(), "release modules.tsv")
|
||||
if err := os.WriteFile(mapPath, []byte("one\tfeature/*\tmodules/one\tprofiles/one.json\ntwo\tfeature/v1.*\tmodules/two\tprofiles/two.json\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if output := runModuleSelectorError(t, helper, "selector", "absent", mapPath); !strings.Contains(output, "no module map row") {
|
||||
t.Fatalf("unknown selector error = %q", output)
|
||||
}
|
||||
if output := runModuleSelectorError(t, helper, "tag", "feature/v1.2.3", mapPath); !strings.Contains(output, "ambiguous") {
|
||||
t.Fatalf("ambiguous event error = %q", output)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(mapPath, []byte("space\tfeature/*\tintegrations/ck config\tprofiles/core release.json\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := runModuleSelector(t, helper, "tag", "feature/v1.2.3", mapPath)
|
||||
want := "space\tfeature/*\tintegrations/ck config\tprofiles/core release.json"
|
||||
if strings.TrimSpace(got) != want {
|
||||
t.Fatalf("paths with spaces = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiModuleLauncherUsesSelectedModuleAndProfile(t *testing.T) {
|
||||
example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module")
|
||||
repository := newCLIRepository(t)
|
||||
copyExampleTree(t, example, repository)
|
||||
|
||||
rootModule := "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n"
|
||||
selectedModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n"
|
||||
writeRepositoryFile(t, repository, "go.mod", rootModule)
|
||||
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", selectedModule)
|
||||
spaceModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.299\n"
|
||||
writeRepositoryFile(t, repository, "integrations/ck config/go.mod", spaceModule)
|
||||
writeRepositoryFile(t, repository, "profiles/core release.json", `{"product":"Example Consumer Integration","module_dir":"integrations/ck config"}`)
|
||||
cliGitRun(t, repository, "add", ".")
|
||||
cliGitRun(t, repository, "commit", "-m", "prepare multi-module launcher fixture")
|
||||
|
||||
mapPath := filepath.Join(t.TempDir(), "release modules.tsv")
|
||||
if err := os.WriteFile(mapPath, []byte("with space\tfeature/*\tintegrations/ck config\tprofiles/core release.json\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shimDir := t.TempDir()
|
||||
goLog := filepath.Join(t.TempDir(), "go commands")
|
||||
argsLog := filepath.Join(t.TempDir(), "tool arguments")
|
||||
shim := `#!/bin/sh
|
||||
set -eu
|
||||
printf 'cwd=%s args=%s\n' "$PWD" "$*" >> "$RELEASEKIT_TEST_GO_LOG"
|
||||
modfile=
|
||||
output=
|
||||
previous=
|
||||
for argument in "$@"; do
|
||||
case "$argument" in -modfile=*) modfile=${argument#-modfile=} ;; esac
|
||||
if [ "$previous" = -o ]; then output=$argument; fi
|
||||
previous=$argument
|
||||
done
|
||||
if [ -n "$modfile" ]; then cat "$modfile" >> "$RELEASEKIT_TEST_GO_LOG"; fi
|
||||
if [ "$1" = build ]; then
|
||||
cat > "$output" <<'TOOL'
|
||||
#!/bin/sh
|
||||
printf '<%s>\n' "$@" > "$RELEASEKIT_TEST_TOOL_ARGS"
|
||||
TOOL
|
||||
chmod +x "$output"
|
||||
fi
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("RELEASEKIT_MODULE_MAP", mapPath)
|
||||
t.Setenv("RELEASEKIT_TEST_GO_LOG", goLog)
|
||||
t.Setenv("RELEASEKIT_TEST_TOOL_ARGS", argsLog)
|
||||
launcher := filepath.Join(repository, "release.sh")
|
||||
command := exec.Command(launcher, "with space", "v1.2.4")
|
||||
var stdout, stderr bytes.Buffer
|
||||
command.Stdout = &stdout
|
||||
command.Stderr = &stderr
|
||||
if err := command.Run(); err != nil {
|
||||
t.Fatalf("launcher error: %v stdout=%q stderr=%q", err, stdout.String(), stderr.String())
|
||||
}
|
||||
|
||||
goCommands, err := os.ReadFile(goLog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantModule := filepath.Join(repository, "integrations", "ck config")
|
||||
if !strings.Contains(string(goCommands), "cwd="+wantModule) || !strings.Contains(string(goCommands), "v0.2.299") || strings.Contains(string(goCommands), "v0.2.197") {
|
||||
t.Fatalf("tool bootstrap did not use selected module metadata: %s", goCommands)
|
||||
}
|
||||
toolArgs, err := os.ReadFile(argsLog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"<publish>", "<--repo>", "<" + repository + ">", "<--config>", "<profiles/core release.json>", "<--expected-module>", "<integrations/ck config>", "<v1.2.4>"} {
|
||||
if !strings.Contains(string(toolArgs), expected) {
|
||||
t.Errorf("publication arguments %q omit %q", toolArgs, expected)
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(goCommands), repository+"/.releasekit-bootstrap.") {
|
||||
t.Fatalf("temporary tool metadata was placed inside the repository: %s", goCommands)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiModuleLauncherBuildsToolFromSelectedDependency(t *testing.T) {
|
||||
example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module")
|
||||
repository := newCLIRepository(t)
|
||||
copyExampleTree(t, example, repository)
|
||||
writeRepositoryFile(t, repository, "go.mod", "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n")
|
||||
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n")
|
||||
cliGitRun(t, repository, "add", ".")
|
||||
cliGitRun(t, repository, "commit", "-m", "prepare selected tool module")
|
||||
|
||||
proxy := writeReleaseKitModuleProxy(t)
|
||||
moduleCache := t.TempDir()
|
||||
t.Setenv("GOMODCACHE", moduleCache)
|
||||
t.Cleanup(func() {
|
||||
_ = filepath.WalkDir(moduleCache, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err == nil && entry.IsDir() {
|
||||
_ = os.Chmod(path, 0o700)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
t.Setenv("GOPROXY", "file://"+filepath.ToSlash(proxy))
|
||||
t.Setenv("GOSUMDB", "off")
|
||||
t.Setenv("GOTOOLCHAIN", "local")
|
||||
moduleBefore, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
command := exec.Command(filepath.Join(repository, "release.sh"), "ckconfig", "v1.2.3")
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("launcher error: %v output=%q", err, output)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
"tool-version=v0.2.198 helper=tool-only dependency",
|
||||
`"publish"`, `"--repo"`, `"` + repository + `"`,
|
||||
`"--config"`, `"releasekit-ckconfig.json"`,
|
||||
`"--expected-module"`, `"integrations/ckconfig"`, `"v1.2.3"`,
|
||||
} {
|
||||
if !strings.Contains(string(output), expected) {
|
||||
t.Errorf("launcher output %q omits %q", output, expected)
|
||||
}
|
||||
}
|
||||
if strings.Contains(string(output), "tool-version=v0.2.197") {
|
||||
t.Fatalf("launcher built the root-selected ReleaseKit version: %s", output)
|
||||
}
|
||||
moduleAfter, err := os.ReadFile(filepath.Join(repository, "integrations", "ckconfig", "go.mod"))
|
||||
if err != nil || !bytes.Equal(moduleAfter, moduleBefore) {
|
||||
t.Fatalf("selected module go.mod changed: got %q want %q error=%v", moduleAfter, moduleBefore, err)
|
||||
}
|
||||
}
|
||||
|
||||
func runModuleSelector(t *testing.T, helper, mode, value, mapPath string) string {
|
||||
t.Helper()
|
||||
command := exec.Command("sh", helper, mode, value)
|
||||
command.Env = os.Environ()
|
||||
if mapPath != "" {
|
||||
command.Env = append(command.Env, "RELEASEKIT_MODULE_MAP="+mapPath)
|
||||
}
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("selector %s %q: %v: %s", mode, value, err, output)
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func runModuleSelectorError(t *testing.T, helper, mode, value, mapPath string) string {
|
||||
t.Helper()
|
||||
command := exec.Command("sh", helper, mode, value)
|
||||
command.Env = append(os.Environ(), "RELEASEKIT_MODULE_MAP="+mapPath)
|
||||
output, err := command.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("selector %s %q unexpectedly succeeded: %s", mode, value, output)
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
func copyExampleTree(t *testing.T, source, destination string) {
|
||||
t.Helper()
|
||||
err := filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relative, err := filepath.Rel(source, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relative == "." {
|
||||
return nil
|
||||
}
|
||||
target := filepath.Join(destination, relative)
|
||||
if entry.IsDir() {
|
||||
return os.MkdirAll(target, 0o700)
|
||||
}
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := info.Mode().Perm()
|
||||
if err := os.WriteFile(target, contents, mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(target, mode)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Errorf("copy multi-module example: %w", err))
|
||||
}
|
||||
}
|
||||
@@ -661,6 +661,19 @@ agreement. Verify the selected module supplies the tool dependency and publicati
|
||||
receives its semantic version/profile. Shell patterns never become arbitrary
|
||||
library tag templates. Update example guidance with the implemented format.
|
||||
|
||||
**Completed:** Added the multi-module profiles, four-column tab-separated
|
||||
mapping, shared POSIX selector, and launcher. The mapping covers root and
|
||||
nested integration tags plus a physical `/v2` module layout. The launcher
|
||||
copies selected module metadata outside the checkout, builds the ReleaseKit
|
||||
version selected by that module, and passes the mapped profile and
|
||||
`--expected-module` to publication. Guidance covers per-module dependency
|
||||
preparation and sequential releases where a sibling dependency is required.
|
||||
Verified selector and ambiguity behavior, unknown selections, paths with
|
||||
spaces, the physical-major row, and an isolated local-proxy launcher run that
|
||||
proved the selected module's tool version and publication arguments. Also
|
||||
passed `sh -n`, `go test ./...`, `go run ./internal/apicheck`, and
|
||||
`git diff --check`.
|
||||
|
||||
## Stage 14: Bootstrap and deliver mapped module tags in Woodpecker
|
||||
|
||||
**Outcome:** A maintained multi-module workflow uses the same declaration and
|
||||
|
||||
55
examples/multi-module/README.md
Normal file
55
examples/multi-module/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Multi-module release example
|
||||
|
||||
This example maps selectors and complete event tags to a physical Go module
|
||||
directory and a repository-root profile. The library derives the release tag
|
||||
from the selected module declaration and physical location; the table only
|
||||
selects the corresponding profile and checkout directory.
|
||||
|
||||
Copy the contents of this directory into a consumer repository root, keeping
|
||||
the listed module directories and profile paths at those locations, then run
|
||||
the launcher from that checkout.
|
||||
|
||||
`release-modules.tsv` is literal tab-separated text with four columns:
|
||||
|
||||
1. Local selector.
|
||||
2. Shell pattern matched against a complete event tag.
|
||||
3. Repository-relative physical module directory (`.` means the root module).
|
||||
4. Repository-relative profile path.
|
||||
|
||||
The checked-in patterns are `v[01].*`, `integrations/ckconfig/v*`, and
|
||||
`integrations/core/v2.*`. The helper passes pattern text to the shell's `case`
|
||||
matcher without `eval`. A tag must match exactly one row; missing and ambiguous
|
||||
matches are configuration errors. Module and profile paths may contain spaces.
|
||||
The `core-v2` row places the Go module physically under `integrations/core/v2`
|
||||
and declares a `/v2` module path. ReleaseKit derives the versioned tag under
|
||||
`integrations/core`, while the table retains the full physical path.
|
||||
|
||||
Before running candidate checks offline, prepare each module's dependencies in
|
||||
its own directory:
|
||||
|
||||
```sh
|
||||
go -C . mod download
|
||||
go -C integrations/ckconfig mod download
|
||||
go -C integrations/core/v2 mod download
|
||||
```
|
||||
|
||||
Then select a module and semantic version with the shared launcher:
|
||||
|
||||
```sh
|
||||
./release.sh root v1.2.3
|
||||
./release.sh ckconfig v1.2.3
|
||||
./release.sh core-v2 v2.1.0
|
||||
```
|
||||
|
||||
The launcher copies the selected module's `go.mod` and `go.sum` to temporary
|
||||
alternate module files outside the checkout, resolves ReleaseKit from that
|
||||
module's local dependency selection, and builds the tool there. It invokes
|
||||
`publish` with the repository profile and `--expected-module` so the library
|
||||
can verify that the profile agrees with the selected table row. The temporary
|
||||
files and tool are removed on exit. As with any `publish` invocation, commit the
|
||||
selected module's release note and prepare its repository before publishing.
|
||||
|
||||
If an integration depends on a sibling release, publish the dependency first,
|
||||
then publish the integration with the version that consumes it. For example,
|
||||
publish the core release before an integration that requires that release.
|
||||
This declaration does not infer dependency order or rerun the standard suite.
|
||||
5
examples/multi-module/go.mod
Normal file
5
examples/multi-module/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module example.com/multi-module-consumer
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197
|
||||
5
examples/multi-module/integrations/ckconfig/go.mod
Normal file
5
examples/multi-module/integrations/ckconfig/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module example.com/multi-module-consumer/integrations/ckconfig
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197
|
||||
5
examples/multi-module/integrations/core/v2/go.mod
Normal file
5
examples/multi-module/integrations/core/v2/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module example.com/multi-module-consumer/integrations/core/v2
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require gitea.maximumdirect.net/eric/ReleaseKit v0.2.197
|
||||
4
examples/multi-module/release-modules.tsv
Normal file
4
examples/multi-module/release-modules.tsv
Normal file
@@ -0,0 +1,4 @@
|
||||
# selector<TAB>tag pattern<TAB>module directory<TAB>profile path
|
||||
root v[01].* . releasekit-root.json
|
||||
ckconfig integrations/ckconfig/v* integrations/ckconfig releasekit-ckconfig.json
|
||||
core-v2 integrations/core/v2.* integrations/core/v2 releasekit-core-v2.json
|
||||
|
48
examples/multi-module/release.sh
Executable file
48
examples/multi-module/release.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "usage: release.sh SELECTOR VERSION" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
selector=$1
|
||||
version=$2
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
repository_dir=$(git -C "$script_dir" rev-parse --show-toplevel)
|
||||
|
||||
bootstrap_dir=$(mktemp -d "$(dirname "$repository_dir")/.releasekit-bootstrap.XXXXXX")
|
||||
selection_file=$bootstrap_dir/module-selection.tsv
|
||||
trap 'rm -rf "$bootstrap_dir"' EXIT
|
||||
"$script_dir/select-module.sh" selector "$selector" > "$selection_file"
|
||||
|
||||
tab=$(printf '\t')
|
||||
IFS="$tab" read -r selected_selector tag_pattern module_dir profile_path < "$selection_file"
|
||||
module_path=$repository_dir
|
||||
if [ "$module_dir" != . ]; then
|
||||
module_path=$repository_dir/$module_dir
|
||||
fi
|
||||
|
||||
cp "$module_path/go.mod" "$bootstrap_dir/releasekit.mod"
|
||||
if [ -f "$module_path/go.sum" ]; then
|
||||
cp "$module_path/go.sum" "$bootstrap_dir/releasekit.sum"
|
||||
else
|
||||
: > "$bootstrap_dir/releasekit.sum"
|
||||
fi
|
||||
|
||||
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit
|
||||
(
|
||||
cd "$module_path"
|
||||
export GOWORK=off
|
||||
export GOTOOLCHAIN=local
|
||||
go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -m "$releasekit_module"
|
||||
go build -mod=mod -modfile="$bootstrap_dir/releasekit.mod" \
|
||||
-o "$bootstrap_dir/releasekit" "$releasekit_module/cmd/releasekit"
|
||||
)
|
||||
|
||||
"$bootstrap_dir/releasekit" publish \
|
||||
--repo "$repository_dir" \
|
||||
--config "$profile_path" \
|
||||
--expected-module "$module_dir" \
|
||||
"$version"
|
||||
4
examples/multi-module/releasekit-ckconfig.json
Normal file
4
examples/multi-module/releasekit-ckconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"product": "Example Consumer Integration",
|
||||
"module_dir": "integrations/ckconfig"
|
||||
}
|
||||
4
examples/multi-module/releasekit-core-v2.json
Normal file
4
examples/multi-module/releasekit-core-v2.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"product": "Example Consumer Core v2",
|
||||
"module_dir": "integrations/core/v2"
|
||||
}
|
||||
4
examples/multi-module/releasekit-root.json
Normal file
4
examples/multi-module/releasekit-root.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"product": "Example Consumer",
|
||||
"module_dir": "."
|
||||
}
|
||||
70
examples/multi-module/select-module.sh
Executable file
70
examples/multi-module/select-module.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
echo "usage: select-module.sh selector|tag VALUE" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
mode=$1
|
||||
value=$2
|
||||
case "$mode" in
|
||||
selector|tag) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd)
|
||||
mapping=${RELEASEKIT_MODULE_MAP:-$script_dir/release-modules.tsv}
|
||||
[ -f "$mapping" ] || { echo "module map not found: $mapping" >&2; exit 2; }
|
||||
|
||||
tab=$(printf '\t')
|
||||
matches=0
|
||||
selected_selector=
|
||||
selected_pattern=
|
||||
selected_module=
|
||||
selected_profile=
|
||||
|
||||
while IFS="$tab" read -r selector pattern module profile || [ -n "${selector:-}${pattern:-}${module:-}${profile:-}" ]; do
|
||||
case "${selector:-}" in
|
||||
''|\#*) continue ;;
|
||||
esac
|
||||
fields=$(awk -F '\t' '{ print NF }' <<EOF
|
||||
$selector$tab$pattern$tab$module$tab$profile
|
||||
EOF
|
||||
)
|
||||
[ "$fields" -eq 4 ] || { echo "module map row must have four tab-separated fields" >&2; exit 2; }
|
||||
[ -n "$pattern" ] && [ -n "$module" ] && [ -n "$profile" ] || {
|
||||
echo "module map row has an empty field for selector $selector" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
matched=false
|
||||
if [ "$mode" = selector ]; then
|
||||
[ "$selector" = "$value" ] && matched=true
|
||||
else
|
||||
# The value remains a shell pattern in this case statement; it is never eval'd.
|
||||
case "$value" in
|
||||
$pattern) matched=true ;;
|
||||
esac
|
||||
fi
|
||||
if [ "$matched" = true ]; then
|
||||
matches=$((matches + 1))
|
||||
selected_selector=$selector
|
||||
selected_pattern=$pattern
|
||||
selected_module=$module
|
||||
selected_profile=$profile
|
||||
fi
|
||||
done < "$mapping"
|
||||
|
||||
if [ "$matches" -ne 1 ]; then
|
||||
if [ "$matches" -eq 0 ]; then
|
||||
echo "no module map row matches $mode $value" >&2
|
||||
else
|
||||
echo "module map has ambiguous rows for $mode $value" >&2
|
||||
fi
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s\t%s\t%s\t%s\n' \
|
||||
"$selected_selector" "$selected_pattern" "$selected_module" "$selected_profile"
|
||||
Reference in New Issue
Block a user