init
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.v]
|
||||||
|
indent_style = tab
|
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.bat eol=crlf
|
||||||
|
|
||||||
|
*.v linguist-language=V
|
||||||
|
*.vv linguist-language=V
|
||||||
|
*.vsh linguist-language=V
|
||||||
|
v.mod linguist-language=V
|
||||||
|
.vdocignore linguist-language=ignore
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
main
|
||||||
|
code
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Ignore binary output folders
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Ignore common editor/system specific metadata
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# ENV
|
||||||
|
.env
|
||||||
|
|
||||||
|
# vweb and database
|
||||||
|
*.db
|
||||||
|
*.js
|
329
shell.v
Normal file
329
shell.v
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
module shell
|
||||||
|
|
||||||
|
import strings.textscanner
|
||||||
|
|
||||||
|
pub const safe_chars = '%+,-./0123456789:=@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
|
||||||
|
pub const word_chars = 'abcdfeghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
|
||||||
|
pub const unicode_word_chars = 'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ'
|
||||||
|
pub const extra_word_chars = '~-./*?='
|
||||||
|
pub const punct_chars = '();<>|&'
|
||||||
|
|
||||||
|
// quote returns a shell-escaped version of string `s`.
|
||||||
|
// Example:
|
||||||
|
// ```
|
||||||
|
// assert shell.quote("d'arc") == 'd\'"\'"\'arc\''
|
||||||
|
// ```
|
||||||
|
pub fn quote(s string) string {
|
||||||
|
if s == '' {
|
||||||
|
return "''"
|
||||||
|
}
|
||||||
|
if s.contains_only(safe_chars) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "'" + s.replace("'", '\'"\'"\'') + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
// join joins `s` array members into a shell-escaped string.
|
||||||
|
// Example:
|
||||||
|
// ```
|
||||||
|
// assert shell.join(['sh', '-c', 'hostname -f']) == "sh -c 'hostname -f'"
|
||||||
|
// ```
|
||||||
|
pub fn join(s []string) string {
|
||||||
|
mut quoted_args := []string{}
|
||||||
|
for arg in s {
|
||||||
|
quoted_args << quote(arg)
|
||||||
|
}
|
||||||
|
return quoted_args.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct SplitParams {
|
||||||
|
pub:
|
||||||
|
posix bool = true
|
||||||
|
comments bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// split splits the string `s` into array using shell-like syntax.
|
||||||
|
// Example:
|
||||||
|
// ```
|
||||||
|
// assert shell.split("sh -c 'hostname -f'") == ['sh', '-c', 'hostname -f']
|
||||||
|
// ```
|
||||||
|
pub fn split(s string, params SplitParams) []string {
|
||||||
|
mut parts := []string{}
|
||||||
|
mut lexer := new(s,
|
||||||
|
posix: params.posix
|
||||||
|
comments: params.comments
|
||||||
|
)
|
||||||
|
for token in lexer {
|
||||||
|
parts << token
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct LexerParams {
|
||||||
|
pub:
|
||||||
|
posix bool
|
||||||
|
comments bool = true
|
||||||
|
punctuation bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// new creates new Lexer instance. See LexerParams docstrings for info.
|
||||||
|
// Instantiate Lexer directly if you need more custom lexer setup.
|
||||||
|
pub fn new(input string, params LexerParams) Lexer {
|
||||||
|
mut words := word_chars
|
||||||
|
if params.posix {
|
||||||
|
words += unicode_word_chars
|
||||||
|
}
|
||||||
|
if params.punctuation {
|
||||||
|
words += extra_word_chars
|
||||||
|
}
|
||||||
|
return Lexer{
|
||||||
|
scanner: textscanner.new(input)
|
||||||
|
word_chars: words
|
||||||
|
punct_chars: if params.punctuation { punct_chars } else { '' }
|
||||||
|
comment_chars: if params.comments { '#' } else { '' }
|
||||||
|
// `whitespace_split` must be true if `punctuation` is false, otherwise
|
||||||
|
// lexer stucks into infinite loop!
|
||||||
|
whitespace_split: if params.punctuation { false } else { true }
|
||||||
|
posix_mode: params.posix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lexer {
|
||||||
|
pub mut:
|
||||||
|
scanner textscanner.TextScanner
|
||||||
|
pub:
|
||||||
|
word_chars string = word_chars
|
||||||
|
punct_chars string = punct_chars
|
||||||
|
comment_chars string = '#'
|
||||||
|
quotes string = '\'"'
|
||||||
|
escape string = '\\'
|
||||||
|
escaped_quotes string = '"'
|
||||||
|
posix_mode bool
|
||||||
|
// whitespace_split must be true if puct_chars is empty to prevent
|
||||||
|
// lexer stucking into infinite loop! The parser sucks, I know.
|
||||||
|
// Use `new` function to cretae Lexer instance to ensure the
|
||||||
|
// correct whitespace_split value.
|
||||||
|
whitespace_split bool
|
||||||
|
mut:
|
||||||
|
// This fields are used internally to store the current parser state.
|
||||||
|
lineno int = 1
|
||||||
|
state string = ' '
|
||||||
|
token string
|
||||||
|
push_back []string
|
||||||
|
push_back_chars []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// next returns parsed tokens until end of input string.
|
||||||
|
pub fn (mut x Lexer) next() ?string {
|
||||||
|
if x.push_back.len != 0 {
|
||||||
|
token := x.push_back.first()
|
||||||
|
x.push_back.drop(1)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
if x.scanner.pos != x.scanner.ilen {
|
||||||
|
return x.token()
|
||||||
|
}
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
|
||||||
|
// token parses and returns one token from input string regarding to current scanner state.
|
||||||
|
fn (mut x Lexer) token() ?string {
|
||||||
|
// TODO: this function must be fixed and completely rewritten
|
||||||
|
mut quoted := false
|
||||||
|
mut escaped_state := ' '
|
||||||
|
for {
|
||||||
|
mut nextchar := x.scanner.peek_u8()
|
||||||
|
if x.punct_chars != '' && x.push_back_chars.len != 0 {
|
||||||
|
nextchar = x.push_back_chars[x.push_back_chars.len - 1..][0].u8()
|
||||||
|
}
|
||||||
|
print_dbg('state=<${x.state}> I see character <${nextchar.ascii_str()}>')
|
||||||
|
if nextchar == `\n` {
|
||||||
|
x.lineno++
|
||||||
|
}
|
||||||
|
match true {
|
||||||
|
x.state == '' {
|
||||||
|
x.token = ''
|
||||||
|
break
|
||||||
|
}
|
||||||
|
x.state == ' ' {
|
||||||
|
match true {
|
||||||
|
nextchar == 0 {
|
||||||
|
// There is `if not nextcahr` check, that means 'EOF reached'
|
||||||
|
x.state = ''
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextchar.is_space() {
|
||||||
|
print_dbg('I see whitespace in whitespace state')
|
||||||
|
if x.token != '' || (x.posix_mode && quoted) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
x.scanner.skip()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
x.comment_chars.contains_u8(nextchar) {
|
||||||
|
// We need to skip all commented characters until \n found.
|
||||||
|
x.scanner.goto_end()
|
||||||
|
x.lineno++
|
||||||
|
}
|
||||||
|
x.posix_mode && x.escape.contains_u8(nextchar) {
|
||||||
|
escaped_state = 'a'
|
||||||
|
x.state = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
x.word_chars.contains_u8(nextchar) {
|
||||||
|
x.token = nextchar.ascii_str()
|
||||||
|
x.state = 'a'
|
||||||
|
}
|
||||||
|
x.punct_chars.contains_u8(nextchar) {
|
||||||
|
x.token = nextchar.ascii_str()
|
||||||
|
x.state = 'c'
|
||||||
|
}
|
||||||
|
x.quotes.contains_u8(nextchar) {
|
||||||
|
if !x.posix_mode {
|
||||||
|
x.token = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
x.state = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
x.whitespace_split == true {
|
||||||
|
x.token = nextchar.ascii_str()
|
||||||
|
x.state = 'a'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x.token = nextchar.ascii_str()
|
||||||
|
if x.token != '' || (x.posix_mode && quoted) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.quotes.contains(x.state) {
|
||||||
|
quoted = true
|
||||||
|
if nextchar == 0 {
|
||||||
|
// There is `if not nextcahr` check, that means 'EOF reached'
|
||||||
|
panic('found non-terminated quote')
|
||||||
|
}
|
||||||
|
match true {
|
||||||
|
nextchar.ascii_str() == x.state {
|
||||||
|
if !x.posix_mode {
|
||||||
|
x.token += nextchar.ascii_str()
|
||||||
|
x.state = ' '
|
||||||
|
} else {
|
||||||
|
x.state = 'a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.posix_mode && x.escape.contains_u8(nextchar)
|
||||||
|
&& x.escaped_quotes.contains(x.state) {
|
||||||
|
escaped_state = x.state
|
||||||
|
x.state = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
x.token += nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.escape.contains(x.state) {
|
||||||
|
if nextchar == 0 {
|
||||||
|
// There is `if not nextcahr` check, that means 'EOF reached'
|
||||||
|
panic('no escaped character found')
|
||||||
|
}
|
||||||
|
if x.quotes.contains(escaped_state) && nextchar.ascii_str() != x.state
|
||||||
|
&& nextchar.ascii_str() != escaped_state {
|
||||||
|
x.token += x.state
|
||||||
|
}
|
||||||
|
x.token += nextchar.ascii_str()
|
||||||
|
x.state = escaped_state
|
||||||
|
}
|
||||||
|
x.state in ['a', 'c'] {
|
||||||
|
match true {
|
||||||
|
nextchar == 0 {
|
||||||
|
x.state = '' // self.state = None
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextchar.is_space() {
|
||||||
|
print_dbg('I see whitespace in word state')
|
||||||
|
x.state = ' '
|
||||||
|
if x.token != '' || (x.posix_mode && quoted) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.comment_chars.contains_u8(nextchar) {
|
||||||
|
// We need to skip all commented characters until \n found.
|
||||||
|
// for {
|
||||||
|
// if x.scanner.peek_u8() == `\n` {
|
||||||
|
// x.scanner.skip()
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// x.scanner.skip()
|
||||||
|
// }
|
||||||
|
// x.lineno++
|
||||||
|
if x.posix_mode {
|
||||||
|
x.state = ' '
|
||||||
|
if x.token != '' || (x.posix_mode && quoted) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.state == 'c' {
|
||||||
|
if x.punct_chars.contains_u8(nextchar) {
|
||||||
|
x.token += nextchar.ascii_str()
|
||||||
|
} else {
|
||||||
|
if !nextchar.is_space() {
|
||||||
|
x.push_back_chars << nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
x.state = ' '
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x.posix_mode && x.quotes.contains_u8(nextchar) {
|
||||||
|
x.state = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
x.posix_mode && x.escape.contains_u8(nextchar) {
|
||||||
|
escaped_state = 'a'
|
||||||
|
x.state = nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
(x.word_chars.contains_u8(nextchar)
|
||||||
|
|| x.quotes.contains_u8(nextchar))
|
||||||
|
|| (x.whitespace_split && x.punct_chars.contains_u8(nextchar)) {
|
||||||
|
x.token += nextchar.ascii_str()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if x.punct_chars != '' {
|
||||||
|
x.push_back_chars << nextchar.ascii_str()
|
||||||
|
} else {
|
||||||
|
x.push_back.prepend(nextchar.ascii_str())
|
||||||
|
}
|
||||||
|
print_dbg('I see punctuation char in word state')
|
||||||
|
x.state = ' '
|
||||||
|
if x.token != '' || (x.posix_mode && quoted) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {}
|
||||||
|
}
|
||||||
|
x.scanner.next()
|
||||||
|
}
|
||||||
|
result := x.token
|
||||||
|
x.token = ''
|
||||||
|
if x.posix_mode && !quoted && result == '' {
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
print_dbg('I got token <${result}>')
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_dbg(s string) {
|
||||||
|
$if trace_shell_lexer ? {
|
||||||
|
eprintln('shell lexer: ' + s)
|
||||||
|
}
|
||||||
|
}
|
27
shell_test.v
Normal file
27
shell_test.v
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import shell
|
||||||
|
|
||||||
|
// TODO: pass all tests from https://github.com/python/cpython/blob/main/Lib/test/test_shlex.py
|
||||||
|
|
||||||
|
fn test_quote() {
|
||||||
|
assert shell.quote("janna d'arc") == '\'janna d\'"\'"\'arc\''
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_join() {
|
||||||
|
assert shell.join(['sh', '-c', 'hostname -f']) == "sh -c 'hostname -f'"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_split() {
|
||||||
|
assert shell.split("sh -c 'hostname -f'") == ['sh', '-c', 'hostname -f']
|
||||||
|
assert shell.split('sh -c hostname') == ['sh', '-c', 'hostname']
|
||||||
|
assert shell.split('hostname -f # some comment') == ['hostname', '-f', '#', 'some', 'comment']
|
||||||
|
assert shell.split('hostname -f # some comment', comments: true) == ['hostname', '-f']
|
||||||
|
assert shell.split('grep -rn "#"') == ['grep', '-rn', '#']
|
||||||
|
assert shell.split('grep -rn "#"', comments: true) == ['grep', '-rn', '#']
|
||||||
|
|
||||||
|
// FIXME: both assertions fails
|
||||||
|
// s := 'grep -rn hello # search hello
|
||||||
|
// awk --help
|
||||||
|
// '.trim_indent()
|
||||||
|
// assert shell.split(s) == ['grep', '-rn', 'hello', '#', 'search', 'hello', 'awk', '--help']
|
||||||
|
// assert shell.split(s, comments: true) == ['grep', '-rn', 'hello', 'awk', '--help']
|
||||||
|
}
|
Reference in New Issue
Block a user