mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-15 11:58:11 -05:00
103 lines
2.3 KiB
Go
103 lines
2.3 KiB
Go
|
package shellquote
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"strings"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
// Join quotes each argument and joins them with a space.
|
||
|
// If passed to /bin/sh, the resulting string will be split back into the
|
||
|
// original arguments.
|
||
|
func Join(args ...string) string {
|
||
|
var buf bytes.Buffer
|
||
|
for i, arg := range args {
|
||
|
if i != 0 {
|
||
|
buf.WriteByte(' ')
|
||
|
}
|
||
|
quote(arg, &buf)
|
||
|
}
|
||
|
return buf.String()
|
||
|
}
|
||
|
|
||
|
const (
|
||
|
specialChars = "\\'\"`${[|&;<>()*?!"
|
||
|
extraSpecialChars = " \t\n"
|
||
|
prefixChars = "~"
|
||
|
)
|
||
|
|
||
|
func quote(word string, buf *bytes.Buffer) {
|
||
|
// We want to try to produce a "nice" output. As such, we will
|
||
|
// backslash-escape most characters, but if we encounter a space, or if we
|
||
|
// encounter an extra-special char (which doesn't work with
|
||
|
// backslash-escaping) we switch over to quoting the whole word. We do this
|
||
|
// with a space because it's typically easier for people to read multi-word
|
||
|
// arguments when quoted with a space rather than with ugly backslashes
|
||
|
// everywhere.
|
||
|
origLen := buf.Len()
|
||
|
|
||
|
if len(word) == 0 {
|
||
|
// oops, no content
|
||
|
buf.WriteString("''")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
cur, prev := word, word
|
||
|
atStart := true
|
||
|
for len(cur) > 0 {
|
||
|
c, l := utf8.DecodeRuneInString(cur)
|
||
|
cur = cur[l:]
|
||
|
if strings.ContainsRune(specialChars, c) || (atStart && strings.ContainsRune(prefixChars, c)) {
|
||
|
// copy the non-special chars up to this point
|
||
|
if len(cur) < len(prev) {
|
||
|
buf.WriteString(prev[0 : len(prev)-len(cur)-l])
|
||
|
}
|
||
|
buf.WriteByte('\\')
|
||
|
buf.WriteRune(c)
|
||
|
prev = cur
|
||
|
} else if strings.ContainsRune(extraSpecialChars, c) {
|
||
|
// start over in quote mode
|
||
|
buf.Truncate(origLen)
|
||
|
goto quote
|
||
|
}
|
||
|
atStart = false
|
||
|
}
|
||
|
if len(prev) > 0 {
|
||
|
buf.WriteString(prev)
|
||
|
}
|
||
|
return
|
||
|
|
||
|
quote:
|
||
|
// quote mode
|
||
|
// Use single-quotes, but if we find a single-quote in the word, we need
|
||
|
// to terminate the string, emit an escaped quote, and start the string up
|
||
|
// again
|
||
|
inQuote := false
|
||
|
for len(word) > 0 {
|
||
|
i := strings.IndexRune(word, '\'')
|
||
|
if i == -1 {
|
||
|
break
|
||
|
}
|
||
|
if i > 0 {
|
||
|
if !inQuote {
|
||
|
buf.WriteByte('\'')
|
||
|
inQuote = true
|
||
|
}
|
||
|
buf.WriteString(word[0:i])
|
||
|
}
|
||
|
word = word[i+1:]
|
||
|
if inQuote {
|
||
|
buf.WriteByte('\'')
|
||
|
inQuote = false
|
||
|
}
|
||
|
buf.WriteString("\\'")
|
||
|
}
|
||
|
if len(word) > 0 {
|
||
|
if !inQuote {
|
||
|
buf.WriteByte('\'')
|
||
|
}
|
||
|
buf.WriteString(word)
|
||
|
buf.WriteByte('\'')
|
||
|
}
|
||
|
}
|