@@ -6,6 +6,7 @@ package sqlcmd
66import (
77 "flag"
88 "fmt"
9+ "io"
910 "os"
1011 "regexp"
1112 "sort"
@@ -29,6 +30,9 @@ type Command struct {
2930 name string
3031 // whether the command is a system command
3132 isSystem bool
33+ // help is the text shown by :help for this command.
34+ // Multiple lines are allowed. Empty means hidden from :help.
35+ help string
3236}
3337
3438// Commands is the set of sqlcmd command implementations
@@ -41,87 +45,107 @@ func newCommands() Commands {
4145 regex : regexp .MustCompile (`(?im)^[\t ]*?:?EXIT([\( \t]+.*\)*$|$)` ),
4246 action : exitCommand ,
4347 name : "EXIT" ,
48+ help : ":exit\n - Quits sqlcmd immediately.\n " +
49+ ":exit()\n - Execute statement cache; quit with no return value.\n " +
50+ ":exit(<query>)\n - Execute the specified query; returns numeric result.\n " ,
4451 },
4552 "QUIT" : {
4653 regex : regexp .MustCompile (`(?im)^[\t ]*?:?QUIT(?:[ \t]+(.*$)|$)` ),
4754 action : quitCommand ,
4855 name : "QUIT" ,
56+ help : ":quit\n - Quits sqlcmd immediately.\n " ,
4957 },
5058 "GO" : {
5159 regex : regexp .MustCompile (batchTerminatorRegex ("GO" )),
5260 action : goCommand ,
5361 name : "GO" ,
62+ help : "go [<n>]\n - Executes the statement cache (n times).\n " ,
5463 },
5564 "OUT" : {
5665 regex : regexp .MustCompile (`(?im)^[ \t]*:OUT(?:[ \t]+(.*$)|$)` ),
5766 action : outCommand ,
5867 name : "OUT" ,
68+ help : ":out <filename>|stderr|stdout\n - Redirects query output to a file, stderr, or stdout.\n " ,
5969 },
6070 "ERROR" : {
6171 regex : regexp .MustCompile (`(?im)^[ \t]*:ERROR(?:[ \t]+(.*$)|$)` ),
6272 action : errorCommand ,
6373 name : "ERROR" ,
74+ help : ":error <dest>\n - Redirects error output to a file, stderr, or stdout.\n " ,
6475 }, "READFILE" : {
6576 regex : regexp .MustCompile (`(?im)^[ \t]*:R(?:[ \t]+(.*$)|$)` ),
6677 action : readFileCommand ,
6778 name : "READFILE" ,
79+ help : ":r <filename>\n - Append file contents to the statement cache.\n " ,
6880 },
6981 "SETVAR" : {
7082 regex : regexp .MustCompile (`(?im)^[ \t]*:SETVAR(?:[ \t]+(.*$)|$)` ),
7183 action : setVarCommand ,
7284 name : "SETVAR" ,
85+ help : ":setvar {variable}\n - Removes a sqlcmd scripting variable.\n " +
86+ ":setvar <variable> <value>\n - Sets a sqlcmd scripting variable.\n " ,
7387 },
7488 "LISTVAR" : {
7589 regex : regexp .MustCompile (`(?im)^[\t ]*?:LISTVAR(?:[ \t]+(.*$)|$)` ),
7690 action : listVarCommand ,
7791 name : "LISTVAR" ,
92+ help : ":listvar\n - Lists the set sqlcmd scripting variables.\n " ,
7893 },
7994 "RESET" : {
8095 regex : regexp .MustCompile (`(?im)^[ \t]*?:?RESET(?:[ \t]+(.*$)|$)` ),
8196 action : resetCommand ,
8297 name : "RESET" ,
98+ help : ":reset\n - Discards the statement cache.\n " ,
8399 },
84100 "LIST" : {
85101 regex : regexp .MustCompile (`(?im)^[ \t]*:LIST(?:[ \t]+(.*$)|$)` ),
86102 action : listCommand ,
87103 name : "LIST" ,
104+ help : ":list\n - Prints the content of the statement cache.\n " ,
88105 },
89106 "CONNECT" : {
90107 regex : regexp .MustCompile (`(?im)^[ \t]*:CONNECT(?:[ \t]+(.*$)|$)` ),
91108 action : connectCommand ,
92109 name : "CONNECT" ,
110+ help : ":connect server[\\ instance] [-l timeout] [-U user [-P password]]\n - Connects to a SQL Server instance.\n " ,
93111 },
94112 "EXEC" : {
95113 regex : regexp .MustCompile (`(?im)^[ \t]*?:?!!(.*$)` ),
96114 action : execCommand ,
97115 name : "EXEC" ,
98116 isSystem : true ,
117+ help : ":!! [<command>]\n - Executes a command in the operating system shell.\n " ,
99118 },
100119 "EDIT" : {
101120 regex : regexp .MustCompile (`(?im)^[\t ]*?:?ED(?:[ \t]+(.*$)|$)` ),
102121 action : editCommand ,
103122 name : "EDIT" ,
104123 isSystem : true ,
124+ help : ":ed\n - Edits the current or last executed statement cache.\n " ,
105125 },
106126 "ONERROR" : {
107127 regex : regexp .MustCompile (`(?im)^[\t ]*?:?ON ERROR(?:[ \t]+(.*$)|$)` ),
108128 action : onerrorCommand ,
109129 name : "ONERROR" ,
130+ help : ":on error [exit|ignore]\n - Action for batch or sqlcmd command errors.\n " ,
110131 },
111132 "XML" : {
112133 regex : regexp .MustCompile (`(?im)^[\t ]*?:XML(?:[ \t]+(.*$)|$)` ),
113134 action : xmlCommand ,
114135 name : "XML" ,
136+ help : ":xml [on|off]\n - Sets XML output mode.\n " ,
115137 },
116138 "HELP" : {
117139 regex : regexp .MustCompile (`(?im)^[ \t]*:HELP(?:[ \t]+(.*$)|$)` ),
118140 action : helpCommand ,
119141 name : "HELP" ,
142+ help : ":help\n - Shows this list of commands.\n " ,
120143 },
121144 "PERFTRACE" : {
122145 regex : regexp .MustCompile (`(?im)^[ \t]*:PERFTRACE(?:[ \t]+(.*$)|$)` ),
123146 action : perftraceCommand ,
124147 name : "PERFTRACE" ,
148+ help : ":perftrace <filename>|stderr|stdout\n - Redirects timing output to a file, stderr, or stdout.\n " ,
125149 },
126150 }
127151}
@@ -310,61 +334,48 @@ func goCommand(s *Sqlcmd, args []string, line uint) error {
310334 return nil
311335}
312336
313- // outCommand changes the output writer to use a file
314- func outCommand (s * Sqlcmd , args []string , line uint ) error {
337+ // redirectWriter resolves a :out/:error/:perftrace argument to stderr, stdout,
338+ // or a new file, then calls setter with the result.
339+ func redirectWriter (s * Sqlcmd , args []string , line uint , name string , setter func (io.WriteCloser )) error {
315340 if len (args ) == 0 || args [0 ] == "" {
316- return InvalidCommandError ("OUT" , line )
341+ return InvalidCommandError (name , line )
317342 }
318343 filePath , err := resolveArgumentVariables (s , []rune (args [0 ]), true )
319344 if err != nil {
320345 return err
321346 }
322-
323347 switch {
324- case strings .EqualFold (filePath , "stdout" ):
325- s .SetOutput (os .Stdout )
326348 case strings .EqualFold (filePath , "stderr" ):
327- s .SetOutput (os .Stderr )
349+ setter (os .Stderr )
350+ case strings .EqualFold (filePath , "stdout" ):
351+ setter (os .Stdout )
328352 default :
329353 o , err := os .OpenFile (filePath , os .O_TRUNC | os .O_CREATE | os .O_WRONLY , 0o644 )
330354 if err != nil {
331355 return InvalidFileError (err , args [0 ])
332356 }
333- if s .UnicodeOutputFile {
334- // ODBC sqlcmd doesn't write a BOM but we will.
335- // Maybe the endian-ness should be configurable.
336- win16le := unicode .UTF16 (unicode .LittleEndian , unicode .UseBOM )
337- encoder := transform .NewWriter (o , win16le .NewEncoder ())
338- s .SetOutput (encoder )
339- } else {
340- s .SetOutput (o )
341- }
357+ setter (o )
342358 }
343359 return nil
344360}
345361
346- // errorCommand changes the error writer to use a file
347- func errorCommand (s * Sqlcmd , args []string , line uint ) error {
348- if len (args ) == 0 || args [0 ] == "" {
349- return InvalidCommandError ("ERROR" , line )
350- }
351- filePath , err := resolveArgumentVariables (s , []rune (args [0 ]), true )
352- if err != nil {
353- return err
362+ // outCommand changes the output writer to use a file
363+ func outCommand (s * Sqlcmd , args []string , line uint ) error {
364+ if ! s .UnicodeOutputFile {
365+ return redirectWriter (s , args , line , "OUT" , s .SetOutput )
354366 }
355- switch {
356- case strings .EqualFold (filePath , "stderr" ):
357- s .SetError (os .Stderr )
358- case strings .EqualFold (filePath , "stdout" ):
359- s .SetError (os .Stdout )
360- default :
361- o , err := os .OpenFile (filePath , os .O_TRUNC | os .O_CREATE | os .O_WRONLY , 0o644 )
362- if err != nil {
363- return InvalidFileError (err , args [0 ])
367+ return redirectWriter (s , args , line , "OUT" , func (w io.WriteCloser ) {
368+ if w != os .Stdout && w != os .Stderr {
369+ win16le := unicode .UTF16 (unicode .LittleEndian , unicode .UseBOM )
370+ w = transform .NewWriter (w , win16le .NewEncoder ())
364371 }
365- s .SetError (o )
366- }
367- return nil
372+ s .SetOutput (w )
373+ })
374+ }
375+
376+ // errorCommand changes the error writer to use a file
377+ func errorCommand (s * Sqlcmd , args []string , line uint ) error {
378+ return redirectWriter (s , args , line , "ERROR" , s .SetError )
368379}
369380
370381func readFileCommand (s * Sqlcmd , args []string , line uint ) error {
@@ -607,72 +618,23 @@ func xmlCommand(s *Sqlcmd, args []string, line uint) error {
607618}
608619
609620func helpCommand (s * Sqlcmd , args []string , line uint ) error {
610- helpText := `:!! [<command>]
611- - Executes a command in the operating system shell.
612- :connect server[\instance] [-l timeout] [-U user [-P password]]
613- - Connects to a SQL Server instance.
614- :ed
615- - Edits the current or last executed statement cache.
616- :error <dest>
617- - Redirects error output to a file, stderr, or stdout.
618- :exit
619- - Quits sqlcmd immediately.
620- :exit()
621- - Execute statement cache; quit with no return value.
622- :exit(<query>)
623- - Execute the specified query; returns numeric result.
624- go [<n>]
625- - Executes the statement cache (n times).
626- :help
627- - Shows this list of commands.
628- :list
629- - Prints the content of the statement cache.
630- :listvar
631- - Lists the set sqlcmd scripting variables.
632- :on error [exit|ignore]
633- - Action for batch or sqlcmd command errors.
634- :out <filename>|stderr|stdout
635- - Redirects query output to a file, stderr, or stdout.
636- :perftrace <filename>|stderr|stdout
637- - Redirects timing output to a file, stderr, or stdout.
638- :quit
639- - Quits sqlcmd immediately.
640- :r <filename>
641- - Append file contents to the statement cache.
642- :reset
643- - Discards the statement cache.
644- :setvar {variable}
645- - Removes a sqlcmd scripting variable.
646- :setvar <variable> <value>
647- - Sets a sqlcmd scripting variable.
648- :xml [on|off]
649- - Sets XML output mode.
650- `
651- _ , err := s .GetOutput ().Write ([]byte (helpText ))
621+ entries := make ([]string , 0 , len (s .Cmd ))
622+ for _ , cmd := range s .Cmd {
623+ if cmd .help != "" {
624+ entries = append (entries , cmd .help )
625+ }
626+ }
627+ sort .Strings (entries )
628+ var b strings.Builder
629+ for _ , entry := range entries {
630+ b .WriteString (entry )
631+ }
632+ _ , err := s .GetOutput ().Write ([]byte (b .String ()))
652633 return err
653634}
654635
655636func perftraceCommand (s * Sqlcmd , args []string , line uint ) error {
656- if len (args ) == 0 || args [0 ] == "" {
657- return InvalidCommandError ("PERFTRACE" , line )
658- }
659- filePath , err := resolveArgumentVariables (s , []rune (args [0 ]), true )
660- if err != nil {
661- return err
662- }
663- switch {
664- case strings .EqualFold (filePath , "stderr" ):
665- s .SetStat (os .Stderr )
666- case strings .EqualFold (filePath , "stdout" ):
667- s .SetStat (os .Stdout )
668- default :
669- o , err := os .OpenFile (filePath , os .O_TRUNC | os .O_CREATE | os .O_WRONLY , 0o644 )
670- if err != nil {
671- return InvalidFileError (err , args [0 ])
672- }
673- s .SetStat (o )
674- }
675- return nil
637+ return redirectWriter (s , args , line , "PERFTRACE" , s .SetStat )
676638}
677639
678640func resolveArgumentVariables (s * Sqlcmd , arg []rune , failOnUnresolved bool ) (string , error ) {
0 commit comments