small pixel drawing of a pufferfish zoa

make zoa copy its own thing
Jes Olson j3s@c3f.net
Tue, 22 Nov 2022 20:40:42 -0800
commit

1ffd2b3104158860fe5d053c189ea672a42d40d1

parent

f440872f9b4c70a08149c74601a316997daee292

4 files changed, 93 insertions(+), 183 deletions(-)

jump to
M READMEREADME

@@ -33,9 +33,16 @@ $ wget https://trash.j3s.sh/zoa

$ chmod +x zoa $ mv zoa /usr/local/bin/ # set up a zoa script - $ printf 'hello, world!\n' > main + $ printf "%s" "echo hello, world!" > hi + $ printf "%s" "ls /noexisty" >> hi + $ printf "%s" "uname -a" >> hi # execute zoa! - $ zoa run main + $ zoa run hi + + you will notice a few interesting things: + - zoa gently asked you to make echo posix compliant + - zoa notified you that ls exited unsuccessfully + - zoa printed all stdout in plaintext

@@ -184,17 +191,16 @@

examples: $ zoa watch /etc/ssh/sshd_config systemctl restart sshd +TODO: zoa fmt --> organization + +-> organization ideas - here's how a zoa project should be organized: + here's how a zoa project might be organized: - main <-- the main shell script + main <-- the script you call files/ <-- dir containing arbitrary text files - scripts/ <-- dir containing other shell scripts - - you need at least main. files/ is optional. main is your entrypoint. you could just stick everything in main and be done with it, if you want.

@@ -206,26 +212,33 @@ files/ contains any files that you might be interested in placing on hosts.

"zoa cp" -::super fun example:: + +-> flexible example if you want zoa to be a little more capable, here's a decent starting point: - case $NODENAME in - git.j3s.sh) - # note that the CERTS env var will - # pass into any scripts called after - # it is defined, as if they're all 1 - # long script - export TLS_CERTS='git.j3s.sh' - zoa run scripts/certs - zoa run scripts/git - ;; - j3s.sh) - export TLS_CERTS='j3s.sh' - zoa run scripts/certs - zoa run scripts/web - ;; - esac +main: + case $NODENAME in + git.j3s.sh) + # note that the CERTS env var will + # pass into any scripts called after + # it is defined, as if they're all 1 + # long script + export TLS_CERTS='git.j3s.sh' + zoa run scripts/certs + zoa run scripts/git + ;; + j3s.sh) + export TLS_CERTS='j3s.sh' + zoa run scripts/certs + zoa run scripts/web + ;; + esac +scripts/certs: + certbot renew "$TLS_CERTS" +scripts/web: + apt install nginx + systemctl enable --start nginx BONUS SECTION! :3 :3 <3 :3 common config management patterns in zoa
M main.gomain.go

