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. Jun 12, 2021 at 18:22

3 Answers 3

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.

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, 2021 at 11:15
  • See also Shell read *sometimes* strips trailing delimiter on SO about the behaviour when the input ends in the delimiter. Jun 12, 2021 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, 2021 at 11:24
3

... 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, 2021 at 21:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .