2

I trying to create a list of strings with spaces in, that I want to choose between in a select - something like this:

sel=""
while read l   
do
  sel=$(printf "%s '%s'" "$sel" "$l")
done< <(cd /some/data/directory;du -sh *) 

select x in $sel
do
  break
done

The string sel looks like expected: "597G 2022" "49G analysis" "25K @Recycle", but the select looks like:

1) "597G      3) "49G       5) "25K
2) 2022"      4) analysis"  6) @Recycle"
#?

What I want to achieve is of course something like:

1) 597G 2022
2) 49G  analysis
3) 25K  @Recycle
#?

And more generally, something where you can select between strings built from several data sources in some way. I have looked for inspiration in several places, like here, but it doesn't quite work for my case.

Edit

I forgot to mention, this bash is rather old (and I can't update it, sadly):

[admin@CoMind-UniCron ~]# bash --version
GNU bash, version 3.2.57(1)-release (x86_64-QNAP-linux-gnu)
Copyright (C) 2007 Free Software Foundation, Inc.
j4nd3r53n
  • 579
  • 2
  • 11
  • 5
    You're not storing a command in a variable here, but all the same, you need to have `select` see multiple words. Use an array instead, see [How can we run a command stored in a variable?](https://unix.stackexchange.com/questions/444946/how-can-we-run-a-command-stored-in-a-variable/444949#444949) and also [Why does my shell script choke on whitespace or other special characters?](https://unix.stackexchange.com/questions/131766/why-does-my-shell-script-choke-on-whitespace-or-other-special-characters) – ilkkachu Jan 06 '23 at 11:40
  • Thank you both for your suggestions - unfortunately they don't seem to work. I'll just have to implement it another way, I think. – j4nd3r53n Jan 06 '23 at 13:36

2 Answers2

5

You want to use an array of multiple strings there, not a single string that must be split correctly by the shell. Something like this:

#!/bin/bash

while read l   
do
  sel+=( "$l" )
done< <(cd /some/data/directory;du -sh *) 

select x in "${sel[@]}"
do
  break
done

Which produces the expected output:

$ foo.sh
1) 597G 2022
2) 50G  analysis
3) 32K  @Recycle
#? 

A safer approach, that can handle arbitrary file/dir names except newlines and is only a little more complex but can be used without worried in all situations is:

#!/bin/bash

while IFS= read -r l   
do
  sel+=( "$l" )
done< <(shopt -s nullglob dotglob; cd /some/data/directory && du -sh -- *) 

select x in "${sel[@]}"
do
  break
done
Gilles 'SO- stop being evil'
  • 807,993
  • 194
  • 1,674
  • 2,175
terdon
  • 234,489
  • 66
  • 447
  • 667
  • Nice one! I'll give that a try. I've accepted your answer, because it looks like it does exactly what I want. – j4nd3r53n Jan 06 '23 at 17:32
  • @j4nd3r53n thanks, but do come back and unaccept if it doesn't work for your version of bash for some reason. I doubt it, simple arrays have been in bash for ages, but I am not 100% sure. I know associative arrays were added in version 4 and I would be very surprised if version 3 didn't have indexed arrays, but since I'm not sure... – terdon Jan 06 '23 at 17:40
  • 2
    Arrays are supported in Bash 3.2, and so should the `+=` assignment operator for adding to an array be. – Kusalananda Jan 06 '23 at 18:02
  • Using `read` without `-r` doesn't make sense here. You're also missing a `--` and not checking the exit status of `cd`, See also `readarray -td '' sel < <(cd ... && du -sh0 -- *)` – Stéphane Chazelas Jan 06 '23 at 18:24
  • Thanks, @StéphaneChazelas, answer updated. Is there any benefit in using `readarray` here apart from brevity/simplicity? I'd rather not bring it in if it doesn't add a clear advantage just to stay close to the OP's original version. – terdon Jan 06 '23 at 18:27
  • 2
    @terdon, arrays were added in bash in 2.0, years after zsh and decades after csh or ksh but still over 25 years ago. += (initially from zsh IIRC) was added in 3.1 – Stéphane Chazelas Jan 06 '23 at 18:29
  • `readarray` is going to be several orders of magnitude more efficient than a while read loop. But for `-d ''` (which you need to handle arbitrary file paths), you need bash 4.4 or above. You'd also need `IFS= read -rd '' file` on a NUL delimited list for arbitrary file paths if using `read`. – Stéphane Chazelas Jan 06 '23 at 18:30
  • Ah, in that case it can't be used here since the OP is using 3.2.57. As for the read, of course I would! Eeek, I completely missed that, thanks again, @StéphaneChazelas. – terdon Jan 06 '23 at 18:39
1

This is really over-thought.

#!/usr/bin/env bash

IFS='
'
select x in $(cd /some/data/directory && du -sh -- *); do
    break
done
unset IFS  # or save/restore it explicitly

As an added bonus, the only bashism here is the select.

Typescript:

$ ./cg
 1) 0   0
 2) 0   000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
 3) 0   000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
 4) 0   000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a
 5) 0   000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ą
 6) 60k 1028501896.pdf
 7) 0   a
 8) 16k a.bkp
 9) 36k a.cpio
10) 10k a.d
11) 36k a.patch
12) 60k a.pax
13) 84k a.png
14) 4k  b.cpio
15) 32k b.pax
16) 12k b.tar
17) 0   bugreport.cgi?bug=910770;mbox=yes;mboxmaint=yes
18) 74.2M       build-output
19) 796k        busybox
20) 428k        busybox_1%3a1.30.1-6+b3_amd64.deb
21) 0   CB_Unix
22) 4k  cg
#? 

The quoting like you're doing doesn't work because quote removal applies only to the original word; the result of parameter expansion is only field-split (and globbed). To do that, you need to tokenise the string as input:

eval "select x in $sel
do
  break
done"

and for obvious reasons you really shouldn't do this, since you haven't actually escaped the filenames:

$ ./cg
./cg: eval: line 11: unexpected EOF while looking for matching `''
./cg: eval: line 15: syntax error: unexpected end of file

if you really need to fork a process for each line and really need to store the du output in a big string then you should've done

#!/usr/bin/env bash

sel=
while read -r l; do
    sel="$(printf '%s %q' "$sel" "$l")"
done < <(cd /some/data/directory && du -sh -- *) 

eval "select x in $sel; do
  break
done"

(I've disabled escape-mangling the paths, which you had on for some reason.)