19

Say I have several variables in a shell script (e.g. in zsh):

FOLDER_1, FOLDER_2, etc.

These variables refer to folders descending from /. For example, if I have a path /home/me/stuff/items

the variables would be:

FOLDER_1='home'
FOLDER_2='me'
FOLDER_3='stuff'

Now, say that I want to build back the corresponding path by concatenating the variables. One possible way is to build the path as follows:

PATH=$FOLDER_1/$FOLDER_2/$FOLDER_3/

However, say that some of the variables FOLDER_i come with trailing forward slashes, while others don't (and we don't know which) e.g.

FOLDER_1='home'
FOLDER_2='stuff/'
FOLDER_3='items'

My question is: How could I build the path robustly? (e.g. avoiding double slashes, and adding them where they need to be).

I thought one way to do this is to add the / always between pairs of variables, and then delete any duplicates with sed, but I can't make it to work (I am not sure I am handling / correctly in sed).

Also, am I reinventing the wheel? (i.e. is there any built-in that does this already?).

Finallly, if the variables are in an array, e.g. FOLDERS, would it be possible to do this without looping? (or alternatively, by looping but without knowing how many FOLDERS there are in the array).

4 Answers4

19

The simple answer is to stop worrying and love multiple slashes. Multiple slashes have the same effect as a single slash (except that a path beginning with // has a different meaning on a few systems; unix emulation layers on Windows are the only ones I can name). That's by design, and being able to assemble file names without having to worry about multiple slashes was a major part of that design decision.

To join array elements, in zsh, you can use the j parameter expansion flag.

dir=${(j:/:)FOLDERS}

You can squish the duplicate slashes while you're at it, but that's purely cosmetic.

setopt extended_glob
dir=${${(j:/:)FOLDERS}//\/\/##/\/}

In ksh and bash, you can join an array using the first character of $IFS as a separator. You can then squish the duplicate slashes. In bash, you'll need to do shopt -s extglob to enable the ksh globs in the last line of the snippet below.

IFS=/
dir="${FOLDERS[*]}"
unset IFS
dir=${dir//\/+(\/)//}
14

You can use printf with an array:

parts=("$FOLDER_1" "$FOLDER_2" "$FOLDER_3");
printf '/%s' "${parts[@]%/}"
# Use "printf -v path" to save it into a variable called "path" instead of printing it

The % operator trims trailing strings, in this case /. By applying it to parts[@], it trims each array member separately.

The key to understanding this printf trick, is this bit from man 1 printf: "The format string is reused as often as necessary to satisfy the arguments."

janmoesen
  • 2,710
  • 1
    The single % method works for the single trailing slash mentioned in the queston. To cater for when there are multiple slashes, ${parts[@]%%/*} works. Here is a link to a bit more info on the slash issue: What do double slashes mean in UNIX path? Is 'cd dir/subdir// valid... – Peter.O Oct 24 '11 at 20:49
  • 2
    @fered: /* in this case does not mean zero or more slashes, but rather a slash followed by any number of characters. That means if your path starts with a slash, the result will be the empty string! – l0b0 Oct 25 '11 at 09:13
  • I had considered, as per the example, that there would only be trailing slashes. However, to adapt to the possibility of a leading slash(es), bash's extglob (with regex) can by used.. shopt -s extglob; ${parts[@]%%/+(/)} ... – Peter.O Oct 26 '11 at 02:54
  • For the sake of completeness, could you modify / add to this code example such that the result of the printf command is also stored in a variable? I think that this would be useful to quite a few people reading this answer. – Leonid Apr 28 '23 at 10:50
3

How is sed not working for you? Try sed 's|/\+|/|g' after concatenating, or sed 's|/||g' before.

Kevin
  • 40,767
1

bash 4 introduced extended globbing, which enables regular expression matching in parameter substitution ${var....} ... It is turned off by default. To enable it for your script, simply set the shell option extglob...

Assuming that $dir == /home/me/////////stuff//items

shopt -s extglob; dir="${dir//+(\/)//}"

Resulting value of $dir

/home/me/stuff/items  

Here are some examples with do-s and-don't-s -- Bash Extended Globbing

Peter.O
  • 32,916