copy paste launcher
parent
9464d393e3
commit
1bdd9249fc
|
@ -0,0 +1,50 @@
|
||||||
|
This is an implementation of an idea we discussed a while ago; a launcher for town-specific
|
||||||
|
commands.
|
||||||
|
|
||||||
|
The idea is to put town commands in one of three places:
|
||||||
|
|
||||||
|
- /town/launcher/core
|
||||||
|
- /town/launcher/contrib
|
||||||
|
- /town/launcher/admin
|
||||||
|
|
||||||
|
and pair each command with a corresponding .yml file.
|
||||||
|
|
||||||
|
For example, the `aup` command is a simple wrapper around elinks that opens our code of conduct. I
|
||||||
|
put the executable `aup` in /town/launcher/core and matched it with /town/launcher/aup.yml. The
|
||||||
|
purpose of the yaml file is to provide documentation for your executable, so `aup.yml` looks like:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
shortDesc: View the town's Acceptable Use Policy
|
||||||
|
longDesc: |
|
||||||
|
This command will open our code of conduct, a type of document that evokes the Acceptable Use
|
||||||
|
Policies that governed servers like this in the past. It will open the elinks browser to a
|
||||||
|
page on the wiki.
|
||||||
|
examples: |
|
||||||
|
$ town aup # open the aup
|
||||||
|
$ town aup --rainbow # open the aup with rainbow colors
|
||||||
|
maintainer: vilmibm
|
||||||
|
```
|
||||||
|
|
||||||
|
and using the launcher is like:
|
||||||
|
|
||||||
|
$ town aup
|
||||||
|
$ town aup --rainbow
|
||||||
|
$ town writo
|
||||||
|
$ town admin ban vilmibm
|
||||||
|
|
||||||
|
You can see all the commands with `town help` as well as their descriptions; `town help
|
||||||
|
aup` would show you the docs from `aup.yml`.
|
||||||
|
|
||||||
|
I'd love feedback on this approach while I wrap up this implementation. I can put it up on
|
||||||
|
git.tilde.town if anyone desires to collaborate (and let me know if you want a git.tilde.town
|
||||||
|
account).
|
||||||
|
|
||||||
|
Remaining TODOs:
|
||||||
|
|
||||||
|
- [ ] make tab completion available for common shells
|
||||||
|
- [ ] document / script submitting a tool for inclusion in contrib
|
||||||
|
- [x] make little wrappers for things like `mail` and `chat`
|
||||||
|
- [x] fix arg passing
|
||||||
|
- [x] test with a command that makes use of stdin/stdout
|
||||||
|
- [x] add all existing commands to the buckets
|
||||||
|
- [x] add to users' paths
|
|
@ -0,0 +1,174 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const binroot = "/town/commands"
|
||||||
|
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "town",
|
||||||
|
Short: "Run commands unique to tilde.town",
|
||||||
|
}
|
||||||
|
|
||||||
|
var adminCmd = &cobra.Command{
|
||||||
|
Use: "admin",
|
||||||
|
Short: "Run administrative commands",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAdmin() (bool, error) {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get information about current user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminGroup, err := user.LookupGroup("admin")
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get admin group info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIds, err := u.GroupIds()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get groups info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, groupId := range groupIds {
|
||||||
|
if groupId == adminGroup.Gid {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommands(targetCmd *cobra.Command, path string) error {
|
||||||
|
binPath := filepath.Join(binroot, path)
|
||||||
|
files, err := ioutil.ReadDir(binPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list directory %s: %s", binPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasSuffix(file.Name(), "yml") {
|
||||||
|
parseCommand(targetCmd, filepath.Join(binPath, file.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandDoc struct {
|
||||||
|
ShortDesc string `yaml:"shortDesc"`
|
||||||
|
LongDesc string `yaml:"longDesc"`
|
||||||
|
Examples string
|
||||||
|
Maintainer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommand(targetCmd *cobra.Command, yamlPath string) {
|
||||||
|
executablePath := strings.TrimSuffix(yamlPath, ".yml")
|
||||||
|
|
||||||
|
// TODO handle when files lack executable bit
|
||||||
|
_, err := os.Stat(executablePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "could not find matching executable for %s; skipping...\n", yamlPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlBytes, err := ioutil.ReadFile(yamlPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "could not read %s; skipping...\n", yamlPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := commandDoc{}
|
||||||
|
err = yaml.Unmarshal(yamlBytes, &doc)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "could not parse %s; skipping...\n", yamlPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc.Maintainer == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s is missing maintainer field; skipping...\n", yamlPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedCmd := &cobra.Command{
|
||||||
|
Use: filepath.Base(executablePath),
|
||||||
|
RunE: execWrapper(executablePath),
|
||||||
|
DisableFlagParsing: true,
|
||||||
|
}
|
||||||
|
if doc.ShortDesc != "" {
|
||||||
|
parsedCmd.Short = doc.ShortDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc.LongDesc != "" {
|
||||||
|
parsedCmd.Long = doc.LongDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedCmd.Long += fmt.Sprintf("\nMaintained by %s; reach out to them via mail or chat with questions", doc.Maintainer)
|
||||||
|
|
||||||
|
if doc.Examples != "" {
|
||||||
|
parsedCmd.Example = doc.Examples
|
||||||
|
}
|
||||||
|
|
||||||
|
targetCmd.AddCommand(parsedCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execWrapper(executablePath string) func(*cobra.Command, []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
execCmd := exec.Command(executablePath, args...)
|
||||||
|
execCmd.Stderr = os.Stderr
|
||||||
|
execCmd.Stdout = os.Stdout
|
||||||
|
execCmd.Stdin = os.Stdin
|
||||||
|
return execCmd.Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cli() int {
|
||||||
|
err := parseCommands(rootCmd, "core")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to parse core commands: %s", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
err = parseCommands(rootCmd, "contrib")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to parse contrib commands: %s", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
admin, err := isAdmin()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to check admin status: %s", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if admin {
|
||||||
|
rootCmd.AddCommand(adminCmd)
|
||||||
|
err = parseCommands(adminCmd, "admin")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to parse admin commands: %s", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I feel like the example/documentation yaml can be frontmatter for non-binary files. to start
|
||||||
|
// i'll just do the accompanying yaml file.
|
||||||
|
rootCmd.Execute()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Exit(cli())
|
||||||
|
}
|
5
go.mod
5
go.mod
|
@ -1,3 +1,8 @@
|
||||||
module git.tilde.town/tildetown/town
|
module git.tilde.town/tildetown/town
|
||||||
|
|
||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/spf13/cobra v1.5.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
13
go.sum
13
go.sum
|
@ -0,0 +1,13 @@
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||||
|
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
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=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
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=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Reference in New Issue