diff --git a/parse/parse.go b/parse/parse.go index 33363cc5..159c478b 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -1038,84 +1038,7 @@ func isSpaceOrNewline(r rune) bool { return isSpace(r) || r == '\n' } -// Helpers. - func addChild(p Node, ch Node) { p.n().children = append(p.n().children, ch) ch.n().parent = p } - -// Quote returns a representation of s in elvish syntax. Bareword is tried -// first, then single quoted string and finally double quoted string. -func Quote(s string) string { - if s == "" { - return `""` - } - bare := s[0] != '~' - for _, r := range s { - if !unicode.IsPrint(r) && r != '\n' { - return quoteDouble(s) - } - if !allowedInBareword(r) { - bare = false - } - } - if bare { - return s - } - return quoteSingle(s) -} - -func quoteSingle(s string) string { - var buf bytes.Buffer - buf.WriteByte('\'') - for _, r := range s { - buf.WriteRune(r) - if r == '\'' { - buf.WriteByte('\'') - } - } - buf.WriteByte('\'') - return buf.String() -} - -func rtohex(r rune, w int) []byte { - bytes := make([]byte, w) - for i := w - 1; i >= 0; i-- { - d := byte(r % 16) - r /= 16 - if d <= 9 { - bytes[i] = '0' + d - } else { - bytes[i] = 'a' + d - 10 - } - } - return bytes -} - -func quoteDouble(s string) string { - var buf bytes.Buffer - buf.WriteByte('"') - for _, r := range s { - if r == '\\' || r == '"' { - buf.WriteByte('\\') - buf.WriteRune(r) - } else if !unicode.IsPrint(r) { - buf.WriteByte('\\') - if r <= 0xff { - buf.WriteByte('x') - buf.Write(rtohex(r, 2)) - } else if r <= 0xffff { - buf.WriteByte('u') - buf.Write(rtohex(r, 4)) - } else { - buf.WriteByte('U') - buf.Write(rtohex(r, 8)) - } - } else { - buf.WriteRune(r) - } - } - buf.WriteByte('"') - return buf.String() -} diff --git a/parse/parse_test.go b/parse/parse_test.go index 27d12054..be7e817e 100644 --- a/parse/parse_test.go +++ b/parse/parse_test.go @@ -350,28 +350,3 @@ func TestParseError(t *testing.T) { } } } - -var quoteTests = []struct { - text, quoted string -}{ - // Empty string is quoted with double quote. - {"", `""`}, - // Bareword when possible. - {"x-y,z@h/d", "x-y,z@h/d"}, - // Single quote when there is special char but no unprintable. - {"x$y[]\nef'", "'x$y[]\nef'''"}, - // Tilde needs quoting only when appearing at the beginning - {"~x", "'~x'"}, - {"x~", "x~"}, - // Double quote when there is unprintable char. - {"\x1b\"\\", `"\x1b\"\\"`}, -} - -func TestQuote(t *testing.T) { - for _, tc := range quoteTests { - got := Quote(tc.text) - if got != tc.quoted { - t.Errorf("Quote(%q) => %s, want %s", tc.text, got, tc.quoted) - } - } -} diff --git a/parse/quote.go b/parse/quote.go new file mode 100644 index 00000000..79e9d370 --- /dev/null +++ b/parse/quote.go @@ -0,0 +1,98 @@ +package parse + +import ( + "bytes" + "unicode" +) + +// Quote returns a representation of s in elvish syntax. Bareword is tried +// first, then single quoted string and finally double quoted string. +func Quote(s string) string { + s, _ = QuoteAs(s, Bareword) + return s +} + +// QuoteAs returns a representation of s in elvish syntax, using the syntax +// specified by q, which must be one of Bareword, SingleQuoted, or +// DoubleQuoted. It returns the quoted string and the actual quoting. +func QuoteAs(s string, q PrimaryType) (string, PrimaryType) { + if q == DoubleQuoted { + // Everything can be quoted using double quotes, return directly. + return quoteDouble(s), DoubleQuoted + } + if s == "" { + return "''", SingleQuoted + } + + // Keep track of whether it is a valid bareword. + bare := s[0] != '~' + for _, r := range s { + if !unicode.IsPrint(r) && r != '\n' { + // Contains unprintable character that is not a newline. Force + // double quote. + return quoteDouble(s), DoubleQuoted + } + if !allowedInBareword(r) { + bare = false + } + } + + if q == Bareword && bare { + return s, Bareword + } + return quoteSingle(s), SingleQuoted +} + +func quoteSingle(s string) string { + var buf bytes.Buffer + buf.WriteByte('\'') + for _, r := range s { + buf.WriteRune(r) + if r == '\'' { + buf.WriteByte('\'') + } + } + buf.WriteByte('\'') + return buf.String() +} + +func rtohex(r rune, w int) []byte { + bytes := make([]byte, w) + for i := w - 1; i >= 0; i-- { + d := byte(r % 16) + r /= 16 + if d <= 9 { + bytes[i] = '0' + d + } else { + bytes[i] = 'a' + d - 10 + } + } + return bytes +} + +func quoteDouble(s string) string { + var buf bytes.Buffer + buf.WriteByte('"') + for _, r := range s { + if r == '\\' || r == '"' { + buf.WriteByte('\\') + buf.WriteRune(r) + } else if !unicode.IsPrint(r) { + buf.WriteByte('\\') + if r <= 0xff { + buf.WriteByte('x') + buf.Write(rtohex(r, 2)) + } else if r <= 0xffff { + buf.WriteByte('u') + buf.Write(rtohex(r, 4)) + } else { + buf.WriteByte('U') + buf.Write(rtohex(r, 8)) + } + } else { + buf.WriteRune(r) + } + } + buf.WriteByte('"') + return buf.String() +} diff --git a/parse/quote_test.go b/parse/quote_test.go new file mode 100644 index 00000000..66c02bef --- /dev/null +++ b/parse/quote_test.go @@ -0,0 +1,28 @@ +package parse + +import "testing" + +var quoteTests = []struct { + text, quoted string +}{ + // Empty string is quoted with single quote. + {"", `''`}, + // Bareword when possible. + {"x-y,z@h/d", "x-y,z@h/d"}, + // Single quote when there is special char but no unprintable. + {"x$y[]\nef'", "'x$y[]\nef'''"}, + // Tilde needs quoting only when appearing at the beginning + {"~x", "'~x'"}, + {"x~", "x~"}, + // Double quote when there is unprintable char. + {"\x1b\"\\", `"\x1b\"\\"`}, +} + +func TestQuote(t *testing.T) { + for _, tc := range quoteTests { + got := Quote(tc.text) + if got != tc.quoted { + t.Errorf("Quote(%q) => %s, want %s", tc.text, got, tc.quoted) + } + } +}