Skip to content

Commit cc72632

Browse files
authored
feat: Go edition scaffolding, CLI framework & multi-call entry point (#489)
feat: Go edition scaffolding, CLI framework & multi-call entry point
2 parents 6ae6519 + b3c9b67 commit cc72632

12 files changed

Lines changed: 239 additions & 0 deletions

File tree

go/cmd/tfenv/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Package main provides the multi-call entry point for the tfenv Go edition.
2+
//
3+
// When invoked as "tfenv", it dispatches to the CLI subcommand handler.
4+
// When invoked as "terraform" (e.g. via symlink), it delegates to the shim.
5+
//
6+
// Multi-call detection uses the raw basename of os.Args[0] (before symlink
7+
// resolution). A symlink named "terraform" → "tfenv" will see "terraform"
8+
// as the basename and route to the shim.
9+
package main
10+
11+
import (
12+
"os"
13+
"path/filepath"
14+
15+
"github.com/tfutils/tfenv/go/internal/cli"
16+
"github.com/tfutils/tfenv/go/internal/shim"
17+
)
18+
19+
// version is set at build time via -ldflags "-X main.version=...".
20+
// It defaults to "dev" for local builds.
21+
var version = "dev"
22+
23+
func main() {
24+
basename := filepath.Base(os.Args[0])
25+
26+
switch basename {
27+
case "terraform":
28+
os.Exit(shim.Run(os.Args[1:]))
29+
default:
30+
os.Exit(cli.Run(version, os.Args[1:]))
31+
}
32+
}

go/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/tfutils/tfenv/go
2+
3+
go 1.24

go/internal/cli/cli.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Package cli provides the command dispatch framework for tfenv.
2+
//
3+
// Subcommands are registered in a map of name to handler function.
4+
// Other packages plug in by adding entries to the registry.
5+
package cli
6+
7+
import (
8+
"fmt"
9+
"os"
10+
"sort"
11+
"strings"
12+
)
13+
14+
// Handler is the function signature for a subcommand handler.
15+
// It receives the remaining command-line arguments and returns an exit code.
16+
type Handler func(args []string) int
17+
18+
// command holds metadata for a registered subcommand.
19+
type command struct {
20+
handler Handler
21+
description string
22+
}
23+
24+
// registry maps subcommand names to their handlers and descriptions.
25+
var registry = map[string]command{}
26+
27+
// Register adds a subcommand to the dispatch registry.
28+
func Register(name string, description string, handler Handler) {
29+
registry[name] = command{
30+
handler: handler,
31+
description: description,
32+
}
33+
}
34+
35+
// Run dispatches to the appropriate subcommand based on args.
36+
// It returns an exit code suitable for os.Exit.
37+
func Run(version string, args []string) int {
38+
if len(args) == 0 {
39+
printUsage(version)
40+
return 0
41+
}
42+
43+
subcmd := args[0]
44+
45+
// Handle --version and version as special cases.
46+
if subcmd == "--version" || subcmd == "version" {
47+
fmt.Fprintf(os.Stdout, "tfenv %s\n", version)
48+
return 0
49+
}
50+
51+
// Handle help as a special case.
52+
if subcmd == "help" || subcmd == "--help" || subcmd == "-h" {
53+
printUsage(version)
54+
return 0
55+
}
56+
57+
// Look up the subcommand in the registry.
58+
cmd, ok := registry[subcmd]
59+
if !ok {
60+
fmt.Fprintf(os.Stderr, "tfenv: unknown command %q\n", subcmd)
61+
fmt.Fprintf(os.Stderr, "Run 'tfenv help' for usage.\n")
62+
return 1
63+
}
64+
65+
return cmd.handler(args[1:])
66+
}
67+
68+
// printUsage prints the help text listing all registered subcommands.
69+
func printUsage(version string) {
70+
fmt.Fprintf(os.Stdout, "tfenv %s\n\n", version)
71+
fmt.Fprintf(os.Stdout, "Usage: tfenv <command> [args]\n\n")
72+
73+
// Always include the built-in commands.
74+
builtins := []struct {
75+
name string
76+
desc string
77+
}{
78+
{"help", "Show this help output"},
79+
{"version", "Print tfenv version"},
80+
}
81+
82+
// Collect registered commands and sort them.
83+
var names []string
84+
for name := range registry {
85+
names = append(names, name)
86+
}
87+
sort.Strings(names)
88+
89+
fmt.Fprintf(os.Stdout, "Commands:\n")
90+
91+
// Print built-in commands first.
92+
for _, b := range builtins {
93+
fmt.Fprintf(os.Stdout, " %-16s %s\n", b.name, b.desc)
94+
}
95+
96+
// Print registered commands.
97+
for _, name := range names {
98+
cmd := registry[name]
99+
fmt.Fprintf(os.Stdout, " %-16s %s\n", name, cmd.description)
100+
}
101+
102+
// Build the full list for "Available commands" summary.
103+
var all []string
104+
for _, b := range builtins {
105+
all = append(all, b.name)
106+
}
107+
all = append(all, names...)
108+
sort.Strings(all)
109+
110+
fmt.Fprintf(os.Stdout, "\nAvailable commands: %s\n", strings.Join(all, ", "))
111+
}

go/internal/cli/cli_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestRunVersion(t *testing.T) {
8+
exit := Run("1.2.3", []string{"--version"})
9+
if exit != 0 {
10+
t.Errorf("expected exit code 0, got %d", exit)
11+
}
12+
}
13+
14+
func TestRunVersionSubcommand(t *testing.T) {
15+
exit := Run("1.2.3", []string{"version"})
16+
if exit != 0 {
17+
t.Errorf("expected exit code 0, got %d", exit)
18+
}
19+
}
20+
21+
func TestRunHelp(t *testing.T) {
22+
exit := Run("1.2.3", []string{"help"})
23+
if exit != 0 {
24+
t.Errorf("expected exit code 0, got %d", exit)
25+
}
26+
}
27+
28+
func TestRunNoArgs(t *testing.T) {
29+
exit := Run("1.2.3", []string{})
30+
if exit != 0 {
31+
t.Errorf("expected exit code 0, got %d", exit)
32+
}
33+
}
34+
35+
func TestRunUnknownCommand(t *testing.T) {
36+
exit := Run("1.2.3", []string{"unknown-command"})
37+
if exit != 1 {
38+
t.Errorf("expected exit code 1, got %d", exit)
39+
}
40+
}
41+
42+
func TestRegisterAndRun(t *testing.T) {
43+
Register("test-cmd", "A test command", func(args []string) int {
44+
return 0
45+
})
46+
defer func() {
47+
delete(registry, "test-cmd")
48+
}()
49+
50+
exit := Run("1.2.3", []string{"test-cmd"})
51+
if exit != 0 {
52+
t.Errorf("expected exit code 0, got %d", exit)
53+
}
54+
}

go/internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package config handles environment variable loading and state directory resolution.
2+
package config

go/internal/install/install.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package install implements Terraform binary download, verification, and installation.
2+
package install

go/internal/list/list.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package list provides local and remote Terraform version listing.
2+
package list

go/internal/logging/logging.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package logging provides structured logging for the tfenv Go edition.
2+
package logging

go/internal/platform/platform.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package platform detects the current OS, architecture, and platform-specific behaviour.
2+
package platform

go/internal/resolve/resolve.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package resolve implements .terraform-version file discovery and version constraint resolution.
2+
package resolve

0 commit comments

Comments
 (0)