Home > Back-end >  Is there a way to "map" the results of `find` before passing them to `-exec`?
Is there a way to "map" the results of `find` before passing them to `-exec`?

Time:05-28

In the following command, I get a list of file paths and I want to get the basename for each before running multiple -exec commands on each of them.

find /usr/local/lib/systemd -type f                          \
    -exec bash -c "basename {} | xargs echo 'stopping'" \;   \
    -exec bash -c "basename {} | xargs systemctl stop" \;    \
    -exec bash -c "basename {} | xargs systemctl disable" \;

Is there a way to do this without having to call basename {} each time?

CodePudding user response:

Answer

find /usr/local/lib/systemd -type f -exec basename {} \; | xargs -L1 -I {} bash -c "echo 'stopping' {}; systemctl stop {}; systemctl disable {}"

Explanation

First, take a look at the output of find

find /usr/local/lib/systemd -type f
/usr/local/lib/systemd/file1.service
/usr/local/lib/systemd/file2.service
/usr/local/lib/systemd/file3.service

If you use -exec, that's essentially a map. For example

find /usr/local/lib/systemd -type f -exec basename {} \;
file1.service
file2.service
file3.service

You can then pipe that to xargs and use the -L option to pass in N arguments at a time to a command. In this case, -L1 will repeat the xargs command for every 1 line in the input. Look at an example where the command just prints "test" and the filename:

find /usr/local/lib/systemd -type f -exec basename {} \; | xargs -L1 echo "test"
test file1.service
test file2.service
test file3.service

You can use the -I option of xargs to substitute in the arguments multiple times in a single bash command, like this:

find /usr/local/lib/systemd -type f -exec basename {} \; | xargs -L1 -I {} echo hello {} world {}
hello file1.service world file1.service
hello file2.service world file2.service
hello file3.service world file3.service

Finally, you can use bash -c "..." to run multiple commands as one command. Use this as the xargs command, like this:

find /usr/local/lib/systemd -type f -exec basename {} \; | xargs -L1 -I {} bash -c "echo 'stopping' {}; systemctl stop {}; systemctl disable {}"

CodePudding user response:

systemctl takes glob patterns, so you can do:

echo stopping everything
systemctl stop \*
systemctl disable \*

It's important to quote the pattern so it's not expanded by the shell.

Also note this from man systemctl > Parameter Syntax: "shell-style globs will be matched against the primary names of all units currently in memory". So it may not be equivalent to your find approach in certain cases.

To answer your question more generically, a good way to get more control over the file list generated by find is to pass it to a shell loop:

find /usr/local/lib/systemd -type f -exec bash -c '
    for i do
        i=$(basename "$i")
        echo "$i"...
        systemctl stop "$i"
        systemctl disable "$i"
    done' _ {}  

Also, in this case, because systemctl can take multiple unit arguments, you could do this instead:

-exec bash -c '
systemctl stop "${@##*/}"
systemctl disable "${@##*/}"' _ {}  

This invokes systemctl twice*, instead of many times, which is more efficient. ${@##*/} strips the prefix of each positional parameter up to the last slash, identically to basename.

* find may invoke the -exec command (bash) multiple times if the file list is large.

  • Related