Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions docs/docs/References/Platform_Libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,13 @@ print response.stderr

##### Using parameters to construct a bash command.

Note: see the helper function [construct_bash_command_string](#construct_bash_command_string)
```python
from dlpx.virtualization import libs

name = virtual_source.parameters.username
port = virtual_source.parameters.port
command = "mysqldump -u {} -p {}".format(name,port)
command = libs.construct_bash_command_string("mysqldump", "-u", name, "-p", port)

response = libs.run_bash(connection, command)
```
Expand Down Expand Up @@ -124,6 +125,34 @@ response = libs.run_bash(connection, command)
```
For more information please go to [Managing Scripts for Remote Execution](/Best_Practices/Managing_Scripts_For_Remote_Execution.md) section.

## construct_bash_command_string

Constructs a full Bash command string from an executable name and list of args.

This helper function is intended to help with the simple case of running a remote
command with a set of arguments. By using this function, you won't have to worry
about the details of escaping/quoting special characters.

### Signature
`def construct_bash_command_string(executable, args)`

### Arguments
Argument | Type | Description
-------- | ---- | -----------
executable | String | Name of the command. This can be the full path to the command, the name of a command that is found on the path, or a shell builtin.
args | List of Strings | Arguments to the command

### Returns
A string representing a full Bash command line, that can be passed to run_bash.

### Example
```python
old_name = "A filename with spaces in it"
new_name = "The file's new $100 name" # <-- note special characters!
move_command = libs.construct_bash_command_string("mv", old_name, new_name)
libs.run_bash(cx, move_command)
```

## run_expect

Executes a tcl command or script on a remote Unix host.
Expand Down Expand Up @@ -151,7 +180,7 @@ stderr | String | Stderr from the command.

### Example

Calling expect with an inline command.
Calling expect with an inline command.

```python
from dlpx.virtualization import libs
Expand Down
77 changes: 77 additions & 0 deletions libs/src/main/python/dlpx/virtualization/libs/libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

__all__ = [
"run_bash",
"construct_bash_command_string",
"run_sync",
"run_powershell",
"run_expect",
Expand Down Expand Up @@ -96,6 +97,82 @@ def _check_exit_code(response, check):
response.return_value.stderr))


def _quote_bash_word(unquoted_string):
"""
This function takes the given input string, and returns a string that can
be passed to Bash, such that Bash will interpret it as a unitary word.

So, the incoming string can have quotes, dollar signs, spaces, etc. These
characters will be quoted/escaped such that Bash does not interpret them
specially.

The returned string can be used as part of a command string passed to
run_bash.

The technique is to enclose everything in single quotes, except for single-
quote characters, which are enclosed by double quotes.

For example, consider this python string that we want to pass to a program:
a'b"c$f$o$o

We cannot simply pass this string to Bash as-is, because Bash would treat
the dollar signs and quote characters specially.

For this case, this function would then return the following string:
'a'"'"'c$f$o$o'

Breaking this down, the returned string contains three separate items:
1) 'a' (a single-quoted string containing no special characters)
2) "'" (a double-quoted string containing a special character)
3) 'c$f$o$o' (a single-quoted string containing some special characters)
Because of the quoting, none of the special characters above will be
interpreted by Bash, and will be left as-is. Bash will remove the outer
quotes on each of these three parts, and then mash the three parts together
into one string.

Thus, once Bash does its interpretation of the string, the result will have
the same content as the original Python string:
a'b"c$f$o$o
"""

# We want to enclose the whole string in single quotes.
# But, any time we see a single-quote in the given string, we need to:
# 1) Close the existing single-quoted section
# 2) Start a new double-quoted section
# 3) Insert the original single-quote character
# 4) Close the double-quoted section that we just opened
# 5) Open a new single-quoted section for the remainder of the string
return "'{}'".format(unquoted_string.replace("'", "'\"'\"'"))


def construct_bash_command_string(executable, *args):
"""
Helper function to assemble a full command string to pass to run_bash.

run_bash expects a single string representing the entire command/script that
is to be run. This is sometimes inconvenient and bug-prone for the simple
case when you want to run a single command with some arguments.

For example, suppose you want to change a filename. You might be tempted to
write this:
run_bash(cx, "mv {} {}".format(oldname, newname))

That code will fail to work if either of the two names has a space or other
special character in it. This function is meant to help with such cases.
So, the above example can change to this:
run_bash(cx, construct_bash_command_string("mv", oldname, newname))

This method will worry about all of the quoting and escaping necessary.
"""
assert executable
exec_string = _quote_bash_word(executable)
if args:
arg_string = " ".join([_quote_bash_word(a) for a in args])
return "{} {}".format(exec_string, arg_string)
else:
return exec_string


def run_bash(remote_connection, command, variables=None, use_login_shell=False,
check=False):
"""run_bash operation wrapper.
Expand Down
32 changes: 32 additions & 0 deletions libs/src/test/python/dlpx/virtualization/test_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,38 @@ def test_run_bash_bad_use_login_shell(remote_connection):
" class 'str' but should be of class 'bool' if defined.")


class TestLibsConstructBashCommand:
@staticmethod
def test_no_args():
result = libs.construct_bash_command_string("/path/to/executable")
assert result == "'/path/to/executable'"

@staticmethod
def test_single_arg():
result = libs.construct_bash_command_string("foo", "bar")
assert result == "'foo' 'bar'"

@staticmethod
def test_many_args():
result = libs.construct_bash_command_string("a", "b", "c", "d", "e")
assert result == "'a' 'b' 'c' 'd' 'e'"

@staticmethod
def test_single_quote_escaping():
result = libs.construct_bash_command_string("a'b'c", "e'f'g")
assert result == "'a'\"'\"'b'\"'\"'c' 'e'\"'\"'f'\"'\"'g'"

@staticmethod
def test_double_quote_escaping():
result = libs.construct_bash_command_string("a\"b\"c", "e\"f\"g")
assert result == "'a\"b\"c' 'e\"f\"g'"

@staticmethod
def test_combined_escaping():
result = libs.construct_bash_command_string("a'b\"c$f$o$o")
assert result == "'a'\"'\"'b\"c$f$o$o'"


class TestLibsRunSync:
@staticmethod
def test_run_sync(remote_connection):
Expand Down