1

This is a follow-on question to this question

In that question, I could get selected files into an array and pass them to a command / function (already exported). This question differs in that I would like the user to complete the command after selecting the files.

Main Aim: I am presented with a list of filenames (FZF). I manually select some of these. FZF then puts this subset into an array. I then want to compose an unfinished command which expects the user to complete the command and press Enter.

The filenames can have spaces in them; hence the choice of Null-separated.

I'm using FZF to select the files. It produces an array containing nul-ending filenames, I think. But the first item that FZF produces is the name of a key-press. That's why the script treats the first item of FZF's output differently.

Currently I have

#!/bin/bash
readarray -d '' out < <(fd .|fzf  --print0 -e -m  --expect=ctrl-e,ctrl-l)
if [ ${#out[@]} -eq 0 ]; then return 0
fi
declare -p out
key="$out"
y=${out[@]:1}
if [ ${#y[@]} -eq 0 ]; then return 0
fi
case "$key" in
ctrl-e ) echo do something ;;
ctrl-l ) echo do something else ;;
* )
printf -v array_str '%q ' "${y[@]}"
cmd="printf '%s\0' ${array_str} | parallel -0 wc"
read -e -i "$cmd" cmd_edited; eval "$cmd_edited" ;; #not working
esac

I have gotten close: the command looks like it should, but the NUL values are not behaving. The last line doesn't work. It is intended to print the array of files on a line with null separator and still allow the user to specify a function (already exported) before hitting Enter. The parallel command would apply the function to each file in the array.

$ls
file 1
file 2
...
...
file 100

Currently, if I choose file 3 and file 2, the output of my script looks like this:

printf "%s\0" file 3 file 2 | parallel -0

to which I might for example, append wc

But then after I type wc and press Enter I get the following result:

printf "%s\0" file 3 file 2 | parallel -0 wc
wc: file030file020: No such file or directory

Edit: I have now included the line declare -p out to make clear what FZF is producing. The results as they now appear, using Charles' modification below is:

declare -a out=([0]="" [1]="file 3" [2]="file 2" [3]="file 1")
printf '%s\0' file\ 3\ file\ 2\ file\ 1  | parallel -0 wc
wc: file030file020file010: No such file or directory

So something has obviously gone wrong with the nuls.

How do I fix the code?

Tim
  • 291
  • 2
  • 17
  • 1
    To clear up the "I think", I'd suggest using `declare -p out` to print the definition of the `out` array. That way folks who want to answer this question don't need to look into behavior of the `fd | fzf` pipeline. :) – Charles Duffy Feb 24 '20 at 04:16
  • BTW, using `eval` is generally not great form. What's the intent behind the decision to do so here? – Charles Duffy Feb 24 '20 at 04:17
  • The `eval` I thought was mandatory, copied from solution https://unix.stackexchange.com/questions/213799/can-bash-write-to-its-own-input-stream in section at very bottom of page. – Tim Feb 24 '20 at 06:07
  • There, the goal is to parse user-entered data, read by `read`, as shell syntax; it's a legitimate use case. Is that something you're trying to do here? For details on why `eval` is frowned on when used unnecessarily (and why many of the new features bash adds over the baseline POSIX sh specification exist to reduce the number of cases where `eval` is necessary), see [BashFAQ #48](http://mywiki.wooledge.org/BashFAQ/048). – Charles Duffy Feb 24 '20 at 13:18
  • BTW, if that *is* your goal, you should probably be more careful about how you form the initial `cmd` string. `${y[@]}` in a string does not generate a string that evaluates back to the contents of the array `y`. – Charles Duffy Feb 24 '20 at 13:20
  • Add `-r` to the `read`. – Charles Duffy Feb 25 '20 at 12:47
  • BTW, you probably don't want an empty `out[0]`. I'd suggest looking into why your `fd | fzf` pipeline is giving you a *leading* NUL, instead of only putting NULs *after* filenames. – Charles Duffy Feb 25 '20 at 22:12

2 Answers2

2

It is still unclear to me what you want to do. Comment your code and make sure every variable name has a name that says what it is used for.

Do you want the user to be able to enter a command and have that command run on the files in the array?

# Set y
y=("file  1" "file \"two\"")
# What command does the user want to run?
# The command is a GNU Parallel command template
# So {} will be replaced with the argument
IFS= read -r command_to_run
# Run $command_to_run for every @y.
# -0 is needed if an element in @y contains \n
parallel -0 "$command_to_run" ::: "${y[@]}"

Or maybe:

# Set default command template
cmd='printf "%s\0" "${y[@]}" | parallel -0 wc'
# Let the user edit the template
IFS= read -r -e -i "$cmd"
# Run the input
eval "$REPLY"
Ole Tange
  • 31,768
  • 5
  • 86
  • 104
  • I'd suggest making that `IFS= read -r command_to_run`. Without the `-r`, backslashes the user enters will be consumed by the `read` command rather than assigned to the variable. Without clearing IFS, leading and trailing whitespace will be removed. – Charles Duffy Feb 24 '20 at 13:44
  • (With that fixed, avoiding the use of `eval` altogether certainly makes this the better-practice answer). – Charles Duffy Feb 24 '20 at 13:45
  • Thanks. Fixed now. – Ole Tange Feb 24 '20 at 14:53
  • still getting error: declare -a out=([0]="" [1]="file 3" [2]="file 2" [3]="file 1") printf "%s\0" ${y[@]} | parallel -0 wc wc: file: No such file or directory wc: 3: No such file or directory ... – Tim Feb 25 '20 at 22:09
  • `${y[@]}`, not `"${y[@]}"`? Quotes matter. I'd suggest switching from double to single quotes for the whole string: `cmd='printf "%s\0" "${y[@]}" | parallel -0 wc'`; that way you avoid the need for internal escaping. – Charles Duffy Feb 25 '20 at 22:15
  • that leads to wc: 'file 3 file 2 file 1': No such file or directory – Tim Feb 25 '20 at 22:24
  • earlier error in my code caused problem. `y=${out[@]:1}` should be `y=( "${out[@]:1}" )`. Code works now. – Tim Feb 25 '20 at 23:11
1

Ignoring whether fzf and parallel do what you want, the following quite certainly doesn't:

cmd="printf \"%s\0\" ${y[@]} | parallel -0 wc"

Why? Because ${y[@]} doesn't insert quoting and escaping necessary to make the contents of the y array be expressed as valid shell syntax (to refer to the data's original contents when fed back through eval).


