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
  • 715

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

terdon
  • 242,166
  • 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.)