2

I need to process some strings containing paths. How do I split such a string by / as delimiter resulting in an unknown number of path-parts and how do I, in the end, extract the resulting path-parts?

cut is obviously not the tool of choice as it needs you to know the number of parts beforehand and it also doesn't output each part such that I could use readarray or mapfile to collect them into an array.

1
  • Note that depending on what you're doing, dirname and basename might be helpful. – Karl Bielefeldt Jun 12 at 18:22
3

In Bash, you can use read -a and a here-string to split the string into an array:

path=/foo/bar/doo
IFS=/ read -r -a parts <<< "$path"

That would give an array with the four elements (empty), foo, bar, and doo.

That doesn't work with paths containing newlines, since read treats the newline as a separator by default. To prevent that, you'd need to add -d '', but then there's the problem that the here-string adds a newline, which then must be removed from the last element:

path=$'/path/with/new\nlines'
IFS=/ read -d '' -r -a parts <<< "$path"
parts[-1]=${parts[-1]%$'\n'}

(parts[-1] refers to the last element of the array, and ${var%text} expands to the value of var with the trailing part matching text removed.)

Also note that if the path can contain duplicate slashes, e.g. foo//bar, you'll get empty array elements in the middle. Similarly if the path ends with a slash, you'll get an empty element at the end.

You could either ignore them, or preprocess the path to remove them, with something like this, to remove duplicate slashes

shopt -s extglob
path="${path//+('/')/'/'}"

and to remove trailing slashes:

shopt -s extglob
path="${path%+('/')}"

But then again, note that at the start of a pathname, a double slash //foo is a reserved special notation, different from a single (or triple etc.) slash, but you're not likely to see that in practice, so I'll ignore it.

3
  • Thanks especially for mentioning the caveat with the herestring adding a \newline :) – von spotz Jun 12 at 11:15
  • See also Shell read *sometimes* strips trailing delimiter on SO about the behaviour when the input ends in the delimiter. – Stéphane Chazelas Jun 12 at 11:18
  • Ah, right, IFS=/ read -r -a arr <<< "/foo/bar/", doesn't produce the empty tailing field. But IFS=/ read -d '' -r -a arr <<< "/foo/bar/" it does produce one with just the newline character – ilkkachu Jun 12 at 11:24
5

In bash, for single character delimiters, you can use the split+glob operator (leaving expansions unquoted in list contexts) after having disabled the glob part:

string='foo/bar
baz/asd..'

IFS=/
set -o noglob
array=( $string )

Note that it splits string='/foo/' into "" and "foo" only (the same as when splitting string='/foo'. To split into "", "foo" and "", you can do:

IFS=/
set -o noglob
array=( $string'' )

Though that then splits string='' into one empty element instead of zero element.

In zsh (which doesn't do split+glob upon unquoted expansion unless in sh/ksh emulation), you can use the s parameter expansion flag which is not limited to single-character delimiters:

array=( ${(s[/])string} )

which removes empty elements, or:

array=( "${(@s[/])string}" )

To preserve empty elements. /foo/ is then split into "", "foo" and "" and the empty string into zero element.

You can split on a delimiter stored in a variable with:

array=( "${(@ps[$delimiter])string}" )

The p flag also lets you enter escape sequences such as \0, \n, though those two have shortcut flags: f to split on linefeed, 0 to split on NUL (useful to split the output of find -print0, grep -lZ, sort -z... such as files=( ${(0)"$(grep -lZ pattern -- *)"} )).

In zsh, you can also tie an array variable to a scalar variable with a given single byte as delimiter. $path in zsh is actually a special array that is ties to $PATH with : as delimiter in that fashion (inspired from csh). You can do that for any variable like:

typeset -T string array /

To tie /-separated $string to the $array array.

2

... doesn't output each part such that I could use readarray or mapfile

With mapfile/readarray, provide the complete string and set a delimiter. For example,

str='/f
oo/bar/'

mapfile -d / arr < <(printf '%s' "$str")

declare -p arr

outputs:

declare -a arr=([0]="/" [1]=$'f\noo/' [2]="bar/")
1
  • plus the -t flag to have mapfile remove the terminators, if you like. – ilkkachu Jun 12 at 21:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.