@@ -4,7 +4,7 @@ import (

"fmt" "log" "os" - "path/filepath" + "strconv" "j3s.sh/zoa/env" "j3s.sh/zoa/shell"

@@ -17,8 +17,9 @@ // TODO: -v -h

fmt.Printf(`usage: zoa <command> [arguments] commands: - run <main|scripts/example> - execute given zoa script - version - print zoa version + fmt TODO + run <posix shell script> - execute given zoa script + version - print zoa version examples: zoa run main

@@ -44,6 +45,24 @@ command := os.Args[1]

switch command { case "run": zoaRun() + case "cp": + var mode os.FileMode + if len(os.Args) < 4 { + fmt.Println("zoa cp requires at least 2 arguments") + os.Exit(1) + } + if len(os.Args) == 4 { + mode = 0644 + } + if len(os.Args) > 4 { + m64, err := strconv.ParseUint(os.Args[4], 0, 32) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + mode = os.FileMode(m64) + } + shell.ZoaCopy(os.Args[2], os.Args[3], mode) case "version": fmt.Println("0.1.0") os.Exit(0)

@@ -57,12 +76,7 @@ if len(os.Args) <= 2 {

fmt.Println("run requires 1 argument: the path to a posix shell script") os.Exit(1) } - path := os.Args[2] - - if path != "main" { - fmt.Println("incorrect script name: script must be named either main or scripts/<scriptname>") - os.Exit(1) - } + script := os.Args[2] envs, err := env.GenerateEnv() if err != nil {

@@ -70,7 +84,7 @@ log.Fatal(err)

} r, err := interp.New( - // TODO: make quiet flag silence stdout + // TODO: make quiet var silence stdout? interp.StdIO(nil, os.Stdout, os.Stdout), interp.CallHandler(shell.CallHandler), interp.Env(envs),

@@ -79,13 +93,6 @@ if err != nil {

log.Fatal(err) } - session := &shell.Session{ - ScriptPath: path, - ScriptBaseDir: filepath.Dir(path), - Runner: r, - SubShell: false, - } - - // todo: err? - session.Run() + // TODO: err? + shell.Run(r, script) }
M shell/shell.goshell/shell.go

@@ -5,8 +5,6 @@ "context"

"fmt" "log" "os" - "os/exec" - "path/filepath" "strings" "j3s.sh/zoa/color"

@@ -16,41 +14,12 @@ "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 - -// r represents our global runner - we use a global here -// because we're very bad at coding, and globals are ez -// 😎 -// var r *interp.Runner - -// we use this to detect the working dir of -// the primary script the user called -// var zoaRoot string - -type Session struct { - Runner *interp.Runner - ScriptPath string - ScriptBaseDir string - SubShell bool -} - -func (s *Session) Run() { - ctx := context.TODO() - // TODO: just pass the whole session u idiot - ctx = context.WithValue(ctx, "script", s.ScriptPath) - ctx = context.WithValue(ctx, "scriptbasedir", s.ScriptBaseDir) - ctx = context.WithValue(ctx, "runner", s.Runner) - f, err := parseFile(s.ScriptPath) +func Run(r *interp.Runner, script string) { + f, err := parseFile(script) if err != nil { log.Fatal(err) } - r := s.Runner - if s.SubShell { - r = s.Runner.Subshell() - } + ctx := context.TODO() for _, stmt := range f.Stmts { // 14:8 pos := stmt.Pos()

@@ -59,7 +28,7 @@ err = r.Run(ctx, stmt)

if err != nil { // we yell but keep plowing forward // twirling, twirling - color.ZoaYell(fmt.Sprintf("%s:%s: %s", s.ScriptPath, pos, err.Error())) + color.ZoaYell(fmt.Sprintf("%s:%s: %s", script, pos, err.Error())) } } }

@@ -69,60 +38,13 @@ script := ctx.Value("script").(string)

command := args[0] fmt.Println() - // "shell tip" commands should go here + // shell tips go here if command == "echo" { - color.ZoaSay(`hewo! please prefer printf over echo where possible :3 -example: printf '%s\n' "hello world!"`) + color.ZoaSay(`prefer printf over echo! +ex: printf '%s\n' "hello world!"`) } fmt.Printf("%s $ %s\n", script, strings.Join(args, " ")) - switch command { - case "zoa-script": - if len(args) <= 1 { - // log.Fatal here? - return args, fmt.Errorf("zoa-script requires 1 argument: the script to run") - } - s := &Session{ - Runner: ctx.Value("runner").(*interp.Runner), - SubShell: true, - ScriptPath: filepath.Join(ctx.Value("scriptbasedir").(string), "scripts", args[1]), - ScriptBaseDir: ctx.Value("scriptbasedir").(string), - } - s.Run() - // session.Run() - // subScript := filepath.Join("scripts", args[1]) - // err := RunScript(subScript) - // if err != nil { - // return args, err - // } - // args has to have _something_ in it or we panic at calltime - return []string{"true"}, nil - case "zoa-file": - if len(args) <= 2 { - // log.Fatal here? - return args, fmt.Errorf("zoa-file requires 2+ arguments: zoa-file <source> <destination> <optional command>") - } - src := filepath.Join(filepath.Dir(string(script)), "files", args[1]) - dst := args[2] - // todo: remove? orrrr - // fmt.Printf("$ zoa-file %s %s\n", src, dst) - var cmd []string - if len(args) >= 3 { - cmd = args[3:] - } - dstChanged, err := zoaCopy(src, dst) - if err != nil { - log.Fatal(err) - } - // if the destination changes and we have - // a command specified, we def want to run it - if len(cmd) > 0 && dstChanged { - args = cmd - } else { - return []string{"true"}, nil - } - } - return args, nil }

@@ -137,79 +59,46 @@ result, err = syntax.NewParser().Parse(f, "")

return result, err } -// 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 copies a file from src to dst, optionally modifying its mode. // zoaCopy defaults to 0666 for permissions (before umask) -func zoaCopy(src string, dst string) (bool, error) { +func ZoaCopy(src string, dst string, mode os.FileMode) { srcChk, err := utils.ChecksumFile(src) if err != nil { // source file should always exist, return error - return false, err + fmt.Println(err) + os.Exit(1) } dstChk, err := utils.ChecksumFile(dst) if err != nil { // dstfile may not exist for a million // reasons, set checksum to blank // to force a mismatch + // TODO: why did i do this? revise errors. dstChk = "" } - if srcChk == dstChk { - fmt.Println(dst + " was unchanged") - return false, nil - } - // TODO: pass the file through a templating engine + // TODO: templating engine? // - expand vars // - loop support? - err = utils.Copy(src, dst) - if err != nil { - return false, err + if srcChk != dstChk { + err = utils.Copy(src, dst) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } - return true, nil -} - -// the code down under is dragons and shit, it's awful, ignore pls -func SSH(command string, hostlist []string) error { - _, err := exec.LookPath("ssh") + fi, err := os.Lstat(dst) if err != nil { - log.Fatal(err) + fmt.Println(err) + os.Exit(1) } - for _, server := range hostlist { - cmd := exec.Command("ssh", server, command) - cmd.Stdout = os.Stdout - err := cmd.Run() - return err + if fi.Mode().Perm() != mode { + err = os.Chmod(dst, mode) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } - return nil -} - -func SCP(file string, hostlist []string) error { - _, err := exec.LookPath("scp") - if err != nil { - log.Fatal(err) - } - for _, server := range hostlist { - cmd := exec.Command("scp", file, server+":") - cmd.Stdout = os.Stdout - err := cmd.Run() - return err - } - return nil -} - -func SCPDir(dir string, hostlist []string) error { - _, err := exec.LookPath("scp") - if err != nil { - log.Fatal(err) - } - for _, server := range hostlist { - cmd := exec.Command("scp", "-r", dir, server+":"+dir) - cmd.Stdout = os.Stdout - err := cmd.Run() - return err - } - return nil }
M test/maintest/main

@@ -1,3 +1,4 @@

+#!/bin/sh # test function inheretence println() { printf "%s\n" "$1"