Search code examples
rubyrake

Prevent Rake's sh command from echoing the command


Whenever I call sh from rake it often echos the command that will be ran right before it is run. How can I prevent sh from logging the commands to stdout. I'd like to prevent this as I have api keys in the command I am calling, and I don't want to expose them in my build log.


Solution

  • There are two parts to solving this. The first is to pass the verbose: false option, which will prevent the command from being printed before it's executed:

    $ cat Rakefile
    SECRET = 'foobarbaz'
    
    task :foo do
      sh "echo #{SECRET} > secrets.txt", verbose: false
    end
    
    $ rake foo
    (no output)
    

    However, this doesn't help if there's an error, since Rake will print the failed command if it returns an error:

    $ cat Rakefile
    SECRET = 'foobarbaz'
    
    task :foo do
      sh "echo #{SECRET} > secrets.txt; exit 1", verbose: false
    end
    
    $ rake foo
    rake aborted!
    Command failed with status (1): [echo foobarbaz > secrets.txt; exit 1...]
    ...
    

    The solution is hinted at in the docs for sh:

    If a block is given, upon command completion the block is called with an OK flag (true on a zero exit status) and a Process::Status object. Without a block a RuntimeError is raised when the command exits non-zero.

    You can see where the default behavior comes from in the Rake source. The solution, then, is to supply our own block:

    $ cat Rakefile
    SECRET = "foobarbaz"
    
    task :foo do
      sh "echo #{SECRET} > secrets.txt; exit 1", verbose: false do |ok, status|
        unless ok
          fail "Command failed with status (#{status.exitstatus}): [command hidden]"
        end
      end
    end
    
    $ rake foo
    rake aborted!
    Command failed with status (1): [command hidden]
    ...
    

    Looks good!

    If you find yourself needing this in multiple places, you could write a convenience method; something like this:

    def quiet_sh(*cmd)
      options = (Hash === cmd.last) ? cmd.pop : {}
      options = { verbose: false }.merge(options)
    
      sh *cmd, options do |ok, status|
        unless ok
          fail "Command failed with status (#{status.exitstatus}): [command hidden]"
        end
      end
    end
    
    SECRET = "foobarbaz"
    
    task :foo do
      quiet_sh "do_secret_things"
    end