The big work in progress nothing works commit

master
Hugo Thunnissen 2 years ago
parent cf1667f0bf
commit ba1ad8f13a

@ -0,0 +1,9 @@
package commands
const fakePath = "/bin:/usr/bin"
const fakePipeBuf = "4096"
const trampSuccess = "tramp_exit_status 0\n"
const trampFailure = "tramp_exit_status 1\n"
const lsExecutable = "/bin/ls"
const testExecutable = "/bin/test"
const statExecutable = "/bin/stat"

@ -0,0 +1,45 @@
package commands
import (
"fmt"
"log"
"strings"
)
type EchoCommand struct{}
func (c *EchoCommand) Execute(shell *Shell, args []string) error {
if len(args) > 1 {
if "\"`uname -sr`\"" == args[0]+" "+args[1] {
log.Println("Tramp requested uname, returning a fake one: " + fakeUname)
_, err := shell.WriteOutput(
[]byte(
fmt.Sprintf(
"\"%s\"\n%s\n",
fakeUname,
"tramp_exit_status 0",
),
),
)
return err
}
if "\"`tty`\"" == args[0] {
log.Println("Telling tramp that it's dealing with a tty")
_, err := shell.WriteOutputString("\"/dev/pts/0\"\n" + trampSuccess)
return err
}
}
if len(args) > 0 {
if args[0] == "~root" {
log.Println("Telling tramp root's home directory")
_, err := shell.WriteOutputString("/root")
return err
}
}
_, err := shell.WriteOutput([]byte(strings.Join(args, " ") + "\n"))
return err
}

@ -0,0 +1,26 @@
package commands
import "strings"
func AssignmentsToMap(args []string) map[string]string {
var assignments = make(map[string]string)
for _, arg := range args {
split := strings.Split(arg, "=")
if len(split) > 1 {
assignments[split[0]] = split[1]
}
}
return assignments
}
func ApplyEnvIfPresent(shell *Shell, comm *Command) {
var args []string
args = append(args, comm.Name)
args = append(args, comm.Arguments...)
assignments := AssignmentsToMap(args)
if ps1, ok := assignments["PS1"]; ok {
shell.SetPrompt(ps1)
}
}

@ -0,0 +1,15 @@
package commands
type GetConfCommand struct{}
func (c *GetConfCommand) Execute(shell *Shell, args []string) error {
if len(args) > 0 {
switch args[0] {
case "PATH":
shell.WriteOutputString(fakePath + "\n" + trampSuccess)
case "PIPE_BUF":
shell.WriteOutputString(fakePipeBuf + "\n" + trampSuccess)
}
}
return nil
}

