command

package
v1.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 6, 2023 License: BSD-2-Clause Imports: 15 Imported by: 0

Documentation

Overview

Package command can be a better replacement of exec/command package, provide more safety and handy methods, chain style.

Safety

The argument for '%s' will be always safely escaped.

The argument for %s and "%s" will be always safely escaped except $VAR and ${VAR}, thus you can use shell variables in side arguments.

Below is true:

reflect.DeepEqual(
	command.NewSh(`echo %s '%s'`, "logs: $HOME/$abc/logs", "logs: $HOME/$abc/logs").Args,
	[]string{"sh", "-c", `echo logs\:\ $HOME/$abc/logs logs\:\ \$HOME/\$abc/logs `}
)

The New and NewSh method will escape any invalid shell characters, to avoid Remote Code Execution (RCE) attack or any form of Shell Injection, the escape will be denoted by below 2 forms:

  • %s or "%s": will escape everything, except for shell variables like $ABC, or ${ABC}, any other variables form not accepted.
  • '%s': will escape everything, shell variables also be escaped.

The New([]string, args...) and NewSh(string, args...) method argments just like fmt.Printf, the first arg is formatString, rest is format arguments, but with one exception: they can only accept %s as format placeholder. If you want use like %v, you can manually invoke [String()] method of the argument to pass as string.

Handy

The package provider chain style invoking, like below:

command.NewSh(`echo %s '%s'`, "logs: $HOME/$abc/logs", "logs: $HOME/$abc/logs")
	.Stdout(os.Stdout)
	.Stdin(os.Stdin)
	.Timeout(time.Second*10)
	.CombinedOutput()

There methods can be chained(in the middle):

But below methods cannot be chained(finalize):

For more information please checkout the godoc.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ReplaceShellString

func ReplaceShellString(s string, token *shlex.Token) string

Types

type Command

type Command struct {
	*exec.Cmd
	// Pid is the pid of command after start
	Pid int
	// LastError is the last recorded error after chain
	LastError error
	// Ctx is the context of the command, can check Err() on OnExit to see if the context be canceled
	Ctx context.Context
	// Cancel the context of the command, command will be killed, and Ctx.Err() not nil
	Cancel context.CancelFunc
	// contains filtered or unexported fields
}

Command is embedded exec.Cmd struct, with some more state to use.

func New

func New(cmdArgs []string, parts ...string) *Command

New return a Command instance to execute the named program with the given arguments, cmdArgs will be safely escaped, to avoid Remote Code Execution (RCE) attack or any form of Shell Injection, the escape will be denoted by below 2 forms:

  • %s or "%s": will escape everything, except for shell variables like $ABC, or ${ABC}, any other variables form not accepted.
  • '%s': will escape everything, shell variables also be escaped.

Command returns the Cmd struct to execute the named program with the given arguments.

It sets only the Path and Args in the returned structure.

If name contains no path separators, Command uses LookPath to resolve name to a complete path if possible. Otherwise it uses name directly as Path.

The returned Cmd's Args field is constructed from the command name followed by the elements of arg, so arg should not include the command name itself. For example, Command("echo", "hello"). Args[0] is always name, not the possibly resolved Path.

On Windows, processes receive the whole command line as a single string and do their own parsing. Command combines and quotes Args into a command line string with an algorithm compatible with applications using CommandLineToArgvW (which is the most common way). Notable exceptions are msiexec.exe and cmd.exe (and thus, all batch files), which have a different unquoting algorithm. In these or other similar cases, you can do the quoting yourself and provide the full command line in SysProcAttr.CmdLine, leaving Args empty.

Example
package main

import (
	"fmt"

	"github.com/futurist/better-command/command"
)

func main() {
	out, err := command.New(
		[]string{"bash", "-c", "printf '%s' | awk '{print $0}'"}, "$(dangerous command) and $PASSWORD",
	).Output()
	// all the `$` will be escaped, so it's safe
	fmt.Println(string(out), err)
}
Output:

$(dangerous command) and $PASSWORD
 <nil>

