Skip to content

Instantly share code, notes, and snippets.

@allex
Last active May 13, 2020 06:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allex/e58d85ee1ddc692af20f8e6758c3cd95 to your computer and use it in GitHub Desktop.
Save allex/e58d85ee1ddc692af20f8e6758c3cd95 to your computer and use it in GitHub Desktop.
#!/bin/sh
# by allex_wang (gist_id:e58d85ee1ddc692af20f8e6758c3cd95)
tmpfile=$(mktemp /tmp/tar.XXXXXX)
trap 'rm -f -- "$tmpfile"' 0 1 2 3 9 13 15
tee $tmpfile >/dev/null
scp2() {
local host="$1" # user@host
local dir="$2" # remote directory
local cmd="(:;$3)" # remote command
[ -n "$host" ] && [ -n "$dir" ] || { echo >&2 "fatal: invalid parameters."; exit 1; }
local arr=( $(printf "$host"|tr , ' ') )
shift 3
for i in "${arr[@]}"; do
if [ -s "$tmpfile" ]; then
(ssh "$i" "$@" "(if [ -n \"$dir\" ]; then mkdir -p -- \"$dir\" && tar -C \"$dir\" --warning=no-timestamp -xzf - && echo \"-> [$i] Done\"; fi) && $cmd") <$tmpfile
else
(ssh "$i" "$@" "$cmd")
fi
done
}
scp2 "$@"
// GistID: e58d85ee1ddc692af20f8e6758c3cd95
// MIT licensed | @allex_wang <https://iallex.com> <https://git.io/fhjV6>
// Version: 1.0.4
// Installation: curl -sL https://git.io/fhjtZ |sh
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"syscall"
"unicode"
)
var VERSION string
// Options represents command line arguments for benchmark
type Options struct {
help bool
source string
dist string
host string
cmd string
helpText string
ignore string
exclude string
verbose bool
stdin bool
}
const ShellToUse = "bash"
func ExecShell(command string, outbuf io.Writer, errbuf io.Writer) (error, int) {
cmd := exec.Command(ShellToUse, "-c", command)
cmd.Stdout = outbuf
cmd.Stderr = errbuf
err := cmd.Run()
exitCode := 0
if err != nil {
// try to get the exit code
if exitError, ok := err.(*exec.ExitError); ok {
ws := exitError.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
} else {
// This will happen (in OSX) if `name` is not available in $PATH,
// in this situation, exit code could not be get, and stderr will be
// empty string very likely, so we use the default fail code, and format err
// to string and set to stderr
log.Printf("Could not get exit code for failed command: %v", command)
exitCode = 1
}
} else {
// success, exitCode should be 0 if go is ok
ws := cmd.ProcessState.Sys().(syscall.WaitStatus)
exitCode = ws.ExitStatus()
}
return err, exitCode
}
var pattern *regexp.Regexp
func init() {
pattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
}
// https://github.com/alessio/shellescape/blob/master/shellescape.go
// Quote returns a shell-escaped version of the string s. The returned value
// is a string that can safely be used as one token in a shell command line.
func Quote(s string) string {
if len(s) == 0 {
return "''"
}
if pattern.MatchString(s) {
return "'" + strings.Replace(s, "'", "'\"'\"'", -1) + "'"
}
return s
}
var defaultOptions = Options{
host: "",
source: "",
dist: "/tmp/spush",
cmd: "",
ignore: ".gitignore",
exclude: "",
verbose: false,
}
var helpText = `spush v%s (c) 2019-2020 Allex Wang <https://git.io/fhjV6> | MIT licensed
USAGE:
spush [OPTIONS] | -
OPTIONS
-s, --source <SOURCE>
The local source directory or file to transfor from.
-h, --host <HOST_LIST>
Provide server host list, seperated by ','.
-t, --remote-dir <REMOTE_DIR>
The destination of remote directory to location, also as the command cwd.
-c, --command <CMD>
Optional execute command in the remote machine.
-E, --exclude <REGEXP PATTERN>
Exclude files/directories that match the given regexp pattern.
-X, --ignore-file <PATH>
Add a custom ignore-file in '.gitignore' format. These files have a low precedence.
-v, --verbose
Verbose mode.
--help
Print help infomation.
`
func buildHelp(message string) Options {
return Options{help: true, helpText: message}
}
func getArgv(args []string, i int) string {
if len(args) > i {
return strings.TrimSpace(args[i])
}
return ""
}
// ParseArguments parses a string array and returns a populated Options struct
func parseArgs(arguments []string) Options {
o := defaultOptions
args := arguments[1:]
l := len(args)
if l == 0 {
return Options{help: true, helpText: fmt.Sprintf(helpText, VERSION)}
}
for i := 0; i < l; i++ {
hasValue := true
if args[i] == "--help" {
return Options{help: true, helpText: fmt.Sprintf(helpText, VERSION)}
} else if args[i] == "--host" || args[i] == "-h" {
i++
o.host = getArgv(args, i)
} else if args[i] == "--command" || args[i] == "-c" {
i++
o.cmd = getArgv(args, i)
} else if args[i] == "--source" || args[i] == "-s" {
i++
o.source = getArgv(args, i)
} else if args[i] == "--remote-dir" || args[i] == "-t" {
i++
argv := getArgv(args, i)
if !isEmpty(argv) {
o.dist = argv
}
} else if args[i] == "--ignore-file" || args[i] == "-X" {
i++
o.ignore = getArgv(args, i)
} else if args[i] == "--exclude" || args[i] == "-E" {
i++
o.exclude = getArgv(args, i)
} else if args[i] == "--verbose" || args[i] == "-v" {
o.verbose = true
hasValue = false
} else if args[i] == "-" {
o.stdin = true
hasValue = false
}
if hasValue && i >= l {
return buildHelp("fatal: incorrect parameters specified.")
}
}
// host, source, dist is mandatory requested
if !o.help {
valid, field := validate(o)
if !valid {
o.help = true
return buildHelp(fmt.Sprintf("fatal: parameter '%s' is mandatory required.", field))
}
}
if o.stdin {
err, cmd := getStdin()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
o.cmd = strings.TrimSpace(cmd)
}
return o
}
func validate(opts Options) (bool, string) {
if isEmpty(opts.host) {
return false, "host"
}
return true, ""
}
func isEmpty(s string) bool {
if len(s) == 0 {
return true
}
r := []rune(s)
l := len(r)
for l > 0 {
l--
if !unicode.IsSpace(r[l]) {
return false
}
}
return true
}
func IIF(b bool, t, f interface{}) interface{} {
if b {
return t
}
return f
}
func getStdin() (error, string) {
bytes, err := ioutil.ReadAll(os.Stdin)
return err, string(bytes)
}
func main() {
opts := parseArgs(os.Args)
if opts.help {
fmt.Println(opts.helpText)
os.Exit(0)
}
source := opts.source
packCmd := ""
distDir := opts.dist
if !isEmpty(source) {
if info, err := os.Stat(source); os.IsNotExist(err) {
fmt.Println("fatal: no such file or directory")
os.Exit(1)
} else if info.IsDir() {
xopts := IIF(opts.verbose, " -v", "").(string)
xopts += IIF(!isEmpty(opts.exclude), " -E '"+opts.exclude+"'", "").(string)
packCmd = fmt.Sprintf("git-pack --work-tree='%s' -X '%s' -o - %s", source, opts.ignore, xopts)
} else {
packCmd = fmt.Sprintf("(cd %s && tar -czf- %s)", filepath.Dir(source), filepath.Base(source))
}
}
cmd := opts.cmd
sshOpts := "-o HostKeyAlgorithms=ssh-rsa -o GSSAPIAuthentication=no -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o LogLevel=error"
if opts.verbose {
fmt.Println("++++++++++++++++++++++++")
lines := strings.Split(cmd, "\n")
for _, line := range lines {
fmt.Println("-> " + line)
}
fmt.Println("++++++++++++++++++++++++")
cmd = "set -x;\n" + cmd
sshOpts += " -vv"
}
if cmd != "" {
// execute sandbox with a closure
cmd = fmt.Sprintf(":(){\n%s\n};:;", cmd)
if distDir != "" {
cmd = fmt.Sprintf("test -d %q && cd %q || true;", distDir, distDir) + cmd
}
}
scp := fmt.Sprintf("scp.sh %s %s %s %s", Quote(opts.host), Quote(distDir), Quote(cmd), sshOpts)
sh := ""
if packCmd != "" {
sh = packCmd + " | " + scp
} else {
sh = scp
}
err, exitCode := ExecShell(sh, os.Stdout, os.Stderr)
if err != nil {
fmt.Fprintln(os.Stderr, "ERROR:", err)
}
os.Exit(exitCode)
}
#!/bin/sh
# by allex_wang (gist_id:e58d85ee1ddc692af20f8e6758c3cd95)
t=$(umask 077; mktemp).go
trap 'rm -f -- "$t"' 0 1 2 3 9 13 15
curl -sfL "https://git.io/fhjV6" > $t \
|| { echo >&2 "Fetch source failed!"; exit 1; }
go build -ldflags "-w -s -X main.VERSION=$(grep "Version: " $t|sed "s#.*: ##")" -o /usr/local/sbin/spush $t \
&& spush --help \
|| { echo >&2 "compile failed!"; exit 1; }
echo
curl -sfL "https://gist.githubusercontent.com/allex/e58d85ee1ddc692af20f8e6758c3cd95/raw/scp.sh" > $t \
|| { echo >&2 "Fetch source failed!"; exit 1; }
[ -s "$t" ] \
&& cp "$t" /usr/bin/scp.sh \
&& chmod +x $_ \
&& echo "install completed. (/usr/local/sbin/spush)"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment