100

I have a file (say called list.txt) that contains relative paths to files, one path per line, i.e. something like this:

foo/bar/file1
foo/bar/baz/file2
goo/file3

I need to write a bash script that processes one path at a time, splits it at the last slash and then launches another process feeding it the two pieces of the path as arguments. So far I have only the looping part:

for p in `cat list.txt`
do
   # split $p like "foo/bar/file1" into "foo/bar/" as part1 and "file1" as part2
   inner_process.sh $part1 $part2
done

How do I split? Will this work in the degenerate case where the path has no slashes?

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
I Z
  • 5,719
  • 19
  • 53
  • 100

5 Answers5

197

Use basename and dirname, that's all you need.

part1=$(dirname "$p")
part2=$(basename "$p")
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
piokuc
  • 25,594
  • 11
  • 72
  • 102
  • 1
    An issue with this is how do you combine the parts to get back the full path? `$part1/$part2` mostly works, except if `$p` is `/etc` you will get `//etc`. – Daniel Darabos Aug 26 '21 at 21:24
  • 1
    @DanielDarabos `//etc` is a valid synonym for `/etc` in most contexts. However, see this question for solutions if you need to remove double slashes: https://stackoverflow.com/q/4638983/3776640 – dwymark May 07 '22 at 01:10
19

A proper 100% bash way and which is safe regarding filenames that have spaces or funny symbols (provided inner_process.sh handles them correctly, but that's another story):

while read -r p; do
    [[ "$p" == */* ]] || p="./$p"
    inner_process.sh "${p%/*}" "${p##*/}"
done < list.txt

and it doesn't fork dirname and basename (in subshells) for each file.

The line [[ "$p" == */* ]] || p="./$p" is here just in case $p doesn't contain any slash, then it prepends ./ to it.

See the Shell Parameter Expansion section in the Bash Reference Manual for more info on the % and ## symbols.

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104
18

I found a great solution from this source.

p=/foo/bar/file1
path=$( echo ${p%/*} )
file=$( echo ${p##*/} )

This also works with spaces in the path!

phrogg
  • 888
  • 1
  • 13
  • 28
  • 5
    rather `${p##*/}` – zar3bski Jul 24 '19 at 09:47
  • 7
    The subshell + echo is also completely unnecessary, and the current solution runs into the usual word splitting issues due to unquoted expansions. Don’t write this. Just use `path=${p%/*}` and `file=${p##*/}`. Or use `basename` and `dirname`. – Konrad Rudolph Jul 13 '21 at 09:52
1

While basename and dirnames are really helpful, maybe you are in the same situation as me:

I need to get only the first Nth folders of a path, and I can be on any folder, like these ones: /home/me/folder/i/want/, /home/me/, or /home/me/folder/i/want/folder/i/dont/want/.

So I used cut.

Here's the command to get only /home/me/folder/i/want, no matter where I am:

echo "/home/me/folder/i/want/folder/i/dont/want" | cut -f 1,2,3,4,5,6 -d "/"

Here, cut is splitting the string by "/" chars, and is displaying 1st, 2nd [...] 6th words only.

Here are some examples:

$ echo $PWD
/home/me/folder/i/want/folder/i/dont/want
$ echo $PWD | cut -f 1,2,3,4,5,6 -d "/"
/home/me/folder/i/want
$ cd ../../../..
$ echo $PWD
/home/me/folder/i/want
$ echo $PWD | cut -f 1,2,3,4,5,6 -d "/" 
/home/me/folder/i/want
$ cd ~
echo $PWD | cut -f 1,2,3,4,5,6 -d "/"       
/home/me
sodimel
  • 864
  • 2
  • 11
  • 24
-2

Here is one example to find and replace file extensions to xml.

for files in $(ls); do

    filelist=$(echo $files |cut -f 1 -d ".");
    mv $files $filelist.xml;
done
jww
  • 97,681
  • 90
  • 411
  • 885