1

I have a file with lines:

file1

./filled/Event_repeatFILLED.svelte
./dir2/Miscellaneous_servicesFILLED.svelte
./outline/Line_weightFILLED.svelte
./two-tone/ArchitectureFILLED.svelte
...many more lines ...

I'd like to create a file with only basenames:

file2

Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...

At the moment the following doesn't work.

sed 's/^.\///' file1 > file2

How can I do this using Shell/Bash?

shin
  • 749
  • You do realise that there is a command called basename which will do what you want? – Bib May 27 '22 at 09:12
  • @Bib, it won't automatically do that for a list of filenames in a file. If you have a solution to the question, post it as answer. – ilkkachu May 27 '22 at 09:49
  • You are already (almost) doing it with shell/bash. A large point of a shell is to glue other commands together to achieve an aim – Chris Davies May 27 '22 at 10:32
  • Please also remember that "the following doesn't work" doesn't really tell us much. We know something's not working because you've posted here. Instead, consider answers to questions like these: What happened? What did you expect to happen? Did you get any errors? – Chris Davies May 27 '22 at 10:39

3 Answers3

4

You need to use a different delimiter in the sed substitution command. For example:

$ sed 's|.*/||' file 
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...many more lines ...

Alternatively:

$ perl -pe 's|.*/||' file 
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...many more lines ...

Or, with awk:

$ awk -F'/' '{print $NF}' file 
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...many more lines ...

Or even something silly like:

$ rev file | cut -d / -f 1 | rev
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...many more lines ...

Or you can use basename as suggested in the comments but that will be more complicated and slower:

$ while IFS= read -r fileName; do basename -- "$fileName"; done < file
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte

Finally, if you really want a pure shell solution, you can do:

$ while IFS= read -r fileName; do printf '%s\n' "${fileName##*/}"; done < file
Event_repeatFILLED.svelte
Miscellaneous_servicesFILLED.svelte
Line_weightFILLED.svelte
ArchitectureFILLED.svelte
...many more lines ...

But don't use the two last ones, they are the least efficient and most complex. See Why is using a shell loop to process text considered bad practice?.

terdon
  • 242,166
  • Note that the sed one would return $'St\xe9phane/file' for an input like $'Chazelas/St\xe9phane/file' for instance if run in a UTF-8 locale for instance. – Stéphane Chazelas May 27 '22 at 12:10
  • Note that most of those would return the empty string for some/dir/ or /. – Stéphane Chazelas May 27 '22 at 12:10
  • @StéphaneChazelas yes. I am assuming the OP wants file names, not directories and the input in the example seems to confirm that. But yes, both unicode weirdness and trailing slashes will confuse most of these approaches. – terdon May 27 '22 at 12:17
3

Not to go too deeply into it, the immediate issue with your command is that the dot means "any one character" in regexes. You need .* to match an arbitrary number of characters (as many as possible in standard regexes).

So try

sed 's/^.*\///' file1 > file2

Caveats mentioned in other answers and comments apply.

ilkkachu
  • 138,973
  • 1
    @roaima, naah, I mostly just meant to point out the mistake that made their command not work. I wasn't planning on doing anything pretty here. Also the caveats mentioned in other answers and comments apply. – ilkkachu May 27 '22 at 12:16
3

With the GNU implementations of xargs and basename, you can do:

xargs -rd '\n' -a file1 basename -a -- > file2

Using basename has the advantage of handling some special cases such as / or /some/dir/ properly.

To do the equivalent by hand:

LC_ALL=C sed -e 's:^//*$:/:;t' -e 's:/*$::; s:.*/::' < file1 > file2
  • LC_ALL=C needed as file paths are not guaranteed to be made of valid text in the locale
  • / or //, ///... treated specially first
  • trailing /s removed
  • and then stripping all up to the rightmost /.

If using zsh, you could also use its :t (for tail) modifier (borrowed from csh):

print -rC1 -- ${${(f)"$(<file1)"}:t} > file2

Beware that for / (or //, ///...) it differs from GNU basename in that it returns the empty string instead of /.

With perl:

perl -MFile::Basename -lpe '$_=basename$_' < file1 > file2