pypyr.steps.shell permalink

run shell statements permalink

Runs the context value cmd in the default shell. On a sensible O/S, this is /bin/sh.

Where the cmd step runs a program or executable, shell passes the command through to the system shell. This means all your usual shell expressions are available, such as ~ expansions and your favorite bashisms.

If you are just looking to run a command or executable with arguments, you do not need to use shell, you can use pypyr.steps.cmd instead. This will incur less processing overhead, because it won’t have to instantiate the shell first.

You do NOT need shell to run a console-based executable or a script file (like .sh, .bat, .ps1), you can just use cmd instead.

This step runs shell statements serially one after the other. Use shells for asynchronous parallel execution instead if you want concurrent execution.

Windows users, the COMSPEC environment variable specifies the default system shell.

This might well be C:\Windows\System32\cmd.exe rather than Powershell.

single shell statement permalink

Input context can take one of two forms for a single shell instruction.

  • Simple syntax is just a simple string giving a single run instruction.
  • Expanded syntax allows you to set additional options to control how you want to run the executable.
steps:
#  simple syntax
- name: pypyr.steps.shell
  comment: passing cmd as a string doesn't save the output to context.
           prints stdout in real-time.
  in:
    cmd: echo $PWD

#  expanded syntax
- name: pypyr.steps.shell
  comment: passing cmd as a dict allows you to save the output to context 
           when save=True.
           prints command output only AFTER it has finished running.
  in:
    cmd:
      run: echo $PWD
      save: True
      cwd: ./set/current/working/dir/here
steps:
#  simple syntax
- name: pypyr.steps.shell
  comment: passing cmd as a string doesn't save the output to context.
           prints stdout in real-time.
  in:
    cmd: echo %cd%

#  expanded syntax
- name: pypyr.steps.shell
  comment: passing cmd as a dict allows you to specify if you want to
           save the output to context if save=True.
           prints command output only AFTER it has finished running.
  in:
    cmd:
      run: echo %cd%
      save: True
      cwd: ./set/current/working/dir/here

All inputs support substitutions.

The full expanded syntax for shell input is:

# when save: True
- name: pypyr.steps.shell
  in:
    cmd:
      run: echo one
      save: True
      cwd: ./
      bytes: False
      encoding: utf-8

# when save: False
- name: pypyr.steps.shell
  in:
    cmd:
      run: echo two
      cwd: ..
      stdout: ./path/out.txt
      stderr: ./path/err.txt
      append: False

Only run is mandatory in expanded syntax. All other inputs are optional. The example shows which options are relevant depending on whether save is True or False.

inject variables into shell permalink

You can inject context variables into your shell. This means you can use any of the pypyr context manipulation steps to manipulate your inputs and then pass these to the command.

Supports string substitutions for all step inputs in expanded syntax.

- name: pypyr.steps.set
  comment: set some arb values in context
  in:
    set:
      in_file: input.avi
      out_file: output.avi
      kbits: 64
      arb-key: log
      my-var: -1
      my-path: ./mypath/mydir

- name: pypyr.steps.shell
  comment: use substitution expressions to inject variables into shell
  in:
    cmd: ffmpeg -i {in_file} -b:v {kbits}k -bufsize {kbits}k {out_file}

- name: pypyr.steps.shell
  comment: substitutions work on all inputs
  in:
    cmd:
      run: git {arb-key} --oneline {my-var}
      cwd: '{my-path}' # set any step input option with formatting expression

See the section on escaping curly braces if you want to pass literal curly braces to your shell statement.

On Posix, in the following example {arb} is from the pypyr context, and ${PATH} comes from the environment variable:

- name: pypyr.steps.shell
  comment: combining pypyr substitutions with literal curly braces.
  in:
    cmd:
      run: echo "{arb} and now the $PATH env ${{PATH}}"

Remember that depending on your shell $var might also get the environment variable for you without needing {curly braces}.

run multiple shell statements in same step permalink

You can use a list input to run multiple shell statements in the same step:

#  simple syntax
- name: pypyr.steps.shell
  comment: list of simple cmd strings to run
  in:
    cmd:
      - echo one
      - echo two

Any of the list items can be in expanded syntax to specify extra options:

- name: pypyr.steps.shell
  in:
    cmd:
      - echo one

      - run: echo two
        save: True
        cwd: ./
        bytes: False
        encoding: utf-8

      - run:
          - echo three
          - echo four
        cwd: ../
        stdout: ./path/out.txt
        stderr: ./path/err.txt
        append: False
      
      - echo five

Notice that in expanded syntax run can be a single run instruction or a list of run instructions:

