91

I'm trying to remove part of the path in a string. I have the path:

/path/to/file/drive/file/path/

I want to remove the first part /path/to/file/drive and produce the output:

file/path/

Note: I have several paths in a while loop, with the same /path/to/file/drive in all of them, but I'm just looking for the 'how to' on removing the desired string.

I found some examples, but I can't get them to work:

echo /path/to/file/drive/file/path/ | sed 's:/path/to/file/drive:\2:'
echo /path/to/file/drive/file/path/ | sed 's:/path/to/file/drive:2'

\2 being the second part of the string and I'm clearly doing something wrong...maybe there is an easier way?

jww
  • 97,681
  • 90
  • 411
  • 885
esausilva
  • 1,964
  • 4
  • 26
  • 54

8 Answers8

127

If you wanted to remove a certain NUMBER of path components, you should use cut with -d'/'. For example, if path=/home/dude/some/deepish/dir:

To remove the first two components:

# (Add 2 to the number of components to remove to get the value to pass to -f)
echo $path | cut -d'/' -f4-
# output:
# some/deepish/dir

To keep the first two components:

echo $path | cut -d'/' -f-3
# output:
# /home/dude

To remove the last two components (rev reverses the string):

echo $path | rev | cut -d'/' -f4- | rev
# output:
# /home/dude/some

To keep the last three components:

echo $path | rev | cut -d'/' -f-3 | rev
# output:
# some/deepish/dir

Or, if you want to remove everything before a particular component, sed would work:

echo $path | sed 's/.*\(some\)/\1/g'
# output:
# some/deepish/dir

Or after a particular component:

echo $path | sed 's/\(dude\).*/\1/g'
# output:
# /home/dude

It's even easier if you don't want to keep the component you're specifying:

echo $path | sed 's/some.*//g'
# output:
# /home/dude/

And if you want to be consistent you can match the trailing slash too:

echo $path | sed 's/\/some.*//g'
# output:
# /home/dude

Of course, if you're matching several slashes, you should switch the sed delimiter:

echo $path | sed 's!/some.*!!g'
# output:
# /home/dude

Note that these examples all use absolute paths, you'll have to play around to make them work with relative paths.

Teocci
  • 7,189
  • 1
  • 50
  • 48