@ -0,0 +1,123 @@
package commands
import (
"fmt"
"log"
"strings"
"git.snorba.art/hugo/nssh/storage"
)
type CommandHandler struct {
shell *Shell
filestore storage.Filestore
}
func NewCommandHandler(shell *Shell, filestore storage.Filestore) *CommandHandler {
return &CommandHandler{
shell: shell,
filestore: filestore,
}
}
func (h *CommandHandler) Handle(comm *Command) error {
if strings.Contains(comm.Name, "=") {
ApplyEnvIfPresent(h.shell, comm)
return nil
}
switch comm.Name {
case "exec":
ApplyEnvIfPresent(h.shell, comm)
case "(cd":
h.shell.WriteOutput([]byte("tramp_exit_status 0\n"))
case "echo":
(&EchoCommand{}).Execute(h.shell, comm.Arguments)
case "(echo":
if strings.Join(comm.Arguments, " ") == "foo ; echo bar)" {
log.Println("Handling tramp's foobar test")
h.shell.WriteOutputString("foo\nbar\n")
}
case "stty":
return (&SttyCommand{}).Execute(h.shell, comm.Arguments)
case "set":
log.Println("Ignoring \"set\" command")
case "locale":
if len(comm.Arguments) > 0 {
if comm.Arguments[0] == "-a" {
locales := []string{"C", "C.UTF-8", "POSIX", "en_US.utf8"}
log.Println("Tramp requested locale, returning fake values: ", locales)
h.shell.WriteOutputString(
strings.Join(locales, "\n") + "\n",
)
return nil
}
}
log.Println("Ignoring \"locale\" command with unsupported parameters: ", comm.Arguments)
case "getconf":
return (&GetConfCommand{}).Execute(h.shell, comm.Arguments)
case "test", "/bin/test":
return (&TestCommand{filestore: h.filestore}).Execute(h.shell, comm.Arguments)
case "while":
if len(comm.Arguments) > 6 {
var err error
log.Printf("Pointing tramp to executable for \"%s\"", comm.Arguments[6])
switch comm.Arguments[6] {
case "$d/ls":
_, err = h.shell.WriteOutputString("tramp_executable " + lsExecutable)
case "$d/test":
_, err = h.shell.WriteOutputString("tramp_executable " + testExecutable)
case "$d/stat":
_, err = h.shell.WriteOutputString("tramp_executable " + statExecutable)
}
return err
}
case "ls", "/bin/ls":
if len(comm.Arguments) > 2 &&
comm.Arguments[1] == "-al" &&
comm.Arguments[2] == "/dev/null" {
_, err := h.shell.WriteOutputString(
fmt.Sprintf(
"crw-rw-rw- 1 root root 1, 3 May 5 00:05 /dev/null\n%s",
trampSuccess,
),
)
return err
}
if len(comm.Arguments) > 1 && comm.Arguments[0] == "-lnd" {
_, err := h.shell.WriteOutputString(
fmt.Sprintf(
"drwxrwxr-x 3 1000 1000 28 Apr 28 14:04 %s\n%s",
comm.Arguments[1],
trampSuccess,
),
)
return err
}
// should handle "--color=never --help 2>&1 | grep -iq busybox" so that we
// can pretend to have busybox ls. The assumption is that this is easier
// to emulate than the --dired flag of ls
if len(comm.Arguments) > 7 &&
comm.Arguments[4] == "grep" &&
comm.Arguments[6] == "busybox" {
_, err := h.shell.WriteOutputString(trampSuccess)
return err
}
_, err := h.shell.WriteOutputString(trampFailure)
return err
case "mkdir":
return (&MkdirCommand{filestore: h.filestore}).Execute(comm.Arguments)
default:
log.Printf("Error: Received unexpected command %s", comm.Name)
}
return nil
}

@ -0,0 +1,34 @@
package commands
import (
"strings"
"git.snorba.art/hugo/nssh/storage"
)
type MkdirCommand struct {
filestore storage.Filestore
}
func (c *MkdirCommand) Execute(arguments []string) error {
for _, dir := range arguments {
if len(dir) > 0 && dir[0] != '-' {
var currentPath string
for _, part := range strings.Split(dir, "/") {
if part == "" {
continue
}
currentPath += "/" + part
if exists, _ := c.filestore.DirectoryExists(currentPath); !exists {
err := c.filestore.MakeDirectory(currentPath)
if err != nil {
return err
}
}
}
}
}
return nil
}

