Why does foo{bar, baz} not expand to "foobar" and "foobaz"? - shell

I have two files in my directory:
com.my.arsys.core.js
com.my.arsys.core-libs.js
Now I want to match these using globbing pattern (curly braces) and copy them to a folder so I run the following:
cp com.my.arsys.{core, core-libs}.js a
However I get the following errors:
cp: cannot stat 'core,': No such file or directory
cp: cannot stat 'core-libs,': No such file or directory
I think the problem is with the syntax. Can anyone please help?

You have an extra blank, try:
cp com.my.arsys.{core,core-libs}.js a
From bash man about Brace Expansion:
A correctly-formed brace expansion must contain unquoted opening and closing braces, and at least one unquoted comma or a valid sequence expression. Any incorrectly formed brace expansion is left unchanged.

Related

I accidentally deleted files in a repository using rm *$*

I was trying to remove a file within a repository which contained a "$" symbol, so I used:
rm *$*
But this deleted all my files in the directory and attempted to delete all subdirectories as well.
Can someone explain why this command did not just remove files containing a $?
*$* first undergoes parameter expansion, with $* expanding to a string consisting of all the current positional parameters. If there are none, the result is *, which then undergoes pathname expansion.
The correct command would have been something like rm *"$"* or rm *\$*, with the $ escaped to prevent any parameter expansion from taking place before pathname expansion.

How can I confirm whether whitespace or special characters are escaped in a wildcard pattern?

I know that when you use a for loop in Bash, the items that you loop through are separated using the $IFS variable.
However, if I run the following commands, I correctly show the two files I have created - even though they have spaces:
touch file\ {1..2}.txt
for file in *.txt; do
echo "Found: ${file}"
done
The output is:
Found: file 1.txt
Found: file 2.txt
I am assuming that this is because when the shell sees the wildcard pattern, it expands it and escapes any special characters or whitespace. This is in contrast to if I run:
touch file\ {1..2}.txt
files=$(ls *.txt)
for file in $files; do
echo "Found: ${file}"
done
This results in:
Found: file
Found: 1.txt
Found: file
Found: 2.txt
Which makes sense - by default $IFS contains whitespace, so the file names are split.
What I want to understand is:
Am I correct that wildcard expansion results in a set of strings that contain escaped special characters
Where is it documented that this is the case, if I am correct?
Is there any way to show that this is happening?
I was hoping I could use something like set -x to show what the wildcard expands to and actually see the escaped characters, because I really want to be able to understand what is going on here.
I am writing a long series of articles on effective shell usage (effective-shell.com) and I'm struggling to find a way to explain the differences of behaviour here, I'm assuming that the shell is escaping characters but I'd like to know if this is the case and how to see it if possible!
Thanks in advance.
done
Am I correct that wildcard expansion results in a set of strings that contain escaped special characters
No. There is no need for the shell to escape special characters at that point, because filename expansion is the last word expansion to be performed; strings resulting from it are not subjected to word splitting or any other expansion; they stay as-is. This is documented in the manual as follows:
The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and filename expansion.

Expand part of the path in bash script

I am trying to list all files located in specific sub-directories of a directory in my bash script. Here is what I tried.
root_dir="/home/shaf/data/"
sub_dirs_prefixes=('ab' 'bp' 'cd' 'cn' 'll' 'mr' 'mb' 'nb' 'nh' 'nw' 'oh' 'st' 'wh')
ls "$root_dir"{$(IFS=,; echo "${sub_dirs_prefixes[*]}")}"rrc/"
Please note that I do not want to expand value stored in $root_dir as it may contain spaces but I do want to expand sub-path contained in {} which is a comma delimited string of contents of $sub_dirs_prefixes. I stored sub-directories prefixes in an array variable, $sub_dirs_prefixes , because I have to use them later on for something else.
I am getting following error:
ls: cannot access /home/shaf/data/{ab,bp,cd,cn,ll,mr,mb,nb,nh,nw,oh,st,wh}rrc/
If I copy the path in error message and run ls from command line, it does display contents of listed sub-directories.
You can command substitution to generate an extended pattern.
shopt -s extglob
ls "$root_dir"/$(IFS="|"; echo "#(${sub_dirs_prefixes[*]})rrc")
By the time parameter can command substitutions have completed, the shell sees this just before performing pathname expansion:
ls "/home/shaf/data/"/#(ab|bp|cd|cn|ll|mr|mb|nb|nh|nw|oh|st|wh)rrc
The #(...) pattern matches one of the enclosed prefixes.
It gets a little trickier if the components of the directory names contain characters that need to be quoted, since we aren't quoting the command substitution.

No such file or directory (ls) in conjunction with tilde expansion

I am writing a simple bash script and wanted to display all the items in a a particular directory. I tried doing the following:
desktop="~/Desktop/testr/"
echo $desktop
echo `ls $desktop`
However I keep getting the output:
~/Desktop/testr/
ls: ~/Desktop/testr/: No such file or directory
But when I run ls from the terminal, I can see the items. I suspect that the problem is that the ~ is not getting expanded but I thought that the double quotes would have taken care of that.
Thanks for your help!
This is because within quoted strings there is no tilde expansion and tilde expansion comes before parameter substitution in the echo line.
The sequence of expansions is:
Tilde expansion
parameter expansion
command substitution
arithmetic expansion
Field splitting
Pathname expansion
Quote removal
See the POSIX Shell Specification on Word Expansions for the gory details.

How to copy multiple files from a different directory using cp, variable and brackets?

My question is very similar to How to copy multiple files from a different directory using cp?
I don't want to use an explicit loop. Here is what I do:
$ FILES_TOOLS="fastboot,fastboot-HW.sh"
$ cp $HOME/tools/{$FILES_TOOLS} $TOP_DIR/removeme
cp: cannot stat `/home/johndoe/tools/{fastboot,fastboot-HW.sh}': No such file or directory
The files are present and destination is valid, because:
$ cp $HOME/tools/{fastboot,fastboot-HW.sh} $TOP_DIR/removeme
$ echo $?
0
I tried to remove the double quote from FILES_TOOLS, no luck.
I tried to quote and double quote {...}, no luck
I tried to backslash the brackets, no luck
I guess this is a problem of when the shell expansion actually occurs.
This answer is limited to the bash.
Prepend an echo to see what your cp command turns into:
echo cp $HOME/tools/{$FILES_TOOLS} $TOP_DIR/removeme
You have to insert an eval inside a sub-shell to make it work:
cp $( eval echo $HOME/tools/{$FILES_TOOLS} ) $TOP_DIR/removeme
I guess this is a problem of when the shell expansion actually occurs.
Yes. Different shells have different rules about brace expansion in relation to variable expansion. Your way works in ksh, but not in zsh or bash. {1..$n} works in ksh and zsh but not in bash. In bash, variable expansion always happens after brace expansion.
The closest you'll get to this in bash is with eval.
As long as the contents of the braces are literals, you can use brace expansion to populate an array with the full path names of the files to copy, then expand the contents of the array in your cp command.
$ FILES_TOOLS=( $HOME/tools/{fastboot,fastboot-HW.sh} )
$ cp "${FILES_TOOLS[#]}" $TOP_DIR/removeme
Update: I realized you might have a reason for having the base names alone in the variable. Here's another array-based solution that lets you prefix each element of the array with a path, again without an explicit loop:
$ FILES_TOOLS=( fastboot fastboot-HW.sh )
$ cp "${FILES_TOOLS[#]/#/$HOME/tools/}" $TOP_DIR/removeme
In this case, you use the pattern substitution operator to replace the empty string at the beginning of each array element with the directory name.

Resources