Creating a command substitution for each wildcard match

Viewed 69

Imagine I have folder foo with files a and b (and maybe more).

I want to pass to paste a command substitution for each file in foo (here, the files a and b).

This is almost it:

paste <(cut -f1 /foo/*)

However, this expands to:

    paste <(cut -f1 /foo/a /foo/b)

While what I actually want is a separate substitution for every file in foo:

    paste <(cut -f1 /foo/a) <(cut -f1 /foo/b)

The solution doesn't have to involve command substitution necessarily, it's just how I formulated it.

2 Answers

Your issue is that <(command group) implicitly creates a FIFO, but this FIFO only exists for the current command scope. It means, you cannot save this FIFO into a variable and come later to consume its stream. It has been released when its invoking command finished execution.

Lets demo this:

echo <(:) <(:); echo <(:)

Output:

/dev/fd/63 /dev/fd/62
/dev/fd/63

The second echo gets the sub-shell command group's allocated File Descriptor number reset.

An option instead is, explicitly creating named FIFO, as these lives until explicit deletion and so can their reference file name be saved for later use:

#!/usr/bin/env bash

tmpdir=$(mktemp -d)

trap 'rm -fr -- "$tmpdir"' EXIT

n=0

fifo_arr=()

for f in 'foo/'*; do
  fifo="$tmpdir/$((n++))"
  mkfifo "$fifo"
  fifo_arr+=("$fifo")
  cut -f1 "$f" >"$fifo" &
done

paste "${fifo_arr[@]}"

Alright, this is what I came up with.

ls *.txt | tr -s ' ' '\n' | sed -E 's/^(.*)$/<(cut -f1 \1)/' | tr -s '\n' ' ' | awk '{ print; }' > cmd.output; sed -i -E 's/^([^\s]+)/paste \1/' cmd.output; bash cmd.output

I have two files in my current directory, a.txt and b.txt.

The first contains only the following.

a
b
c

The second file is similar.

1
2
3

I begin by getting all of the files I'm interested in. For this example, it's all the *.txt files.

$ ls *.txt
a.txt b.txt

I then split each into it's own line by transcribing each space into a newline character (I suppose col works for this, too).

$ ls *.txt | tr -s ' ' '\n'
a.txt
b.txt

Next, I create the cut command for each of the listed files using a sed regex substitution.

$ ls *.txt | tr -s ' ' '\n' | sed -E 's/^(.*)$/<(cut -f1 \1)'
<(cut -f1 a.txt)
<(cut -f1 b.txt)

I now reverse the newline-space transcription to re-format this as a single line.

$ ls *.txt | tr -s ' ' '\n' | sed -E 's/^(.*)$/<(cut -f1 \1)'
<(cut -f1 a.txt) <(cut -f1 b.txt)

I then pipe this to a file, cmd.sh (I was using tee to see the results of my fiddling while experimenting.)

$ ls *.txt | tr -s ' ' '\n' | sed -E 's/^(.*)$/<(cut -f1 \1)' > cmd.sh

Having built up the command parameters, I then used another sed regex to edit the command file in place, replacing <args> with paste <args>.

$ sed -i -E 's/^([^\s]+)/paste \1/' cmd.sh

Before:

$ cat cmd.sh
<(cut -f1 a.txt) <(cut -f1 b.txt)

After:

$ cat cmd.sh
paste <(cut -f1 a.txt) <(cut -f1 b.txt)

Finally, I call bash, passing in the filename as an argument.

$ bash cmd.sh
a   1
b   2
c   3

I'm curious to see if anyone finds a better solution. This seems... hacky.

Related