@ -0,0 +1,363 @@
package commands
import (
"bufio"
"errors"
"io"
"log"
"os"
"strings"
"unsafe"
"github.com/google/shlex"
"github.com/pkg/term/termios"
"golang.org/x/sys/unix"
)
type Shell struct {
pty *os.File
tty *os.File
prompt []byte
readData []byte
readIndex int
winsize *Winsize
}
type Command struct {
Name string
Arguments []string
}
func (s *Shell) GetPty() *os.File {
return s.pty
}
func (s *Shell) Attach(conn io.ReadWriteCloser) error {
pty, tty, err := termios.Pty()
if err != nil {
return err
}
s.pty = pty
s.tty = tty
if s.winsize != nil {
SetWinsize(s.pty.Fd(), s.winsize)
}
go func() {
defer s.pty.Close()
io.Copy(s.pty, conn)
}()
go func() {
defer conn.Close()
io.Copy(conn, s.pty)
}()
_, err = s.tty.Write(s.prompt)
return err
}
func NewShell(prompt string) *Shell {
return &Shell{
prompt: []byte(prompt),
}
}
func (s *Shell) SetWinsize(w, h uint32) {
s.winsize = &Winsize{Width: uint16(w), Height: uint16(h)}
if s.pty != nil {
SetWinsize(s.pty.Fd(), s.winsize)
}
}
func (s *Shell) Close() (error, error) {
return s.pty.Close(), s.tty.Close()
}
var ErrEmptyCommand error = errors.New("Empty command line")
type State struct {
Variables map[string]string
Commands map[string]CommandExecutor
}
type Statement interface {
Words() []Word
Statements() []Statement
AddStatement(Statement)
Evaluate(state State, output io.Writer) (exitcode uint8)
}
type BaseStatement struct {
words []Word
statements []Statement
}
func (s *BaseStatement) Words() []Word {
return s.words
}
func (s *BaseStatement) Statements() []Statement {
return s.statements
}
func (s *BaseStatement) AddStatement(statement Statement) {
s.statements = append(s.statements, statement)
}
func (s *BaseStatement) AddWord(word Word) {
s.words = append(s.words, word)
}
type Word string
func (w Word) Statements() []Statement {
return []Statement{}
}
func (w Word) AddStatement(statement Statement) {}
func (w Word) AddWord(word Word) {}
type ParserParameters interface {
MakeStatement() Statement
ShouldLeave(Word, rune) bool
Supports(rune) bool
SubParsers() []Parser
}
type Parser interface {
Parse(*bufio.Reader) (Statement, error)
Parameters() ParserParameters
}
type BaseParser struct {
parameters ParserParameters
}
func (p *BaseParser) Parameters() ParserParameters {
return p.parameters
}
type BaseParameters struct {
EntryMarkers []rune
subParsers []Parser
}
func (p *BaseParameters) SubParsers() []Parser {
return p.subParsers
}
func (p *BaseParameters) Supports(r rune) bool {
for _, char := range p.EntryMarkers {
if char == r {
return true
}
}
return false
}
func (p *BaseParameters) MakeStatement() Statement {
return &BaseStatement{}
}
func (p *BaseParameters) ShouldLeave(w Word, r rune) bool {
if r == '\n' || r == ';' {
return true
}
}
func (p *BaseParser) Parse(r *bufio.Reader) (Statement, error) {
var currentWord Word = ""
statement := p.Parameters().MakeStatement()
for {
char, _, err := r.ReadRune()
if err != nil {
return statement, err
}
if char == ' ' || char == '\t' {
statement.AddWord(currentWord)
currentWord = ""
}
if p.Parameters().ShouldLeave(currentWord, char) {
return statement, nil
}
var matchedParser bool
for _, parser := range p.Parameters().SubParsers() {
if parser.Parameters().Supports(char) {
matchedParser = true
nestedStatement, err := parser.Parse(r)
if word, ok := nestedStatement.(Word); ok {
statement.AddWord(word)
} else {
statement.AddStatement(nestedStatement)
}
if err != nil {
return statement, err
}
}
}
if !matchedParser {
currentWord += Word(char)
}
}
}
func (s *Shell) ReadCommand() (*Command, error) {
var currentLine string
var backslash bool
var insideDoubleQuote bool
var insideSingleQuote bool
var maybeHeredoc bool
var insideHeredoc bool
var heredocMarker string
var heredocMarkerComplete bool
var heredocCurrentWord string
reader := bufio.NewReader(s.tty)
GatherCommand:
for {
char, _, err := reader.ReadRune()
if err != nil {
return nil, err
}
if insideHeredoc {
currentLine += string(char)
if !heredocMarkerComplete && char == '<' {
insideHeredoc = false
continue
}
if char == '\n' {
log.Printf("Heredoc current word: \"%s\"", heredocCurrentWord)
if heredocCurrentWord == heredocMarker {
log.Println("exiting heredoc")
insideHeredoc = false
heredocMarkerComplete = false
break GatherCommand
}
if !heredocMarkerComplete {
log.Println("Encountered heredoc marker: " + heredocCurrentWord)
heredocMarker = strings.Trim(heredocCurrentWord, "'")
heredocMarkerComplete = true
}
heredocCurrentWord = ""
continue
}
heredocCurrentWord += string(char)
continue
}
if backslash {
if !insideDoubleQuote && char == '\n' {
currentLine += string(" ")
continue
}
currentLine += "\\" + string(char)
backslash = false
continue
}
switch rune(char) {
case '<':
currentLine += string(char)
if maybeHeredoc {
maybeHeredoc = false
insideHeredoc = true
continue
}
maybeHeredoc = true
continue
case '\\':
backslash = true
continue
case '"':
insideDoubleQuote = !insideDoubleQuote
currentLine += string(char)
case '\'':
insideSingleQuote = !insideSingleQuote
currentLine += string(char)
case '\n':
if insideSingleQuote || insideDoubleQuote {
currentLine += string(char)
continue
}
break GatherCommand
default:
currentLine += string(char)
}
}
commandLine, err := shlex.Split(currentLine)
if err != nil {
return nil, err
}
if len(commandLine) == 0 {
return nil, ErrEmptyCommand
}
comm := &Command{Name: commandLine[0]}
if len(commandLine) > 1 {
comm.Arguments = commandLine[1:]
}
return comm, nil
}
func (s *Shell) SetPrompt(prompt string) {
log.Printf("Changing prompt to \"%s\"", prompt)
s.prompt = []byte(prompt)
}
func (s *Shell) WritePrompt() error {
_, err := s.tty.Write(s.prompt)
return err
}
func (s *Shell) WriteOutput(output []byte) (int, error) {
return s.tty.Write(output)
}
func (s *Shell) WriteOutputString(output string) (int, error) {
return s.WriteOutput([]byte(output))
}
// Winsize stores the Height and Width of a terminal.
type Winsize struct {
Height uint16
Width uint16
x uint16 // unused
y uint16 // unused
}
// SetWinsize sets the size of the given pty.
func SetWinsize(fd uintptr, winsize *Winsize) {
log.Printf("window resize %dx%d", winsize.Width, winsize.Height)
unix.Syscall(
unix.SYS_IOCTL,
fd,
uintptr(unix.TIOCSWINSZ), uintptr(unsafe.Pointer(winsize)),
)
}

