added support for quoted strings.

This commit is contained in:
Stefan Friese 2025-01-28 16:01:58 +00:00
parent 201969ef90
commit 84377914ac
1 changed files with 132 additions and 89 deletions

View File

@ -2,6 +2,8 @@ package main
import ( import (
// "bytes" // "bytes"
"bufio"
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
@ -36,12 +38,13 @@ func executeCommand(command string, args []string, dir string) (string, error) {
func executeSimpleCommand(command string, currentDir string) CommandOutput { func executeSimpleCommand(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput var cmdOutput CommandOutput
parts := strings.Fields(command) // args := strings.Fields(command)
if len(parts) == 0 { args := parseCommandWithQuotes(command)
if len(args) == 0 {
return cmdOutput return cmdOutput
} }
cmd := exec.Command(parts[0], parts[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = currentDir cmd.Dir = currentDir
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
cmdOutput.Command = command cmdOutput.Command = command
@ -125,35 +128,29 @@ func fileUploadHandler(w http.ResponseWriter, r *http.Request) {
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
func executeCommandWithRedirection(command string, currentDir string) CommandOutput { func processCommand(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput
// First pipe
if strings.Contains(command, "|") { if strings.Contains(command, "|") {
cmdOutput = executeCommandWithPipe(command, currentDir) return executeCommandWithPipe(command, currentDir)
} else { } else if strings.Contains(command, ">") {
if strings.Contains(command, ">") { return handleOutputRedirection(command, currentDir)
cmdOutput = handleOutputRedirection(command, currentDir)
} else if strings.Contains(command, "<") { } else if strings.Contains(command, "<") {
cmdOutput = handleInputRedirection(command, currentDir) return handleInputRedirection(command, currentDir)
} else {
cmdOutput = executeSimpleCommand(command, currentDir)
}
} }
return cmdOutput return executeSimpleCommand(command, currentDir)
} }
// Handle output redirection which is '>' // Handle output redirection which is '>'
func handleOutputRedirection(command string, currentDir string) CommandOutput { func handleOutputRedirection(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput var cmdOutput CommandOutput
parts := strings.Split(command, ">") parts := strings.SplitN(command, ">", 2)
// parts := strings.Split(command, ">")
cmdStr := strings.TrimSpace(parts[0]) cmdStr := strings.TrimSpace(parts[0])
outputFile := strings.TrimSpace(parts[1]) outputFile := strings.TrimSpace(parts[1])
cmdParts := strings.Fields(cmdStr) // cmdParts := strings.Fields(cmdStr)
cmdParts := parseCommandWithQuotes(cmdStr)
if len(cmdParts) == 0 { if len(cmdParts) == 0 {
return cmdOutput return cmdOutput
} }
@ -184,11 +181,12 @@ func handleOutputRedirection(command string, currentDir string) CommandOutput {
func handleInputRedirection(command string, currentDir string) CommandOutput { func handleInputRedirection(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput var cmdOutput CommandOutput
parts := strings.Split(command, "<") // parts := strings.Split(command, "<")
parts := strings.SplitN(command, "<", 2)
cmdStr := strings.TrimSpace(parts[0]) cmdStr := strings.TrimSpace(parts[0])
inputFile := strings.TrimSpace(parts[1]) inputFile := strings.TrimSpace(parts[1])
cmdParts := strings.Fields(cmdStr) cmdParts := parseCommandWithQuotes(cmdStr)
if len(cmdParts) == 0 { if len(cmdParts) == 0 {
return cmdOutput return cmdOutput
} }
@ -216,78 +214,131 @@ func handleInputRedirection(command string, currentDir string) CommandOutput {
func executeCommandWithPipe(command string, currentDir string) CommandOutput { func executeCommandWithPipe(command string, currentDir string) CommandOutput {
var cmdOutput CommandOutput var cmdOutput CommandOutput
commands := strings.Split(command, "|") cmdOutput.Command = command
// var prevCmdOutput []byte pipeCommands := strings.Split(command, "|")
var err error var prevOutput []byte
var cmds []*exec.Cmd // Pipeline of commands for i, cmdPart := range pipeCommands {
for _, cmdPart := range commands {
cmdPart = strings.TrimSpace(cmdPart) cmdPart = strings.TrimSpace(cmdPart)
parts := strings.Fields(cmdPart) args := parseCommandWithQuotes(cmdPart)
if len(parts) == 0 { if len(args) == 0 {
continue continue
} }
cmd := exec.Command(parts[0], parts[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = currentDir cmd.Dir = currentDir
cmds = append(cmds, cmd)
if i > 0 {
cmd.Stdin = strings.NewReader(string(prevOutput))
} }
// Connect the commands through the pipeline
for i := 0; i < len(cmds)-1; i++ {
pipe, pipeErr := cmds[i].StdoutPipe()
if pipeErr != nil {
cmdOutput.Command = command
cmdOutput.Error = fmt.Sprintf("Error creating pipe: %v", pipeErr)
return cmdOutput
}
cmds[i+1].Stdin = pipe
}
// Capture the final output of the last command
var outputBuffer strings.Builder var outputBuffer strings.Builder
cmds[len(cmds)-1].Stdout = &outputBuffer cmd.Stdout = &outputBuffer
cmds[len(cmds)-1].Stderr = &outputBuffer cmd.Stderr = &outputBuffer
// Start each command in the pipeline err := cmd.Run()
for _, cmd := range cmds {
err = cmd.Start()
if err != nil { if err != nil {
cmdOutput.Command = command cmdOutput.Error = fmt.Sprintf("Error executing command '%s': %v", cmdPart, err)
cmdOutput.Error = fmt.Sprintf("Error starting command '%s': %v", cmd.Path, err)
return cmdOutput return cmdOutput
} }
// Store the output for the next pipe stage
prevOutput = []byte(outputBuffer.String())
} }
// Wait for each command to complete // Final output from the last command in the pipe
for _, cmd := range cmds { cmdOutput.Output = string(prevOutput)
err = cmd.Wait()
if err != nil {
cmdOutput.Command = command
cmdOutput.Error = fmt.Sprintf("Error executing command '%s': %v", cmd.Path, err)
return cmdOutput return cmdOutput
}
}
// if i > 0 {
// cmd.Stdin = bytes.NewReader(prevCmdOutput)
// }
// prevCmdOutput, err = cmd.CombinedOutput()
// if err != nil {
// cmdOutput.Command = command
// cmdOutput.Error = fmt.Sprintf("Error executing command '%s': %v", cmdPart, err)
// return cmdOutput
// }
// }
cmdOutput.Command = command
cmdOutput.Output = outputBuffer.String()
// cmdOutput.Output = string(prevCmdOutput)
return cmdOutput
} }
func splitCommand(input string) []string {
scanner := bufio.NewScanner(strings.NewReader(input))
scanner.Split(bufio.ScanWords)
var parts []string
var buffer bytes.Buffer
inQuotes := false
quoteChar := rune(0)
for scanner.Scan() {
token := scanner.Text()
if inQuotes {
buffer.WriteString(" ")
buffer.WriteString(token)
if strings.HasSuffix(token, string(quoteChar)) {
inQuotes = false
parts = append(parts, strings.Trim(buffer.String(), `"'`))
buffer.Reset()
}
} else {
if strings.HasPrefix(token, `"`) || strings.HasPrefix(token, `'`) {
inQuotes = true
quoteChar = rune(token[0])
buffer.WriteString(token)
} else {
parts = append(parts, token)
}
}
}
if buffer.Len() > 0 {
parts = append(parts, buffer.String())
}
return parts
}
func parseCommandWithQuotes(command string) []string {
var args []string
var current strings.Builder
inSingleQuote, inDoubleQuote := false, false
for i := 0; i < len(command); i++ {
c := command[i]
switch c {
case '\'':
if !inDoubleQuote {
inSingleQuote = !inSingleQuote
continue
}
case '"':
if !inSingleQuote {
inDoubleQuote = !inDoubleQuote
continue
}
case ' ':
if !inSingleQuote && !inDoubleQuote {
if current.Len() > 0 {
args = append(args, current.String())
current.Reset()
}
continue
}
case '\\':
// Handle escaped characters
if i+1 < len(command) {
next := command[i+1]
if next == '"' || next == '\'' || next == '\\' {
current.WriteByte(next)
i++
continue
}
}
}
current.WriteByte(c)
}
// Add the last argument
if current.Len() > 0 {
args = append(args, current.String())
}
return args
}
func handler(w http.ResponseWriter, r *http.Request) { func handler(w http.ResponseWriter, r *http.Request) {
currentDir, _ := os.Getwd() currentDir, _ := os.Getwd()
@ -304,16 +355,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
cmdOutput := changeDirectory(command, args, &currentDir) cmdOutput := changeDirectory(command, args, &currentDir)
commandLog = append(commandLog, cmdOutput) commandLog = append(commandLog, cmdOutput)
} else { } else {
cmdOutput := executeCommandWithRedirection(input, currentDir) cmdOutput := processCommand(input, currentDir)
// output, err := executeCommand(command, args, currentDir)
// cmdOutput := CommandOutput{
// Command: command + " " + strings.Join(args, " "),
// Output: output,
// }
// if err != nil {
// cmdOutput.Error = err.Error()
// }
commandLog = append(commandLog, cmdOutput) commandLog = append(commandLog, cmdOutput)
} }
} }
@ -331,7 +373,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
background-color: #222; background-color: #222;
color: #eee; color: #eee;
font-family: monospace; font-family: monospace;
font-size: 10pt; font-size: 14pt;
margin: 0; margin: 0;
padding: 0; padding: 0;
height: 100vh; height: 100vh;
@ -340,7 +382,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
} }
#terminal { #terminal {
flex: 1; flex: 1;
padding: 10px; padding: 12px;
overflow-y: auto; overflow-y: auto;
white-space: pre-wrap; white-space: pre-wrap;
border: none; border: none;
@ -349,6 +391,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
flex-direction: column; flex-direction: column;
} }
input { input {
font-size: 14pt;
background: #222; background: #222;
color: #eee; color: #eee;
border: none; border: none;
@ -381,9 +424,9 @@ func handler(w http.ResponseWriter, r *http.Request) {
<div class="error">{{.Error}}</div> <div class="error">{{.Error}}</div>
{{end}} {{end}}
{{end}} {{end}}
<form method="POST"> <form method="POST" autocomplete="off">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span> $ </span> <span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
<input type="text" name="command" placeholder="Type a command here..." autofocus required> <input type="text" name="command" placeholder="Type a command here..." autofocus required>
</div> </div>
</form> </form>