├── examples ├── ping.go ├── python.go ├── ftp.go └── screen.go ├── LICENCE ├── README.md ├── gexpect.go └── gexpect_test.go /examples/ping.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import gexpect "github.com/ThomasRooney/gexpect" 4 | import "log" 5 | 6 | func main() { 7 | log.Printf("Testing Ping interact... \n") 8 | 9 | child, err := gexpect.Spawn("ping -c8 127.0.0.1") 10 | if err != nil { 11 | panic(err) 12 | } 13 | child.Interact() 14 | log.Printf("Success\n") 15 | } 16 | -------------------------------------------------------------------------------- /examples/python.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/ThomasRooney/gexpect" 4 | import "fmt" 5 | 6 | func main() { 7 | fmt.Printf("Starting python.. \n") 8 | child, err := gexpect.Spawn("python") 9 | if err != nil { 10 | panic(err) 11 | } 12 | fmt.Printf("Expecting >>>.. \n") 13 | child.Expect(">>>") 14 | fmt.Printf("print 'Hello World'..\n") 15 | child.SendLine("print 'Hello World'") 16 | child.Expect(">>>") 17 | 18 | fmt.Printf("Interacting.. \n") 19 | child.Interact() 20 | fmt.Printf("Done \n") 21 | child.Close() 22 | } 23 | -------------------------------------------------------------------------------- /examples/ftp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import gexpect "github.com/ThomasRooney/gexpect" 4 | import "log" 5 | 6 | func main() { 7 | log.Printf("Testing Ftp... ") 8 | 9 | child, err := gexpect.Spawn("ftp ftp.openbsd.org") 10 | if err != nil { 11 | panic(err) 12 | } 13 | child.Expect("Name") 14 | child.SendLine("anonymous") 15 | child.Expect("Password") 16 | child.SendLine("pexpect@sourceforge.net") 17 | child.Expect("ftp> ") 18 | child.SendLine("cd /pub/OpenBSD/3.7/packages/i386") 19 | child.Expect("ftp> ") 20 | child.SendLine("bin") 21 | child.Expect("ftp> ") 22 | child.SendLine("prompt") 23 | child.Expect("ftp> ") 24 | child.SendLine("pwd") 25 | child.Expect("ftp> ") 26 | log.Printf("Success\n") 27 | } 28 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Thomas Rooney 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /examples/screen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/ThomasRooney/gexpect" 4 | import "fmt" 5 | import "strings" 6 | 7 | func main() { 8 | waitChan := make(chan string) 9 | 10 | fmt.Printf("Starting screen.. \n") 11 | 12 | child, err := gexpect.Spawn("screen") 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | sender, reciever := child.AsyncInteractChannels() 18 | go func() { 19 | waitString := "" 20 | count := 0 21 | for { 22 | select { 23 | case waitString = <-waitChan: 24 | count++ 25 | case msg, open := <-reciever: 26 | if !open { 27 | return 28 | } 29 | fmt.Printf("Recieved: %s\n", msg) 30 | 31 | if strings.Contains(msg, waitString) { 32 | if count >= 1 { 33 | waitChan <- msg 34 | count -= 1 35 | } 36 | } 37 | } 38 | } 39 | }() 40 | wait := func(str string) { 41 | waitChan <- str 42 | <-waitChan 43 | } 44 | fmt.Printf("Waiting until started.. \n") 45 | wait(" ") 46 | fmt.Printf("Sending Enter.. \n") 47 | sender <- "\n" 48 | wait("$") 49 | fmt.Printf("Sending echo.. \n") 50 | sender <- "echo Hello World\n" 51 | wait("Hello World") 52 | fmt.Printf("Received echo. \n") 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gexpect 2 | 3 | Gexpect is a pure golang expect-like module. 4 | 5 | It makes it simpler to create and control other terminal applications. 6 | 7 | child, err := gexpect.Spawn("python") 8 | if err != nil { 9 | panic(err) 10 | } 11 | child.Expect(">>>") 12 | child.SendLine("print 'Hello World'") 13 | child.Interact() 14 | child.Close() 15 | 16 | ## Examples 17 | 18 | `Spawn` handles the argument parsing from a string 19 | 20 | child.Spawn("/bin/sh -c 'echo \"my complicated command\" | tee log | cat > log2'") 21 | child.ReadLine() // ReadLine() (string, error) 22 | child.ReadUntil(' ') // ReadUntil(delim byte) ([]byte, error) 23 | 24 | `ReadLine`, `ReadUntil` and `SendLine` send strings from/to `stdout/stdin` respectively 25 | 26 | child, _ := gexpect.Spawn("cat") 27 | child.SendLine("echoing process_stdin") // SendLine(command string) (error) 28 | msg, _ := child.ReadLine() // msg = echoing process_stdin 29 | 30 | `Wait` and `Close` allow for graceful and ungraceful termination. 31 | 32 | child.Wait() // Waits until the child terminates naturally. 33 | child.Close() // Sends a kill command 34 | 35 | `AsyncInteractChannels` spawns two go routines to pipe into and from `stdout`/`stdin`, allowing for some usecases to be a little simpler. 36 | 37 | child, _ := gexpect.Spawn("sh") 38 | sender, receiver := child.AsyncInteractChannels() 39 | sender <- "echo Hello World\n" // Send to stdin 40 | line, open := <- receiver // Recieve a line from stdout/stderr 41 | // When the subprocess stops (e.g. with child.Close()) , receiver is closed 42 | if open { 43 | fmt.Printf("Received %s", line) 44 | } 45 | 46 | `ExpectRegex` uses golang's internal regex engine to wait until a match from the process with the given regular expression (or an error on process termination with no match). 47 | 48 | child, _ := gexpect.Spawn("echo accb") 49 | match, _ := child.ExpectRegex("a..b") 50 | // (match=true) 51 | 52 | `ExpectRegexFind` allows for groups to be extracted from process stdout. The first element is an array of containing the total matched text, followed by each subexpression group match. 53 | 54 | child, _ := gexpect.Spawn("echo 123 456 789") 55 | result, _ := child.ExpectRegexFind("\d+ (\d+) (\d+)") 56 | // result = []string{"123 456 789", "456", "789"} 57 | 58 | See `gexpect_test.go` and the `examples` folder for full syntax 59 | 60 | ## Credits 61 | 62 | github.com/kballard/go-shellquote 63 | github.com/kr/pty 64 | KMP Algorithm: "http://blog.databigbang.com/searching-for-substrings-in-streams-a-slight-modification-of-the-knuth-morris-pratt-algorithm-in-haxe/" 65 | -------------------------------------------------------------------------------- /gexpect.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package gexpect 4 | 5 | import ( 6 | "bytes" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "time" 14 | "unicode/utf8" 15 | 16 | shell "github.com/kballard/go-shellquote" 17 | "github.com/kr/pty" 18 | ) 19 | 20 | var ( 21 | ErrEmptySearch = errors.New("empty search string") 22 | ) 23 | 24 | type ExpectSubprocess struct { 25 | Cmd *exec.Cmd 26 | buf *buffer 27 | outputBuffer []byte 28 | } 29 | 30 | type buffer struct { 31 | f *os.File 32 | b bytes.Buffer 33 | collect bool 34 | 35 | collection bytes.Buffer 36 | } 37 | 38 | func (buf *buffer) StartCollecting() { 39 | buf.collect = true 40 | } 41 | 42 | func (buf *buffer) StopCollecting() (result string) { 43 | result = string(buf.collection.Bytes()) 44 | buf.collect = false 45 | buf.collection.Reset() 46 | return result 47 | } 48 | 49 | func (buf *buffer) Read(chunk []byte) (int, error) { 50 | nread := 0 51 | if buf.b.Len() > 0 { 52 | n, err := buf.b.Read(chunk) 53 | if err != nil { 54 | return n, err 55 | } 56 | if n == len(chunk) { 57 | return n, nil 58 | } 59 | nread = n 60 | } 61 | fn, err := buf.f.Read(chunk[nread:]) 62 | return fn + nread, err 63 | } 64 | 65 | func (buf *buffer) ReadRune() (r rune, size int, err error) { 66 | l := buf.b.Len() 67 | 68 | chunk := make([]byte, utf8.UTFMax) 69 | if l > 0 { 70 | n, err := buf.b.Read(chunk) 71 | if err != nil { 72 | return 0, 0, err 73 | } 74 | if utf8.FullRune(chunk[:n]) { 75 | r, rL := utf8.DecodeRune(chunk) 76 | if n > rL { 77 | buf.PutBack(chunk[rL:n]) 78 | } 79 | if buf.collect { 80 | buf.collection.WriteRune(r) 81 | } 82 | return r, rL, nil 83 | } 84 | } 85 | // else add bytes from the file, then try that 86 | for l < utf8.UTFMax { 87 | fn, err := buf.f.Read(chunk[l : l+1]) 88 | if err != nil { 89 | return 0, 0, err 90 | } 91 | l = l + fn 92 | 93 | if utf8.FullRune(chunk[:l]) { 94 | r, rL := utf8.DecodeRune(chunk) 95 | if buf.collect { 96 | buf.collection.WriteRune(r) 97 | } 98 | return r, rL, nil 99 | } 100 | } 101 | return 0, 0, errors.New("File is not a valid UTF=8 encoding") 102 | } 103 | 104 | func (buf *buffer) PutBack(chunk []byte) { 105 | if len(chunk) == 0 { 106 | return 107 | } 108 | if buf.b.Len() == 0 { 109 | buf.b.Write(chunk) 110 | return 111 | } 112 | d := make([]byte, 0, len(chunk)+buf.b.Len()) 113 | d = append(d, chunk...) 114 | d = append(d, buf.b.Bytes()...) 115 | buf.b.Reset() 116 | buf.b.Write(d) 117 | } 118 | 119 | func SpawnAtDirectory(command string, directory string) (*ExpectSubprocess, error) { 120 | expect, err := _spawn(command) 121 | if err != nil { 122 | return nil, err 123 | } 124 | expect.Cmd.Dir = directory 125 | return _start(expect) 126 | } 127 | 128 | func Command(command string) (*ExpectSubprocess, error) { 129 | expect, err := _spawn(command) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return expect, nil 134 | } 135 | 136 | func (expect *ExpectSubprocess) Start() error { 137 | _, err := _start(expect) 138 | return err 139 | } 140 | 141 | func Spawn(command string) (*ExpectSubprocess, error) { 142 | expect, err := _spawn(command) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return _start(expect) 147 | } 148 | 149 | func (expect *ExpectSubprocess) Close() error { 150 | if err := expect.Cmd.Process.Kill(); err != nil { 151 | return err 152 | } 153 | if err := expect.buf.f.Close(); err != nil { 154 | return err 155 | } 156 | return nil 157 | } 158 | 159 | func (expect *ExpectSubprocess) AsyncInteractChannels() (send chan string, receive chan string) { 160 | receive = make(chan string) 161 | send = make(chan string) 162 | 163 | go func() { 164 | for { 165 | str, err := expect.ReadLine() 166 | if err != nil { 167 | close(receive) 168 | return 169 | } 170 | receive <- str 171 | } 172 | }() 173 | 174 | go func() { 175 | for { 176 | select { 177 | case sendCommand, exists := <-send: 178 | { 179 | if !exists { 180 | return 181 | } 182 | err := expect.Send(sendCommand) 183 | if err != nil { 184 | receive <- "gexpect Error: " + err.Error() 185 | return 186 | } 187 | } 188 | } 189 | } 190 | }() 191 | 192 | return 193 | } 194 | 195 | func (expect *ExpectSubprocess) ExpectRegex(regex string) (bool, error) { 196 | return regexp.MatchReader(regex, expect.buf) 197 | } 198 | 199 | func (expect *ExpectSubprocess) expectRegexFind(regex string, output bool) ([]string, string, error) { 200 | re, err := regexp.Compile(regex) 201 | if err != nil { 202 | return nil, "", err 203 | } 204 | expect.buf.StartCollecting() 205 | pairs := re.FindReaderSubmatchIndex(expect.buf) 206 | stringIndexedInto := expect.buf.StopCollecting() 207 | l := len(pairs) 208 | numPairs := l / 2 209 | result := make([]string, numPairs) 210 | for i := 0; i < numPairs; i += 1 { 211 | result[i] = stringIndexedInto[pairs[i*2]:pairs[i*2+1]] 212 | } 213 | // convert indexes to strings 214 | 215 | if len(result) == 0 { 216 | err = fmt.Errorf("ExpectRegex didn't find regex '%v'.", regex) 217 | } else { 218 | // The number in pairs[1] is an index of a first 219 | // character outside the whole match 220 | putBackIdx := pairs[1] 221 | if len(stringIndexedInto) > putBackIdx { 222 | stringToPutBack := stringIndexedInto[putBackIdx:] 223 | stringIndexedInto = stringIndexedInto[:putBackIdx] 224 | expect.buf.PutBack([]byte(stringToPutBack)) 225 | } 226 | } 227 | return result, stringIndexedInto, err 228 | } 229 | 230 | func (expect *ExpectSubprocess) expectTimeoutRegexFind(regex string, timeout time.Duration) (result []string, out string, err error) { 231 | t := make(chan bool) 232 | go func() { 233 | result, out, err = expect.ExpectRegexFindWithOutput(regex) 234 | t <- false 235 | }() 236 | go func() { 237 | time.Sleep(timeout) 238 | err = fmt.Errorf("ExpectRegex timed out after %v finding '%v'.\nOutput:\n%s", timeout, regex, expect.Collect()) 239 | t <- true 240 | }() 241 | <-t 242 | return result, out, err 243 | } 244 | 245 | func (expect *ExpectSubprocess) ExpectRegexFind(regex string) ([]string, error) { 246 | result, _, err := expect.expectRegexFind(regex, false) 247 | return result, err 248 | } 249 | 250 | func (expect *ExpectSubprocess) ExpectTimeoutRegexFind(regex string, timeout time.Duration) ([]string, error) { 251 | result, _, err := expect.expectTimeoutRegexFind(regex, timeout) 252 | return result, err 253 | } 254 | 255 | func (expect *ExpectSubprocess) ExpectRegexFindWithOutput(regex string) ([]string, string, error) { 256 | return expect.expectRegexFind(regex, true) 257 | } 258 | 259 | func (expect *ExpectSubprocess) ExpectTimeoutRegexFindWithOutput(regex string, timeout time.Duration) ([]string, string, error) { 260 | return expect.expectTimeoutRegexFind(regex, timeout) 261 | } 262 | 263 | func buildKMPTable(searchString string) []int { 264 | pos := 2 265 | cnd := 0 266 | length := len(searchString) 267 | 268 | var table []int 269 | if length < 2 { 270 | length = 2 271 | } 272 | 273 | table = make([]int, length) 274 | table[0] = -1 275 | table[1] = 0 276 | 277 | for pos < len(searchString) { 278 | if searchString[pos-1] == searchString[cnd] { 279 | cnd += 1 280 | table[pos] = cnd 281 | pos += 1 282 | } else if cnd > 0 { 283 | cnd = table[cnd] 284 | } else { 285 | table[pos] = 0 286 | pos += 1 287 | } 288 | } 289 | return table 290 | } 291 | 292 | func (expect *ExpectSubprocess) ExpectTimeout(searchString string, timeout time.Duration) (e error) { 293 | result := make(chan error) 294 | go func() { 295 | result <- expect.Expect(searchString) 296 | }() 297 | select { 298 | case e = <-result: 299 | case <-time.After(timeout): 300 | e = fmt.Errorf("Expect timed out after %v waiting for '%v'.\nOutput:\n%s", timeout, searchString, expect.Collect()) 301 | } 302 | return e 303 | } 304 | 305 | func (expect *ExpectSubprocess) Expect(searchString string) (e error) { 306 | target := len(searchString) 307 | if target < 1 { 308 | return ErrEmptySearch 309 | } 310 | chunk := make([]byte, target*2) 311 | if expect.outputBuffer != nil { 312 | expect.outputBuffer = expect.outputBuffer[0:] 313 | } 314 | m := 0 315 | i := 0 316 | // Build KMP Table 317 | table := buildKMPTable(searchString) 318 | 319 | for { 320 | n, err := expect.buf.Read(chunk) 321 | if n == 0 && err != nil { 322 | return err 323 | } 324 | if expect.outputBuffer != nil { 325 | expect.outputBuffer = append(expect.outputBuffer, chunk[:n]...) 326 | } 327 | offset := m + i 328 | for m+i-offset < n { 329 | if searchString[i] == chunk[m+i-offset] { 330 | i += 1 331 | if i == target { 332 | unreadIndex := m + i - offset 333 | if len(chunk) > unreadIndex { 334 | expect.buf.PutBack(chunk[unreadIndex:n]) 335 | } 336 | return nil 337 | } 338 | } else { 339 | m += i - table[i] 340 | if table[i] > -1 { 341 | i = table[i] 342 | } else { 343 | i = 0 344 | } 345 | } 346 | } 347 | } 348 | } 349 | 350 | func (expect *ExpectSubprocess) Send(command string) error { 351 | _, err := io.WriteString(expect.buf.f, command) 352 | return err 353 | } 354 | 355 | func (expect *ExpectSubprocess) Capture() { 356 | if expect.outputBuffer == nil { 357 | expect.outputBuffer = make([]byte, 0) 358 | } 359 | } 360 | 361 | func (expect *ExpectSubprocess) Collect() []byte { 362 | collectOutput := make([]byte, len(expect.outputBuffer)) 363 | copy(collectOutput, expect.outputBuffer) 364 | expect.outputBuffer = nil 365 | return collectOutput 366 | } 367 | 368 | func (expect *ExpectSubprocess) SendLine(command string) error { 369 | _, err := io.WriteString(expect.buf.f, command+"\r\n") 370 | return err 371 | } 372 | 373 | func (expect *ExpectSubprocess) Interact() { 374 | defer expect.Cmd.Wait() 375 | io.Copy(os.Stdout, &expect.buf.b) 376 | go io.Copy(os.Stdout, expect.buf.f) 377 | go io.Copy(expect.buf.f, os.Stdin) 378 | } 379 | 380 | func (expect *ExpectSubprocess) ReadUntil(delim byte) ([]byte, error) { 381 | join := make([]byte, 0, 512) 382 | chunk := make([]byte, 255) 383 | 384 | for { 385 | n, err := expect.buf.Read(chunk) 386 | 387 | for i := 0; i < n; i++ { 388 | if chunk[i] == delim { 389 | if len(chunk) > i+1 { 390 | expect.buf.PutBack(chunk[i+1:n]) 391 | } 392 | return join, nil 393 | } else { 394 | join = append(join, chunk[i]) 395 | } 396 | } 397 | 398 | if err != nil { 399 | return join, err 400 | } 401 | } 402 | } 403 | 404 | func (expect *ExpectSubprocess) Wait() error { 405 | return expect.Cmd.Wait() 406 | } 407 | 408 | func (expect *ExpectSubprocess) ReadLine() (string, error) { 409 | str, err := expect.ReadUntil('\n') 410 | return string(str), err 411 | } 412 | 413 | func _start(expect *ExpectSubprocess) (*ExpectSubprocess, error) { 414 | f, err := pty.Start(expect.Cmd) 415 | if err != nil { 416 | return nil, err 417 | } 418 | expect.buf.f = f 419 | 420 | return expect, nil 421 | } 422 | 423 | func _spawn(command string) (*ExpectSubprocess, error) { 424 | wrapper := new(ExpectSubprocess) 425 | 426 | wrapper.outputBuffer = nil 427 | 428 | splitArgs, err := shell.Split(command) 429 | if err != nil { 430 | return nil, err 431 | } 432 | numArguments := len(splitArgs) - 1 433 | if numArguments < 0 { 434 | return nil, errors.New("gexpect: No command given to spawn") 435 | } 436 | path, err := exec.LookPath(splitArgs[0]) 437 | if err != nil { 438 | return nil, err 439 | } 440 | 441 | if numArguments >= 1 { 442 | wrapper.Cmd = exec.Command(path, splitArgs[1:]...) 443 | } else { 444 | wrapper.Cmd = exec.Command(path) 445 | } 446 | wrapper.buf = new(buffer) 447 | 448 | return wrapper, nil 449 | } 450 | -------------------------------------------------------------------------------- /gexpect_test.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package gexpect 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io/ioutil" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestEmptySearchString(t *testing.T) { 15 | t.Logf("Testing empty search string...") 16 | child, err := Spawn("echo Hello World") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | err = child.Expect("") 21 | if err != ErrEmptySearch { 22 | t.Fatalf("Expected empty search error, got %v", err) 23 | } 24 | } 25 | 26 | func TestHelloWorld(t *testing.T) { 27 | t.Logf("Testing Hello World... ") 28 | child, err := Spawn("echo \"Hello World\"") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | err = child.Expect("Hello World") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | } 37 | 38 | func TestDoubleHelloWorld(t *testing.T) { 39 | t.Logf("Testing Double Hello World... ") 40 | child, err := Spawn(`sh -c "echo Hello World ; echo Hello ; echo Hi"`) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | err = child.Expect("Hello World") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | err = child.Expect("Hello") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | err = child.Expect("Hi") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | } 57 | 58 | func TestHelloWorldFailureCase(t *testing.T) { 59 | t.Logf("Testing Hello World Failure case... ") 60 | child, err := Spawn("echo \"Hello World\"") 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | err = child.Expect("YOU WILL NEVER FIND ME") 65 | if err != nil { 66 | return 67 | } 68 | t.Fatal("Expected an error for TestHelloWorldFailureCase") 69 | } 70 | 71 | func TestBiChannel(t *testing.T) { 72 | 73 | t.Logf("Testing BiChannel screen... ") 74 | child, err := Spawn("cat") 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | sender, receiver := child.AsyncInteractChannels() 79 | wait := func(str string) { 80 | for { 81 | msg, open := <-receiver 82 | if !open { 83 | return 84 | } 85 | if strings.Contains(msg, str) { 86 | return 87 | } 88 | } 89 | } 90 | 91 | endlChar := fmt.Sprintln("") 92 | sender <- fmt.Sprintf("echo%v", endlChar) 93 | wait("echo") 94 | sender <- fmt.Sprintf("echo2%v", endlChar) 95 | wait("echo2") 96 | child.Close() 97 | child.Wait() 98 | } 99 | 100 | func TestCommandStart(t *testing.T) { 101 | t.Logf("Testing Command... ") 102 | 103 | // Doing this allows you to modify the cmd struct prior to execution, for example to add environment variables 104 | child, err := Command("echo 'Hello World'") 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | child.Start() 109 | child.Expect("Hello World") 110 | } 111 | 112 | var regexMatchTests = []struct { 113 | re string 114 | good string 115 | bad string 116 | }{ 117 | {`a`, `a`, `b`}, 118 | {`.b`, `ab`, `ac`}, 119 | {`a+hello`, `aaaahello`, `bhello`}, 120 | {`(hello|world)`, `hello`, `unknown`}, 121 | {`(hello|world)`, `world`, `unknown`}, 122 | {"\u00a9", "\u00a9", `unknown`}, // 2 bytes long unicode character "copyright sign" 123 | } 124 | 125 | func TestRegexMatch(t *testing.T) { 126 | t.Logf("Testing Regular Expression Matching... ") 127 | for _, tt := range regexMatchTests { 128 | runTest := func(input string) bool { 129 | var match bool 130 | child, err := Spawn("echo \"" + input + "\"") 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | match, err = child.ExpectRegex(tt.re) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | return match 139 | } 140 | if !runTest(tt.good) { 141 | t.Errorf("Regex Not matching [%#q] with pattern [%#q]", tt.good, tt.re) 142 | } 143 | if runTest(tt.bad) { 144 | t.Errorf("Regex Matching [%#q] with pattern [%#q]", tt.bad, tt.re) 145 | } 146 | } 147 | } 148 | 149 | var regexFindTests = []struct { 150 | re string 151 | input string 152 | matches []string 153 | }{ 154 | {`he(l)lo wo(r)ld`, `hello world`, []string{"hello world", "l", "r"}}, 155 | {`(a)`, `a`, []string{"a", "a"}}, 156 | {`so.. (hello|world)`, `so.. hello`, []string{"so.. hello", "hello"}}, 157 | {`(a+)hello`, `aaaahello`, []string{"aaaahello", "aaaa"}}, 158 | {`\d+ (\d+) (\d+)`, `123 456 789`, []string{"123 456 789", "456", "789"}}, 159 | {`\d+ (\d+) (\d+)`, "\u00a9 123 456 789 \u00a9", []string{"123 456 789", "456", "789"}}, // check unicode characters 160 | } 161 | 162 | func TestRegexFind(t *testing.T) { 163 | t.Logf("Testing Regular Expression Search... ") 164 | for _, tt := range regexFindTests { 165 | runTest := func(input string) []string { 166 | child, err := Spawn("echo \"" + input + "\"") 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | matches, err := child.ExpectRegexFind(tt.re) 171 | if err != nil { 172 | t.Fatal(err) 173 | } 174 | return matches 175 | } 176 | matches := runTest(tt.input) 177 | if len(matches) != len(tt.matches) { 178 | t.Fatalf("Regex not producing the expected number of patterns.. got[%d] ([%s]) expected[%d] ([%s])", 179 | len(matches), strings.Join(matches, ","), 180 | len(tt.matches), strings.Join(tt.matches, ",")) 181 | } 182 | for i, _ := range matches { 183 | if matches[i] != tt.matches[i] { 184 | t.Errorf("Regex Expected group [%s] and got group [%s] with pattern [%#q] and input [%s]", 185 | tt.matches[i], matches[i], tt.re, tt.input) 186 | } 187 | } 188 | } 189 | } 190 | 191 | func TestReadLine(t *testing.T) { 192 | t.Logf("Testing ReadLine...") 193 | 194 | child, err := Spawn("echo \"foo\nbar\"") 195 | 196 | if err != nil { 197 | t.Fatal(err) 198 | } 199 | s, err := child.ReadLine() 200 | 201 | if err != nil { 202 | t.Fatal(err) 203 | } 204 | if s != "foo\r" { 205 | t.Fatalf("expected 'foo\\r', got '%s'", s) 206 | } 207 | s, err = child.ReadLine() 208 | if err != nil { 209 | t.Fatal(err) 210 | } 211 | if s != "bar\r" { 212 | t.Fatalf("expected 'bar\\r', got '%s'", s) 213 | } 214 | } 215 | 216 | func TestRegexWithOutput(t *testing.T) { 217 | t.Logf("Testing Regular Expression search with output...") 218 | 219 | s := "You will not find me" 220 | p, err := Spawn("echo -n " + s) 221 | if err != nil { 222 | t.Fatalf("Cannot exec rkt: %v", err) 223 | } 224 | searchPattern := `I should not find you` 225 | result, out, err := p.ExpectRegexFindWithOutput(searchPattern) 226 | if err == nil { 227 | t.Fatalf("Shouldn't have found `%v` in `%v`", searchPattern, out) 228 | } 229 | if s != out { 230 | t.Fatalf("Child output didn't match: %s", out) 231 | } 232 | 233 | err = p.Wait() 234 | if err != nil { 235 | t.Fatalf("Child didn't terminate correctly: %v", err) 236 | } 237 | 238 | p, err = Spawn("echo You will find me") 239 | if err != nil { 240 | t.Fatalf("Cannot exec rkt: %v", err) 241 | } 242 | searchPattern = `.*(You will).*` 243 | result, out, err = p.ExpectRegexFindWithOutput(searchPattern) 244 | if err != nil || result[1] != "You will" { 245 | t.Fatalf("Did not find pattern `%v` in `%v'\n", searchPattern, out) 246 | } 247 | err = p.Wait() 248 | if err != nil { 249 | t.Fatalf("Child didn't terminate correctly: %v", err) 250 | } 251 | } 252 | 253 | func TestRegexTimeoutWithOutput(t *testing.T) { 254 | t.Logf("Testing Regular Expression search with timeout and output...") 255 | 256 | seconds := 2 257 | timeout := time.Duration(seconds-1) * time.Second 258 | 259 | p, err := Spawn(fmt.Sprintf("sh -c 'sleep %d && echo You find me'", seconds)) 260 | if err != nil { 261 | t.Fatalf("Cannot exec rkt: %v", err) 262 | } 263 | searchPattern := `find me` 264 | result, out, err := p.ExpectTimeoutRegexFindWithOutput(searchPattern, timeout) 265 | if err == nil { 266 | t.Fatalf("Shouldn't have finished call with result: %v", result) 267 | } 268 | 269 | seconds = 2 270 | timeout = time.Duration(seconds+1) * time.Second 271 | 272 | p, err = Spawn(fmt.Sprintf("sh -c 'sleep %d && echo You find me'", seconds)) 273 | if err != nil { 274 | t.Fatalf("Cannot exec rkt: %v", err) 275 | } 276 | searchPattern = `find me` 277 | result, out, err = p.ExpectTimeoutRegexFindWithOutput(searchPattern, timeout) 278 | if err != nil { 279 | t.Fatalf("Didn't find %v in output: %v", searchPattern, out) 280 | } 281 | } 282 | 283 | func TestRegexFindNoExcessBytes(t *testing.T) { 284 | t.Logf("Testing Regular Expressions returning output with no excess strings") 285 | repeats := 50 286 | tests := []struct { 287 | desc string 288 | loopBody string 289 | searchPattern string 290 | expectFullTmpl string 291 | unmatchedData string 292 | }{ 293 | { 294 | desc: `matching lines line by line with $ at the end of the regexp`, 295 | loopBody: `echo "prefix: ${i} line"`, 296 | searchPattern: `(?m)^prefix:\s+(\d+) line\s??$`, 297 | expectFullTmpl: `prefix: %d line`, 298 | unmatchedData: "\n", 299 | // the "$" char at the end of regexp does not 300 | // match the \n, so it is left as an unmatched 301 | // data 302 | }, 303 | { 304 | desc: `matching lines line by line with \n at the end of the regexp`, 305 | loopBody: `echo "prefix: ${i} line"`, 306 | searchPattern: `(?m)^prefix:\s+(\d+) line\s??\n`, 307 | expectFullTmpl: `prefix: %d line`, 308 | unmatchedData: "", 309 | }, 310 | { 311 | desc: `matching chunks in single line chunk by chunk`, 312 | loopBody: `printf "a ${i} b"`, 313 | searchPattern: `a\s+(\d+)\s+b`, 314 | expectFullTmpl: `a %d b`, 315 | unmatchedData: "", 316 | }, 317 | } 318 | seqCmd := fmt.Sprintf("`seq 1 %d`", repeats) 319 | shCmdTmpl := fmt.Sprintf(`sh -c 'for i in %s; do %%s; done'`, seqCmd) 320 | for _, tt := range tests { 321 | t.Logf("Test: %s", tt.desc) 322 | shCmd := fmt.Sprintf(shCmdTmpl, tt.loopBody) 323 | t.Logf("Running command: %s", shCmd) 324 | p, err := Spawn(shCmd) 325 | if err != nil { 326 | t.Fatalf("Cannot exec shell script: %v", err) 327 | } 328 | defer func() { 329 | if err := p.Wait(); err != nil { 330 | t.Fatalf("shell script didn't terminate correctly: %v", err) 331 | } 332 | }() 333 | for i := 1; i <= repeats; i++ { 334 | matches, output, err := p.ExpectRegexFindWithOutput(tt.searchPattern) 335 | if err != nil { 336 | t.Fatalf("Failed to get the match number %d: %v", i, err) 337 | } 338 | if len(matches) != 2 { 339 | t.Fatalf("Expected only 2 matches, got %d", len(matches)) 340 | } 341 | full := strings.TrimSpace(matches[0]) 342 | expFull := fmt.Sprintf(tt.expectFullTmpl, i) 343 | partial := matches[1] 344 | expPartial := fmt.Sprintf("%d", i) 345 | if full != expFull { 346 | t.Fatalf("Did not the expected full match %q, got %q", expFull, full) 347 | } 348 | if partial != expPartial { 349 | t.Fatalf("Did not the expected partial match %q, got %q", expPartial, partial) 350 | } 351 | // The output variable usually contains the 352 | // unmatched data followed by the whole match. 353 | // The first line is special as it has no data 354 | // preceding it. 355 | var expectedOutput string 356 | if i == 1 || tt.unmatchedData == "" { 357 | expectedOutput = matches[0] 358 | } else { 359 | expectedOutput = fmt.Sprintf("%s%s", tt.unmatchedData, matches[0]) 360 | } 361 | if output != expectedOutput { 362 | t.Fatalf("The collected output %q should be the same as the whole match %q", output, expectedOutput) 363 | } 364 | } 365 | } 366 | } 367 | 368 | func TestBufferReadRune(t *testing.T) { 369 | tests := []struct { 370 | bufferContent []byte 371 | fileContent []byte 372 | expectedRune rune 373 | }{ 374 | // unicode "copyright char" is \u00a9 is encoded as two bytes in utf8 0xc2 0xa9 375 | {[]byte{0xc2, 0xa9}, []byte{}, '\u00a9'}, // whole rune is already in buffer.b 376 | {[]byte{0xc2}, []byte{0xa9}, '\u00a9'}, // half of is in the buffer.b and another half still in buffer.f (file) 377 | {[]byte{}, []byte{0xc2, 0xa9}, '\u00a9'}, // whole rune is the file 378 | // some random noise in the end of file 379 | {[]byte{0xc2, 0xa9}, []byte{0x20, 0x20, 0x20, 0x20}, '\u00a9'}, 380 | {[]byte{0xc2}, []byte{0xa9, 0x20, 0x20, 0x20, 0x20}, '\u00a9'}, 381 | {[]byte{}, []byte{0xc2, 0xa9, 0x20, 0x20, 0x20, 0x20}, '\u00a9'}, 382 | } 383 | 384 | for i, tt := range tests { 385 | 386 | // prepare tmp file with fileContent 387 | f, err := ioutil.TempFile("", "") 388 | if err != nil { 389 | t.Fatal(err) 390 | } 391 | n, err := f.Write(tt.fileContent) 392 | if err != nil { 393 | t.Fatal(err) 394 | } 395 | if n != len(tt.fileContent) { 396 | t.Fatal("expected fileContent written to temp file") 397 | } 398 | _, err = f.Seek(0, 0) 399 | if err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | // new buffer 404 | buf := buffer{f: f, b: *bytes.NewBuffer(tt.bufferContent)} 405 | 406 | // call ReadRune 407 | r, size, err := buf.ReadRune() 408 | 409 | if r != tt.expectedRune { 410 | t.Fatalf("#%d: expected rune %+q but go is %+q", i, tt.expectedRune, r) 411 | } 412 | 413 | if size != len(string(tt.expectedRune)) { 414 | t.Fatalf("#%d: expected rune %d bytes long but got just %d bytes long", i, len(string(tt.expectedRune)), size) 415 | } 416 | 417 | } 418 | 419 | } 420 | --------------------------------------------------------------------------------