]> Cypherpunks repositories - gostls13.git/commitdiff
os: parse command line without shell32.dll
authorAlex Brainman <alex.brainman@gmail.com>
Sun, 24 Apr 2016 03:37:12 +0000 (20:37 -0700)
committerAlex Brainman <alex.brainman@gmail.com>
Fri, 24 Mar 2017 00:53:03 +0000 (00:53 +0000)
Go uses CommandLineToArgV from shell32.dll to parse command
line parameters. But shell32.dll is slow to load. Implement
Windows command line parsing in Go. This should make starting
Go programs faster.

I can see these speed ups for runtime.BenchmarkRunningGoProgram

on my Windows 7 amd64:
name                old time/op  new time/op  delta
RunningGoProgram-2  11.2ms ± 1%  10.4ms ± 2%  -6.63%  (p=0.000 n=9+10)

on my Windows XP 386:
name                old time/op  new time/op  delta
RunningGoProgram-2  19.0ms ± 3%  12.1ms ± 1%  -36.20%  (p=0.000 n=10+10)

on @egonelbre Windows 10 amd64:
name                old time/op  new time/op  delta
RunningGoProgram-8  17.0ms ± 1%  15.3ms ± 2%  -9.71%  (p=0.000 n=10+10)

This CL is based on CL 22932 by John Starks.

Fixes #15588.

Change-Id: Ib14be0206544d0d4492ca1f0d91fac968be52241
Reviewed-on: https://go-review.googlesource.com/37915
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>

src/os/exec_windows.go
src/os/export_windows_test.go
src/os/os_windows_test.go

index d89db2022ccb299a292f8621aade9eb55880a087..d5d553a2f6b33d6098ff0979a1abc6fc7b7f6a82 100644 (file)
@@ -97,17 +97,79 @@ func findProcess(pid int) (p *Process, err error) {
 }
 
 func init() {
-       var argc int32
-       cmd := syscall.GetCommandLine()
-       argv, e := syscall.CommandLineToArgv(cmd, &argc)
-       if e != nil {
-               return
+       p := syscall.GetCommandLine()
+       cmd := syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(p))[:])
+       if len(cmd) == 0 {
+               arg0, _ := Executable()
+               Args = []string{arg0}
+       } else {
+               Args = commandLineToArgv(cmd)
        }
-       defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv))))
-       Args = make([]string, argc)
-       for i, v := range (*argv)[:argc] {
-               Args[i] = syscall.UTF16ToString((*v)[:])
+}
+
+// appendBSBytes appends n '\\' bytes to b and returns the resulting slice.
+func appendBSBytes(b []byte, n int) []byte {
+       for ; n > 0; n-- {
+               b = append(b, '\\')
+       }
+       return b
+}
+
+// readNextArg splits command line string cmd into next
+// argument and command line remainder.
+func readNextArg(cmd string) (arg []byte, rest string) {
+       var b []byte
+       var inquote bool
+       var nslash int
+       for ; len(cmd) > 0; cmd = cmd[1:] {
+               c := cmd[0]
+               switch c {
+               case ' ', '\t':
+                       if !inquote {
+                               return appendBSBytes(b, nslash), cmd[1:]
+                       }
+               case '"':
+                       b = appendBSBytes(b, nslash/2)
+                       if nslash%2 == 0 {
+                               // use "Prior to 2008" rule from
+                               // http://daviddeley.com/autohotkey/parameters/parameters.htm
+                               // section 5.2 to deal with double double quotes
+                               if inquote && len(cmd) > 1 && cmd[1] == '"' {
+                                       b = append(b, c)
+                                       cmd = cmd[1:]
+                               }
+                               inquote = !inquote
+                       } else {
+                               b = append(b, c)
+                       }
+                       nslash = 0
+                       continue
+               case '\\':
+                       nslash++
+                       continue
+               }
+               b = appendBSBytes(b, nslash)
+               nslash = 0
+               b = append(b, c)
+       }
+       return appendBSBytes(b, nslash), ""
+}
+
+// commandLineToArgv splits a command line into individual argument
+// strings, following the Windows conventions documented
+// at http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV
+func commandLineToArgv(cmd string) []string {
+       var args []string
+       for len(cmd) > 0 {
+               if cmd[0] == ' ' || cmd[0] == '\t' {
+                       cmd = cmd[1:]
+                       continue
+               }
+               var arg []byte
+               arg, cmd = readNextArg(cmd)
+               args = append(args, string(arg))
        }
+       return args
 }
 
 func ftToDuration(ft *syscall.Filetime) time.Duration {
index d08bd747cef3bfd677f80b372c5c560b4a056dc8..f36fadb58bc3fc6ec6b5a8d4620e4bab7295d050 100644 (file)
@@ -7,6 +7,7 @@ package os
 // Export for testing.
 
 var (
-       FixLongPath    = fixLongPath
-       NewConsoleFile = newConsoleFile
+       FixLongPath       = fixLongPath
+       NewConsoleFile    = newConsoleFile
+       CommandLineToArgv = commandLineToArgv
 )
index 761931e9e98b906e6481a894914f45ba855ac648..61ca0c91bffc968400ae4450450d085f708becd6 100644 (file)
@@ -723,3 +723,137 @@ func TestStatPagefile(t *testing.T) {
        }
        t.Fatal(err)
 }
