Martin
Martin

Reputation: 993

Bash - Brace expansion on result of command substitution?

I want to prepend a string to all the files in a directory. What I want to do is something like:

echo string{$(ls some_dir)}

This won't work because ls separates words with spaces, and brace expansion requires commas. So I thought I'd use tr to replace the spaces with commas, like:

echo string{$(ls some_dir) | tr ' ' ','}

But that doesn't work either because the pipe takes precedence.

What's the correct way to do this? I know I could probably use a sane language like Python, but it's frustrating that Bash can't even do something as simple as that.

Upvotes: 0

Views: 361

Answers (4)

Larry
Larry

Reputation: 312

$ rename -nv '' PREFIX_ * | grep -Po "' -> \`\K.*(?='$)”

This creates a file renaming sequence without actually doing the rename, then parsed the output for just the target of the rename. E.g.,

$ ls -A --fu --si
total 0
-rw-r--r-- 1 larry wheel 0 2024-01-02 16:58:46.870589000 -0600 a
-rw-r--r-- 1 larry wheel 0 2024-01-02 16:58:46.870678000 -0600 b
-rw-r--r-- 1 larry wheel 0 2024-01-02 16:58:46.870737000 -0600 c
$ rename -nv '' PREFIX_ * | grep -Po "^\`[^']+' -> \`\K.*(?='$)"
PREFIX_a
PREFIX_b
PREFIX_c

This may not work with really weird file names, or some unicode characters in file names.

Upvotes: 0

Bruce
Bruce

Reputation: 597

Of course, bash can do it.

Let's go step by step.

1. fix an issue in your second example

This is your second example

echo string{$(ls some_dir) | tr ' ' ','}

You put pipe outside the command substitution, which is totally wrong.

I believe you want to pipe the stream from ls output to tr input, so it's obvious that the pipe is supposed to be put inside the command substitution, like this

echo string{$(ls some_dir | tr ' ' ',')}

2. output of ls is separated by newline rather than whitespace

so here we go

echo string{$(ls some_dir | tr '\n' ',')}

3. brace expansion is performed prior to command substitution

In the other word, after command substitution is expanded to f1,f2,f3,d1,, the brace expansion will not be performed any more.

So, no doubt, the command will print string{f1,f2,f3,d1,}.

The solution is letting bash evaluate it again.

eval echo string{$(ls some_dir | tr '\n' ',')}

OK, up to now, the result looks very good (try it yourself, you'll get it), it is very close to what you were looking for, except one tiny spot.

You may already noticed the comma at the end of the output I demonstrated above. The comma results in an unnecessary string appearing at the end of the final output.

So let's make it done.

4. remove the ending comma

eval echo string{$(echo -n "$(ls some_dir)" | tr '\n' ',')}

OK, this is it.


Oh... BTW., this is just an specific solution for your specific question. You may develop new variants of your question, and this specific solution may not fit your new question. If so, I suggest you run man bash, and read it from head to toe very very carefully, then you will become unstoppable.

Upvotes: 0

rici
rici

Reputation: 241671

One way to do it, which will deal gracefully with whitespace in filenames:

files=("$dir"/*); files=("${files[@]/#"$dir"\//"$prefix"}")

That will store the prefixed strings in the array $files; you could iterate over them using an array expansion:

for file in "${files[@]}"; do 
  # Something with file
done

or print them out using printf:

printf "%s\n" "${files[@]}"

The advantage of using the array expansion is that it does not involve word-splitting, so even if the elements have whitespace in them, the array expansion will contain each element as a single word.

Upvotes: 0

kojiro
kojiro

Reputation: 77059

If you really want to interpolate the contents of a directory (which is what $(ls some_dir) would give you) then you can do

printf 'string%s ' some_dir/*

IRL, you probably want it to end with a newline.

{ printf 'string%s ' some_dir/*; echo; }

You can generalize this to the output of any glob or brace expansion:

printf 'foo%d\n' {11..22}

Edit

Based on your comment, you want to eliminate the "some_dir/" part, you can't merely do that with printf. You can either cd to the directory so the globs expand as desired, or use parameter expansion to clean up the leading directory name:

( cd some_dir && printf 'string%s ' *; echo )

or

{ cd some_dir && printf 'string%s ' * && cd - >/dev/null; echo; }

or

names=( some_dir/* ) names=( "${names[@]#some_dir/}" )
{ printf 'string%s ' "${names[@]}"; echo; }

Upvotes: 3

Related Questions