1

I need to run a script that uses readarray -d / -t to split filepaths into arrays, but the readarray of the targeted system doesn't support the -d option (bash version 4.2.46)

As I don't know the exact behaviour of readarray -d / -t, it is difficult for me to write a workaround for it. It doesn't seem possible to replace it with IFS=/ read -a because filenames containing control characters will break it, as shown here:

IFS=/ read -a arr < <(echo /home/fravadona/$'\n'/underneath)
declare -p arr
# OUTPUT: declare -a arr='([0]="" [1]="home" [2]="fravadona")'

So, my first question is, what's the expected result of:

readarray -d / -t arr < <(echo /home/fravadona)
readarray -d / -t arr < <(echo /home/fravadona/)
readarray -d / -t arr < <(echo /home/fravadona/$'\n'/underneath)

And lastly, does the following code emulates readline -d / -t correctly?

filepath=/home/fravadona/$'\n'/underneath

unset arr; declare -a arr

prefix="${filepath%%/*}"
suffix="${filepath#*/}"

until [ "X$prefix" == "X$suffix" ]
do
    arr+=( "$prefix" )
    prefix="${suffix%%/*}"
    suffix="${suffix#*/}"
done

arr+=( "$prefix" )
Fravadona
  • 13,917
  • 1
  • 23
  • 35
  • 1
    `readarray` reads all input and produces one array. `read` reads a single line at a time producing an array for each. I notice you have a directory that's a non-printable character. Is this malware? – jordanm Oct 21 '21 at 21:41
  • @jordanm: That's only an example that breaks a possible `IFS=/ read -a' workaround. – Fravadona Oct 21 '21 at 21:48
  • `what's the expected result of:` sooo, why don't you run it and see? `readarray` is internal to bash, the expected output is what bash wants it to be, documentation is basically documentation of code, not specification. – KamilCuk Oct 21 '21 at 22:18
  • @KamilCuk: `readarray -d / arr <<< /home/KamilCuk` `-bash: readarray: -d: invalid option`. As you can see, it doesn't work on the target system ;-) – Fravadona Oct 21 '21 at 22:24
  • 1
    Och, so you mean you do not have bash5 available at hand? Try it out online. https://replit.com/ has bash Also, use docker, docker is always handy. – KamilCuk Oct 21 '21 at 22:25

3 Answers3

2

Hope this would achieve what you expected :

filepath=/home/fravadona/$'\n'/underneath
IFS=/ read -d "" -r -a arr < <(printf "%s" "$filepath")
declare -p arr
Philippe
  • 20,025
  • 2
  • 23
  • 32
1

What is the exact behaviour of bash "readarray -d / -t"?

Read all input. Split it on /. Remove /. Assign to array.

what's the expected result of:

The following script:

readarray -d / -t arr < <(echo /home/fravadona)
declare -p arr
readarray -d / -t arr < <(echo /home/fravadona/)
declare -p arr
readarray -d / -t arr < <(echo /home/fravadona/$'\n'/underneath)
declare -p arr

outputs:

declare -a arr=([0]="" [1]="home" [2]=$'fravadona\n')
declare -a arr=([0]="" [1]="home" [2]="fravadona" [3]=$'\n')
declare -a arr=([0]="" [1]="home" [2]="fravadona" [3]=$'\n' [4]=$'underneath\n')

does the following code emulates readline -d / -t correctly?

Your code does not read from stdin, so it does not emulate readline. It seems you assume the input is stored in some variable.

The following code:

f() {
    IFS= read -d '' -r filepath
    arr=()

    prefix="${filepath%%/*}"
    suffix="${filepath#*/}"

    until [ "X$prefix" == "X$suffix" ]
    do
        arr+=( "$prefix" )
        prefix="${suffix%%/*}"
        suffix="${suffix#*/}"
    done

    arr+=( "$prefix" )
}
f < <(echo /home/fravadona)
declare -p arr
f < <(echo /home/fravadona/)
declare -p arr
f < <(echo /home/fravadona/$'\n'/underneath)
declare -p arr

outputs:

declare -a arr=([0]="" [1]="home" [2]=$'fravadona\n')
declare -a arr=([0]="" [1]="home" [2]="fravadona" [3]=$'\n')
declare -a arr=([0]="" [1]="home" [2]="fravadona" [3]=$'\n' [4]=$'underneath\n')

So it's the same, so the answer after some adjustments would be yes.

It's odd that you use [ X trick nawadays - the old bugs are long gone, and anyway == is non-standard for [.. In bash, just use [[ "$prefix" == "$suffix" ]]

Related: Need alternative to readarray/mapfile for script on older version of Bash . I would go with zero bytes, something like:

f() {
   arr=();
   while IFS= read -d '' -r elem || [[ -n "$elem" ]]; do
         arr+=("$elem")
   done < <(
         tr '/' '\0'
   )
}
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
1

It doesn't [seem] possible to replace it with IFS=/ read -a because filenames containing control characters will break it,

Specifically, filenames containing newlines will break it. Other control characters should not be an issue.

So, my first question is, what's the expected result of:

readarray -d / -t arr < <(echo /home/fravadona)
readarray -d / -t arr < <(echo /home/fravadona/)
readarray -d / -t arr < <(echo /home/fravadona/$'\n'/underneath)

They are equivalent to, respectively,

arr=('' home $'fravadona\n')
arr=('' home fravadona $'\n')
arr=('' home fravadona $'\n' $'underneath\n' )

Note that each array has an empty first element corresponding to the first "line", with the trailing delimiter removed.

And note in particular that the last element of every result contains a trailing newline. This is because echo ends its output with one by default, and you are instructing readarray to use a different line delimiter.

You might, then, also be interested in these ...

readarray -d / -t arr < <(echo -n /home/fravadona)
readarray -d / -t arr < <(echo -n /home/fravadona/)
readarray -d / -t arr < <(echo -n /home/fravadona/$'\n'/underneath)

... which produce the same results as ...

arr=('' home fravadona)
arr=('' home fravadona)
arr=('' home fravadona $'\n' underneath)

And lastly, does the following code emulates readline -d / -t correctly?

unset arr; declare -a arr

prefix="${filepath%%/*}"
suffix="${filepath#*/}"

until [ "X$prefix" == "X$suffix" ]
do
    arr+=( "$prefix" )
    prefix="${suffix%%/*}"
    suffix="${suffix#*/}"
done

arr+=( "$prefix" )

No, it does not, it least because the condition [ "X$prefix" == "X$suffix" ] is wrong. For example, if $filepath is foo/foo then no iterations of the loop will be performed, and the final value of arr will contain only one element, not two. Or if $filepath is /foo/bar/bar then the end result has only two elements, not three.

This would be better:

unset arr
declare -a arr

case $filepath in
*/) suffix=$filepath;;
*) suffix=${filepath}/;;
esac

while [[ -n "$suffix" ]]; do
  arr+=(${suffix%%/*})
  suffix=${suffix#*/}
done
John Bollinger
  • 160,171
  • 8
  • 81
  • 157