+
+// syscallCommandLineToArgv calls syscall.CommandLineToArgv
+// and converts returned result into []string.
+func syscallCommandLineToArgv(cmd string) ([]string, error) {
+       var argc int32
+       argv, err := syscall.CommandLineToArgv(&syscall.StringToUTF16(cmd)[0], &argc)
+       if err != nil {
+               return nil, err
+       }
+       defer syscall.LocalFree(syscall.Handle(uintptr(unsafe.Pointer(argv))))
+
+       var args []string
+       for _, v := range (*argv)[:argc] {
+               args = append(args, syscall.UTF16ToString((*v)[:]))
+       }
+       return args, nil
+}
+
+// compareCommandLineToArgvWithSyscall ensures that
+// os.CommandLineToArgv(cmd) and syscall.CommandLineToArgv(cmd)
+// return the same result.
+func compareCommandLineToArgvWithSyscall(t *testing.T, cmd string) {
+       syscallArgs, err := syscallCommandLineToArgv(cmd)
+       if err != nil {
+               t.Fatal(err)
+       }
+       args := os.CommandLineToArgv(cmd)
+       if want, have := fmt.Sprintf("%q", syscallArgs), fmt.Sprintf("%q", args); want != have {
+               t.Errorf("testing os.commandLineToArgv(%q) failed: have %q want %q", cmd, args, syscallArgs)
+               return
+       }
+}
+
+func TestCmdArgs(t *testing.T) {
+       tmpdir, err := ioutil.TempDir("", "TestCmdArgs")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(tmpdir)
+
+       const prog = `
+package main
+
+import (
+       "fmt"
+       "os"
+)
+
+func main() {
+       fmt.Printf("%q", os.Args)
+}
+`
+       src := filepath.Join(tmpdir, "main.go")
+       err = ioutil.WriteFile(src, []byte(prog), 0666)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       exe := filepath.Join(tmpdir, "main.exe")
+       cmd := osexec.Command("go", "build", "-o", exe, src)
+       cmd.Dir = tmpdir
+       out, err := cmd.CombinedOutput()
+       if err != nil {
+               t.Fatalf("building main.exe failed: %v\n%s", err, out)
+       }
+
+       var cmds = []string{
+               ``,
+               ` a b c`,
+               ` "`,
+               ` ""`,
+               ` """`,
+               ` "" a`,
+               ` "123"`,
+               ` \"123\"`,
+               ` \"123 456\"`,
+               ` \\"`,
+               ` \\\"`,
+               ` \\\\\"`,
+               ` \\\"x`,
+               ` """"\""\\\"`,
+               ` abc`,
+               ` \\\\\""x"""y z`,
+               "\tb\t\"x\ty\"",
+               ` "Брад" d e`,
+               // examples from https://msdn.microsoft.com/en-us/library/17w5ykft.aspx
+               ` "abc" d e`,
+               ` a\\b d"e f"g h`,
+               ` a\\\"b c d`,
+               ` a\\\\"b c" d e`,
+               // http://daviddeley.com/autohotkey/parameters/parameters.htm#WINARGV
+               // from 5.4  Examples
+               ` CallMeIshmael`,
+               ` "Call Me Ishmael"`,
+               ` Cal"l Me I"shmael`,
+               ` CallMe\"Ishmael`,
+               ` "CallMe\"Ishmael"`,
+               ` "Call Me Ishmael\\"`,
+               ` "CallMe\\\"Ishmael"`,
+               ` a\\\b`,
+               ` "a\\\b"`,
+               // from 5.5  Some Common Tasks
+               ` "\"Call Me Ishmael\""`,
+               ` "C:\TEST A\\"`,
+               ` "\"C:\TEST A\\\""`,
+               // from 5.6  The Microsoft Examples Explained
+               ` "a b c"  d  e`,
+               ` "ab\"c"  "\\"  d`,
+               ` a\\\b d"e f"g h`,
+               ` a\\\"b c d`,
+               ` a\\\\"b c" d e`,
+               // from 5.7  Double Double Quote Examples (pre 2008)
+               ` "a b c""`,
+               ` """CallMeIshmael"""  b  c`,
+               ` """Call Me Ishmael"""`,
+               ` """"Call Me Ishmael"" b c`,
+       }
+       for _, cmd := range cmds {
+               compareCommandLineToArgvWithSyscall(t, "test"+cmd)
+               compareCommandLineToArgvWithSyscall(t, `"cmd line"`+cmd)
+               compareCommandLineToArgvWithSyscall(t, exe+cmd)
+
+               // test both syscall.EscapeArg and os.commandLineToArgv
+               args := os.CommandLineToArgv(exe + cmd)
+               out, err := osexec.Command(args[0], args[1:]...).CombinedOutput()
+               if err != nil {
+                       t.Fatalf("runing %q failed: %v\n%v", args, err, string(out))
+               }
+               if want, have := fmt.Sprintf("%q", args), string(out); want != have {
+                       t.Errorf("wrong output of executing %q: have %q want %q", args, have, want)
+                       continue
+               }
+       }
+}