Skip to content

Commit

Permalink
use parts of go internals to recreate command functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
rusq committed Oct 23, 2022
1 parent 650288f commit f84530d
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 1 deletion.
70 changes: 70 additions & 0 deletions cmd/slackdump/internal/base/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ package base
import (
"context"
"flag"
"fmt"
"os"
"strings"
"sync"
)

var CmdName string

// A Command is an implementation of a slackdump command.
type Command struct {
// Run runs the command.
Expand All @@ -30,4 +36,68 @@ type Command struct {

// Flag is a set of flags specific to this command.
Flag flag.FlagSet

// Commands lists the available commands and help topics.
// The order here is the order in which they are printed by 'go help'.
// Note that subcommands are in general best avoided.
Commands []*Command
}

var Slackdump = &Command{
UsageLine: "slackdump",
Long: `Slackdump is a tool for exporting Slack conversations, emojis, users, etc.`,
// Commands initialised in main.
}

var exitStatus = 0
var exitMu sync.Mutex

func SetExitStatus(n int) {
exitMu.Lock()
if exitStatus < n {
exitStatus = n
}
exitMu.Unlock()
}

func Exit() {
os.Exit(exitStatus)
}

// Runnable reports whether the command can be run; otherwise
// it is a documentation pseudo-command such as importpath.
func (c *Command) Runnable() bool {
return c.Run != nil
}

// LongName returns the command's long name: all the words in the usage line between "go" and a flag or argument,
func (c *Command) LongName() string {
name := c.UsageLine
if i := strings.Index(name, " ["); i >= 0 {
name = name[:i]
}
if name == "slackdump" {
return ""
}
return strings.TrimPrefix(name, "slackdump ")
}

// Name returns the command's short name: the last word in the usage line before a flag or argument.
func (c *Command) Name() string {
name := c.LongName()
if i := strings.LastIndex(name, " "); i >= 0 {
name = name[i+1:]
}
return name
}

// Usage is the usage-reporting function, filled in by package main
// but here for reference by other packages.
var Usage func()

func (c *Command) Usage() {
fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine)
fmt.Fprintf(os.Stderr, "Run 'slackdump help %s' for details.\n", c.LongName())
SetExitStatus(2)
Exit()
}
157 changes: 157 additions & 0 deletions cmd/slackdump/internal/help/help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// Copyright 2022 rusq, GPL 3.0.
//
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package help implements "slackdump help" command.
package help

import (
"bufio"
"bytes"
"fmt"
"html/template"
"io"
"log"
"os"
"strings"
"unicode"
"unicode/utf8"

"github.com/rusq/slackdump/v2/cmd/slackdump/internal/base"
)

func PrintUsage(w io.Writer, cmd *base.Command) {
bw := bufio.NewWriter(w)
tmpl(bw, usageTemplate, cmd)
bw.Flush()
}

// tmpl executes the given template text on data, writing the result to w.
func tmpl(w io.Writer, text string, data any) {
t := template.New("top")
t.Funcs(template.FuncMap{"trim": strings.TrimSpace, "capitalize": capitalize})
template.Must(t.Parse(text))
ew := &errWriter{w: w}
err := t.Execute(ew, data)
if ew.err != nil {
// I/O error writing. Ignore write on closed pipe.
if strings.Contains(ew.err.Error(), "pipe") {
base.SetExitStatus(1)
base.Exit()
}
log.Fatalf("writing output: %v", ew.err)
}
if err != nil {
panic(err)
}
}

func capitalize(s string) string {
if s == "" {
return s
}
r, n := utf8.DecodeRuneInString(s)
return string(unicode.ToTitle(r)) + s[n:]
}

// An errWriter wraps a writer, recording whether a write error occurred.
type errWriter struct {
w io.Writer
err error
}

func (w *errWriter) Write(b []byte) (int, error) {
n, err := w.w.Write(b)
if err != nil {
w.err = err
}
return n, err
}

// Help implements the 'help' command.
func Help(w io.Writer, args []string) {
// 'go help documentation' generates doc.go.
if len(args) == 1 && args[0] == "documentation" {
fmt.Fprintln(w, "// Copyright 2011 The Go Authors. All rights reserved.")
fmt.Fprintln(w, "// Use of this source code is governed by a BSD-style")
fmt.Fprintln(w, "// license that can be found in the LICENSE file.")
fmt.Fprintln(w)
fmt.Fprintln(w, "// Code generated by mkalldocs.sh; DO NOT EDIT.")
fmt.Fprintln(w, "// Edit the documentation in other files and rerun mkalldocs.sh to generate this one.")
fmt.Fprintln(w)
buf := new(bytes.Buffer)
PrintUsage(buf, base.Slackdump)
usage := &base.Command{Long: buf.String()}
cmds := []*base.Command{usage}
for _, cmd := range base.Slackdump.Commands {
// Avoid duplication of the "get" documentation.
cmds = append(cmds, cmd)
cmds = append(cmds, cmd.Commands...)
}
tmpl(&commentWriter{W: w}, documentationTemplate, cmds)
fmt.Fprintln(w, "package main")
return
}

cmd := base.Slackdump
Args:
for i, arg := range args {
for _, sub := range cmd.Commands {
if sub.Name() == arg {
cmd = sub
continue Args
}
}

// helpSuccess is the help command using as many args as possible that would succeed.
helpSuccess := "slackdump help"
if i > 0 {
helpSuccess += " " + strings.Join(args[:i], " ")
}
fmt.Fprintf(os.Stderr, "go help %s: unknown help topic. Run '%s'.\n", strings.Join(args, " "), helpSuccess)
base.SetExitStatus(2) // failed at 'go help cmd'
base.Exit()
}

if len(cmd.Commands) > 0 {
PrintUsage(os.Stdout, cmd)
} else {
tmpl(os.Stdout, helpTemplate, cmd)
}
// not exit 2: succeeded at 'go help cmd'.
return
}

