user5047085
user5047085

Reputation:

How to find basename of path via pipe

This doesn't work:

find "$all_locks" -mindepth 1 -maxdepth 1 -type d | basename

apparently basename cannot read from stdin - in any case basename requires at least one argument.

Upvotes: 28

Views: 22182

Answers (4)

GreatEmerald
GreatEmerald

Reputation: 239

I needed to do something a bit more complex (such as chaining basename and dirname), and solved it by calling bash on each filename, then using Bash subshells:

find "$all_locks" -mindepth 1 -maxdepth 1 -type d -exec bash -c 'basename $(dirname {})' \;

This solution allows using {} multiple times, which is handy for more complex cases.

Upvotes: 0

Peter J. Mello
Peter J. Mello

Reputation: 475

xargs to the rescue

To apply a command to every result of a piped operation, xargs—the unsung hero of the GNU findutils project—is your friend. The POSIX 1003.1-2024 specification prescribes its behavior like this…

xargs [ -0oprtx ] [ -P max-procs ] [ -E eof-str ] [ -a file ] [ -d delim ]
      [ -I replace-str | -L max-lines | -n max-args ] [ -s max-chars ]
      utility [ argument… ]

The xargs utility shall construct a command line consisting of the utility and argument operands specified followed by as many arguments read in sequence from standard input as fit in length and number constraints specified by the options. The xargs utility shall then invoke the constructed command line and wait for its completion. This sequence shall be repeated until […] an end-of-file condition is detected on standard input […] or an invocation of a constructed command line returns an exit status of 255.

Put more plainly, xargs acts as a "wrapper" for some other "utility," and is only concerned with converting what is sent over stdin into arguments for that command. If inserted into your example pipeline operation after the | and before basename, it will take each search result from find and run basename <search_result> on it, ad nauseum, until find exits. I believe the command you need is going to look a lot like this:

A wild solution appears…

find "$all_locks" -mindepth 1 -maxdepth 1 -type d | xargs -- basename

Now the output of find is piped to xargs, who then reformats it into typical command line arguments to basename. The -- is between them to explicitly end xargs’ option processing, ensuring that basename is treated as the target "utility," in their terms.

Of course when dealing with file paths you're often reminded of the need to be mindful of the potential for some to include perfectly valid whitespace and/or control characters that love to foul up your plans. Thankfully, there exists a complementary pair of options, find -print0 and xargs -0, that switch the input field separator (IFS) for both commands to a null byte so that all of this is a non-issue. Let's add them to the command as well…

Final answer

find "$all_locks" -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 -- basename

That's probably as close to an "optimal" approach as I can offer without having more knowledge of exactly what you're doing. I hope this answers your question and provided a bit of insight along the way, and I wish you good fortune.


Some extra credit

While we're on this subject, I'm inclined to go a little further and mention a few other ways to leverage some of the options available to both GNU find and xargs to enhance their performance.

Parallel processing

Let's say this operation is going to produce a huge number of results, and adding xargs to the pipe is proving to be a bottleneck. It can run these tasks in parallel with just a couple more options.

First up is xargs’ -P option for parallel processing, which needs an integer argument for the number of threads to use or a 0 can be used for "pedal to the metal" mode (max. possible threads). Moving at this much faster pace then requires us to restrict each operation to using just one search result from find, lest things get messy. That's why we'll also add -n 1 to xargs, as that option controls the maximum number of inputs that can be passed to any single invocation of the utility. That brings your command to now read:

find "$all_locks" -mindepth 1 -maxdepth 1 -type d -print0 |
  xargs -0P 0 -n1 -- basename
Redirecting from stdout to a file

Redirecting data streams in the shell is such a straightforward and powerful tool that it's often done without much thought. I'm bringing it up here, though, because the nature of how xargs operates introduces some complications to the process, though thankfully the workarounds are reliable and easy-to-use.

The difficulty arises because xargs’ default behavior is to append the string from stdin as the last argument to the utility command. That doesn't sit well with basename, however, which expects its arguments to be the first thing it encounters once it's done parsing options. The workaround is to use xargs’ -I/--replace option to define the position for the piped-in data to be placed, like this:

find "$all_locks" -mindepth 1 -maxdepth 1 -type d -print0 |
  xargs -0 --replace -- basename '{}' >>file

There are a couple important aspects to cover here. First is that the long option form, --replace defaults to using the same replacement token ('{}') as find -exec when an explicit token definition doesn't follow it on the command line. That's both easy-to-remember and helps keep things simple. Secondly, when redirecting from xargs, it is necessary to use a non-destructive or appending operator. Why, you ask? Because xargs works iteratively, and if you used a clobbering redirection, each invocation of the utility would erase the prior output, leaving you with only the output of the very last invocation at the end.

Upvotes: 34

oguz ismail
oguz ismail

Reputation: 50795

With GNU find:

find "$all_locks" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'

Upvotes: 7

susenj
susenj

Reputation: 428

The problem here is basename doesn't accept the stdin and hence unnamed pipes may not be useful. I would like to modify your command a little bit. Let me know if it serves the purpose.

find -mindepth 1 -maxdepth 1 -type d -exec basename {}  \;

Note: Not enough reputation to comment, hence posting it here.

Upvotes: 8

Related Questions