Search code examples
bashfunctionhistory

Why does history require a numeric value for grep?


I am trying to make a custom function (hisgrep) to grep from history.

I had it working before, when the code was basically "history | grep $1", but I want the implementation to be able to grep multiple keywords. (e.g. "hisgrep docker client" would equal "history | grep docker | grep client").

My problem is that, when I try to do this I get this error: "-bash: history: |: numeric argument required."

I've tried changing how the command was called in the end from $cmd to just $cmd, but that did nothing.

Here's the code:

#!/bin/bash

function hisgrep() {
    cmd='history'
    for arg in "$@"; do
        cmd="$cmd | grep $arg"
    done
    `$cmd`
}

Solution

  • Sadly, bash doesn't have something called "foldl" or similar function.

    You can do it like this:

    histgrep() {
        local str;
        # save the history into some list
        # I already filter the first argument, so the initial list is shorter
        str=$(history | grep -e "$1");
        shift;
        # for each argument
        for i; do
           # pass the string via grep
           str=$(<<<"$str" grep "$i")
        done
        printf "%s\n" "$str"
    }
    

    Notes:

    • Doing cmd="$cmd | grep $arg" and then doing `$cmd` looks unsafe.
    • Remember to quote your variables.
    • Use https://www.shellcheck.net/ to check your scripts.
    • Backticks ` are deprecated. Use $() command substitution.
    • using both function and parenthesis function func() is not portable. Just do func().

    It is possible to hack with eval (and eval is evil) with something that generates bash code with a long pipeline for each argument:

    histgrep() { eval "history $(printf '| grep -e "$%s" ' $(seq $#))"; }
    

    The eval here will see history | grep -e "$1" | grep -e "$2" | ... which I think looks actually quite safe.

    The recursive solution in the comment by @melpomene also looks amazing. This would be my code on the idea:

    _multigrep_in() {
      case $# in
      0) cat ;;
      1) grep "$1" ;;
      *) grep "$1" | _multigrep_in "${@:2}" ;;
      esac
    }
    multigrep() {
       history | _multigrep_in "$@"
    }