diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f1f36e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[bug-report]" +labels: bug +assignees: TLINDEN + +--- + +**Description** + + + +**Steps To Reproduce** + + + +**Expected behavior** + + + +**Version information** + + + +**Additional informations** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..723c9a3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest a feature +title: "[feature-request]" +labels: feature-request +assignees: TLINDEN + +--- + +**Describtion** + + + + +**Version information** + + diff --git a/.github/assets/logo.png b/.github/assets/logo.png new file mode 100644 index 0000000..d9e46fe Binary files /dev/null and b/.github/assets/logo.png differ diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..8b676ca --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,36 @@ +name: build-and-test-gfn +on: [push, pull_request] +jobs: + build: + strategy: + matrix: + version: [1.22] + os: [ubuntu-latest, windows-latest, macos-latest] + name: Build + runs-on: ${{ matrix.os }} + steps: + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + id: go + + - name: checkout + uses: actions/checkout@v3 + + - name: build + run: go build + + - name: test + run: make test + + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.22 + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fde216d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +gfn +coverage.out +releases diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d4c9e15 --- /dev/null +++ b/Makefile @@ -0,0 +1,87 @@ +# Copyright © 2024 Thomas von Dein + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# +# no need to modify anything below +tool = gfn +VERSION = $(shell grep VERSION config.go | head -1 | cut -d '"' -f2) +archs = darwin freebsd linux windows +PREFIX = /usr/local +UID = root +GID = 0 +HAVE_POD := $(shell pod2text -h 2>/dev/null) + +all: buildlocal + + +buildlocal: + CGO_LDFLAGS='-static' go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o $(tool) + +install: buildlocal + install -d -o $(UID) -g $(GID) $(PREFIX)/bin + install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1 + install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/ + install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/ + +clean: + rm -rf $(tool) coverage.out testdata t/out + +test: clean + mkdir -p t/out + go test ./... $(ARGS) + +testlint: test lint + +lint: + golangci-lint run + +lint-full: + golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest + +testfuzzy: clean + go test -fuzz ./... $(ARGS) + +singletest: + @echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v" + go test -run $(TEST) $(ARGS) + +cover-report: + go test ./... -cover -coverprofile=coverage.out + go tool cover -html=coverage.out + +goupdate: + go get -t -u=patch ./... + +buildall: + ./mkrel.sh $(tool) $(VERSION) + +release: buildall + gh release create v$(VERSION) --generate-notes releases/* + +show-versions: buildlocal + @echo "### gfn version:" + @./gfn -V + + @echo + @echo "### go module versions:" + @go list -m all + + @echo + @echo "### go version used for building:" + @grep -m 1 go go.mod + +# lint: +# golangci-lint run -p bugs -p unused diff --git a/README.md b/README.md index 63f7d02..04edc6b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# gfn +# gfn - generate fantasy names for games and stories on the commandline + +![Gfn Logo](https://github.com/TLINDEN/gfn/blob/main/.github/assets/logo.png) + +[![Actions](https://github.com/tlinden/gfnc/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/gfnc/actions) +[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/gfnc/blob/master/LICENSE) +[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/gfnc)](https://goreportcard.com/report/github.com/tlinden/gfnc) Generate fantasy names for games and stories. It uses the fine [fantasyname module](https://github.com/s0rg/fantasyname) by @@ -12,14 +18,51 @@ guide](http://rinkworks.com/namegen/reference.shtml). In case the site vanishes some day, a copy of those documents is contained here in the repository. -# Install +## Installation -Execute +The tool does not have any dependencies. Just download the binary for +your platform from the releases page and you're good to go. + +### Installation using a pre-compiled binary + +Go to the [latest release page](https://github.com/TLINDEN/gfn/releases/latest) +and look for your OS and platform. There are two options to install the binary: + +Directly download the binary for your platform, +e.g. `gfn-linux-amd64-0.0.2`, rename it to `gfn` (or whatever +you like more!) and put it into your bin dir (e.g. `$HOME/bin` or as +root to `/usr/local/bin`). + +Be sure to verify the signature of the binary file. For this also +download the matching `gfn-linux-amd64-0.0.2.sha256` file and: ```shell -% go build -% cp gfn $HOME/bin +cat gfn-linux-amd64-0.0.2.sha25 && sha256sum gfn-linux-amd64-0.0.2 ``` +You should see the same SHA256 hash. + +You may also download a binary tarball for your platform, e.g. +`gfn-linux-amd64-0.0.2.tar.gz`, unpack and install it. GNU Make is +required for this: + +```shell +tar xvfz gfn-linux-amd64-0.0.2.tar.gz +cd gfn-linux-amd64-0.0.2 +sudo make install +``` + +### Installation from source + +You will need the Golang toolchain in order to build from source. GNU +Make will also help but is not strictly neccessary. + +If you want to compile the tool yourself, use `git clone` to clone the +repository. Then execute `go mod tidy` to install all +dependencies. Then just enter `go build` or - if you have GNU Make +installed - `make`. + +To install after building either copy the binary or execute `sudo make +install`. # Usage @@ -34,19 +77,21 @@ To use one of them and limit the number of words generated: ```shell -% gfn JapaneseNamesDiverse -c 24 -yumufuchi afuchin keyu amorekin ekimuwo ashihewani rosa chireki -oterun ruwahi uwamine emiyumu temimon yuwa awayason fuki -emiwa nushiron achihora yomichi saniyutan kewaritsu saroru uhashi +% gfn JapaneseNamesDiverse -n 24 +iyonen isuyaro iwamo remi kikune chikeyu iwamun +ruri orasenin wamo oramamo ironisuru hokoku kumun +ewoyani imanoma enenoya sawo enunumiken wayumu itamachi ``` You can also write a code yourself: ```shell -gfn '!sVm' -c 24 -Quaoobunker Emeemoopsie Angeepookie Osousmoosh Umuisweetie Ustoesnookum Sulealover Imopookie -Skelaesnoogle Echiapookie Cereepoochie Gariwuddle Echaewookie Tiaieschmoopie Queaubooble Athesmoosh -Undousnuggy Urnuigooble Mosoesnuggy Eldoegooble Denoipoochie Mosoosmooch Shyucuddly Tiaeylovey +gfn '!sVm' -n 25 +Angeeschnookum Arysmooch Oraepookie Rothaudoodle Hinaypoochie +Keloosnookum Esteeschnookum Ageausmoosh Erucuddle Tonuilover +Ghaeaschnoogle Seraylover Dynysnoogle Chaibunker Poloebaby +Leroepoochie Tinowuggy Baniaschmoopie Banoesnoogy Taseamoopie +Entheysmooch Ustamooglie Taneidoodle Hatiesnoogy Belacuddle ``` A short outline of the code will be printed if you add the `-h` @@ -54,11 +99,13 @@ parameter: ```shell This is gfn, a fantasy name generator cli. -Usage: gfn [-vlc] +Usage: gfn [-vld] [-c ] [-n ] Options: --c --count How many names to generate +-c --config Config file to use (optional) +-n --number Number of names to generate -l --list List pre-compiled shortcuts +-d --debug Show debugging output -v --version Show program version pattern syntax The letters s, v, V, c, B, C, i, m, M, D, and d @@ -94,6 +141,28 @@ it. For example, !(foo) will emit Foo and v!s will emit a lowercase vowel followed by a capitalized syllable, like eRod. ``` +You can use a config file to store your own codes, once you found one +you like. A configfile is searched in these locations in this order: + +* `/etc/gfn.conf` +* `/usr/local/etc/gfn.conf` +* `$HOME/.config/gfn/config` +* `$HOME/.gfn` + +You may also specify a config file on the commandline using the `-c` +flag. + +You can add multiple codes, here's an example: + +```toml +# example config file +[[Templates]] +morph = "!s(na|ha|ma|va)v" +morphium = "!s(na|ha|ma|va)v(ius|ium|aum|oum|eum)" +``` + +Config files are expected to be in the [TOML format](https://toml.io/en/). + # Report bugs [Please open an issue](https://github.com/TLINDEN/gfn/issues). Thanks! diff --git a/README.pdf b/README.pdf new file mode 100644 index 0000000..aa632af Binary files /dev/null and b/README.pdf differ diff --git a/config.go b/config.go index f6b3700..ea3e0ae 100644 --- a/config.go +++ b/config.go @@ -1,11 +1,31 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package main import ( "fmt" "io" "os" + "path/filepath" + "github.com/knadh/koanf/parsers/toml" "github.com/knadh/koanf/providers/confmap" + "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/providers/posflag" "github.com/knadh/koanf/v2" flag "github.com/spf13/pflag" @@ -104,16 +124,19 @@ var Templates = map[string]string{ } const ( - VERSION string = "0.0.1" - DefaultCount int = 160 // number of words to generate if -c is omitted - Columns int = 8 // number of columns to print - Usage string = `This is gfn, a fantasy name generator cli. + VERSION string = "0.0.2" + DefaultCount int = 160 // number of words to generate if -c is omitted + DefaultColumns int = 10 // number of columns to print + MaxWidth int = 72 // max width of output, adjusts columns + Usage string = `This is gfn, a fantasy name generator cli. -Usage: gfn [-vlc] +Usage: gfn [-vld] [-c ] [-n ] Options: --c --count How many names to generate +-c --config Config file to use (optional) +-n --number Number of names to generate -l --list List pre-compiled shortcuts +-d --debug Show debugging output -v --version Show program version pattern syntax The letters s, v, V, c, B, C, i, m, M, D, and d @@ -150,10 +173,15 @@ vowel followed by a capitalized syllable, like eRod.` ) type Config struct { - Showversion bool `koanf:"version"` // -v - Listshortcuts bool `koanf:"list"` // -l - Code string // arg - Count int `koanf:"count"` // -c + Showversion bool `koanf:"version"` // -v + Debug bool `koanf:"debug"` // -d + Listshortcuts bool `koanf:"list"` // -l + Number int `koanf:"number"` // -c + Templates map[string]string `koanf:"templates"` + Config string `koanf:"config"` + Columns int `koanf:"columns"` // number of columns to use + Code string // arg + WordWidth int // max width of generated words } func InitConfig(output io.Writer) (*Config, error) { @@ -161,7 +189,8 @@ func InitConfig(output io.Writer) (*Config, error) { // Load default values using the confmap provider. if err := kloader.Load(confmap.Provider(map[string]interface{}{ - "count": DefaultCount, + "number": DefaultCount, + "columns": DefaultColumns, }, "."), nil); err != nil { return nil, fmt.Errorf("failed to load default values into koanf: %w", err) } @@ -175,13 +204,44 @@ func InitConfig(output io.Writer) (*Config, error) { // parse commandline flags flagset.BoolP("list", "l", false, "show list of precompiled codes") - flagset.BoolP("version", "V", false, "show program version") - flagset.IntP("count", "c", 1, "how many names to generate") + flagset.BoolP("version", "v", false, "show program version") + flagset.BoolP("debug", "d", false, "enable debug output") + flagset.IntP("number", "n", 1, "number of names to generate") + flagset.StringP("config", "c", "", "config file") if err := flagset.Parse(os.Args[1:]); err != nil { return nil, fmt.Errorf("failed to parse program arguments: %w", err) } + // generate a list of config files to try to load, including the + // one provided via -c, if any + var configfiles []string + + configfile, _ := flagset.GetString("config") + home, _ := os.UserHomeDir() + + if configfile != "" { + configfiles = []string{configfile} + } else { + configfiles = []string{ + "/etc/gfn.conf", "/usr/local/etc/gfn.conf", // unix variants + filepath.Join(home, ".config", "gfn", "config"), + filepath.Join(home, ".gfn"), + "gfn.conf", + } + } + + // Load the config file[s] + for _, cfgfile := range configfiles { + if path, err := os.Stat(cfgfile); !os.IsNotExist(err) { + if !path.IsDir() { + if err := kloader.Load(file.Provider(cfgfile), toml.Parser()); err != nil { + return nil, fmt.Errorf("error loading config file: %w", err) + } + } + } // else: we ignore the file if it doesn't exists + } + // command line setup if err := kloader.Load(posflag.Provider(flagset, ".", kloader), nil); err != nil { return nil, fmt.Errorf("error loading flags: %w", err) @@ -198,5 +258,14 @@ func InitConfig(output io.Writer) (*Config, error) { conf.Code = flagset.Args()[0] } + // merge configured and hardcoded templates + if conf.Templates == nil { + conf.Templates = Templates + } else { + for name, code := range Templates { + conf.Templates[name] = code + } + } + return conf, nil } diff --git a/generate.go b/generate.go index 38c34a5..65f6fb1 100644 --- a/generate.go +++ b/generate.go @@ -1,29 +1,75 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package main import ( "fmt" + "log/slog" "math/rand" "time" fn "github.com/s0rg/fantasyname" ) -func Generate(count int, code string) ([]string, error) { +// Actual fantasy name generation +func Generate(conf *Config) ([]string, error) { rand.Seed(time.Now().UnixNano()) + + // we register each generated word to avoid duplicates, which + // naturally happens every while reg := map[string]int{} - gen, err := fn.Compile(code, fn.Collapse(true)) + // library call + gen, err := fn.Compile(conf.Code, fn.Collapse(true)) if err != nil { return nil, fmt.Errorf("could not compile FN code: %w", err) } - for i := 0; i < count; i++ { + // fetch requested number of names + for i := 0; len(reg) < conf.Number; i++ { name := gen.String() + if !Exists(reg, name) { reg[name] = 1 + + if conf.WordWidth < len(name) { + conf.WordWidth = len(name) + } + } + + // static codes (like 'akx', which is no FN code, just a + // literal) generates just 1 item + if i > conf.Number*2 { + break } } + fmt.Println("fetched names") + + slog.Debug("Generated fantasy names from code", + "code", conf.Code, "count-names", len(reg)) + + // adjust columns, if needed + if conf.WordWidth*conf.Columns > MaxWidth { + conf.Columns = MaxWidth / conf.WordWidth + } + + // we just return a slice of names names := make([]string, len(reg)) i := 0 diff --git a/generate_test.go b/generate_test.go new file mode 100644 index 0000000..0cee322 --- /dev/null +++ b/generate_test.go @@ -0,0 +1,77 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "fmt" + "testing" +) + +var tests = []struct { + name string + want bool + code string +}{ + { + name: "code-ok", + want: true, + code: "!s(na|ha|ma|va)v", + }, + { + name: "code-fail", + want: false, + code: "!s(na|ha|ma|vav", + }, +} + +var conf = &Config{ + Number: 3, +} + +func TestGenerate(t *testing.T) { + for _, tt := range tests { + testname := fmt.Sprintf("generate-%s", tt.name) + t.Run(testname, func(t *testing.T) { + conf.Code = tt.code + + names, err := Generate(conf) + + if err != nil { + if tt.want { + t.Errorf("Generate() returned an unexpected error: %s", err) + } + return + } + + if len(names) != 3 { + t.Errorf("Generate() returned wrong number of results\nExp: %+v\nGot: %+v\n", + conf.Number, len(names)) + return + } + + for idx, name := range names { + if len(name) == 0 { + t.Errorf("Generate() returned empty results\nIndex: %d\nGot: <%s>\n", + idx, name) + return + } + } + }) + } + +} diff --git a/gfn b/gfn deleted file mode 100755 index da5f034..0000000 Binary files a/gfn and /dev/null differ diff --git a/gfn.conf b/gfn.conf new file mode 100644 index 0000000..0332bee --- /dev/null +++ b/gfn.conf @@ -0,0 +1,5 @@ +# example config file +[[Templates]] +morph = "!s(na|ha|ma|va)v" +morphium = "!s(na|ha|ma|va)v(ius|ium|aum|oum|eum)" + diff --git a/go.mod b/go.mod index d04aa73..74a04a6 100644 --- a/go.mod +++ b/go.mod @@ -7,18 +7,24 @@ require github.com/s0rg/fantasyname v1.3.4 require ( github.com/alecthomas/repr v0.4.0 // indirect github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/parsers/toml v0.1.0 // indirect github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/providers/file v0.1.0 // indirect github.com/knadh/koanf/providers/posflag v0.1.0 // indirect github.com/knadh/koanf/v2 v2.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tlinden/yadu v0.1.3 // indirect golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a065457..03d166e 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,18 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= @@ -21,6 +27,10 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/s0rg/fantasyname v1.3.4 h1:zqmKri+2MGVxgRfTm0XViZPtoy0wm9BBc6oqOCuALZw= github.com/s0rg/fantasyname v1.3.4/go.mod h1:XqTfA5mtZAxnjcSkAwijm1FGoI9FkjRyUOWEEbjsRd0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -28,11 +38,14 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 2f34eb3..79df29a 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,43 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package main import ( "fmt" "io" "os" + "runtime/debug" + + "log/slog" + + "github.com/tlinden/yadu" ) func main() { os.Exit(Main(os.Stdout)) } +func TMain() int { + return Main(os.Stdout) +} + func Main(output io.Writer) int { + // parse config file and command line parameters, if any conf, err := InitConfig(output) if err != nil { return Die(err) @@ -22,25 +49,57 @@ func Main(output io.Writer) int { return 0 } + // enable debugging, if needed. We only use log/slog for + // debugging, so there's no need to configure it outside debugging + if conf.Debug { + logLevel := &slog.LevelVar{} + // we're using a more verbose logger in debug mode + buildInfo, _ := debug.ReadBuildInfo() + opts := &yadu.Options{ + Level: logLevel, + AddSource: true, + } + + logLevel.Set(slog.LevelDebug) + + handler := yadu.NewHandler(output, opts) + debuglogger := slog.New(handler).With( + slog.Group("program_info", + slog.Int("pid", os.Getpid()), + slog.String("go_version", buildInfo.GoVersion), + ), + ) + slog.SetDefault(debuglogger) + } + + // just show what we have if conf.Listshortcuts { - ListTemplates(output) + ListTemplates(conf, output) return 0 } + // code argument is mandatory if len(conf.Code) == 0 { fmt.Fprintln(output, Usage) + return 1 } - if Exists(Templates, conf.Code) { - conf.Code = Templates[conf.Code] + // check if we can use a template, otherwise consider the argument + // to be FN code + if Exists(conf.Templates, conf.Code) { + slog.Debug("Argument resolves to template code", + "name", conf.Code, "code", conf.Templates[conf.Code]) + + conf.Code = conf.Templates[conf.Code] } - names, err := Generate(conf.Count, conf.Code) + // all prepared, run baby run + names, err := Generate(conf) if err != nil { return Die(err) } - if err = PrintColumns(names, output); err != nil { + if err = PrintColumns(conf, names, output); err != nil { return Die(err) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..bdba295 --- /dev/null +++ b/main_test.go @@ -0,0 +1,39 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "os" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +// see https://bitfieldconsulting.com/golang/test-scripts + +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "testgfn": TMain, + })) +} + +func TestFfn(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "t", + }) +} diff --git a/mkrel.sh b/mkrel.sh new file mode 100755 index 0000000..6a7d03e --- /dev/null +++ b/mkrel.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Copyright © 2024 Thomas von Dein + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# get list with: go tool dist list +DIST="darwin/amd64 +freebsd/amd64 +linux/amd64 +netbsd/amd64 +openbsd/amd64 +windows/amd64 +freebsd/arm64 +linux/arm64 +netbsd/arm64 +openbsd/arm64 +windows/arm64" + +tool="$1" +version="$2" + +if test -z "$version"; then + echo "Usage: $0 " + exit 1 +fi + +rm -rf releases +mkdir -p releases + + +for D in $DIST; do + os=${D/\/*/} + arch=${D/*\//} + binfile="releases/${tool}-${os}-${arch}-${version}" + + if test "$os" = "windows"; then + binfile="${binfile}.exe" + fi + + tardir="${tool}-${os}-${arch}-${version}" + tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz" + set -x + GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o ${binfile} + mkdir -p ${tardir} + cp ${binfile} README.md LICENSE ${tardir}/ + echo 'tool = gfn +PREFIX = /usr/local +UID = root +GID = 0 + +install: + install -d -o $(UID) -g $(GID) $(PREFIX)/bin + install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1 + install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/ + install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile + tar cpzf ${tarfile} ${tardir} + sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256 + sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256 + rm -rf ${tardir} + set +x +done + diff --git a/printer.go b/printer.go index e8dea56..0191af1 100644 --- a/printer.go +++ b/printer.go @@ -1,15 +1,36 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package main import ( "fmt" "io" + "log/slog" "sort" ) -func ListTemplates(output io.Writer) { +// Output a list of hardcoded FN code templates +func ListTemplates(conf *Config, output io.Writer) { + slog.Debug("Listing configured templates", "templates", conf.Templates) + names := []string{} - for name := range Templates { + for name := range conf.Templates { names = append(names, name) } @@ -20,11 +41,12 @@ func ListTemplates(output io.Writer) { } } -func PrintColumns(names []string, output io.Writer) error { +// Columnar output +func PrintColumns(conf *Config, names []string, output io.Writer) error { count := len(names) // no need for the hassle to calculate columns - if count <= Columns { + if count <= conf.Columns { for _, name := range names { fmt.Fprintln(output, name) } @@ -33,7 +55,7 @@ func PrintColumns(names []string, output io.Writer) error { } // get a transposed list of columns - padlist, max := Getcolumns(names, Columns) + padlist, max := Getcolumns(names, conf.Columns) // make sure there's enough spacing between the columns format := fmt.Sprintf("%%-%ds", max+1) diff --git a/t/debug.txtar b/t/debug.txtar new file mode 100644 index 0000000..74d2829 --- /dev/null +++ b/t/debug.txtar @@ -0,0 +1,2 @@ +exec testgfn -d Vs +stdout 'gfn/generate.go:' diff --git a/t/help.txtar b/t/help.txtar new file mode 100644 index 0000000..6008e83 --- /dev/null +++ b/t/help.txtar @@ -0,0 +1,2 @@ +exec testgfn -h +stdout 'This is gfn' diff --git a/t/list.txtar b/t/list.txtar new file mode 100644 index 0000000..fabc8b5 --- /dev/null +++ b/t/list.txtar @@ -0,0 +1,2 @@ +exec testgfn -l +stdout 'JapaneseNamesDiverse' diff --git a/t/noarg.txtar b/t/noarg.txtar new file mode 100644 index 0000000..90db264 --- /dev/null +++ b/t/noarg.txtar @@ -0,0 +1 @@ +! exec testgfn diff --git a/t/nocode.txtar b/t/nocode.txtar new file mode 100644 index 0000000..7f5e724 --- /dev/null +++ b/t/nocode.txtar @@ -0,0 +1,2 @@ +exec testgfn akx +stdout 'akx' diff --git a/t/nocodeloop.txtar b/t/nocodeloop.txtar new file mode 100644 index 0000000..7ea9b91 --- /dev/null +++ b/t/nocodeloop.txtar @@ -0,0 +1,2 @@ +exec testgfn akx +! stdout 'akx akx' diff --git a/t/template.txtar b/t/template.txtar new file mode 100644 index 0000000..cfeca63 --- /dev/null +++ b/t/template.txtar @@ -0,0 +1,2 @@ +exec testgfn Idiots +stdout 'face' diff --git a/t/valid.txtar b/t/valid.txtar new file mode 100644 index 0000000..d4f6c0b --- /dev/null +++ b/t/valid.txtar @@ -0,0 +1,2 @@ +exec testgfn sVdmi(usus|hehe) +stdout 'usus' diff --git a/t/version.txtar b/t/version.txtar new file mode 100644 index 0000000..47a5ff0 --- /dev/null +++ b/t/version.txtar @@ -0,0 +1,2 @@ +exec testgfn -v +stdout 'This is gfn version' diff --git a/util.go b/util.go index 4397550..3d7552e 100644 --- a/util.go +++ b/util.go @@ -1,3 +1,20 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + package main import "log" diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..74dded3 --- /dev/null +++ b/util_test.go @@ -0,0 +1,139 @@ +/* +Copyright © 2024 Thomas von Dein + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package main + +import ( + "fmt" + "reflect" + "testing" +) + +var testexists = []struct { + name string + want bool + key int + hash map[int]int +}{ + { + name: "have-key", + want: true, + key: 1, + hash: map[int]int{1: 4, 2: 5}, + }, + { + name: "miss-key", + want: false, + key: 3, + hash: map[int]int{1: 4, 2: 5}, + }, +} + +func TestExists(t *testing.T) { + for _, tt := range testexists { + testname := fmt.Sprintf("exists-%s", tt.name) + t.Run(testname, func(t *testing.T) { + got := Exists(tt.hash, tt.key) + + if got != tt.want { + t.Errorf("Exists() returned wrong result\nExp: %+v\nGot: %+v\n", + tt.want, got) + } + }) + } + +} + +var testcontains = []struct { + name string + want bool + key int + list []int +}{ + { + name: "have-item", + want: true, + key: 1, + list: []int{1, 2}, + }, + { + name: "miss-item", + want: false, + key: 3, + list: []int{1, 2}, + }, +} + +func TestContains(t *testing.T) { + for _, tt := range testcontains { + testname := fmt.Sprintf("contains-%s", tt.name) + t.Run(testname, func(t *testing.T) { + got := Contains(tt.list, tt.key) + + if got != tt.want { + t.Errorf("Contains() returned wrong result\nExp: %+v\nGot: %+v\n", + tt.want, got) + } + }) + } + +} + +var testtranspose = []struct { + name string + slice [][]string + expect [][]string +}{ + { + name: "2x2-matrix", + slice: [][]string{ + {"a1", "a2"}, + {"b1", "b2"}, + }, + expect: [][]string{ + {"a1", "b1"}, + {"a2", "b2"}, + }, + }, + { + name: "2x3-matrix", + slice: [][]string{ + {"a1", "a2", "a3"}, + {"b1", "b2", "b3"}, + }, + expect: [][]string{ + {"a1", "b1"}, + {"a2", "b2"}, + {"a3", "b3"}, + }, + }, +} + +func TestTranspose(t *testing.T) { + for _, tt := range testtranspose { + testname := fmt.Sprintf("transpose-%s", tt.name) + t.Run(testname, func(t *testing.T) { + got := Transpose(tt.slice) + + if !reflect.DeepEqual(tt.expect, got) { + t.Errorf("Transpose() returned wrong result\nExp: %+v\nGot: %+v\n", + tt.expect, got) + } + }) + } + +}