- name: pypyr.steps.shell
  comment: apply the same settings to multiple shell commands
  in:
    cmd:
      run:
        - echo one
        - echo two
      save: True
      cwd: ./path/mydir

Note that each run instruction executes in its own shell session. If you instead want to run multiple shell statements in the same shell session, see multiple inline shell statements for details.

change working directory permalink

If you specify cwd, it will change the current working directory to cwd to execute this command.

The directory change is only for the duration of this step, not any subsequent steps.

When you do set cwd, the executable or program specified in run is relative to the cwd if you use a relative path. You can of course use an absolute path instead.

If you do not specify cwd, it defaults to the current working directory, which is from wherever you are running pypyr.

steps:
- name: pypyr.steps.shell
  in:
    cmd:
      run: pwd
      cwd: ..
steps:
- name: pypyr.steps.shell
  comment: bare cd with no args prints current dir
  in:
    cmd:
      run: cd
      cwd: ..

As usual for paths, you can use . for current and .. for parent directory.

In the example we’re using the .. directive to go up one level to the parent directory, but you can of course use an absolute path or relative path as you require instead.

The cwd you set applies to all the shell statements in run.

- name: pypyr.steps.shell
  comment: each cmd in run will execute in cwd
  in:
    cmd:
      run:
        - echo one %cd%
        - echo two %cd%
      cwd: path/mydir # cwd applies to each instruction in run

capture shell output permalink

Set save: True to capture the shell statement’s output.

- name: pypyr.steps.shell
  comment: save output to context.
  in:
    cmd:
      run: ./my-executable --arg1 value
      save: True

If save is True, pypyr will save the output to context cmdOut as follows:

cmdOut.returncode: 0
cmdOut.stdout: 'stdout str here. None if empty.'
cmdOut.stderr: 'stderr str here. None if empty.'
cmdOut.cmd: './my-executable --arg1 value'

cmdOut.returncode is the exit status of the called process. Typically 0 means OK. A negative value -N indicates that the child was terminated by signal N (POSIX only).

You can use cmdOut in subsequent steps like this:

- name: pypyr.steps.shell
  in:
    cmd:
      run: echo 1
      save: True

- name: pypyr.steps.echo
  run: !py cmdOut.returncode == 0
  in:
    echoMe: "you'll only see me if cmd ran successfully with return code 0.
            the command output was: {cmdOut.stdout}.
            the error output was: {cmdOut.stderr}."

Be aware that if save is True, all of the command output ends up in memory. Don’t set it unless your pipeline actually uses the stdout/stderr response in subsequent steps. Instead of saving the output to memory like this, you can alternatively write output to file.

Only use save: True when you actually need to use the stdout or stderr output in subsequent steps - you don’t need to set it just to check that the return code is 0, since pypyr will raise an error automatically if it’s not. If this is not what you want, you can suppress errors with the swallow decorator and use the runErrors list on subsequent steps.

The cmdOut key contains a SubprocessResult instance. The full schema for this object is:

SubProcessResult():
  cmd: str # the original shell input
  returncode: int
  stdout: str | bytes | None
  stderr: str | bytes | None

save output for multiple shell statements permalink

When the shell step runs more than one instruction where save: True, the cmdOut output context will be a list. Each list item is a dict/mapping in the same format as the output for a single command:

cmdOut:
  - returncode: 0
    stdout: 'stdout str here. None if empty.'
    stderr: 'stderr str here. None if empty.'
  - returncode: 0
    stdout: 'stdout str here. None if empty.'
    stderr: 'stderr str here. None if empty.'

The list is in the order the shell statements were executed. The list will contain only commands where save is True. This means that if your step input is a list where only some commands have save: True, only those will be in cmdOut and the other commands will not be:

- name: pypyr.steps.shell
  comment: only 2, 3 & 5 saves to cmdOut
  in:
    cmd:
      - a-executable --arg one
      - run:
          - b-executable --arg two
          - c-executable --arg three
        save: True
      - d-executable --arg four
      - run: e-executable --arg five
        save: True

In this example the cmdOut list will contain 3 items - the output for b, c and e-executable.

You can use the cmdOut list in subsequent steps like this:

- name: pypyr.steps.echo
  run: !py cmdOut[0].returncode == 0
  in:
    echoMe: |
            you'll only see me if cmd ran successfully with return code 0.

            For the first command,
            the command output was: {cmdOut[0].stdout}
            the error output was: {cmdOut[0].stderr}
            
            And for the second command,
            the command output was: {cmdOut[1].stdout}
            the error output was: {cmdOut[1].stderr}            

Notice that you reference each cmd’s output on a zero-based index - i.e the 1st item is in position 0.

