Last active
May 13, 2020 06:31
-
-
Save allex/e58d85ee1ddc692af20f8e6758c3cd95 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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