Bash – way to use HEREDOC for Bash and Zsh, and be able to use arguments

bashhere-documentshell-scriptzsh

Bash and Zsh's HEREDOC seems to act like a file, instead of string, and if I hope to do something like

foo() {
    ruby << 'EOF'
        3.times do
            puts "Ruby is getting the argument #{ARGV[0]}"
        end
EOF 
}

is there a way to pass in an argument to the Ruby program? It will be best not to interpolate the $1 into the Ruby code, so that's why I am using 'EOF' instead of EOF, as interpolating into the Ruby code can be messy.

There is one way to use the HEREDOC as a string, by the following method:

foo() {
    ruby -e "$(cat << 'EOF'
        3.times do
            puts "Ruby is getting the argument #{ARGV[0]}"
        end
EOF
)" $1    
}

and it works (although a little bit hacky). But is there a way then to use the HEREDOC's usual way of treating it as a file and be able to supply an argument to Ruby?

Best Answer

On systems with /dev/fd/n, you can always do:

foo() {
  ruby /dev/fd/3 "$@" 3<< 'EOF'
    3.times do
      puts "Ruby is getting the argument #{ARGV[0]}"
    end
EOF
}

Here using a fd above 2 so your ruby script can still use stdin/stdout/stderr unaffected.

If your system is one of the rare few that still don't support /dev/fd/n, you can do:

foo() {
  ruby - "$@" << 'EOF'
    3.times do
      puts "Ruby is getting the argument #{ARGV[0]}"
    end
EOF
}

(where ruby itself interprets - as meaning stdin).

But that means that the ruby inline script's stdin is now that heredoc, so that script won't be able to query the user via the original stdin unless you provide that stream some other way.

Heredoc is a feature that came with the Bourne shell in the late 70s. It's a redirection operator, it's meant do redirect some file descriptor (0 aka stdin by default) to some fixed content. Originally, that was implemented via a temporary file, though some shells, including bash5.1+ use (sometimes) pipes instead.

Also note that in ruby -e code -- arbitrary-argument, like in sed -e code or perl -e code but unlike in sh -c code, python -c code, you do need that -- to mark the end of options as otherwise if arbitrary-argument started with -, it would be treated as an option to ruby.

We don't need it in ruby /dev/fd/3 arbitrary-argument nor ruby - arbitrary-argument as options are not expected after that non-option argument that is - or /dev/fd/3.