If you want to insert data into a string that's going to be parsed as code, it needs to be escaped first. The shell can do that for you using printf %q:

printf -v array_str '%q ' "${y[@]}"
cmd="printf '%s\0' ${array_str} | parallel -0 wc"
IFS= read -r -e -i "$cmd" cmd_edited; eval "$cmd_edited"
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • updated my code to include your changes; but still getting error. – Tim Feb 25 '20 at 06:14
  • used updated code, still getting error: declare -a out=([0]="" [1]="file 3" [2]="file 2" [3]="file 1") printf '%s\0' file\ 3\ file\ 2\ file\ 1 | parallel -0 wc wc: 'file 3 file 2 file 1': No such file or directory – Tim Feb 25 '20 at 22:07
  • `xargs -0` doesn't work either: `wc: 'file 3 file 2 file 1': No such file or directory` – Tim Feb 25 '20 at 22:18
  • does this mean that the obstacle might be the fzf output? does the `declare` statement reveal nulls or lack thereof? – Tim Feb 25 '20 at 22:19
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/208526/discussion-between-charles-duffy-and-tim). – Charles Duffy Feb 25 '20 at 22:23
  • earlier error in my code caused problem. `y=${out[@]:1}` should be `y=( "${out[@]:1}" )`. Code works now – Tim Feb 25 '20 at 23:12