func NewBash added in v1.0.3

func NewBash(cmdString string, parts ...string) *Command

NewBash just like New, but run []string{"bash", "-c", cmdString} by default

func NewSh

func NewSh(cmdString string, parts ...string) *Command

NewSh just like New, but run []string{"sh", "-c", cmdString} by default

Example
package main

import (
	"fmt"

	"github.com/futurist/better-command/command"
)

func main() {
	out, err := command.NewSh(
		"printf '%s' | awk '{print $0}'", "$(dangerous command) and $PASSWORD",
	).Output()
	// all the `$` will be escaped, so it's safe
	fmt.Println(string(out), err)
}
Output:

$(dangerous command) and $PASSWORD
 <nil>

func (*Command) AsUser

func (c *Command) AsUser(osuser string) *Command

AsUser run command with osuser

func (*Command) CombinedOutput

func (c *Command) CombinedOutput() ([]byte, error)

CombinedOutput runs the command and returns its combined standard output and standard error.

func (*Command) Context

func (c *Command) Context(ctx context.Context) *Command

Context can set command context that can cause the command be killed when canceled.

The provided context is used to kill the process (by calling os.Process.Kill) if the context becomes done before the command completes on its own.

func (*Command) Dir

func (c *Command) Dir(dir string) *Command

Dir run command with PWD set to dir

func (*Command) Env

func (c *Command) Env(env []string) *Command

Env set command env to run

func (*Command) OnExit

func (c *Command) OnExit(f ...func(*Command)) *Command

OnExit set functions to run when command just exit, here can check the Ctx.Err() etc.

func (*Command) OnStart

func (c *Command) OnStart(f ...func(*Command)) *Command

OnStart set functions to run when command just started

Example
package main

import (
	"fmt"
	"strings"

	"github.com/futurist/better-command/command"
)

func main() {
	out, err := command.New(
		[]string{"bash", "-c", `echo "%s" | awk '{print '"$Var"' $0}'`}, "$(dangerous command) and normal $Var",
	).Env(
		[]string{"Var=123"},
	).OnStart(
		func(c *command.Command) {
			fmt.Printf("%#v", c.Cmd.Args)
		},
	).Output()
	// all the `$` will be escaped, so it's safe
	fmt.Println("--"+strings.TrimSpace(string(out))+"--", err)
}
Output:

[]string{"bash", "-c", "echo \\$\\(dangerous\\ command\\)\\ and\\ normal\\ $Var | awk '{print '$Var' $0}'"}--123$(dangerous command) and normal 123-- <nil>

func (*Command) Output

func (c *Command) Output() ([]byte, error)

Output runs the command and returns its standard output. Any returned error will usually be of type *ExitError. If c.Stderr was nil, Output populates ExitError.Stderr.

func (*Command) Run

func (c *Command) Run() error

Run starts the specified command and waits for it to complete.

The returned error is nil if the command runs, has no problems copying stdin, stdout, and stderr, and exits with a zero exit status.

If the command starts but does not complete successfully, the error is of type *ExitError. Other error types may be returned for other situations.

If the calling goroutine has locked the operating system thread with runtime.LockOSThread and modified any inheritable OS-level thread state (for example, Linux or Plan 9 name spaces), the new process will inherit the caller's thread state.

func (*Command) Shell

func (c *Command) Shell(shellName string) *Command

Shell set command shell to shellName instead of 'sh', it must accept '-c' as second arg

func (*Command) Stderr

func (c *Command) Stderr(f io.Writer) *Command

Stderr set command stderr to f

func (*Command) Stdin

func (c *Command) Stdin(f io.Reader) *Command

Stdin set command stdin to f

func (*Command) Stdout

func (c *Command) Stdout(f io.Writer) *Command

Stdout set command stdout to f

func (*Command) Timeout

func (c *Command) Timeout(timeout time.Duration) *Command

Timeout run command with timeout, then kill the process.

func (*Command) UseSudo

func (c *Command) UseSudo() *Command

UseSudo to run command use `sudo` if not root, otherwise run normally

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL