1 | package shellquote
|
---|
2 |
|
---|
3 | import (
|
---|
4 | "bytes"
|
---|
5 | "strings"
|
---|
6 | "unicode/utf8"
|
---|
7 | )
|
---|
8 |
|
---|
9 | // Join quotes each argument and joins them with a space.
|
---|
10 | // If passed to /bin/sh, the resulting string will be split back into the
|
---|
11 | // original arguments.
|
---|
12 | func Join(args ...string) string {
|
---|
13 | var buf bytes.Buffer
|
---|
14 | for i, arg := range args {
|
---|
15 | if i != 0 {
|
---|
16 | buf.WriteByte(' ')
|
---|
17 | }
|
---|
18 | quote(arg, &buf)
|
---|
19 | }
|
---|
20 | return buf.String()
|
---|
21 | }
|
---|
22 |
|
---|
23 | const (
|
---|
24 | specialChars = "\\'\"`${[|&;<>()*?!"
|
---|
25 | extraSpecialChars = " \t\n"
|
---|
26 | prefixChars = "~"
|
---|
27 | )
|
---|
28 |
|
---|
29 | func quote(word string, buf *bytes.Buffer) {
|
---|
30 | // We want to try to produce a "nice" output. As such, we will
|
---|
31 | // backslash-escape most characters, but if we encounter a space, or if we
|
---|
32 | // encounter an extra-special char (which doesn't work with
|
---|
33 | // backslash-escaping) we switch over to quoting the whole word. We do this
|
---|
34 | // with a space because it's typically easier for people to read multi-word
|
---|
35 | // arguments when quoted with a space rather than with ugly backslashes
|
---|
36 | // everywhere.
|
---|
37 | origLen := buf.Len()
|
---|
38 |
|
---|
39 | if len(word) == 0 {
|
---|
40 | // oops, no content
|
---|
41 | buf.WriteString("''")
|
---|
42 | return
|
---|
43 | }
|
---|
44 |
|
---|
45 | cur, prev := word, word
|
---|
46 | atStart := true
|
---|
47 | for len(cur) > 0 {
|
---|
48 | c, l := utf8.DecodeRuneInString(cur)
|
---|
49 | cur = cur[l:]
|
---|
50 | if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) {
|
---|
51 | // copy the non-special chars up to this point
|
---|
52 | if len(cur) < len(prev) {
|
---|
53 | buf.WriteString(prev[0 : len(prev)-len(cur)-l])
|
---|
54 | }
|
---|
55 | buf.WriteByte('\\')
|
---|
56 | buf.WriteRune(c)
|
---|
57 | prev = cur
|
---|
58 | } else if strings.ContainsRune(extraSpecialChars, c) {
|
---|
59 | // start over in quote mode
|
---|
60 | buf.Truncate(origLen)
|
---|
61 | goto quote
|
---|
62 | }
|
---|
63 | atStart = false
|
---|
64 | }
|
---|
65 | if len(prev) > 0 {
|
---|
66 | buf.WriteString(prev)
|
---|
67 | }
|
---|
68 | return
|
---|
69 |
|
---|
70 | quote:
|
---|
71 | // quote mode
|
---|
72 | // Use single-quotes, but if we find a single-quote in the word, we need
|
---|
73 | // to terminate the string, emit an escaped quote, and start the string up
|
---|
74 | // again
|
---|
75 | inQuote := false
|
---|
76 | for len(word) > 0 {
|
---|
77 | i := strings.IndexRune(word, '\'')
|
---|
78 | if i == -1 {
|
---|
79 | break
|
---|
80 | }
|
---|
81 | if i > 0 {
|
---|
82 | if !inQuote {
|
---|
83 | buf.WriteByte('\'')
|
---|
84 | inQuote = true
|
---|
85 | }
|
---|
86 | buf.WriteString(word[0:i])
|
---|
87 | }
|
---|
88 | word = word[i+1:]
|
---|
89 | if inQuote {
|
---|
90 | buf.WriteByte('\'')
|
---|
91 | inQuote = false
|
---|
92 | }
|
---|
93 | buf.WriteString("\\'")
|
---|
94 | }
|
---|
95 | if len(word) > 0 {
|
---|
96 | if !inQuote {
|
---|
97 | buf.WriteByte('\'')
|
---|
98 | }
|
---|
99 | buf.WriteString(word)
|
---|
100 | buf.WriteByte('\'')
|
---|
101 | }
|
---|
102 | }
|
---|