Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Output, Assign, Pipe and Redirect a Heredoc in a Shell Script

blog/cards/output-assign-pipe-and-redirect-a-heredoc-in-a-shell-script.jpg

You may want to do this when working with multi-line strings.

Quick Jump: Output | Assign | Pipe | Redirect | Demo Video

A “Here Document” is a useful way to output text.

It’s a feature of most programming languages. It’s commonly used to output multiple lines of text In such a way that you don’t need to worry about using a convoluted way of breaking up lines or escaping quotes.

We’ll break down the first example in the most detail. The other examples incrementally combine using other shell features to handle specific use cases.

It’s worth pointing out all of these examples will work with sh, bash and other compatible shells. There is 1 exception that only works with Bash or Bash-compatible shells and that will be called out, however in that case we’ll use an alternative method that works with sh too.

Output

This could be useful to output a multi-line string.

With interpolation

numbers="123"

cat << EOF
a
b
c
${numbers}
EOF
# Here's the output of the above script:
a
b
c
123

We’re sending input to cat which then outputs it back. A heredoc expects us to pass in a string delimiter too. Using EOF (end of file) is commonly used but it’s not required, technically you can use any string as long as it matches on the top and bottom.

It’s really important that the bottom EOF exists on its own line with no extra indentation. If you deviate from that, it won’t be considered delimited.

Here we can see that variable interpolation works too because ${numbers} got interpolated to its value of 123. All expressions will get interpolated.

If our multi-line string had single ' or double " quotes inside of it everything will just work without needing to escape them. That’s quite nice and also important to call out because you may choose to use a heredoc even for 1 line of output if you find yourself working with a string that has a lot of mixed quotes.

Without interpolation

This is the same as above except variables and other expression won’t get interpolated.

numbers="123"

cat << 'EOF'
a
b
c
${numbers}
$(echo "wow")
EOF
# Here's the output of the above script:
a
b
c
${numbers}
$(echo "wow")

The only difference is we wrapped EOF in single quotes. Technically double quotes works too but I prefer using single quotes because when working with shell scripts if you wrap a string in single quotes it won’t interpolate your value. This matches the same pattern.

It’s also worth pointing out if you didn’t use 'EOF' you can still output something like ${numbers} literally by escaping the $ with \${numbers}.

Assign

What if you want to assign the heredoc to a variable?

letters=$(cat << EOF
a
b
c
EOF
)

echo "${letters}"
# Here's the output of the above script:
a
b
c

This is pretty interesting because we’re doing the same cat strategy as before. The only difference is using command substitution to run it in a sub-shell and assign it to a variable.

If you Google around for this you might see this StackOverflow answer that suggests using read instead to “avoid a useless cat”.

That would look like this:

read -r -d "" letters << EOF
a
b
c
EOF

echo "${letters}"

The above produces the same output as the cat version, but it won’t work with sh because POSIX compliant shell doesn’t support the -d flag with read. This flag says that input lines should be delimited by a null byte.

There is a comment that I agree with which questions if it’s really worth avoiding the useless cat. I don’t mind the useless cat because in my opinion it’s more understandable given it piggy backs off prior knowledge of using a heredoc in other ways. It’s also more portable.

Pipe

You can also pipe your heredoc into another command.

cat << EOF | sort
c
b
a
EOF
# Here's the output of the above script:
a
b
c

Notice how the output got sorted. It might seem weird to see the pipe at the top instead of after the bottom EOF but if you think about the heredoc “owning” the delimiter then it might make more sense.

The pipe is still operating like any other pipe sequence you might use on the shell. The output of cat is being piped into sort as input.

Pipe (with file support)

Certain tools may accept input but only in the form of a file. We’re going to cover 2 examples using kubectl and curl. It’s really not different than the above example.

kubectl
cat << EOF | kubectl apply -f -
apiVersion: "v1"

# ...
EOF

I’ve omit the output here because it’s not important. You may have seen this pattern used in a bunch of spots within Kubernetes’ documentation. If not, that’s ok because none of this is specific to Kubernetes.

The main takeaway is kubectl apply expects you to pass in a file. A common but not enforced Unix convention is to allow using - as a file name which will read the file from STDIN instead of a real file on disk.

It’s what allows us to pass in a multi-line string file as input to kubectl and other tools that use this pattern. It’s quite handy for writing scripts that use these tools because it lets you avoid creating temporary files on disk only to delete them after the command is run.

curl
curl localhost --data-binary @- << EOF
{
  "name": "nick"
}
EOF
# Here's the output of the above script:
{
  "name": "nick"
}
EOF
curl: (7) Failed to connect to localhost port 80 after 0 ms: Connection refused

The curl error makes sense because I don’t have a web server running on localhost but this pattern does work for when you want to make a real HTTP request to a site while sending your payload in as a multi-line string.

It uses the - trick as mentioned before, except in this case curl excepts you to use @- because @ lets curl differentiate that we want to use a file.

By the way, --data-binary is being used instead of --data (-d) so that when you send your payload with the request, new lines aren’t stripped.

Redirect

Lastly we can redirect our multi-line string to a file. You can choose to use >> instead of > to append to the file too.

cat << EOF > example_output
a
b
c
EOF

Technically the script doesn’t output anything but it did write an example_output file out with the contents of a b c on new lines.

If you ran the above command you can run cat example_output && rm $_ to both see the file and then delete it. I’ve made a post in the past about what $_ does.

The demo video below runs all of these commands and shows how they work.

Demo Video

Timestamps

  • 0:20 – A run down on heredocs
  • 2:56 – Avoiding variable or expression interpolation
  • 4:31 – Assigning a multi-line string to a variable with cat vs read
  • 7:19 – Piping a heredoc to another command as input
  • 8:20 – Piping things into kubectl where it expects a file as input
  • 10:04 – Doing the same as before but with curl
  • 11:14 – Redirecting a multi-line string to a file

What’s your preferred way to hide user input? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.



Comments