Notice the difference in formatting expression when the step only ran a single command vs when multiple commands had save set to True:

# single cmd output
'{cmdOut.stdout}'

# list of command outputs
# the 1st output is at index 0
'{cmdOut[0].stdout}'

debugging output permalink

A quick way of seeing exactly what is in cmdOut is just to echo it out:

- name: pypyr.steps.echo
  in:
    echoMe: '{cmdOut}'

encoding permalink

By default pypyr treats the shell output as text. It decodes the bytes returned by the shell using the default system encoding and strips white-space like line-feeds from the end.

You can set the encoding explicitly:

- name: pypyr.steps.shell
  comment: save output & decode in specified encoding.
  in:
    cmd:
      run: ./my-executable --arg1 value
      save: True
      encoding: utf-16

The encoding setting is only applicable when save is True. Both stdout & stderr will use the encoding you set.

The default system encoding is very likely to be utf-8, unless you’re on Windows. See here for a list of available encoding codecs.

You can change the default_cmd_encoding setting in config to use a different default here.

binary output permalink

By default pypyr deals with the shell’s output as text in the system’s default encoding. If you want to capture the output as raw bytes without any text decoding & line-ending handling, you can set bytes: True.

- name: pypyr.steps.shell
  comment: save the output as raw bytes.
           this will bypass all text decoding.
  in:
    cmd:
      run: ./my-executable --arg1 value
      save: True
      bytes: True

The bytes setting is only applicable when save is True. Both stdout & stderr will save the raw bytes returned by the shell when bytes: True.

If you combine bytes: True and encoding pypyr will decode the output in the specified encoding but it won’t perform line-ending normalization - so your output might have line-endings at the end depending on the shell & executables you’re calling.

save output to file permalink

By default, the shell output writes to the parent process’ stdout & stderr streams. Normally this means that any program output will print in the terminal from where you are running pypyr.

You can save the output to file instead:

- name: pypyr.steps.shell
  comment: save the output to file.
           this will NOT print the output to console,
           but redirect it to the output files instead.
  in:
    cmd:
      run: ./my-executable --arg1 value
      stdout: mydir/out.txt # optional. write stdout to this file.
      stderr: mydir/err.txt # optional. write stderr to this file.
      append: False # optional. Default False.

Set append to True to append output if the file(s) exists already. If append is False it will overwrite any existing file - this is the default option when you don’t explicitly set append.

If you want error output to write to the same file as stdout, you can use the special value /dev/stdout to redirect stderr to stdout:

- name: pypyr.steps.shell
  comment: save both stdout & stderr output to the same file.
           this will NOT print the output to console,
           but redirect it to the output file instead.
  in:
    cmd:
      run: ./my-executable --arg1 value
      stdout: mydir/out.txt
      stderr: /dev/stdout

A lot of cli tools have a built-in option to output to file, which you might want to use instead of redirecting stdout via pypyr.

For example, with curl you use the -o/--output switch to specify an output file:

curl https://myurl.arb/blah -o myfile.txt

If you set either/both of stdout & stderr, you cannot also set save: True, since these are mutually exclusive.

Since you’re in the shell already, you could of course instead use a shell redirect in the shell statement itself:

- name: pypyr.steps.shell
  comment: use a shell redirect to dump to file
  in:
    cmd:
      run: echo "do stuff" >file.log 2>&1

redirect output to null device permalink

You can discard the shell’s output when you redirect any or both of stdout & stderr to the system’s Null device with the special value /dev/null:

- name: pypyr.steps.shell
  comment: discard all output.
  in:
    cmd:
      run: ./my-executable --arg1 value
      stdout: /dev/null
      stderr: /dev/null

The previous example redirects both stdout & stderr. You can also selectively redirect only one of these to dump to null - so if you want to see stderr output but not standard output:

- name: pypyr.steps.shell
  comment: only suppress stdout
  in:
    cmd:
      run: ./my-executable --arg1 value
      stdout: /dev/null

When you redirect output to the null device it will not print to console.

Since you’re in the shell already, you could of course instead use a shell redirect in the shell statement itself:

- name: pypyr.steps.shell
  comment: use a shell redirect to dump to null
  in:
    cmd:
      run: echo "do stuff" > /dev/null 2>&1

split long shell statement over multiple lines permalink

For ease of reading, you can split long shell statements over multiple lines using the yaml folding indicator >. This will replace newlines in the pipeline yaml with spaces when parsing the statement.

- name: pypyr.steps.shell
  in:
    cmd: >
      curl -X POST "https://httpbin.org/post"
      -H "accept: application/json"
      -d "arg1=value1"
      -d "arg2=value2"
      | grep User-Agent      

spaces in paths & args permalink

