Skip to content

Commit

Permalink
Execute shell as single command
Browse files Browse the repository at this point in the history
  • Loading branch information
adambabik committed Aug 3, 2022
1 parent 32858a8 commit deffd96
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 32 deletions.
18 changes: 18 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ This is a basic snippet with shell command:
$ echo "Hello, rdme!"
```

With `{name=hello}` you can annotate it and give it a nice name:

```sh {name=echo}
$ echo "Hello, rdme!"
```

It can contain multiple lines too:

```sh
Expand All @@ -22,6 +28,18 @@ Also, the dollar sign is not needed:
echo "Hello, rdme! Again!"
```

It works with `cd`, `pushd`, and similar because all lines are executed as a single script:

```sh
temp_dir=$(mktemp -d -t "rdme-")
pushd $temp_dir
echo "hi!" > hi.txt
pwd
cat hi.txt
popd
pwd
```

## Go

It can also execute a snippet of Go code:
Expand Down
19 changes: 18 additions & 1 deletion internal/cmd/print.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package cmd

import (
"io"

"github.com/pkg/errors"
"github.com/spf13/cobra"
)
Expand All @@ -23,10 +25,25 @@ func printCmd() *cobra.Command {
return err
}

_, err = cmd.OutOrStdout().Write([]byte(snippet.Content()))
w := &bulkWriter{Writer: cmd.OutOrStdout()}

_, _ = w.Write([]byte(snippet.Content()))
_, err = w.Write([]byte{'\n'})
return errors.Wrap(err, "failed to write to stdout")
},
}

return &cmd
}

type bulkWriter struct {
io.Writer
err error
}

func (w *bulkWriter) Write(p []byte) (n int, err error) {
if w.err != nil {
return 0, err
}
return w.Writer.Write(p)
}
6 changes: 4 additions & 2 deletions internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import (
)

func runCmd() *cobra.Command {
var dryRun bool
var replaceScripts []string
var (
dryRun bool
replaceScripts []string
)

cmd := cobra.Command{
Use: "run",
Expand Down
64 changes: 42 additions & 22 deletions internal/parser/parser_snippets.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,30 +36,24 @@ func (p *Parser) Snippets() (result Snippets) {
language: string(codeBlock.Language(p.src)),
}

if s.Name() == "" {
fragments := strings.Split(s.FirstLine(), " ")
executable := fragments[0]

// Usually the command looks like this: EXECUTABLE subcommand arg1, arg2, ...
if len(fragments) > 1 {
suffixBuf := bytes.NewBuffer(nil)
for _, r := range strings.ToLower(fragments[1]) {
if r >= '0' && r <= '9' || r >= 'a' && r <= 'z' {
_, _ = suffixBuf.WriteRune(r)
}
}

executable += "-" + suffixBuf.String()
}
nameIndexes[executable]++

if nameIndexes[executable] == 1 {
s.attributes["name"] = executable
} else {
s.attributes["name"] = fmt.Sprintf("%s_%d", executable, nameIndexes[executable])
}
// Set a name for the snippet.
// First option is that the name is set explicitly.
// Other option is to get the name from the first line
// of the snippet.
// Both options require us to dedup names.
var name string
if n, ok := s.attributes["name"]; ok && n != "" {
name = n
} else {
name = sanitizeName(s.FirstLine())
}
nameIndexes[name]++
if nameIndexes[name] > 1 {
name = fmt.Sprintf("%s_%d", name, nameIndexes[name])
}

s.name = name

result = append(result, &s)
}

Expand Down Expand Up @@ -121,3 +115,29 @@ func extractRawAttributes(source []byte, n *ast.FencedCodeBlock) []byte {

return nil
}

func sanitizeName(s string) string {
// Handle cases when the first line is defining a variable.
if idx := strings.Index(s, "="); idx > 0 {
return sanitizeName(s[:idx])
}

fragments := strings.Split(s, " ")
if len(fragments) > 1 {
s = strings.Join(fragments[:2], " ")
} else {
s = fragments[0]
}

var b bytes.Buffer

for _, r := range strings.ToLower(s) {
if r == ' ' {
_, _ = b.WriteRune('-')
} else if r >= '0' && r <= '9' || r >= 'a' && r <= 'z' {
_, _ = b.WriteRune(r)
}
}

return b.String()
}
3 changes: 2 additions & 1 deletion internal/parser/snippet.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Snippet struct {
attributes map[string]string
content string
description string // preceeding paragraph
name string
language string
}

Expand Down Expand Up @@ -73,7 +74,7 @@ func (s *Snippet) Description() string {
}

func (s *Snippet) Name() string {
return s.attributes["name"]
return s.name
}

type Snippets []*Snippet
Expand Down
29 changes: 23 additions & 6 deletions internal/runner/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import (
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"

"github.com/pkg/errors"
)
Expand All @@ -23,8 +25,15 @@ func (s Shell) DryRun(ctx context.Context, w io.Writer) {
sh = "/bin/sh"
}

for _, cmd := range s.Cmds {
_, _ = fmt.Fprintf(w, "%s in %s: %s\n", sh, s.Dir, cmd)
var b strings.Builder

_, _ = b.WriteString(fmt.Sprintf("#!%s\n\n", sh))
_, _ = b.WriteString(fmt.Sprintf("// run in %q\n\n", s.Dir))
_, _ = b.WriteString(s.prepareScript())

_, err := w.Write([]byte(b.String()))
if err != nil {
log.Fatalf("failed to write: %s", err)
}
}

Expand All @@ -34,13 +43,21 @@ func (s Shell) Run(ctx context.Context) error {
sh = "/bin/sh"
}

return execSingle(ctx, sh, s.Dir, s.prepareScript(), s.Stdin, s.Stdout, s.Stderr)
}

func (s Shell) prepareScript() string {
var b strings.Builder

_, _ = b.WriteString("set -e -o pipefail;")

for _, cmd := range s.Cmds {
if err := execSingle(ctx, sh, s.Dir, cmd, s.Stdin, s.Stdout, s.Stderr); err != nil {
return err
}
_, _ = b.WriteString(fmt.Sprintf("%s;", cmd))
}

return nil
_, _ = b.WriteRune('\n')

return b.String()
}

func execSingle(ctx context.Context, sh, dir, cmd string, stdin io.Reader, stdout, stderr io.Writer) error {
Expand Down

0 comments on commit deffd96

Please sign in to comment.