mirror of
https://github.com/gechandesu/shellish.git
synced 2026-01-02 14:09:35 +03:00
273 lines
6.2 KiB
V
273 lines
6.2 KiB
V
module shellish
|
|
|
|
import strings
|
|
|
|
// safe_chars contains ASCII characters that can be used in shell without any escaping.
|
|
pub const safe_chars = '%+,-./0123456789:=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
|
|
|
|
// special_chars contains ASCII characters that must be escaped in shell.
|
|
pub const special_chars = ' \$"\'\\!'
|
|
|
|
const special_chars_runes = [` `, `$`, `"`, `'`, `\``, `\\`, `!`]
|
|
|
|
// quote returns a quoted version of string `s`.
|
|
//
|
|
// Note: String will be quoted with single quotes (`'`). If you expect `"`, use `double_quote()` instead.
|
|
//
|
|
// Example:
|
|
// ```
|
|
// assert shellish.quote('Hello, world!') == "'Hello, world!'"
|
|
// assert shellish.quote("John's notebook") == '\'John\'"\'"\'s notebook\''
|
|
// ```
|
|
pub fn quote(s string) string {
|
|
if s == '' {
|
|
return "''"
|
|
}
|
|
if s.contains_only(safe_chars) {
|
|
return s
|
|
}
|
|
return "'" + s.replace("'", '\'"\'"\'') + "'"
|
|
}
|
|
|
|
// double_quote returns a `"` quoted version of string. In-string double
|
|
// quote will be escaped with `\`.
|
|
//
|
|
// Note: Shell interprets special characters in string quoted by double quotes.
|
|
// To prevent this use `quote()` instead or use `double_quote()` in conjunction with `escape()`.
|
|
// Example:
|
|
// ```
|
|
// assert shellish.double_quote('NAME="Arch Linux"') == r'"NAME=\"Arch Linux\""'
|
|
// assert shellish.double_quote(r'Hello, ${NAME}!') == r'"Hello, ${NAME}!"'
|
|
// ```
|
|
pub fn double_quote(s string) string {
|
|
if s == '' {
|
|
return '""'
|
|
}
|
|
if s.contains_only(safe_chars) {
|
|
return s
|
|
}
|
|
return '"' + s.replace('"', r'\"') + '"'
|
|
}
|
|
|
|
// unquote removes the leading and trailing quotes (any of `"`, `'`) from string.
|
|
pub fn unquote(s string) string {
|
|
mut ret := strings.new_builder(s.len)
|
|
for i := 0; i < s.len; i++ {
|
|
if (i == 0 || i == s.len - 1) && s[i] in [`"`, `'`] {
|
|
continue
|
|
}
|
|
ret.write_byte(s[i])
|
|
}
|
|
return ret.str()
|
|
}
|
|
|
|
// escape returns a shell-escaped version of string `s`.
|
|
// Shell special characters will be escaped with backslashes without inserting quotes.
|
|
// Example: assert shellish.escape('Hello, World!') == r'Hello,\ World\!'
|
|
pub fn escape(s string) string {
|
|
mut ret := strings.new_builder(s.len)
|
|
for i := 0; i < s.len; i++ {
|
|
if s[i] in special_chars_runes {
|
|
ret.write_byte(`\\`)
|
|
ret.write_byte(s[i])
|
|
} else {
|
|
ret.write_byte(s[i])
|
|
}
|
|
}
|
|
return ret.str()
|
|
}
|
|
|
|
// unescape unescapes the escaped string by removing backslash escapes.
|
|
// Example: assert shellish.unescape(r'line\ with\ spaces\\n') == r'line with spaces\n'
|
|
pub fn unescape(s string) string {
|
|
mut ret := strings.new_builder(s.len)
|
|
for i := 0; i < s.len; i++ {
|
|
if s[i] == `\\` && i + 1 < s.len {
|
|
ret.write_byte(s[i + 1])
|
|
i++
|
|
} else {
|
|
ret.write_byte(s[i])
|
|
}
|
|
}
|
|
return ret.str()
|
|
}
|
|
|
|
// strip_non_printable strips non-printable ASCII characters (from `0x00` to `0x1f` and `0x7f`) from string.
|
|
pub fn strip_non_printable(s string) string {
|
|
mut ret := strings.new_builder(s.len)
|
|
for c in s {
|
|
if c > 0x1f || c != 0x7f {
|
|
ret.write_byte(c)
|
|
}
|
|
}
|
|
return ret.str()
|
|
}
|
|
|
|
// strip_ansi_escape_codes strips ANSI escape sequences starting with `ESC [` from string.
|
|
pub fn strip_ansi_escape_codes(s string) string {
|
|
mut ret := strings.new_builder(s.len)
|
|
mut esc := false
|
|
mut lsbr := false
|
|
for c in s {
|
|
if c == 0x1b { // ESC
|
|
esc = true
|
|
continue
|
|
}
|
|
if esc && c == `[` { // ESC [
|
|
lsbr = true
|
|
continue
|
|
}
|
|
if esc && lsbr {
|
|
if c >= 0x40 && c <= 0x7e { // end of sequence
|
|
esc = false
|
|
lsbr = false
|
|
}
|
|
continue
|
|
}
|
|
ret.write_byte(c)
|
|
}
|
|
|
|
return ret.str()
|
|
}
|
|
|
|
// split splits the `s` string into tokens using shell-like syntax in POSIX manner.
|
|
// Example: assert shellish.split('echo "Hello, World!"') == ['echo', 'Hello, World!']
|
|
pub fn split(s string) ![]string {
|
|
if s.is_blank() {
|
|
return error('non-blank string expected')
|
|
}
|
|
return parse(s)!
|
|
}
|
|
|
|
// join joins `a` array members into a shell command.
|
|
// Example: assert shellish.join(['sh', '-c', 'hostname -f']) == "sh -c 'hostname -f'"
|
|
pub fn join(a []string) string {
|
|
mut quoted_args := []string{}
|
|
for arg in a {
|
|
quoted_args << quote(arg)
|
|
}
|
|
return quoted_args.join(' ')
|
|
}
|
|
|
|
enum Mode {
|
|
no
|
|
normal
|
|
quoted
|
|
}
|
|
|
|
fn parse(line string) ![]string {
|
|
mut tokens := []string{}
|
|
mut buf := []u8{}
|
|
|
|
mut escaped := false
|
|
mut single_quoted := false
|
|
mut double_quoted := false
|
|
mut back_quoted := false
|
|
mut dollar_quoted := false
|
|
|
|
mut got := Mode.no
|
|
|
|
for i, c in line {
|
|
mut r := c
|
|
|
|
if escaped {
|
|
if r == `t` {
|
|
r = `\t`
|
|
}
|
|
if r == `n` {
|
|
r = `\n`
|
|
}
|
|
buf << r
|
|
escaped = false
|
|
got = .normal
|
|
continue
|
|
}
|
|
|
|
if r == `\\` {
|
|
if single_quoted {
|
|
buf << r
|
|
} else {
|
|
if double_quoted && i + 1 <= line.len {
|
|
// POSIX-compliant shells removes backslash only if backslash is followed
|
|
// by $, `, ", and \ characters. Otherwise backslash is accepted as literal.
|
|
if line[i + 1] !in [`$`, `\``, `"`, `\\`] {
|
|
buf << r
|
|
}
|
|
}
|
|
escaped = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
if r.is_space() {
|
|
if single_quoted || double_quoted || back_quoted || dollar_quoted {
|
|
buf << r
|
|
} else if got != .no {
|
|
tokens << buf.bytestr()
|
|
buf = []u8{}
|
|
got = .no
|
|
}
|
|
continue
|
|
}
|
|
|
|
match r {
|
|
`\`` {
|
|
if !single_quoted && !double_quoted && !dollar_quoted {
|
|
back_quoted = !back_quoted
|
|
}
|
|
}
|
|
`(` {
|
|
if !single_quoted && !double_quoted && !dollar_quoted {
|
|
if !dollar_quoted && buf.len - 1 >= 0 && buf[buf.len - 1..][0] == `$` {
|
|
dollar_quoted = true
|
|
buf << r
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
`)` {
|
|
if !single_quoted && !double_quoted && !dollar_quoted {
|
|
dollar_quoted = false
|
|
}
|
|
}
|
|
`"` {
|
|
if !single_quoted && !dollar_quoted {
|
|
if double_quoted {
|
|
got = .quoted
|
|
}
|
|
double_quoted = !double_quoted
|
|
continue
|
|
}
|
|
}
|
|
`'` {
|
|
if !double_quoted && !dollar_quoted {
|
|
if single_quoted {
|
|
got = .quoted
|
|
}
|
|
single_quoted = !single_quoted
|
|
continue
|
|
}
|
|
}
|
|
else {}
|
|
}
|
|
|
|
got = .normal
|
|
buf << r
|
|
}
|
|
|
|
if got != .no {
|
|
tokens << buf.bytestr()
|
|
}
|
|
|
|
match true {
|
|
escaped { return error('invalid escape in string') }
|
|
single_quoted { return error('non-terminated quote in string') }
|
|
double_quoted { return error('non-terminated double quote in string') }
|
|
back_quoted { return error('non-terminated backtick in string') }
|
|
dollar_quoted { return error('non-terminated dollar expression in string') }
|
|
else {}
|
|
}
|
|
|
|
return tokens
|
|
}
|