5

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?

2
  • 1
    I might be missing something. But why not write a ruby script directly? Dec 17, 2020 at 22:17
  • 1
    @EricDuminil there are two reasons: (1) to be able to do things in a different way (2) even the Unix and Linux Handbook authors think shell scripting might be dated from the 70's while Perl was from the 80's and Ruby and Python are from the 90's and have advanced quite a bit and may be able to do the job better. Then, how about if we make short commands using Ruby or Python, wrap them in a shell function, and have them directly inside .bashrc and .zshrc? We could have a bunch of scripts in the ~/bin folder but if we have a few short commands inside of .bashrc, that's easier to manage Dec 17, 2020 at 22:40

3 Answers 3

11

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.

8
  • The /dev/fd/3 method worked on Mac Big Sur... but does that mean it may not work in some other systems (Mojave... Linux)? If that's the case, then the ruby -e "$(cat << 'EOF' method actually might be best because we can supply some arguments and at the same time let STDIN open for the Ruby script to use and more universally acceptable? Dec 17, 2020 at 18:07
  • @nonopolarity, to test if it has it, try: cat /dev/fd/3 3<<< OK instead. The ruby -e "$(cat << 'EOF' approach would definitely be the more portable. Dec 17, 2020 at 18:12
  • <<< foo,from zsh is a herestring. That's different from Bourne's here doc in that here foo just makes up the contents of the heredoc. Dec 17, 2020 at 18:17
  • 1
    Most modern systems support /dev/fd. Linux has had for decades. That feature is from the 80s. The only system I've personally come across that didn't have it was FreeBSD where it's available, but not enabled by default (or at least wasn't the last time I checked). Dec 17, 2020 at 18:25
  • 1
    See also Portability of file descriptor links Dec 17, 2020 at 18:33
8

You probably want to use this syntax, just include the ruby program into single quotes:

#!/bin/sh -

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

foo test-arg

Similar to python -c 'command' or awk '' or sh -c 'command' etc. Followed by the arguments. Inside the quotes you can write multiline text with your code. (-- is importart to define the end of options, e.g. for the case a following argument starts with -, see also this answer)

This is actually a shell wrapper for the ruby command, you can use the shell to filter the input, process the output, call more programs, etc. Also here you can see more about referring the arguments, I have replaced with "$@" to refer all arguments, passing from shell script to the ruby command, you can also use $1, $2 etc.

Some attention needed about single quotes of course. If you can, avoid them. There are probably more than one ways to use them, like \047 or interpolating \' between two single quoted strings. Example:

$ ruby -e 'puts "Ruby\047s #{ARGV[0]}"' great
Ruby's great
$ ruby -e 'puts "Ruby'\''s #{ARGV[0]}"' great
Ruby's great

Here some more about quoting.

9
  • Thanks @Stéphane Chazelas for the edit.
    – thanasisp
    Dec 17, 2020 at 12:10
  • I was thinking of putting the function foo() { ... } into .zshrc and .bashrc... if it is a separate file, maybe it could be directly a .sh with #!/usr/bin/env ruby and Ruby code? Dec 17, 2020 at 16:34
  • You have a shell/bash/zsh function, that is calling a ruby program, it executes command ruby. Not a ruby program directly. You can define your function, like you say, or include it in a .sh file with sh/bash/zsh shebang. The ruby shebang is for pure ruby scripts, not for this case (it could exist at the beginning of the ruby code inside quotes, but it's not necessary as we call the ruby -e '' command)
    – thanasisp
    Dec 17, 2020 at 16:40
  • the reason is, if it is a separate file, why make it #!/bin/sh and invoke ruby -e? Why not directly make it #!/usr/bin/ruby or #!/usr/bin/env ruby? Dec 17, 2020 at 16:47
  • For the case you need it. You can have a shell script, calling 10 different programs, including a python script of 2-3 lines, a ruby script of 2-3 lines, etc. You can include them with this syntax into the shell script. There are many different use cases. If you want to have a separate ruby program, just have it.
    – thanasisp
    Dec 17, 2020 at 16:51
0

A parameter of "-" indicates "standard input".

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.