読者です 読者をやめる 読者になる 読者になる

Goでシェルもどきを作る

go unix

Rubyでシェルもどきを作る - @tmtms のメモ


Goの勉強で, 上記の記事の Go版を書いてみました

基本

forkでなくて go routineを使っています. 終了の待ち合わせはチャネルを
用いました.

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
)

func createSubprocess(command string) chan bool {
	ch := make(chan bool)
	go func() {
		cmd := exec.Command(command)

		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		err := cmd.Start()
		if err != nil {
			log.Println(err)
		} else {
			err = cmd.Wait()
			if err != nil {
				log.Println(err)
			}
		}

		ch <- true
		close(ch)
	}()

	return ch
}

func waitSubprocess(ch <-chan bool) {
	<-ch
}

func main() {
	bio := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("-> ")
		line, hasMoreLine, err := bio.ReadLine()
		if !hasMoreLine && err == io.EOF {
			fmt.Println("Bye")
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		ch := createSubprocess(string(line))

		waitSubprocess(ch)
	}
}

コマンドラインのパース

入力文字列を strings.Splitで区切って, さらに各要素を strings.TrimSpaceで
空白を除去しています.

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strings"
)

func parseLine(line string) []string {
	words := strings.Split(strings.TrimSpace(line), " ")

	command := make([]string, 0)
	for _, word := range words {
		trimed := strings.TrimSpace(word)
		command = append(command, trimed)
	}

	return command
}

func createSubprocess(command []string) chan bool {
	ch := make(chan bool)
	go func() {
		cmd := exec.Command(command[0], command[1:]...)

		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		err := cmd.Start()
		if err != nil {
			log.Println(err)
		} else {
			err = cmd.Wait()
			if err != nil {
				log.Println(err)
			}
		}

		ch <- true
		close(ch)
	}()

	return ch
}

func waitSubprocess(ch <-chan bool) {
	<-ch
}

func main() {
	bio := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("-> ")
		line, hasMoreLine, err := bio.ReadLine()
		if !hasMoreLine && err == io.EOF {
			fmt.Println("Bye")
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		command := parseLine(string(line))
		ch := createSubprocess(command)

		waitSubprocess(ch)
	}
}

ワイルドカード

オリジナルは '*'と文字列比較をしていましたが, 正規表現を使うようにして
みました.

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
)

var globRegexp = regexp.MustCompile(`\*`)

func parseLine(line string) ([]string, *os.File) {
	var output *os.File

	words := strings.Split(strings.TrimSpace(line), " ")

	commands := make([]string, 0)
	for _, word := range words {
		trimed := strings.TrimSpace(word)

		if globRegexp.Match([]byte(trimed)) {
			expandeds, err := filepath.Glob(trimed)
			if err != nil {
				log.Fatal(err)
			}
			commands = append(commands, expandeds...)
		} else {
			commands = append(commands, trimed)
		}
	}

	return commands, output
}

func createSubprocess(command []string, output *os.File) chan bool {
	ch := make(chan bool)
	go func() {
		cmd := exec.Command(command[0], command[1:]...)

		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr

		err := cmd.Start()
		if err != nil {
			log.Println(err)
		} else {
			err = cmd.Wait()
			if err != nil {
				log.Println(err)
			}
		}

		ch <- true
		close(ch)
	}()

	return ch
}

func waitSubprocess(ch <-chan bool) {
	<-ch
}

func main() {
	bio := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("-> ")
		line, hasMoreLine, err := bio.ReadLine()
		if !hasMoreLine && err == io.EOF {
			fmt.Println("Bye")
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		command, output := parseLine(string(line))
		ch := createSubprocess(command, output)

		waitSubprocess(ch)
	}
}

リダイレクト

Cmdの Stdoutを差し替えることで, リダイレクトを実現しました

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
)

var globRegexp = regexp.MustCompile(`\*`)
var redirectRegex = regexp.MustCompile(`>(\S+)`)

func parseLine(line string) ([]string, *os.File) {
	var output *os.File

	words := strings.Split(strings.TrimSpace(line), " ")

	command := make([]string, 0)
	for _, word := range words {
		trimed := strings.TrimSpace(word)

		if globRegexp.Match([]byte(trimed)) {
			expandeds, err := filepath.Glob(trimed)
			if err != nil {
				log.Fatal(err)
			}
			command = append(command, expandeds...)
		} else if redirectRegex.Match([]byte(trimed)) {
			matches := redirectRegex.FindAllStringSubmatch(trimed, -1)
			file := matches[0][1]

			var err error
			output, err = os.Create(file)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			command = append(command, trimed)
		}
	}

	return command, output
}

