diff --git a/README.md b/README.md
index df37c03..e1cc764 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Go High-level Open Service Templater
-GHOST creates in working dir fully functional microservice with basic functionality:
+GHOST creates in output dir fully functional microservice code with basic functionality:
- Ready to use Makefile with help target
- Ready to use docker-compose with override template
@@ -16,19 +16,22 @@ GHOST creates in working dir fully functional microservice with basic functional
## Usage
+> [!NOTE]
+> Output directory (relative or absolute). Files will be created at paths relative to this directory. Existing files at target paths will be overwritten.
+
- Example
+ Help
```shell
$ ghost -h
-Usage: ghost [options...]
+Usage: ghost [options...] [arguments...]
OPTIONS
-d, --description (default: Go microservice) Project short description
- -g, --go-image (default: nafigat0r/go:1.24.4) Go docker image
+ -g, --go-image (default: nafigat0r/go:1.26.0) Go docker image
-c, --govulncheck-image (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image
-h, --help display this help message
- -l, --linter-image (default: nafigat0r/golangci-lint:2.1.6) Linter docker image
+ -l, --linter-image (default: nafigat0r/golangci-lint:2.9.0) Linter docker image
-m, --module-name (default: github.com/test/test) Go module name
-n, --name (default: test) Project short name
-t, --shutdown-timeout (default: 10s) Timeout for graceful shutdown
@@ -37,9 +40,9 @@ OPTIONS
ENVIRONMENT
GHOST_DESCRIPTION (default: Go microservice) Project short description
- GHOST_GO_IMAGE (default: nafigat0r/go:1.24.4) Go docker image
+ GHOST_GO_IMAGE (default: nafigat0r/go:1.26.0) Go docker image
GHOST_GOVULNCHECK_IMAGE (default: nafigat0r/govulncheck:1.1.4) Govulncheck docker image
- GHOST_LINTER_IMAGE (default: nafigat0r/golangci-lint:2.1.6) Linter docker image
+ GHOST_LINTER_IMAGE (default: nafigat0r/golangci-lint:2.9.0) Linter docker image
GHOST_MODULE_NAME (default: github.com/test/test) Go module name
GHOST_NAME (default: test) Project short name
GHOST_SHUTDOWN_TIMEOUT (default: 10s) Timeout for graceful shutdown
@@ -47,6 +50,28 @@ ENVIRONMENT
```
+
+ Create REST microservice in dir
+
+```shell
+$ ghost -r dir
+2026-09-19 15:26:46.005 INFO config/config.go:60 Initial config:
+ --args=[dir]
+ --build=develop
+ --desc=GHOST (Go High-level Open Service Templater)
+ --description=Go microservice
+ --go-image=nafigat0r/go:1.26.0
+ --govulncheck-image=nafigat0r/govulncheck:1.1.4
+ --linter-image=nafigat0r/golangci-lint:2.9.0
+ --module-name=github.com/test/test
+ --name=test
+ --shutdown-timeout=10s
+ --with-rest=true
+2026-09-19 15:26:46.005 INFO app/app.go:33 ghost start
+2026-09-19 15:26:46.007 INFO app/app.go:40 ghost shutdown
+```
+
+
#### Docker
diff --git a/cmd/main.go b/cmd/main.go
index faa7a7a..bc4584a 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -4,7 +4,6 @@ import (
"errors"
"io/fs"
- c "github.com/ardanlabs/conf/v3"
"github.com/nafigator/zapper"
"github.com/nafigator/zapper/conf"
"go.uber.org/zap"
@@ -17,7 +16,7 @@ func main() {
defer sync(log)
if err := app.Run(log); err != nil {
- if errors.Is(err, c.ErrHelpWanted) {
+ if errors.Is(err, app.ErrInfo) {
return
}
diff --git a/internal/app/app.go b/internal/app/app.go
index 7721e33..4638266 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -15,14 +15,17 @@ const (
shutdownMsg = "ghost shutdown"
)
-var buildInfo = "develop" //nolint:gochecknoglobals //build-flag
+var (
+ buildInfo = "develop" //nolint:gochecknoglobals //build-flag
+ // ErrInfo signals that the application has output requested information
+ // (help, version) and should exit successfully.
+ ErrInfo = config.ErrInfo
+)
// Run runs application.
func Run(log *zap.SugaredLogger) error {
- var c *config.Conf
- var err error
-
- if c, err = config.Init(buildInfo, log); err != nil {
+ c, err := config.Init(buildInfo, log)
+ if err != nil {
return err
}
diff --git a/internal/app/config/config.go b/internal/app/config/config.go
index 5dc89f6..28fab6e 100644
--- a/internal/app/config/config.go
+++ b/internal/app/config/config.go
@@ -4,10 +4,11 @@ package config
import (
"errors"
"fmt"
+ "io/fs"
+ "os"
"time"
"github.com/ardanlabs/conf/v3"
- "go.uber.org/zap"
)
const (
@@ -15,9 +16,15 @@ const (
cfgPrefix = "GHOST"
)
-type Conf struct {
+type logger interface {
+ Infof(format string, args ...any)
+}
+
+type Conf struct { //nolint:govet // Acknowledged
+ conf.Args
conf.Version
+ OutputDir string `conf:"-"`
Name string `conf:"default:test,short:n,help:Project short name"`
Description string `conf:"default:Go microservice,short:d,help:Project short description"`
ModuleName string `conf:"default:github.com/test/test,short:m,help:Go module name"`
@@ -28,7 +35,11 @@ type Conf struct {
WithREST bool `conf:"default:false,short:r,help:Add HTTP server with REST API functionality"`
}
-func Init(build string, log *zap.SugaredLogger) (*Conf, error) {
+// ErrInfo is returned when the user requested informational output
+// (help, version) and the application should exit without error.
+var ErrInfo = errors.New("info wanted")
+
+func Init(build string, log logger) (*Conf, error) {
var c string
var err error
@@ -40,13 +51,19 @@ func Init(build string, log *zap.SugaredLogger) (*Conf, error) {
}
if c, err = conf.Parse(cfgPrefix, &cfg); err != nil {
- if errors.Is(err, conf.ErrHelpWanted) {
+ if errors.Is(err, conf.ErrHelpWanted) || errors.Is(err, conf.ErrVersionWanted) {
fmt.Println(c) //nolint:forbidigo // Need for raw output of help message
+
+ return nil, ErrInfo
}
return nil, err
}
+ if err = resolveOutputDir(&cfg); err != nil {
+ return nil, err
+ }
+
if c, err = conf.String(&cfg); err != nil {
return nil, err
}
@@ -55,3 +72,20 @@ func Init(build string, log *zap.SugaredLogger) (*Conf, error) {
return &cfg, nil
}
+
+func resolveOutputDir(cfg *Conf) error {
+ cfg.OutputDir = cfg.Num(0)
+ if cfg.OutputDir == "" {
+ return errors.New("output directory not specified")
+ }
+
+ if d, err := os.Stat(cfg.OutputDir); err == nil {
+ if !d.IsDir() {
+ return fmt.Errorf("output dir %q exists and is not a directory", cfg.OutputDir)
+ }
+ } else if !errors.Is(err, fs.ErrNotExist) {
+ return fmt.Errorf("stat %s: %w", cfg.OutputDir, err)
+ }
+
+ return nil
+}
diff --git a/internal/app/generate.go b/internal/app/generate.go
index 9bd97ad..277602a 100644
--- a/internal/app/generate.go
+++ b/internal/app/generate.go
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"os"
+ "path/filepath"
"strings"
"text/template"
@@ -37,7 +38,7 @@ func generate(c *config.Conf) error {
}
for _, t := range tt {
- if err = write(t, fn, vars); err != nil {
+ if err = write(c.OutputDir, t, fn, vars); err != nil {
return err
}
}
@@ -57,23 +58,23 @@ func createDir(d string) error {
return nil
}
-func write(t tp, fn template.FuncMap, vars map[string]any) error {
- if err := createDir(t.dir); err != nil {
+func write(target string, t tp, fn template.FuncMap, vars map[string]any) error {
+ if err := createDir(filepath.Join(target, t.dir)); err != nil {
return err
}
- tpl, err := template.New(t.file).Funcs(fn).Parse(t.src)
+ tpl, err := template.New(t.path).Funcs(fn).Parse(t.src)
if err != nil {
- return err
+ return fmt.Errorf("parse %s: %w", t.path, err)
}
var buf bytes.Buffer
if err = tpl.Execute(&buf, vars); err != nil {
- return fmt.Errorf("execute %s: %w", t.file, err)
+ return fmt.Errorf("execute %s: %w", t.path, err)
}
- if err = os.WriteFile(t.file, buf.Bytes(), fileStrictMode); err != nil {
- return fmt.Errorf("write %s: %w", t.file, err)
+ if err = os.WriteFile(filepath.Join(target, t.path), buf.Bytes(), fileStrictMode); err != nil {
+ return fmt.Errorf("write %s: %w", t.path, err)
}
return nil
diff --git a/internal/app/init.go b/internal/app/init.go
index e8a5a0d..23d6cf1 100644
--- a/internal/app/init.go
+++ b/internal/app/init.go
@@ -12,7 +12,7 @@ import (
type tp struct {
dir string
- file string
+ path string
src string
}
@@ -28,7 +28,7 @@ func transformName(name string) string {
return name
}
-var rename = map[string]string{ //nolint: gochecknoglobals // Acknowledged
+var rename = map[string]string{ //nolint:gochecknoglobals // Acknowledged
"makefile.gotmpl": "Makefile",
"gomod.gotmpl": "go.mod",
"gosum.gotmpl": "go.sum",
@@ -93,7 +93,7 @@ func load(tt tps, rfs fs.FS) (tps, error) {
t := tp{
dir: filepath.Dir(out),
- file: out,
+ path: out,
src: string(b),
}