Skip to main content

Executing Commands

/bin/bash is used on Unix-like systems and cmd.exe is used on Windows for executing Pipe commands that will:

The Pipe directory is contained in $PWD (Present Working Directory). As a rule, do not expect any files written to this directory to survive a Pipe update, instead use absolute file paths. This directory is also in the $PATH environment variable.

Input Exec

The command is executed by /bin/bash, with all line feeds removed by default. It is possible to arrange a complicated command like the one below without needing backslashes:

input:
exec:
command: |
complex-command
--first-flag 1
--second-flag 2

You can disable this removal using no-strip-linefeeds: true.

By default, each line of output is considered a fresh event and quoted:

input:
exec:
command: echo ay; echo bee

# Output: {"_raw":"ay"}, {"_raw":"bee"}

Use raw: true to disable quoting for output that is already in JSON or that requires further mangling with action-raw.

input:
exec:
no-strip-linefeeds: true # (or use semicolons)
command: |
echo '{"msg":"hello"}'
echo '{"msg":"dolly"}'
raw: true

# Output: {"msg":"hello"}, {"msg":"dolly"}

By default, the command is run once, which is appropriate for commands such as ping that continuously create output with a specified interval.

Scheduling Commands

To run at regular intervals — the easiest way is to specify a value for interval.

Commands can be scheduled to run at regular intervals by specifying an interval value.

Scheduled commands give you the option to process the entire output from each invocation as a single event using ignore-line-breaks: true:

input:
exec:
command: echo ay; echo bee
ignore-line-breaks: true
interval: 2s

# Output: {"_raw":"ay\nbee"}

count: 1 allows you to specify a command to execute precisely once. This may be needed as ignore-line-breaks currently only works with scheduled inputs.

An event that corresponds to multiple lines of output (multiline event), requires that all data be consolidated as a single piece of text, e.g.:

$> openssl s_client -connect google.com:443 </dev/null 2>/dev/null | openssl x509 -fingerprint -dates -nocert

SHA1 Fingerprint=95:3A:FF:D9:19:64:D9:09:40:8D:EE:DA:40:48:0E:FF:5E:DA:52:8C
notBefore=Sep 3 06:36:33 2020 GMT
notAfter=Nov 26 06:36:33 2020 GMT

Here we can use expand.key-value, note delim: '\n', which is a newline.

name: openssl_client

input:
exec:
command: openssl s_client -connect google.com:443 </dev/null 2>/dev/null | openssl x509 -fingerprint -dates -nocert
ignore-linebreaks: true
interval: 1s
count: 1

actions:
# Expand key values using line-feed as a delimiter.
- expand:
input-field: _raw
remove: true
delim: '\n'
key-value:
key-value-delim: '='

output:
write: console

# Output:
# {"SHA1 Fingerprint":"95:3A:FF:D9:19:64:D9:09:40:8D:EE:DA:40:48:0E:FF:5E:DA:52:8C","notBefore":"Sep 3 06:36:33 2020 GMT","notAfter":"Nov 26 06:36:33 2020 GMT"}

Handling Errors

The previous examples have illustrated positive situations where the command has executed successfully. The result field allows for standard output (stdout), standard error (stderr), and status to be captured as custom fields:

input:
exec:
command: echo hello && foo
result:
stdout-field: out
stderr-field: err
status-field: status

# Output: {"err":"sh: 1: foo: not found\n","out":"hello\n","status":127}

stdout-field: _raw is the default in this case.

In this example, netcat (see man netcat) will try to send hi there to TCP port 3030. If unsuccessful, due to network connectivity, pause: 5s before attempting a retry. Not pausing will result in too many Connection refused errors:

input:
exec:
command: echo 'hi there' | netcat -N -v 127.0.0.1 3030
retry:
count: 3
pause: 5s
$> hotrod pipes run -f netcat.yml 