@ -0,0 +1,60 @@
package shell
type Block struct {
BaseToken
}
type DollarBlock struct {
BaseToken
}
type BlockParameters struct{}
type DollarBlockParameters struct {
BlockParameters
}
func (p *BlockParameters) Enter(i *CharIterator) error {
return i.Next()
}
func (p *DollarBlockParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &WordParameters{},
},
}
}
func (p *DollarBlockParameters) MakeToken() Token {
return &DollarBlock{}
}
func (p *BlockParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &StatementParameters{},
},
}
}
func (p *BlockParameters) Supports(charsBefore []rune, r rune) bool {
if r == '{' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *BlockParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if r == '}' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *BlockParameters) MakeToken() Token {
return &Block{}
}

@ -0,0 +1,22 @@
package builtins
import (
"io"
"strings"
)
type Echo struct{}
func (e *Echo) Name() string {
return "echo"
}
func (e *Echo) Execute(arguments []string, _ io.Reader, stdout io.Writer, stderr io.Writer) uint8 {
_, err := stdout.Write([]byte(strings.Join(arguments, " ")))
if err != nil {
stderr.Write([]byte(err.Error()))
return 1
}
return 0
}

@ -0,0 +1,8 @@
package shell
import "io"
type CommandExecutor interface {
Name() string
Execute(arguments []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (exitcode uint8)
}

@ -0,0 +1,132 @@
package shell
import (
"io"
"log"
)
type DollarParameters struct {
ignoreParams *IgnoreFirstCharParameters
}
func (p *DollarParameters) SubParsers() []Parser {
if p.ignoreParams == nil {
p.ignoreParams = &IgnoreFirstCharParameters{}
}
return []Parser{
&BaseParser{
parameters: p.ignoreParams,
},
&BaseParser{
parameters: &WordParameters{},
},
&BaseParser{
parameters: &SubshellParameters{},
},
&BaseParser{
parameters: &DollarBlockParameters{},
},
}
}
func (p *DollarParameters) Enter(_ *CharIterator) error {
if p.ignoreParams != nil {
p.ignoreParams.Reset()
}
return nil
}
func (p *DollarParameters) Supports(charsBefore []rune, r rune) bool {
if r == '$' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *DollarParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if isWhitespace(r) || r == ';' {
return true
}
return false
}
func (p *DollarParameters) Leave(i *CharIterator) error {
return i.Previous()
}
func (p *DollarParameters) MakeToken() Token {
return &Dollar{}
}
type Dollar struct {
BaseToken
}
func (d *Dollar) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) {
if len(d.tokens) > 1 {
logstr := "Unexpected tokens after dollar sign: " + d.tokens[1].String()
log.Println(logstr)
stderr.Write([]byte(logstr))
return []string{""}, 2
}
if word, ok := d.tokens[0].(*Word); ok {
return []string{state.Variable(word.String())}, 0
} else if block, ok := d.tokens[0].(*DollarBlock); ok && len(block.Tokens()) > 0 {
if word, ok := block.Tokens()[0].(*Word); ok {
return []string{state.Variable(word.String())}, 0
} else {
log.Println("Whaat")
}
} else {
logstr := "Unexpected tokens after dollar sign: " + d.String()
log.Println(logstr)
stderr.Write([]byte(logstr))
return []string{""}, 2
}
return []string{""}, 1
}
type IgnoreFirstCharParameters struct {
entered bool
}
func (p *IgnoreFirstCharParameters) Reset() {
p.entered = false
}
func (p *IgnoreFirstCharParameters) Enter(_ *CharIterator) error {
p.entered = true
return nil
}
func (p *IgnoreFirstCharParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &CharParserParameters{},
},
}
}
func (p *IgnoreFirstCharParameters) MakeToken() Token {
return nil
}
func (p *IgnoreFirstCharParameters) Supports(_ []rune, r rune) bool {
if !p.entered {
return true
}
return false
}
func (p *IgnoreFirstCharParameters) ShouldLeave(_ []rune, r rune) bool {
return true
}