func createSubprocess(command []string, output *os.File) chan bool {
	ch := make(chan bool)
	go func() {
		cmd := exec.Command(command[0], command[1:]...)
		if output != nil {
			cmd.Stdout = output
		} else {
			cmd.Stdout = os.Stdout
		}

		cmd.Stderr = os.Stderr

		err := cmd.Start()
		if err != nil {
			log.Println(err)
		} else {
			err = cmd.Wait()
			if err != nil {
				log.Println(err)
			}
		}

		ch <- true
		close(ch)
	}()

	return ch
}

func waitSubprocess(ch <-chan bool) {
	<-ch
}

func main() {
	bio := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("-> ")
		line, hasMoreLine, err := bio.ReadLine()
		if !hasMoreLine && err == io.EOF {
			fmt.Println("Bye")
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		command, output := parseLine(string(line))
		ch := createSubprocess(command, output)

		waitSubprocess(ch)
	}
}

パイプ

パイプの使い方はオリジナルのものと同じです.

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
)

var globRegexp = regexp.MustCompile(`\*`)

func parseLine(line string) [][]string {
	words := strings.Split(strings.TrimSpace(line), " ")

	commands := make([][]string, 0)
	cmd := make([]string, 0)

	for _, word := range words {
		trimed := strings.TrimSpace(word)

		if globRegexp.Match([]byte(trimed)) {
			expandeds, err := filepath.Glob(trimed)
			if err != nil {
				log.Fatal(err)
			}
			cmd = append(cmd, expandeds...)
		} else if trimed == "|" {
			commands = append(commands, cmd)
			cmd = make([]string, 0)
		} else {
			cmd = append(cmd, trimed)
		}
	}

	commands = append(commands, cmd)

	return commands
}

func createSubprocess(command []string, in *io.PipeReader, out *io.PipeWriter, ch chan<- bool) {
	go func() {
		cmd := exec.Command(command[0], command[1:]...)

		if in == nil {
			cmd.Stdin = os.Stdout
		} else {
			cmd.Stdin = in
		}

		if out == nil {
			cmd.Stdout = os.Stdout
		} else {
			cmd.Stdout = out
		}

		err := cmd.Start()
		if err != nil {
			log.Println(err)
		} else {
			err = cmd.Wait()
			if err != nil {
				log.Println(err)
			}

			if in != nil {
				in.Close()
			}

			if out != nil {
				out.Close()
			}
		}

		ch <- true
	}()
}

func waitSubprocess(processes int, ch <-chan bool) {
	for i := 0; i < processes; i++ {
		<-ch
	}
}

type DataPipes struct {
	in  *io.PipeReader
	out *io.PipeWriter
}

func makeSubprocessPipes(processes int) []*DataPipes {
	pipes := make([]*DataPipes, 0)

	for i := 0; i < processes; i++ {
		in, out := io.Pipe()

		data := &DataPipes{in, out}
		pipes = append(pipes, data)
	}

	return pipes
}

func main() {
	bio := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("-> ")
		line, hasMoreLine, err := bio.ReadLine()
		if !hasMoreLine && err == io.EOF {
			fmt.Println("Bye")
			break
		}
		if err != nil {
			log.Fatal(err)
		}

		commands := parseLine(string(line))
		processes := len(commands)

		pipes := makeSubprocessPipes(processes)

		ch := make(chan bool, len(commands))
		for i, command := range commands {
			var in *io.PipeReader
			var out *io.PipeWriter

			if i != 0 {
				in = pipes[i-1].in
			}

			if i != len(commands)-1 {
				out = pipes[i].out
			}

			createSubprocess(command, in, out, ch)
		}

		waitSubprocess(processes, ch)
	}
}

おわりに

go力がまだまだ低いのでもっと言い方があるかもしれません. ツッコミどころが
あればコメントなり, githubの issues等で突っ込んでいただければと思います.


感想としては goは Rubyなんかに比べるとだいぶコード量的には多くなって
しまいますが、そこまで苦ではないですね. まあ私が LL言語を使いこなせて
いないというのもあるのですが・・・。もっと精進したいです.