// commentWriter writes a Go comment to the underlying io.Writer,
// using line comment form (//).
type commentWriter struct {
W io.Writer
wroteSlashes bool // Wrote "//" at the beginning of the current line.
}

func (c *commentWriter) Write(p []byte) (int, error) {
var n int
for i, b := range p {
if !c.wroteSlashes {
s := "//"
if b != '\n' {
s = "// "
}
if _, err := io.WriteString(c.W, s); err != nil {
return n, err
}
c.wroteSlashes = true
}
n0, err := c.W.Write(p[i : i+1])
n += n0
if err != nil {
return n, err
}
if b == '\n' {
c.wroteSlashes = false
}
}
return len(p), nil
}
37 changes: 37 additions & 0 deletions cmd/slackdump/internal/help/templates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package help

var (
helpTemplate = `{{if .Runnable}}usage: {{.UsageLine}}
{{end}}{{.Long | trim}}
`
usageTemplate = `{{.Long | trim}}
Usage:
{{.UsageLine}} <command> [arguments]
The commands are:
{{range .Commands}}{{if or (.Runnable) .Commands}}
{{.Name | printf "%-11s"}} {{.Short}}{{end}}{{end}}
Use "slackdump help{{with .LongName}} {{.}}{{end}} <command>" for more information about a command.
{{if eq (.UsageLine) "slackdump"}}
Additional help topics:
{{range .Commands}}{{if and (not .Runnable) (not .Commands)}}
{{.Name | printf "%-15s"}} {{.Short}}{{end}}{{end}}
Use "slackdump help{{with .LongName}} {{.}}{{end}} <topic>" for more information about that topic.
{{end}}
`
documentationTemplate = `{{range .}}{{if .Short}}{{.Short | capitalize}}
{{end}}{{if .Commands}}` + usageTemplate + `{{else}}{{if .Runnable}}Usage:
{{.UsageLine}}
{{end}}{{.Long | trim}}
{{end}}{{end}}`
)
11 changes: 10 additions & 1 deletion cmd/slackdump/internal/v1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ const (
bannerFmt = "Slackdump %[1]s Copyright (c) 2018-%[2]s rusq (build: %s)\n\n"
)

var CmdV1 = &base.Command{
Run: runV1,
UsageLine: "slackdump v1",
Short: "slackdump legacy mode",
Long: `
V1 starts slackdump in legacy mode, that supports all legacy flags.
`,
}

// defFilenameTemplate is the default file naming template.
const defFilenameTemplate = "{{.ID}}{{ if .ThreadTS}}-{{.ThreadTS}}{{end}}"

Expand Down Expand Up @@ -67,7 +76,7 @@ func runV1(ctx context.Context, cmd *base.Command, args []string) {
banner(os.Stderr)
loadSecrets(secrets)

params, err := parseCmdLine(args[1:])
params, err := parseCmdLine(args[0:])
if err == config.ErrNothingToDo {
// if the user hasn't provided any required flags, let's offer
// an interactive prompt to fill them.
Expand Down
90 changes: 90 additions & 0 deletions cmd/slackdump/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"runtime/trace"
"strings"

"github.com/rusq/slackdump/v2/cmd/slackdump/internal/base"
"github.com/rusq/slackdump/v2/cmd/slackdump/internal/help"
v1 "github.com/rusq/slackdump/v2/cmd/slackdump/internal/v1"
)

func init() {
base.Slackdump.Commands = []*base.Command{
v1.CmdV1,
}
}

func main() {
flag.Usage = base.Usage
flag.Parse()
log.SetFlags(0)

args := flag.Args()
base.CmdName = args[0]
if args[0] == "help" {
help.Help(os.Stdout, args[1:])
return
}
BigCmdLoop:
for bigCmd := base.Slackdump; ; {
for _, cmd := range bigCmd.Commands {
if cmd.Name() != args[0] {
continue
}
if len(cmd.Commands) > 0 {
bigCmd = cmd
args = args[1:]
if len(args) == 0 {
help.PrintUsage(os.Stderr, bigCmd)
base.SetExitStatus(2)
base.Exit()
}
if args[0] == "help" {
// Accept 'go mod help' and 'go mod help foo' for 'go help mod' and 'go help mod foo'.
help.Help(os.Stdout, append(strings.Split(base.CmdName, " "), args[1:]...))
return
}
base.CmdName += " " + args[0]
continue BigCmdLoop
}
if !cmd.Runnable() {
continue
}
invoke(cmd, args)
base.Exit()
return
}
helpArg := ""
if i := strings.LastIndex(base.CmdName, " "); i >= 0 {
helpArg = " " + base.CmdName[:i]
}
fmt.Fprintf(os.Stderr, "slackdump %s: unknown command\nRun 'go help%s' for usage.\n", base.CmdName, helpArg)
base.SetExitStatus(2)
base.Exit()
}
}

func init() {
base.Usage = mainUsage
}

func mainUsage() {
help.PrintUsage(os.Stderr, base.Slackdump)
os.Exit(2)
}

func invoke(cmd *base.Command, args []string) {
cmd.Flag.Usage = func() { cmd.Usage() }
cmd.Flag.Parse(args[1:])
args = cmd.Flag.Args()
// maybe start trace
ctx, task := trace.NewTask(context.Background(), fmt.Sprint("Running ", cmd.Name(), " command"))
defer task.End()
cmd.Run(ctx, cmd, args)
}

0 comments on commit f84530d

Please sign in to comment.