@ -0,0 +1,54 @@
package shell
import (
"bufio"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
)
type DollarTest struct {
suite.Suite
}
func (t *DollarTest) TestParse() {
parser := &BaseParser{&DollarParameters{}}
reader := bufio.NewReader(strings.NewReader("$variable"))
iterator, err := NewCharIterator(reader)
t.NoError(err)
token, err := parser.Parse(iterator, &CharCollection{})
t.ErrorIs(err, io.EOF)
t.IsType(&Dollar{}, token)
t.IsType(&Word{}, token.Tokens()[0])
t.Equal(token.Tokens()[0].String(), "variable")
}
func (t *DollarTest) TestExpand() {
parser := &BaseParser{&DollarParameters{}}
reader := bufio.NewReader(strings.NewReader("$variable $hey"))
iterator, err := NewCharIterator(reader)
t.NoError(err)
token, err := parser.Parse(iterator, &CharCollection{})
t.IsType(&Dollar{}, token)
t.IsType(&Word{}, token.Tokens()[0])
state := NewShellState()
state.SetVariable("variable", "Test")
expanded, exit := token.(Expandable).Expand(state, nil, nil)
t.Equal(uint8(0), exit)
t.Equal(expanded[0], "Test")
}
func TestDollarTest(t *testing.T) {
suite.Run(t, new(DollarTest))
}

