George V. Reilly

Jenkins #4: The sh Step

[Pre­vi­ous­ly published at the now defunct MetaBrite Dev Blog.]

If there isn’t a built-in Pipeline step to accomplish something, you’ll almost certainly use the sh step.

#4 in a series on Jenkins Pipelines

The sh step runs the Bourne shell—/bin/sh, not Bash aka the Bourne-again shell—with the -x (xtrace) and -e (errexit) options.

The xtrace option means that every step in the sh block is echoed to the Jenkins log, after commands have been expanded by the shell. This is useful but you could echo the contents of passwords or secret keys in­ad­ver­tent­ly. Use set +x in your sh block to control this.

The errexit option means that the script will abort on the first error. You can control this by testing if ! command; then … or using command || true.

You can override the choice of shell by providing a shebang on the first line:

sh """#!/usr/bin/env python

    print([x**2 for x in range(5)])
"""

Be sure to put the shebang on the first line, im­me­di­ate­ly after the opening quote.

Groovy supports multiple syntaxes for strings: single-quote (one line, no in­ter­po­la­tion), double-quote (one line, in­ter­po­la­tion aka GStrings), triple single-quote (multiline, no in­ter­po­la­tion), and triple double-quote (multiline, in­ter­po­la­tion).

GString in­ter­po­la­tion is very, very useful but you must be careful to escape dollar signs and back­slash­es meant for the shell.

sh """
    sudo docker run --rm --env-file \$TMPDIR/envfile \\
        --volume "\$(pwd)/${results_dir}:/webapp/build_output/test" \\
        "$image_name" ${command}
"""

For example, re­sult­s_dir, image_name, and command are all Groovy string variables, while \$TMPDIR is an (escaped) shell en­vi­ron­ment variable, \$(pwd) is an (escaped) example of shell command sub­sti­tu­tion, and the trailing \\s turn into single \s (line con­tin­u­a­tion markers) when they are seen by the shell.

I’ve oc­ca­sion­al­ly found it helpful to put cat -n \$0 into some sh blocks. Pipeline creates a temporary file holding the contents of the sh block after GString in­ter­po­la­tion. When executed, $0 is the name of the temporary script file. The -n argument to cat precedes each line with its line number. In other words, this shows the actual script with line numbers. This technique helped me debug a problem where an in­ter­po­lat­ed string contained an unexpected trailing newline, which split the Groovy string across two shell lines. I fixed that problem with trim().

If you need to capture the output of a sh block’s execution, use re­turn­Std­out:

String commit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()

Typically, you will want to trim() (single line) or split() (multiline) the result.

Finally, if you have more than a few lines in a sh block, consider putting them into a separate script, which can be in­de­pen­dent­ly tested and kept in source code.

blog comments powered by Disqus
Jenkins #3: GitHub Integration » « Old Presentations