1

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?

nullromo
  • 2,165
  • 2
  • 18
  • 39

2 Answers2

2

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 {}"
nullromo
  • 2,165
  • 2
  • 18
  • 39
  • Piping the list creates problems with white space. Why not just `-exec bash -c ...`? – dan May 27 '22 at 21:47
1

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.

dan
  • 4,846
  • 6
  • 15
  • Thanks for the insight. [This answer](https://stackoverflow.com/a/2059836/4476484) explains `${@##*/}`, since it's particularly un-googleable. And [this answer](https://stackoverflow.com/a/5163260/4476484) explains what the `@` does here. Correct me if I'm wrong, but it seems the `_` here is just used to take up the first slot of the argument list passed to the `bash` command (i.e. `$0`), while the `{}` substitutes in the actual file list from `find` into the `$1` of the `bash` command. With the list as `$1`, the `${@}` can be used as the whole list of files, while the `##*/` trims each one. – nullromo May 27 '22 at 22:48
  • 1
    Yes, I was kind of brief because there's really three answers here. If you search for `##` in `man bash` you will find the Parameter Expansion section. Worth noting that `"${@##*/}"` is bash specific, but `"${var##*/}"` (one variable) works in `/bin/sh`. Underscore is used for `$0`. Also, `-exec cmd {} +` executes the command with multiple file arguments, `-exec cmd {} \;` executes once per file. – dan May 27 '22 at 23:12
  • Woah, the point about `\;` vs. `+` is key and I totally missed that. Thanks – nullromo May 27 '22 at 23:23