@ -0,0 +1,55 @@
package shell
import (
"fmt"
"io"
)
type LineParameters struct{}
func (p *LineParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &StatementParameters{},
},
}
}
func (p *LineParameters) Supports(charsBefore []rune, r rune) bool {
return true
}
func (p *LineParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if r == '\n' {
return countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *LineParameters) MakeToken() Token {
return &Line{}
}
type Line struct {
BaseToken
}
func (l *Line) Evaluate(state State, stdin io.Reader, stdout io.Writer, stderr io.Writer) uint8 {
var retval uint8 = 0
for _, token := range l.Tokens() {
if eval, ok := token.(Evalable); ok {
retval = eval.Evaluate(state, stdin, stdout, stderr)
} else {
stderr.Write([]byte(
fmt.Sprintf(
"shell: Syntax error near unexpected token: %s",
token.String(),
),
))
}
}
return retval
}

@ -0,0 +1,143 @@
package shell
import (
"bufio"
"errors"
"fmt"
"reflect"
)
type Parser interface {
Parse(*CharIterator, *CharCollection) (Token, error)
Parameters() ParserParameters
}
type BaseParser struct {
parameters ParserParameters
}
func (p *BaseParser) Parameters() ParserParameters {
return p.parameters
}
type CharIterator struct {
reader *bufio.Reader
currentChar rune
lastChar rune
}
func NewCharIterator(reader *bufio.Reader) (*CharIterator, error) {
char, _, err := reader.ReadRune()
if err != nil {
return nil, err
}
return &CharIterator{
reader: reader,
currentChar: char,
}, nil
}
func (i *CharIterator) Next() error {
var err error
i.lastChar = i.currentChar
i.currentChar, _, err = i.reader.ReadRune()
return err
}
func (i *CharIterator) Previous() error {
if i.lastChar == -1 {
return errors.New("Chariterator can only go back once")
}
err := i.reader.UnreadRune()
if err != nil {
return err
}
i.currentChar = i.lastChar
i.lastChar = -1
return err
}
type CharCollection struct {
chars []rune
}
func (c *CharCollection) Append(r rune) {
c.chars = append(c.chars, r)
}
func (c *CharCollection) Chars() []rune {
return c.chars
}
func (i CharIterator) Current() rune {
return i.currentChar
}
func (p *BaseParser) Parse(i *CharIterator, charsBefore *CharCollection) (Token, error) {
token := p.Parameters().MakeToken()
if enter, ok := p.parameters.(Enterable); ok {
err := enter.Enter(i)
if err != nil {
return nil, err
}
}
parsers := p.Parameters().SubParsers()
ParseLoop:
for {
char := i.Current()
if p.Parameters().ShouldLeave(charsBefore.Chars(), char) {
if leave, ok := p.Parameters().(Leavable); ok {
err := leave.Leave(i)
if err != nil {
return token, err
}
}
return token, nil
}
var matchedParser bool
for _, parser := range parsers {
if parser.Parameters().Supports(charsBefore.Chars(), char) {
matchedParser = true
nestedToken, err := parser.Parse(i, charsBefore)
if token != nil {
token.AddToken(nestedToken)
}
if err != nil {
return token, err
}
charsBefore.Append(char)
err = i.Next()
if err != nil {
return token, err
}
continue ParseLoop
}
}
if !matchedParser {
return token, fmt.Errorf(
"Parser encountered unsupported token or char. Parser: %s full text: %s -\n Char: %s",
reflect.TypeOf(*&p.parameters).String(),
string(charsBefore.Chars()),
string(char),
)
}
}
}

@ -0,0 +1,20 @@
package shell
type ParserParameters interface {
MakeToken() Token
ShouldLeave(charsBefore []rune, r rune) bool
Supports(charsBefore []rune, r rune) bool
SubParsers() []Parser
}
type Enterable interface {
Enter(*CharIterator) error
}
type Resetable interface {
Reset()
}
type Leavable interface {
Leave(*CharIterator) error
}

@ -0,0 +1,44 @@
package shell
import (
"bufio"
"strings"
"testing"
"github.com/stretchr/testify/suite"
)
type CharIteratorTest struct {
suite.Suite
}
func (t *CharIteratorTest) TestNextAndBacktrack() {
charIterator, err := NewCharIterator(bufio.NewReader(strings.NewReader("abcdefg")))
t.NoError(err)
t.Equal(string('a'), string(charIterator.Current()))
err = charIterator.Next()
t.NoError(err)
t.Equal(string('b'), string(charIterator.Current()))
err = charIterator.Previous()
t.NoError(err)
t.Equal(string('a'), string(charIterator.Current()))
err = charIterator.Previous()
t.Error(err)
t.Equal("Chariterator can only go back once", err.Error())
err = charIterator.Next()
t.NoError(err)
t.Equal(string('b'), string(charIterator.Current()))
}
func TestCharIteratorTest(t *testing.T) {
suite.Run(t, new(CharIteratorTest))
}

@ -0,0 +1,30 @@
package shell
type Pipe struct {
BaseToken
}
type PipeParameters struct {
StatementParameters
}
func (p *PipeParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &StatementParameters{},
},
}
}
func (p *PipeParameters) Supports(charsBefore []rune, r rune) bool {
if r == '|' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *PipeParameters) MakeToken() Token {
return &Pipe{}
}