netcat: connect to 127.0.0.1 port 3030 (tcp) failed: Connection refused
[ERROR] exec: failed Exited(1) input-exec step 0
LINE:
netcat: connect to 127.0.0.1 port 3030 (tcp) failed: Connection refused
[ERROR] exec: failed Exited(1) input-exec step 0
LINE:
{"_raw":"","time":"2023-03-30T08:50:11.029Z"}

Trying indefinitely is not recommended for a scheduled command. Using count: 3 instead of forever: true will result in only 3 retry attempts.

Action Exec

It’s useful to invoke a command as a series of actions. You can do so using action.exec:

  • run a command for its side effects
  • execute commands conditionally
  • merge the output of another command into an event

By default, data passes through unmodified when using action.exec:

# Input: {"msg":"hello"}

actions:
- exec:
command: echo ${msg} >temp.txt

Here, as with any other action, we can use field expansions:

$> hotrod pipes run -f exec2.yml 

{"msg":"hello"}

$> cat temp.txt

{"msg":"hello"}

Note that the event itself is passed in as the input of the command:

actions:
- exec:
command: cat >temp.txt

With previous input, temp.txt contains {"msg":"hello"} as expected.

Specifying input-field enables us to choose what we want to pass to a command input:

actions:
- exec:
input-field: msg
command: cat >temp.txt

Afterwards, temp.txt contains hello.

danger

Do not write to any Pipe directories. Pipe directories are managed, meaning that there is no guarantee that any such files will still be available after the Pipe is re-started.

action-exec does not prompt for a missing input-field by default. This is useful because it allows for conditional execution of commands.

# Input: {"msg":"hello"}

actions:
- exec:
input-field: msg
command: netcat -N -v 127.0.0.1 3030

# Output: {"greeting":"bye"}

A TCP server listening on the port in question will receive hello when msg is an existing field. Note that it isn't a requirement for input-field to be a string — we can simply pass the entire event.

The output of a command can merge into a current event using result, which works as in input-exec:

# Input: {"msg":"hello"}

actions:
- exec:
command: whoami
result:
stdout-field: who

# Output: {"msg":"hello","who":"steve"}

This is a simple example that showcases the power of using this approach to combine various views of a system as one event.

Below is a Pipe used to monitor load average and CPU usage extracted from the uptime and mpstat commands respectively:

name: mpstat

input:
exec:
command: mpstat | tail -n 1
interval: 5s

actions:
- exec:
command: uptime
result:
stdout-field: uptime

# Extract and convert mpstat fields.
- expand:
input-field: _raw
remove: true
delim: ' '
csv:
fields:
- time: str
- CPU: str
- usr: num
- nice: num
- sys: num
- iowait: num
- irq: num
- soft: num
- steal: num
- guest: num
- gnice: num
- idle: num

# Likewise for uptime.
- extract:
input-field: uptime
remove: true
pattern: 'load average: ([^,]+),'
output-fields: [avg1]

- convert:
- avg1: num

output:
write: console

Output Exec

We recommend using the most specific output possible when executing a command. Another option is to use output-exec.

Field Expansion can be used to pass all or part of an event as a command input, as with action-exec.

A restriction applies — once the command is started, all events thereafter are written to its input by default:

output:
exec:
command: cat

While in this case it makes more sense to use write: console, this example illustrates the long-lived purpose of command and that it serves as the end of a pipeline.

As the data is passed through directly in this instance, Field Expansion cannot be used. Should you wish to use Field Expansion, command is implicitly run for each event.

output:
exec:
command: echo ${msg} >> /var/log/mypipe.log

Use streaming: false to explicitly force command to run once per event. However, streaming cannot be forced if any Field Expansion is present in the data.

As is the case with action-exec, you can provide input-field. In previous versions, input-field implied streaming: false but in this version, it is possible to override and force streaming by using streaming: true.

Using retry allows for the modification of the default behaviour (3 times with a 300ms pause).