ACK_stoverflow
  • 3,148
  • 4
  • 24
  • 32
  • Amazing answer, in particular the double `rev` trick. Will upvote when my vote cap wears off tomorrow. – Ciro Santilli OurBigBook.com May 10 '16 at 10:59
  • 1
    @CiroSantilli巴拿馬文件六四事件法轮功 :) glad it was helpful – ACK_stoverflow May 10 '16 at 21:06
  • 1
    excellent, informative answer in the original Unix spirit, and so much better than {chopsquiggleSquiggle}:only:shellversionyoumaynothave. – narration_sd May 27 '16 at 01:43
  • Thank you for the examples! But isn't the "cut -d'/' -f-3" actually "keep the first two components (assuming leading slash)" instead of "remove the last three components"? I know it will make no difference for the exact example but it would be more generell. – jojoob Oct 20 '16 at 12:40
  • To remove the last three components (no matter how many components the path has): echo $path | rev | cut -d'/' -f4- | rev – jojoob Oct 20 '16 at 12:46
  • @jojoob You are correct. Feel free to update the answer. – ACK_stoverflow Feb 10 '17 at 01:50
  • @ACK_stoverflow my edit was rejected with "intended to address the author [...] makes no sense as an edit". It's your content, therefore only you can change it. – jojoob Feb 14 '17 at 13:00
  • @jojoob That rejection makes no sense to me either. Sometimes SO mods and power users are crazy. I'll see if I can fix it. It could be your edit summary they took issue with. – ACK_stoverflow Feb 14 '17 at 14:44
  • 1
    This is amazing. What about to remove everything before a particular component but not keeping that component (I don't know what the next one will be) I.e. to keep everything after a specified component? – Gamora Jun 09 '21 at 14:10
  • 1
    @Gamora Glad it's helpful :) To do that it's simple, just don't capture the path component you know. So: `echo $path | sed 's/.*some//g'` And honestly, looking at this again years later - today I would ensure that "some" wouldn't match some other dir substring - so something like `sed 's!.*/some/!!g'` (which may not work if "some" may be the basename of path). – ACK_stoverflow Jun 09 '21 at 20:22
  • The `sed` examples will all break horrible and silently, whenever there’s a character in the path that’s also regex/sed syntax. – Evi1M4chine Jun 13 '22 at 22:34
  • @Evi1M4chine This is true any time you try to match strings that contain characters that sed assigns special meaning to. In the examples above, if `$path` contains the sed syntax characters it's fine, while if your match string ("some" or "dude" above) contains the characters then you have to be careful. If you're using a variable for your sed match then you have to be _really_ careful. – ACK_stoverflow Jun 17 '22 at 17:04
90

You can also use POSIX shell variable expansion to do this.

path=/path/to/file/drive/file/path/
echo ${path#/path/to/file/drive/}

The #.. part strips off a leading matching string when the variable is expanded; this is especially useful if your strings are already in shell variables, like if you're using a for loop. You can strip matching strings (e.g., an extension) from the end of a variable also, using %.... See the bash man page for the gory details.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
evil otto
  • 10,348
  • 25
  • 38
  • Thanks...Yes, I am using shell variables to perform this. Both parts are variables, the one I need to delete and the full path. – esausilva Jun 11 '12 at 20:48
  • Excellent! But how can I use this when the string is coming "from a pipe"? – birgersp Apr 05 '17 at 08:25
  • @Birger if the string is coming from a pipe then you need to get it into a variable to use shell variable expansion. Or just use `sed` in that case. – evil otto Apr 13 '17 at 01:37
  • https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html – Roland Apr 07 '20 at 09:07
  • 1
    @birgersp: Then, instead of `command | …`, use `myvar="$( command )"; echo ${path#$myvar/}`. It works as expected. (Note the trailing `/`) @Birger: If the path that’s to be removed uses characters that are regex/sed syntax, sed will fail. And escaping is a nightmare, that is always one bit harder than you thought it was. ^^ – Evi1M4chine Jun 13 '22 at 22:42
33

If you don't want to hardcode the part you're removing:

$ s='/path/to/file/drive/file/path/'
$ echo ${s#$(dirname "$(dirname "$s")")/}
file/path/
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 2
    I spent 2 hours looking how to do this. and you were the only one to post a way to display only the name of a directory without hardcoding. I needed to do: 'find /path/to/file/drive/file/*_TST -maxdepth 0 -type d' But I wanted to show only the names of directories w/o the full path. and this did it. – Whitecat May 30 '13 at 19:58
  • @Whitecat is that's what you wanted, read "man find", in particular "-printf". You can specify '-printf "%h\n"'. Just remember that it will print the directory for every matching file so you'd want to filter this through "uniq". – Capt. Crunch Oct 17 '13 at 03:52
12

One way to do this with sed is

echo /path/to/file/drive/file/path/ | sed 's:^/path/to/file/drive/::'
Jonathan Callen
  • 11,301
  • 2
  • 23
  • 44
  • 1
    Awesome--I had no idea that the sed delimiter could be a ":" (colon) rather than "/" (slash). Elegant and works perfectly. – stachyra Jul 17 '17 at 21:02
  • 2
    You can use almost any delimiter with sed, not just / and : as long as you're consistent. – Prisoner 13 Oct 10 '17 at 20:44
  • @Prisoner13 something new everyday! – coffman21 Sep 03 '19 at 10:51
  • This too will break horribly and possibly silently, whenever the path also contains regex/sed syntax. Assuming the path isn’t hard-coded, which it should not be anyway, it’s a case of Bobby Tables. ;) – Evi1M4chine Jun 13 '22 at 22:36
7

If you want to remove the first N parts of the path, you could of course use N calls to dirname, as in glenn's answer, but it's probably easier to use globbing:

path=/path/to/file/drive/file/path/
echo "${path#*/*/*/*/*/}"   #  file/path/

Specifically, ${path#*/*/*/*/*/} means "return $path minus the shortest prefix that contains 5 slashes".

KenIchi
  • 1,129
  • 10
  • 22
Fritz
  • 1,293
  • 15
  • 27
3

Using ${path#/path/to/file/drive/} as suggested by evil otto is certainly the typical/best way to do this, but since there are many sed suggestions it is worth pointing out that sed is overkill if you are working with a fixed string. You can also do:

echo $PATH | cut -b 21-

To discard the first 20 characters. Similarly, you can use ${PATH:20} in bash or $PATH[20,-1] in zsh.

William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • Thanks. I'm using evil otto's answer. The part I want to remove is fixed, but can change if I move my script to a different box/different path. So, having the part I want to remove from the path in a variable makes it easier than counting....but information you provided is good to know :) – esausilva Jun 15 '12 at 15:08
1

Pure bash, without hard coding the answer

basenames()
{
  local d="${2}"
  for ((x=0; x<"${1}"; x++)); do
    d="${d%/*}"
  done
  echo "${2#"${d}"/}"
}
  • Argument 1 - How many levels do you want to keep (2 in the original question)
  • Argument 2 - The full path

Taken from vsi_common(original version)

Andy
  • 2,982
  • 1
  • 19
  • 23
  • 1
    OMG, best answer on this page! I needed the general case, in the most portable constructs possible. You nailed it! Thanks! – James Madison Mar 15 '20 at 10:09
1

Here's a solution using simple bash syntax that accommodates variables (in case you don't want to hard code full paths), removes the need for piping stdin to sed, and includes a for loop, for good measure:

FULLPATH="/path/to/file/drive/file/path/"
SUBPATH="/path/to/file/drive/"
for i in $FULLPATH;
do
echo ${i#$SUBPATH}
done

as mentioned above by @evil otto, the # symbol is used to remove a prefix in this scenario.

itsmisterbrown
  • 471
  • 4
  • 3