Skip to content
Open
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
75 changes: 51 additions & 24 deletions cmd/gopherlings/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"runtime"
"text/tabwriter"
"time"

Expand All @@ -24,13 +25,17 @@ const exercisesDir = "exercises"
// maxRuntime is the maximum time an exercise is allowed to run in watch mode.
const maxRuntime = 5 * time.Second

var reStillGoing = regexp.MustCompile(`(?m)^\s*//\s+I\s+AM\s+STILL\s+GOING`)
var reExercise = regexp.MustCompile(`^(?P<id>\d+)-(?P<name>.+)$`)
var (
reStillGoing = regexp.MustCompile(`(?m)^\s*//\s+I\s+AM\s+STILL\s+GOING`)
reExercise = regexp.MustCompile(`^(?P<id>\d+)-(?P<name>.+)$`)
)

var plain = color.New()
var red = color.New(color.FgRed)
var yellow = color.New(color.FgYellow)
var green = color.New(color.FgGreen)
var (
plain = color.New()
red = color.New(color.FgRed)
yellow = color.New(color.FgYellow)
green = color.New(color.FgGreen)
)

var rootCmd = &cobra.Command{
Use: "gopherlings",
Expand All @@ -47,8 +52,10 @@ func init() {
func main() {
err := rootCmd.Execute()
if err != nil {
red.Fprintln(os.Stderr, err)
os.Exit(1)
_, err = red.Fprintln(os.Stderr, err)
if err != nil {
os.Exit(1)
}
Comment on lines +55 to +58
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main no longer exits with a non-zero status when rootCmd.Execute() returns an error. After printing the error, the program falls through and returns exit code 0 unless printing fails, which will mask failures for callers/CI. Ensure the error path still calls os.Exit(1) (or returns an error to main).

Suggested change
_, err = red.Fprintln(os.Stderr, err)
if err != nil {
os.Exit(1)
}
_, printErr := red.Fprintln(os.Stderr, err)
if printErr != nil {
os.Exit(1)
}
os.Exit(1)

Copilot uses AI. Check for mistakes.
}
}

Expand All @@ -62,13 +69,25 @@ func errExit(err error) {

if errors.Is(err, ErrAllDone) {
// Error: success ;)
plain.Printf(goBuildSomething)
_, err = plain.Printf(goBuildSomething)
if err != nil {
os.Exit(1)
}
os.Exit(0)
} else if errors.Is(err, ErrNoExercisesDir) {
red.Fprintln(os.Stderr, `No "exercises" directory.`)
red.Fprintln(os.Stderr, "Please run this from the root of a Gopherlings repo checkout.")
_, err = red.Fprintln(os.Stderr, `No "exercises" directory.`)
if err != nil {
os.Exit(1)
}
_, err = red.Fprintln(os.Stderr, "Please run this from the root of a Gopherlings repo checkout.")
if err != nil {
os.Exit(1)
}
} else {
red.Fprintln(os.Stderr, err)
_, err = red.Fprintln(os.Stderr, err)
if err != nil {
os.Exit(1)
}
}

os.Exit(1)
Expand Down Expand Up @@ -344,45 +363,53 @@ func runPkg(ctx context.Context, pkgPath string) error {
// subprocess if the context is canceled.

plain.Println("Compiling", pkgPath)
f, err := compilePkg(ctx, pkgPath)
tmpPath, err := compilePkg(ctx, pkgPath)
if err != nil {
return err
}
defer os.Remove(f.Name())
defer os.Remove(tmpPath)

plain.Println("Executing", pkgPath)
plain.Println()
return execFile(ctx, f)
return execFile(ctx, tmpPath)
}

func compilePkg(ctx context.Context, pkg string) (*os.File, error) {
func compilePkg(ctx context.Context, pkg string) (string, error) {
name := filepath.Base(pkg)
if runtime.GOOS == "windows" {
name += ".exe"
}
f, err := os.CreateTemp("", name)
Comment on lines +379 to 382
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Windows, appending .exe to the pattern passed to os.CreateTemp doesn't guarantee the resulting filename ends with .exe (CreateTemp appends random characters after the pattern unless you include *). This can still produce a non-executable path like foo.exe12345, so the Windows suffix issue likely persists. Use a pattern like name+"*.exe" (or create a temp dir and join name+".exe") so the final file actually has the .exe suffix.

Suggested change
if runtime.GOOS == "windows" {
name += ".exe"
}
f, err := os.CreateTemp("", name)
pattern := name
if runtime.GOOS == "windows" {
pattern = name + "*.exe"
}
f, err := os.CreateTemp("", pattern)

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, err
return "", err
}
tmpPath := f.Name()
f.Close()
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of f.Close() is ignored. Since the file must be closed on Windows before it can be executed/overwritten reliably, handle the close error and clean up the temp file on failure.

Suggested change
f.Close()
if err := f.Close(); err != nil {
// Best-effort cleanup if we fail to close the temp file.
_ = os.Remove(tmpPath)
return "", err
}

Copilot uses AI. Check for mistakes.

cmd := exec.CommandContext(ctx, "go", "build", "-o", f.Name(), pkg)
cmd := exec.CommandContext(ctx, "go", "build", "-o", tmpPath, pkg)

stderr := new(bytes.Buffer)
cmd.Stderr = stderr

err = cmd.Run()
if err != nil {
os.Remove(f.Name())
os.Remove(tmpPath)

plain.Println("Compilation failed.")
plain.Println()

red.Println(stderr)
return nil, err
_, printErr := red.Println(stderr)
if printErr != nil {
os.Exit(1)
}
return "", err
}

return f, nil
return tmpPath, nil
}

func execFile(ctx context.Context, f *os.File) error {
cmd := exec.CommandContext(ctx, f.Name())
func execFile(ctx context.Context, tmpPath string) error {
cmd := exec.CommandContext(ctx, tmpPath)

// TODO: Use io.LimitReader to prevent excessive output.
cmd.Stdout = os.Stdout
Expand Down
18 changes: 9 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
module github.com/soypat/gopherlings

go 1.18
go 1.26

require (
github.com/fatih/color v1.13.0
github.com/fsnotify/fsnotify v1.5.4
github.com/spf13/cobra v1.5.0
github.com/fatih/color v1.19.0
github.com/fsnotify/fsnotify v1.9.0
github.com/spf13/cobra v1.10.2
Comment on lines +3 to +8
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description is about a Windows suffix bug, but this change also bumps the module go version from 1.18 to 1.26 and updates multiple dependencies. Raising the go directive is a compatibility-breaking change for users/build environments pinned to older Go versions, so it should be justified in the PR (or reverted if not required for the fix).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

)

require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.42.0 // indirect
)
42 changes: 20 additions & 22 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Loading