gosh
所属分类:代码编辑器
开发工具:GO
文件大小:46KB
下载次数:0
上传日期:2017-01-08 02:12:54
上 传 者:
sh-1993
说明: 使用Go和markdown,以普通编程风格编写的UNIX shell。
(A UNIX shell written in a literate programming style, using Go and markdown.)
文件列表:
BackgroundProcesses.md (14192, 2017-01-08)
BackgroundProcessesRevisited.md (16016, 2017-01-08)
Environment.md (5270, 2017-01-08)
Globbing.md (6148, 2017-01-08)
Makefile (253, 2017-01-08)
Piping.md (14144, 2017-01-08)
Prompts.md (5273, 2017-01-08)
TabCompletion.md (8613, 2017-01-08)
TabCompletionRevisited.md (27593, 2017-01-08)
Tokenization.md (10107, 2017-01-08)
completion.go (5014, 2017-01-08)
goshrc (220, 2017-01-08)
main.go (13311, 2017-01-08)
prefix.go (286, 2017-01-08)
prefix_test.go (636, 2017-01-08)
tokenize.go (1704, 2017-01-08)
tokenize_test.go (2543, 2017-01-08)
# gosh Shell
This is an attempt to write a simple UNIX shell using literate programming. We'll
be using Go to write it, because I like Go.
(I intend to formalize the literate programming syntax I'm using with
markdown later, but it should be fairly straight forward. A header immediately
before a code block in quotation marks is a name for that code block. It can be
referenced in other code blocks as `<<<
>>>`. A `+=` at the end of the
header means append to the code block, don't replace it. A header without
quotation marks means the code block should be the contents of that filename.)
## What does a simple shell do?
A simple shell needs to do a few things.
1. Read user input.
2. Interpret the user input.
3. Execute the user's input.
And repeat, until the user inputs something like an `exit` command. A good
shell does much more (like tab completion, file globbing, etc) but for now
we'll stick to the absolute basics.
## Main Body
We'll start with the main loop. Most Go programs have a file that looks
something like this:
### main.go
```go
package main
import (
<<>>
)
<<>>
<<>>
```
### "main.go funcs"
```go
func main() {
<<>>
}
```
Our main body is going to be a loop that repeatedly reads from `os.Stdin`.
This means that we should probably start by adding `os` to the import list.
### "main.go imports"
```go
"os"
```
And we'll start with a loop that just repeatedly reads a rune from the user.
`*os.File` (`os.Stdin`'s type) doesn't support the ReadRune interface, but
fortunately the `bufio` package provides a wrapper which allows us to convert
any `io.Reader` into a RuneReader, so let's import that too.
### "main.go imports" +=
```go
"bufio"
```
Then the basis of our main body becomes a loop that initializes and repeatedly
reads a rune from `os.Stdio`. For now, we'll just print it and see how it goes:
### "mainbody"
```go
r := bufio.NewReader(os.Stdin)
for {
c, _, err := r.ReadRune()
if err != nil {
panic(err)
}
print(c)
}
```
The problem with this, is that `os.Stdin` sends things to the underlying `io.Reader`
one line at a time, and doesn't provide a way to change this. It turns out
that there's no way in the standard library to force it to send one character
at a time. Luckily for us, the third party package `github.com/pkg/term` *does*
provide a way to manipulate the settings of POSIX ttys, which is all we need
to support. So instead, let's import that so that we can convert the terminal
to raw mode. (We'll still use bufio for the simplicity of ReadRune.)
(We'll actually use CbreakMode(), which is like raw mode in that it'll send
us 1 key at a time, but unlike raw mode in that special command sequences
will still be interpreted.)
### "main.go imports"
```go
"github.com/pkg/term"
"bufio"
```
### "mainbody"
```go
// Initialize the terminal
t, err := term.Open("/dev/tty")
if err != nil {
panic(err)
}
// Restore the previous terminal settings at the end of the program
defer t.Restore()
t.SetCbreak()
r := bufio.NewReader(t)
for {
c, _, err := r.ReadRune()
if err != nil {
panic(err)
}
println(c)
}
```
## The Command Loop
That's a good start, but we probably want to handle the rune that's read in a
way other than just printing it. Let's keep track of what the current command
read is in a string, and when a newline is pressed, call a function to handle
the current command and reset the string. We'll declare a type for commands,
and do a little refactoring, just to be proactive.
### "mainbody"
```go
<<>>
<<>>
```
### "main.go globals"
```go
type Command string
```
### "Initialize Terminal"
```go
// Initialize the terminal
t, err := term.Open("/dev/tty")
if err != nil {
panic(err)
}
// Restore the previous terminal settings at the end of the program
defer t.Restore()
t.SetCbreak()
```
### "Command Loop"
```go
r := bufio.NewReader(t)
var cmd Command
for {
c, _, err := r.ReadRune()
if err != nil {
panic(err)
}
switch c {
case '\n':
// The terminal doesn't echo in raw mode,
// so print the newline itself to the terminal.
fmt.Printf("\n")
<<>>
cmd = ""
default:
fmt.Printf("%c", c)
cmd += Command(c)
}
}
```
Okay, but there's a problem. Since we're getting sent one character
at a time, when we get the error `exec: "ls\u007f": executable file not found in $PATH`
(0x7F is the ASCII code for "DEL". 0x08 is the code for "Backspace".)
Let's make both of those work as backspace.
### "Command Loop"
```go
r := bufio.NewReader(t)
var cmd Command
for {
c, _, err := r.ReadRune()
if err != nil {
panic(err)
}
switch c {
case '\n':
// The terminal doesn't echo in raw mode,
// so print the newline itself to the terminal.
fmt.Printf("\n")
<<>>
cmd = ""
case '\u007f', '\u0008':
<<>>
default:
fmt.Printf("%c", c)
cmd += Command(c)
}
}
```
How do we handle the backspace key? We want to cut the last
character off cmd, and erase it from the screen. Let's try
printing '\u0008' and see if that erases the last character
in Cbreak mode:
### "Handle Backspace"
```go
if len(cmd) > 0 {
cmd = cmd[:len(cmd)-1]
fmt.Printf("\u0008")
}
```
It moves the cursor, but doesn't actually delete the character. There might be
a more appropriate character to print, but for now we'll just print backspace,
space to overwrite the character, and then backspace again.
### "Handle Backspace"
```go
if len(cmd) > 0 {
cmd = cmd[:len(cmd)-1]
fmt.Printf("\u0008 \u0008")
}
```
## Handling the Command
Okay, so how do we handle the command? If it's the string "exit"
we probably should exit. Otherwise, we'll want to execute it using
the [`os.exec`](https://golang.org/pkg/os/exec/) package. We were
proactive about declaring cmd as a type instead of a string, so
we can just define some kind of HandleCmd() method on the type and
call that.
### "Handle Command"
```go
if cmd == "exit" || cmd == "quit" {
os.Exit(0)
} else {
cmd.HandleCmd();
}
```
### "main.go funcs" +=
```go
<<>>
```
### "HandleCmd Implementation"
```go
func (c Command) HandleCmd() error {
cmd := exec.Command(string(c))
return cmd.Run()
}
```
We'll need to add `os` and `os/exec` to our imports, while we're
at it.
### "main.go imports" +=
```go
"os"
"os/exec"
```
If we run it and try executing something, it doesn't seem to be working.
What's going on? Let's print the error if it happens to find out.
### "Handle Command"
```go
if cmd == "exit" || cmd == "quit" {
os.Exit(0)
} else {
err := cmd.HandleCmd()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
}
```
### "main.go imports" +=
```go
"fmt"
```
There's still no error (unless we just hit enter without entering anything,
in which case it complains about not being able to execute "". (We should
probably handle that as a special case, too.))
So what's going on with the lack of any output or error? It turns out if we look at the `os/exec.Command`
documentation we'll see:
```go
// Stdout and Stderr specify the process's standard output and error.
//
// If either is nil, Run connects the corresponding file descriptor
// to the null device (os.DevNull).
//
// If Stdout and Stderr are the same writer, at most one
// goroutine at a time will call Write.
Stdout io.Writer
Stderr io.Writer
```
`Stdin` makes a similar claim. So let's hook those up to os.Stdin, os.Stdout
and os.Stderr.
While we're at it, let's print a simple prompt.
So our new command handling code is:
### "Handle Command"
```go
if cmd == "exit" || cmd == "quit" {
os.Exit(0)
} else if cmd == "" {
PrintPrompt()
} else {
err := cmd.HandleCmd()
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
}
PrintPrompt()
}
```
We need to define PrintPrompt() that we just used.
### "main.go funcs" +=
```go
func PrintPrompt() {
<<>>
}
```
We'll start with a simple implementation that just prints a ">" so that we don't
get confused and think we're in a POSIX compliant sh prompt, then work on adding
a better prompt later.
# "PrintPrompt Implementation"
```go
fmt.Printf("\n> ")
```
And we'll want to print it on startup too:
### "Initialize Terminal" +=
```go
PrintPrompt()
```
And our new HandleCmd() implementation.
### "HandleCmd Implementation"
```go
func (c Command) HandleCmd() error {
cmd := exec.Command(string(c))
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
```
Now we can finally run some commands from our shell!
But wait, when we run anything with arguments, we get an
error: `exec: "ls -l": executable file not found in $PATH`.
exec is trying to run the command named "ls -l", not the
command "ls" with the parameter "-l".
This is the signature from the GoDoc:
`func Command(name string, arg ...string) *Cmd`
*Not*
`func Command(command string) *Cmd`
We can use `Fields` function in the standard Go [`strings`](https://golang.org/pkg/strings)
package to split a string on any whitespace, which is basically what we want
right now. There will be some problems with that (notably we won't be able
to enclose arguments in quotation marks if they contain whitespace), but at
least we won't have to write our own tokenizer.
While we're at it, the "$PATH" in the error message reminds me. If there *are*
any arguments that start with a "$", we should probably expand that to the OS
environment variable.
### "HandleCmd Implementation"
```go
func (c Command) HandleCmd() error {
parsed := strings.Fields(string(c))
if len(parsed) == 0 {
// There was no command, it's not an error, the user just hit
// enter.
PrintPrompt()
return nil
}
var args []string
for _, val := range parsed[1:] {
if val[0] == '$' {
args = append(args, os.Getenv(val[1:])
} else {
args = append(args, val)
}
}
cmd := exec.Command(parsed[0], args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
```
### "main.go imports" +=
```go
"strings"
```
There's one other command that needs to be implemented internally: `cd`. Each
program in unix contains it's own working directory. When it's spawned, it
inherits its parent's working directory. If `cd` were implemented as an
external program, the new directory would never make it back to the
parent (our shell.) It's fairly easy to change the working directory,
we just add a check after we've parsed the args if the command is "cd", and
call `os.Chdir` instead of `exec.Command` as appropriate.
### "HandleCmd Implementation"
```go
func (c Command) HandleCmd() error {
parsed := strings.Fields(string(c))
if len(parsed) == 0 {
// There was no command, it's not an error, the user just hit
// enter.
PrintPrompt()
return nil
}
var args []string
for _, val := range parsed[1:] {
if val[0] == '$' {
args = append(args, os.Getenv(val[1:]))
} else {
args = append(args, val)
}
}
if parsed[0] == "cd" {
if len(args) == 0 {
return fmt.Errorf("Must provide an argument to cd")
}
return os.Chdir(args[0])
}
cmd := exec.Command(parsed[0], args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
```
### Handling EOFs
There's still one minor noticable bug. If we hit `^D` on an empty line, it should
be treated as an EOF instead of adding the character `0x04`. (And
if it's not an empty line, we probably still shouldn't add it to
the command.)
### "Command Loop"
```go
r := bufio.NewReader(t)
var cmd Command
for {
c, _, err := r.ReadRune()
if err != nil {
panic(err)
}
switch c {
case '\n':
// The terminal doesn't echo in raw mode,
// so print the newline itself to the terminal.
fmt.Printf("\n")
<<>>
cmd = ""
case '\u0004':
if len(cmd) == 0 {
os.Exit(0)
}
case '\u007f', '\u0008':
<<>>
default:
fmt.Printf("%c", c)
cmd += Command(c)
}
}
```
And now.. hooray! We have a simple shell that works! We should add tab completion,
a smarter tokenizer, and a lot of other features if we were going to use this
every day, but at least we have a proof-of-concept, and maybe you learned something
by doing it.
If there's any other features you'd like to see added, feel free to either
create a pull request telling the story of how to you'd do it, or just create
an issue on GitHub and see if someone else does. Feel free to also file bug
reports in either the code or prose.
The Tokenization.md file builds on this and improves on the tokenization by
adding support for string literals (with spaces) in arguments.
TabCompletion.md builds on Tokenization.md to add rudimentary command and file
tab completion to the shell.
Piping.md adds support for stdin/stdout redirection and piping processes
together with `|`.
The final result of putting this all together after running `go fmt` is in the
accompanying `*.go` files in this repo, so it should be go gettable.
近期下载者:
相关文件:
收藏者: