small pixel drawing of a pufferfish zoa

shell/shell.go

package shell

import (
	"bytes"
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"

	"j3s.sh/zoa/utils"

	"mvdan.cc/sh/v3/interp"
	"mvdan.cc/sh/v3/syntax"
)

// this is used to detect when the script
// name changes from run to run, which allows
// us to prettily-print
var lastScriptPath string
var ctx = context.Background()

func RunCommands(zoaRoot string, scriptName string, r *interp.Runner) {
	scriptPath := filepath.Join(zoaRoot, scriptName)
	script, err := parseFile(scriptPath)
	if err != nil {
		fmt.Println("error in " + scriptPath)
		fmt.Println(err)
		os.Exit(1)
	}

	// execute every statement individually, decorating
	// each with ->, and doing some speshul logicks against
	// certain strings
	for _, stmt := range script.Stmts {
		fullCmd := commandName(stmt)
		command, after, _ := strings.Cut(fullCmd, " ")

		if command == "zoa-script" {
			// recursion detected!!!!!! :3 :3 :3
			subScriptPath := filepath.Join("scripts/" + after)
			RunCommands(zoaRoot, subScriptPath, r)
			continue
		}

		if command == "zoa-file" {
			// after = "nginx /etc/nginx/conf.d/jesse.conf systemctl nginx reload"
			zoaFileParts := strings.Split(after, " ")
			if len(zoaFileParts) < 2 {
				log.Fatal("zoa-file requires 2+ arguments")
			}
			src := zoaFileParts[0]
			dst := zoaFileParts[1]
			optionalCmd := ""
			if len(zoaFileParts) > 2 {
				optionalCmd = strings.Join(zoaFileParts[2:], " ")
			}
			fmt.Printf("$ zoa-file %s %s\n", src, dst)
			filePath := filepath.Join(zoaRoot, "files", src)
			dstChanged, err := zoaCopy(filePath, dst)
			if err != nil {
				log.Fatal(err)
			}
			// if there's an optional argument
			if optionalCmd != "" && dstChanged {
				re := strings.NewReader(optionalCmd)
				f, err := syntax.NewParser().Parse(re, "")
				if err != nil {
					log.Fatal(err)
				}

				for _, stmt := range f.Stmts {
					runCommand(ctx, stmt, r)
				}
			}
			continue
		}

		// if the script name changed between runs,
		// print it
		if scriptPath != lastScriptPath {
			utils.BluePrintln("-> " + scriptPath)
			lastScriptPath = scriptPath
		}

		fmt.Printf("$ %s\n", fullCmd)
		// todo: better colorz idk utils.BluePrintln("$ " + cmdName)
		err = r.Run(ctx, stmt)
		if err != nil {
			// ignore err here bc it's just the status code
			os.Exit(1)
		}
	}
}

func parseFile(filename string) (*syntax.File, error) {
	var result = &syntax.File{}
	f, err := os.Open(filename)
	if err != nil {
		return result, err
	}
	defer f.Close()
	result, err = syntax.NewParser().Parse(f, "")
	return result, err
}

func runCommand(c context.Context, s *syntax.Stmt, r *interp.Runner) {
	name := commandName(s)
	fmt.Printf("$ %s\n", name)
	err := r.Run(c, s)
	if err != nil {
		os.Exit(1)
	}
}

func commandName(statement *syntax.Stmt) string {
	b := new(bytes.Buffer)
	syntax.NewPrinter().Print(b, statement)
	return b.String()
}

// zoaCopy copies a file from zoaRoot/scripts/src to dst.
// if the dst was changed, zoaCopy will return true,
// otherwise it will return false
// zoaCopy defaults to 0666 for permissions (before umask)
func zoaCopy(src string, dst string) (bool, error) {
	srcChk, err := utils.ChecksumFile(src)
	if err != nil {
		// source file should always exist, return error
		return false, err
	}
	dstChk, err := utils.ChecksumFile(dst)
	if err != nil {
		// dstfile may not exist for a million
		// reasons, set checksum to blank
		// to force a mismatch
		dstChk = ""
	}

	if srcChk == dstChk {
		fmt.Println("file unchanged")
		return false, nil
	}
	// TODO: pass the file through a templating engine
	// - expand vars
	// - loop support?

	err = utils.Copy(src, dst)
	if err != nil {
		return false, err
	}
	return true, nil
}