Depending on your O/S, shell and file-system, it’s up to you to deal with special characters in the path of the command or program you want to run.

Generally you can put either just the path-segment with a space into quotes, or you can escape the entire path by putting quotes around the whole thing.

If a single argument contains a space, surround if with double-quotes.

To illustrate some of the options:

- name: pypyr.steps.shell
  in:
    cmd: '"dir with space/file with space" arg1 "arg2 with space"'

- name: pypyr.steps.shell
  in:
    cmd: '"dir with space"/"file with space"'

- name: pypyr.steps.shell
  in:
    cmd: ./"dir with space"/"file with space"

Note the “extra” pair of single-quotes (’) is there when the string begins and ends with double-quotes so that the YAML parser reads the double quotes (") literally instead of interpreting these double quotes as structural YAML markers meaning string.

curly braces permalink

If you want to pass {curly braces} through to the shell, and NOT have pypyr interpret these as {formatting expressions}, you can escape the braces by {{doubling}}, or by using a sic string literal:

- name: pypyr.steps.shell
  comment: if you want to pass curlies to the shell, use sic strings
  in:
    cmd: !sic echo ${PWD};

- name: pypyr.steps.shell
  comment: alternatively, escape by doubling
  in:
    cmd: echo ${{PWD}};

examples permalink

Example pipeline yaml using a pipe:

steps:
  - name: pypyr.steps.shell
    comment: if you have something pipey in working dir it should show up
    in:
      cmd: ls | grep pipe

See a worked example for shell pipeline power.

multiple inline shell statements permalink

You can use shell expressions to combine multiple shell statements into a single run instruction.

In this example, both steps do the the same thing:

# multiple shell statements combined into single pypyr run instruction
- name: pypyr.steps.shell
  comment: run in same shell session
  in:
    cmd: echo one && echo two && echo three

# multiple shell statements as separate pypyr run instructions
- name: pypyr.steps.shell
  comment: each run instruction in its own shell session
  in:
    cmd:
      - echo one
      - echo two
      - echo three

The difference is that when you combine different shell commands into the same run instruction (echo one && echo two && echo three) the entire combined instruction executes in the same shell session.

In the second example each run instruction executes in its own shell scope.

posix permalink

Friendly reminder of the difference between separating your shell statements with ; or &&:

  • ; will continue to the next statement even if the previous command errored. It won’t exit with an error code if it wasn’t the last statement.
  • && stops and exits reporting error on first error.

windows permalink

Friendly reminder of the difference between separating your shell statements with & or &&:

  • & will continue to the next statement even if the previous command errored. It won’t exit with an error code if it wasn’t the last statement.
  • && stops and exits reporting error on first error.

changing directory permalink

steps:
- name: pypyr.steps.shell
  comment: hop one up from current working dir
  in:
    cmd: echo $PWD; cd ../; echo $PWD

- name: pypyr.steps.shell
  comment: back in your current working dir
  in:
    cmd: echo $PWD
steps:
- name: pypyr.steps.shell
  comment: hop one up from current working dir.
  in:
    cmd: cd & cd .. & cd

- name: pypyr.steps.shell
  comment: back in your current working dir
  in:
    cmd: cd

You can change directory multiple times during a shell step using cd, but dir changes are only in scope for subsequent statements in that same run instruction, not for subsequent run instructions or steps.

- name: pypyr.steps.shell
  comment: cd only applies to the same run instruction
  in:
    cmd:
      - cd mydir && echo hello from mydir
      - echo not in mydir anymore

- name: pypyr.steps.shell
  comment: cd only applies to the same run instruction
  in:
    cmd:
      run:
        - cd mydir && echo hello from mydir
        - echo not in mydir anymore

Instead prefer using the cwd input as described above for an easy life, which sets the working directory for the entire step without you having to code it in with chained shell commands.

invoking python permalink

steps:
  name: pypyr.steps.shell
  in:
    cmd: python -m stuff.do

Note that this is a bit of a silly example - since we’re not using any shell functions we might as well have used pypyr.steps.cmd instead.

If you want to invoke Python as a sub-process from pypyr, you might want to specify the full path to the Python interpreter to avoid problems with accidentally ending up running a different, unexpected interpreter on your system. This is especially relevant if you’re using virtual environments.

See pypyr.steps.python for details on getting the full path to the current Python executable.

Remember you can also invoke your Python code directly by using pypyr.steps.py, which will automatically be in the current Python environment.

Alternatively, if you do have an external Python file you want to run, you can just add a def run_step(context) function to your file and run it natively as a pypyr step. This is described in how to make a custom step. This will also automatically execute in the current Python environment.

see also

last updated on .