@ -0,0 +1,92 @@
package shell
import (
"io"
"log"
"strings"
)
type QuoteParameters struct{}
func (p *QuoteParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &DollarParameters{},
},
&BaseParser{
parameters: &CharParserParameters{},
},
}
}
func (p *QuoteParameters) Supports(charsBefore []rune, r rune) bool {
if r == '"' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *QuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if r == '"' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *QuoteParameters) MakeToken() Token {
return &Quote{}
}
type Quote struct {
BaseToken
}
func (q *Quote) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) {
var retcode uint8
var str string
for _, token := range q.Tokens() {
if expand, ok := token.(Expandable); ok {
log.Println(expand)
var expansion []string
expansion, retcode = expand.Expand(state, stdin, stderr)
str += strings.Join(expansion, " ")
continue
}
str += token.String()
}
return []string{strings.Trim(str, "'")}, retcode
}
type SingleQuoteParameters struct {
QuoteParameters
}
type SingleQuote struct {
BaseToken
}
func (p *SingleQuoteParameters) Supports(charsBefore []rune, r rune) bool {
if r == '\'' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}
func (p *SingleQuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if r == '\'' {
return len(charsBefore) == 0 ||
countBackslashSuffixes(charsBefore)%2 == 0
}
return false
}

@ -0,0 +1,61 @@
package shell
import (
"bufio"
"fmt"
"io"
"strings"
"testing"
"git.snorba.art/hugo/nssh/commands/shell/builtins"
"github.com/stretchr/testify/suite"
)
type QuotesTest struct {
suite.Suite
}
func (t *QuotesTest) TestParse() {
expectedContents := fmt.Sprintf("'%s'", "word1 word2 word3\n word4")
parser := &BaseParser{&QuoteParameters{}}
reader := bufio.NewReader(strings.NewReader(expectedContents))
iterator, err := NewCharIterator(reader)
t.NoError(err)
token, err := parser.Parse(iterator, &CharCollection{})
t.ErrorIs(err, io.EOF)
t.Equal(expectedContents, token.String())
expanded, _ := token.(Expandable).Expand(nil, nil, nil)
t.Equal("word1 word2 word3\n word4", strings.Join(expanded, " "))
}
func (t *QuotesTest) TestExpandVariables() {
parser := &BaseParser{&QuoteParameters{}}
reader := bufio.NewReader(
strings.NewReader("This is a $variable ${variable2} $variable"),
)
state := NewShellState()
state.SetVariable("variable", "supergood")
state.SetVariable("variable2", "thing")
echo := &builtins.Echo{}
state.SetCommand(echo.Name(), echo)
iterator, err := NewCharIterator(reader)
t.NoError(err)
token, err := parser.Parse(iterator, &CharCollection{})
t.ErrorIs(err, io.EOF)
expanded, _ := token.(Expandable).Expand(state, nil, nil)
t.Equal("This is a supergood thing supergood", expanded[0])
}
func TestQuotesTest(t *testing.T) {
suite.Run(t, new(QuotesTest))
}

