ewok
ewok

Reputation: 21503

bash scripting: build a command then execute

I'm trying to make a function that basically takes in arguments like f hello there my friend and searches a directory using find for all occurences of any of those strings, so it would be find | grep 'hello\|there\|my\|friend'. I'm new to shell scripting, but my code is below:

function f { 
  cmd="find | grep '"
  for var in "$@"
  do 
    cmd="$cmd$var\\|"
  done
  cmd="${cmd%\\|}'"
  echo "$cmd"
  $cmd 
}

When I execute the command, I get this:

# f hello there my friend
find | grep 'hello\|there\|my\|friend'
find: `|': No such file or directory
find: `grep': No such file or directory
find: `\'hello\\|there\\|my\\|friend\'': No such file or directory

Why does it not work, and how can I make it work? I imagine it's something to do with the string not being converted to a command, but I don't know enough about how shell scripting works to figure it out.

Upvotes: 17

Views: 14962

Answers (5)

Samuel Åslund
Samuel Åslund

Reputation: 3294

I got here because it was the only google SO hit for when I wanted to build a commandline and my parameters included spaces.

The solution was to use a bash array:

#! /bin/bash
# using /bin/sh here might work if sh is broken or mapped to bash but this is not compatible with a real sh.
MY_ARGS=( "--HI" "--there" "--With spaces" )
if [ "X$OPTIONAL" != "X" ] ; then MY_ARGS+=( "$OPTIONAL" );fi
MY_ARGS+=( "$@" )

my_command "${MY_ARGS[@]}"

Adapted from: http://mywiki.wooledge.org/BashFAQ/050

Upvotes: 4

miken32
miken32

Reputation: 42753

It looks like your command syntax is correct. To run the command from a script in bash and capture the result, use this syntax:

cmd_string="ls"
result=$($cmd_string)
echo $result

Upvotes: 11

user14850728
user14850728

Reputation: 11

The most universal way: you can use symbols like "|" (pipe) and other variables into the input and get the result from stdout.

function runcmd(){
  eval "$1"
}

result=$(runcmd "Any command ${here}")

Upvotes: 1

gniourf_gniourf
gniourf_gniourf

Reputation: 46903

Instead of piping the output of find through grep, you might as well use the full capabilities of find. You'll want to build up a an array that contains the options:

-false -o -name '*string1*' ... -o -name '*stringn*'

to pass to find (where string1 ... stringn are the strings passed as arguments):

f() {
   local i args=( -false )
   for i; do
      args+=( -o -name "*$i*" )
   done
   find "${args[@]}"
}

We're using -false as an initializer, so that building up the array of options is simple; this also has the benefit (or flaw, depending on your point of view) that if no options are given then find exits early without listing all the content of the directory recursively.

With grep you could use regexes to have more powerful matching capabilities; here we're using find's -name option, so we can only use the basic globs: *, ? and [...]. If your find supports the -regex option (GNU find does), and if you really need regexes, then it's trivial to modify the previous function.


Another possibility is to use Bash's extended globs:

f() (
   IFS='|' eval 'glob="$*"'
   shopt -s globstar extglob nullglob
   IFS=
   printf '%s\n' **/*@($glob)*
)

A few things to note here:

  • The whole function is included in a subshell—it's not a typo. That's to simplify a few things: no need to use local variables, and no need to save the shell options to restore them at the end of the function.
  • The first line uses the evil eval but in a safe way: it's actually an idiomatic way to join the elements of the positional parameters with the first character of IFS (here a pipe character).
  • We need to set IFS to the empty string so as to avoid word splitting in the glob **/*@($glob)*.
  • The glob **/*@($glob)* uses globstar and the extglob @($glob) (with no quotes, it's not a typo). See Pattern Matching in the reference manual.

This function uses Bash's extended globs, that differ from (and aren't as powerful as) regexes (yet this should be enough for most cases).

Upvotes: 4

chepner
chepner

Reputation: 532313

Don't put the entire command in a string; just build the argument for grep

f () { 
  local grep_arg=''
  delim=''
  for var in "$@"; do
      grep_arg+="$delim$var"
      delim='\|'
  done
  echo "find | grep '$grep_arg'"
  find | grep "$grep_arg"
}

Upvotes: 3

Related Questions