I am wanting to build a custom builtin in bash -- like time for example. Is this possible? If it's not, is it possible in another shell?

For example, I want to do this: map find -name '*.js' | xargs grep func1 and have it run find -name '*.js' | xargs grep func1 inside every directory within CWD. Notice that everything including the pipe gets passed to map.

The goal is to apply a command across multiple directories. I wrote a script called map which evals $1 across whatever ls produces. However, if the command I want to run includes pipes, redirects, etc, then I have to quote everything inside a string. I am looking for a way to not have to do that.

Also, I'm just plain curious if this is possible ;)

  • 4
    Adding a builtin would be possible if you're willing to download the source, add your functionality and recompile/use that version of bash. For the example you gave, perhaps it would just be better to use -exec for find then though and do away with the pipe (and need to handle quoting which your version doesn't do right now) – Eric Renouf May 19 '17 at 13:18
  • 1
    May you map $(find $BASE_DIR -type d)? – uprego May 19 '17 at 13:30
  • alias map='find . -print0 -name \*.js | xargs -0 grep func1'? – DopeGhoti May 19 '17 at 16:25
  • Using eval to parse ls output seems generally unsafe -- it'd be safe only so long as it's known for certain the filenames ls outputs aren't mischievous. – agc May 20 '17 at 1:10

ATT ksh, bash and zsh each have a dynamic module loading system that allows loading additional code into an existing shell instance, and the dynamically loaded code can implement new builtins. What you're asking requires more than a new builtin, though: it requires new syntax, and I think that none of the shells can handle that. You would have to modify the parser in the shell's source code.

In zsh, you can get close to the syntax you want:

for x (*/) (cd $x && pipeline)

You can make an alias alias map='for x (*/) (cd $x &&' but you still need the closing parenthesis at the end. You can get exactly the syntax you want with a gross hack:

alias map='map__PWD=$PWD; for x (./*/ "") cd $map__PWD${x#.}/ && [[ -z $x ]] || '

This is so ugly, fragile and non-generalizable that I'm not going to explain it. Don't think about using it if you don't completely understand how it works.


In this case, something like

find . -type f -name "*.js" -exec grep -qF "func1" {} \; -print

would probably be enough to find the names of the *.js files anywhere in or below the current directory that contain the string func1.

If you want to avoid the files that are in the current directory:

find . -mindepth 2 -type f -name "*.js" -exec grep -qF "func1" {} \; -print

With -mindepth 2, find will not match anything on "depth 1", i.e. in the current directory.

This last thing basically does what you ask for. It effectively runs grep in each sub-directory of the current directory.

  • If all you want is find things in JavaScript files there are much better solutions: ack, ag, ucg, ripgrep, even GNU grep alone (grep -r --include=\*.js func1 .). But I believe the point of the question was to solve a more general problem. – Satō Katsura May 19 '17 at 17:26

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.