@ -1,4 +1,4 @@
package main
package shell
import (
"bufio"

@ -0,0 +1,112 @@
package shell
type State interface {
Variable(string) string
Command(string) CommandExecutor
Env(string) string
Pushd(string) error
Popd() error
Cwd() string
SetVariable(string, string)
SetCommand(string, CommandExecutor)
SetEnv(string, string)
Clone() State
}
func NewShellState() *ShellState {
return &ShellState{
variables: map[string]string{},
commands: map[string]CommandExecutor{},
env: map[string]string{},
dirstack: []string{},
}
}
type ShellState struct {
variables map[string]string
commands map[string]CommandExecutor
env map[string]string
dirstack []string
}
func (s *ShellState) Variable(n string) string {
if v, ok := s.variables[n]; ok {
return v
}
return ""
}
func (s *ShellState) Command(n string) CommandExecutor {
if v, ok := s.commands[n]; ok {
return v
}
return nil
}
func (s *ShellState) Env(n string) string {
if v, ok := s.env[n]; ok {
return v
}
return ""
}
func (s *ShellState) SetVariable(n string, v string) {
s.variables[n] = v
}
func (s *ShellState) SetCommand(n string, e CommandExecutor) {
s.commands[n] = e
}
func (s *ShellState) SetEnv(n string, v string) {
s.env[n] = v
}
func (s *ShellState) Pushd(dir string) error {
s.dirstack = append([]string{dir}, s.dirstack...)
return nil
}
func (s *ShellState) Popd() error {
if len(s.dirstack) == 0 {
return nil
}
s.dirstack = s.dirstack[1:]
return nil
}
func (s *ShellState) Cwd() string {
if len(s.dirstack) == 0 {
return ""
}
return s.dirstack[0]
}
func (s *ShellState) Clone() State {
state := NewShellState()
for k, v := range s.commands {
state.SetCommand(k, v)
}
for k, v := range s.variables {
state.SetVariable(k, v)
}
for k, v := range s.env {
state.SetEnv(k, v)
}
state.dirstack = make([]string, len(s.dirstack))
copy(s.dirstack, state.dirstack)
return state
}

@ -0,0 +1,49 @@
package shell
type Statement struct {
BaseToken
}
type StatementParameters struct{}
func (p *StatementParameters) SubParsers() []Parser {
return []Parser{
&BaseParser{
parameters: &QuoteParameters{},
},
&BaseParser{
parameters: &SingleQuoteParameters{},
},
&BaseParser{
parameters: &WordParameters{},
},
&BaseParser{
parameters: &DollarParameters{},
},
&BaseParser{
parameters: &PipeParameters{},
},
&BaseParser{
parameters: &SubshellParameters{},
},
&BaseParser{
parameters: &BlockParameters{},
},
}
}
func (p *StatementParameters) Supports(charsBefore []rune, r rune) bool {
return true
}
func (p *StatementParameters) MakeToken() Token {
return &Statement{}
}
func (p *StatementParameters) ShouldLeave(charsBefore []rune, r rune) bool {
if r == '\n' || r == ';' {
return countBackslashSuffixes(charsBefore)%2 == 0
}