Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions go/cmd/tfenv/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Package main provides the multi-call entry point for the tfenv Go edition.
//
// When invoked as "tfenv", it dispatches to the CLI subcommand handler.
// When invoked as "terraform" (e.g. via symlink), it delegates to the shim.
//
// Multi-call detection uses the raw basename of os.Args[0] (before symlink
// resolution). A symlink named "terraform" → "tfenv" will see "terraform"
// as the basename and route to the shim.
package main

import (
"os"
"path/filepath"

"github.com/tfutils/tfenv/go/internal/cli"
"github.com/tfutils/tfenv/go/internal/shim"
)

// version is set at build time via -ldflags "-X main.version=...".
// It defaults to "dev" for local builds.
var version = "dev"

func main() {
basename := filepath.Base(os.Args[0])

switch basename {
case "terraform":
os.Exit(shim.Run(os.Args[1:]))
default:
os.Exit(cli.Run(version, os.Args[1:]))
}
}
3 changes: 3 additions & 0 deletions go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/tfutils/tfenv/go

go 1.24
111 changes: 111 additions & 0 deletions go/internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Package cli provides the command dispatch framework for tfenv.
//
// Subcommands are registered in a map of name to handler function.
// Other packages plug in by adding entries to the registry.
package cli

import (
"fmt"
"os"
"sort"
"strings"
)

// Handler is the function signature for a subcommand handler.
// It receives the remaining command-line arguments and returns an exit code.
type Handler func(args []string) int

// command holds metadata for a registered subcommand.
type command struct {
handler Handler
description string
}

// registry maps subcommand names to their handlers and descriptions.
var registry = map[string]command{}

// Register adds a subcommand to the dispatch registry.
func Register(name string, description string, handler Handler) {
registry[name] = command{
handler: handler,
description: description,
}
}

// Run dispatches to the appropriate subcommand based on args.
// It returns an exit code suitable for os.Exit.
func Run(version string, args []string) int {
if len(args) == 0 {
printUsage(version)
return 0
}

subcmd := args[0]

// Handle --version and version as special cases.
if subcmd == "--version" || subcmd == "version" {
fmt.Fprintf(os.Stdout, "tfenv %s\n", version)
return 0
}

// Handle help as a special case.
if subcmd == "help" || subcmd == "--help" || subcmd == "-h" {
printUsage(version)
return 0
}

// Look up the subcommand in the registry.
cmd, ok := registry[subcmd]
if !ok {
fmt.Fprintf(os.Stderr, "tfenv: unknown command %q\n", subcmd)
fmt.Fprintf(os.Stderr, "Run 'tfenv help' for usage.\n")
return 1
}

return cmd.handler(args[1:])
}

// printUsage prints the help text listing all registered subcommands.
func printUsage(version string) {
fmt.Fprintf(os.Stdout, "tfenv %s\n\n", version)
fmt.Fprintf(os.Stdout, "Usage: tfenv <command> [args]\n\n")

// Always include the built-in commands.
builtins := []struct {
name string
desc string
}{
{"help", "Show this help output"},
{"version", "Print tfenv version"},
}

// Collect registered commands and sort them.
var names []string
for name := range registry {
names = append(names, name)
}
sort.Strings(names)

fmt.Fprintf(os.Stdout, "Commands:\n")

// Print built-in commands first.
for _, b := range builtins {
fmt.Fprintf(os.Stdout, " %-16s %s\n", b.name, b.desc)
}

// Print registered commands.
for _, name := range names {
cmd := registry[name]
fmt.Fprintf(os.Stdout, " %-16s %s\n", name, cmd.description)
}

// Build the full list for "Available commands" summary.
var all []string
for _, b := range builtins {
all = append(all, b.name)
}
all = append(all, names...)
sort.Strings(all)

fmt.Fprintf(os.Stdout, "\nAvailable commands: %s\n", strings.Join(all, ", "))
}
54 changes: 54 additions & 0 deletions go/internal/cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cli

import (
"testing"
)

func TestRunVersion(t *testing.T) {
exit := Run("1.2.3", []string{"--version"})
if exit != 0 {
t.Errorf("expected exit code 0, got %d", exit)
}
}

func TestRunVersionSubcommand(t *testing.T) {
exit := Run("1.2.3", []string{"version"})
if exit != 0 {
t.Errorf("expected exit code 0, got %d", exit)
}
}

func TestRunHelp(t *testing.T) {
exit := Run("1.2.3", []string{"help"})
if exit != 0 {
t.Errorf("expected exit code 0, got %d", exit)
}
}

func TestRunNoArgs(t *testing.T) {
exit := Run("1.2.3", []string{})
if exit != 0 {
t.Errorf("expected exit code 0, got %d", exit)
}
}

func TestRunUnknownCommand(t *testing.T) {
exit := Run("1.2.3", []string{"unknown-command"})
if exit != 1 {
t.Errorf("expected exit code 1, got %d", exit)
}
}

func TestRegisterAndRun(t *testing.T) {
Register("test-cmd", "A test command", func(args []string) int {
return 0
})
defer func() {
delete(registry, "test-cmd")
}()

exit := Run("1.2.3", []string{"test-cmd"})
if exit != 0 {
t.Errorf("expected exit code 0, got %d", exit)
}
}
2 changes: 2 additions & 0 deletions go/internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package config handles environment variable loading and state directory resolution.
package config
2 changes: 2 additions & 0 deletions go/internal/install/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package install implements Terraform binary download, verification, and installation.
package install
2 changes: 2 additions & 0 deletions go/internal/list/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package list provides local and remote Terraform version listing.
package list
2 changes: 2 additions & 0 deletions go/internal/logging/logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package logging provides structured logging for the tfenv Go edition.
package logging
2 changes: 2 additions & 0 deletions go/internal/platform/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package platform detects the current OS, architecture, and platform-specific behaviour.
package platform
2 changes: 2 additions & 0 deletions go/internal/resolve/resolve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package resolve implements .terraform-version file discovery and version constraint resolution.
package resolve
15 changes: 15 additions & 0 deletions go/internal/shim/shim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Package shim implements the Terraform shim, intercepting calls to the
// terraform binary and delegating to the correct installed version.
package shim

import (
"fmt"
"os"
)

// Run is the entry point for the terraform shim.
// It is invoked when the binary is called as "terraform" via symlink.
func Run(args []string) int {
fmt.Fprintf(os.Stderr, "terraform shim not yet implemented\n")
return 1
}
12 changes: 12 additions & 0 deletions go/internal/shim/shim_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package shim

import (
"testing"
)

func TestRunReturnsOne(t *testing.T) {
exit := Run([]string{"version"})
if exit != 1 {
t.Errorf("expected exit code 1 (stub not implemented), got %d", exit)
}
}
Loading