for loop executes wrongly when no file found [duplicate] - bash

This question already has answers here:
How to skip the for loop when there are no matching files?
(2 answers)
Closed 3 years ago.
I have this bash "for in" loop that looks for pdf files in a directory and prompt them (simplified for the example)
#!/bin/bash
for pic in "$INPUT"/*.pdf
do
echo "found: ${pic}"
done
This script works well when there are pdf files in $INPUT directory, however, when there are no pdf files in the directory, I get :
found: /home/.../input-folder/*.pdf
Is it the expected behavior ?
How can I deal with it with a for in loop ?
Do I need to use ls or find ?
I tried with and without quotes around "$INPUT". There are no spaces in files names and directory names.

This is the expected behavior. According to the bash man page, in the Pathname Expansion section:
After word splitting, unless the -f option has been set, bash scans each word for the characters *, ?, and [. If one of these characters appears, then the word is regarded as a pattern, and replaced with an alphabetically sorted list of file names matching the pattern. If no matching file names are found, and the shell option nullglob is not enabled, the word is left unchanged.
As a result, if no matches for "$INPUT"/*.pdf are found, the loop will be executed on the pattern itself. But in the next sentence of the man page:
If the nullglob option is set, and no matches are found, the word is removed.
That's what you want! So just do:
#!/bin/bash
shopt -s nullglob
for pic in "$INPUT"/*.pdf
do
echo "found: ${pic}"
done
(But be aware that this may change the behavior of other things in unexpected ways. For example, try running shopt -s nullglob; ls *.unmatchedextension and see what happens.)

I would just add a file exists test like this:
#!/bin/bash
if test -e `echo "$INPUT"/*.pdf | cut -d' ' -f1`
then
for pic in "$INPUT"/*.pdf
do
echo "found: ${pic}"
done
fi

Related

use bash to rename a file with spaces and regex

If I have a file name with spaces and a random set of numbers that looks like this:
file name1234.csv
I want to rename it to this (assuming date is previously specified):
file_name_${date}.csv
I am able to do it like this:
mv 'file name'*'.csv file_name_${date}.csv
However, in a situation that 'file name*.csv' can actually match multiple files, I want to specify that it's 'file name[random numbers].csv'
I've searched around and can't find any relevant answers.
You need what is called a "pathname expansion", to match one or more digits:
+([0-9])
A functional script could be like this one:
date=$(date +'%Y-%m-%d')
shopt -s extglob nullglob
for f in 'file name'+([[:digit:]]).csv; do
file="${f%%[0-9]*}"
echo mv "$f" "${file// /_}_${date}.csv"
done
Warning: all files found will be renamed to just one name, make sure that that is what you want before removing the echo.
To activate the extended version of "Pathname Expansion" we use shopt -s extglob.
To avoid the case where no file is matched, we also need the nullglob set.
We can set the positional arguments to the result of the above expansion.
Then we loop over all files found to change each of their names.
The ${f%%[0-9]*} removes all from the digits to the end.
The ${file// /_} replaces spaces with underscores.
The mv is not actually done with the script presented because of the echo.
If after running a test, you want the change(s) performed, remove the echo.
Use Extended Globs and Parameter Expansion
You can do what you want with Bash extended globs and a few parameter expansions, without resorting to external or non-standard utilities.
date="2016-11-21"
shopt -s extglob
for file in 'file name'+([[:digit:]]).csv; do
newfile="${file%%[0-9]*}"
newfile="${newfile// /_}"
mv "$file" "${newfile}_${date}.csv"
done

How to capture Filename Expansion? (expanding globs)

--Disclaimer--
I am open to better titles for this question.
I am trying to get the full name of a file matching: "target/cs-*.jar".
The glob is the version number.
Right now the version is 0.0.1-SNAPSHOT.
So, below, I would like jar_location to evaluate to cs-0.0.1-SNAPSHOT.jar
I've tried a few solutions, some of them work, some don't and I'm not sure what I'm missing.
Works
jar_location=( $( echo "target/cs-*.jar") )
echo "${jar_location[0]}"
Doesn't work
jar_location=$( echo "target/cs-*.jar")
echo "$jar_location"
jar_location=( "/target/cs-*.jar" )
echo "${jar_location}"
jar_location=$( ls "target/cs-*.jar" )
echo "${jar_location}"
--EDIT--
Added Filename Expansion to the title
Link to Bash Globbing / Filename Expansion
Similar question: The best way to expand glob pattern?
If you're using bash, the best option is to use an array to expand the glob:
shopt -s nullglob
jar_locations=( target/cs-*.jar )
if [[ ${#jar_locations[#]} -gt 0 ]]; then
jar_location=${jar_locations##*/}
fi
Enabling nullglob means that the array will be empty if there are no matches; without this shell option enabled, the array would contain the literal string target/cs-*.jar in the case of no matches.
If the length of the array is greater than zero, then set the variable, using the expansion to remove everything up to the last / from the first element of the array. This uses the fact that ${jar_locations[0]} and $jar_locations get you the same thing, namely the first element of the array. If you don't like that, you can always assign to a temporary variable.
An alternative for those with GNU find:
jar_location=$(find target -name 'cs-*.jar' -printf '%f' -quit)
This prints the filename of the first result and quits.
Note that if there is more than one file found, the output of these two commands may differ.

Matching *.jpg but not *.foo.jpg in bash

This must be simple but I can't figure it out.
for filename in *[^\.foo].jpg
do
echo $filename
done
Instead of the matched filenames, echo shows the pattern:
+ echo '*[^.foo].jpg'
*[^.foo].jpg
Intention is to find all files ending in .jpg but not .foo.jpg.
EDIT: Tried this as per (misunderstood) advice:
for filename in *[!".foo"].jpg
Still not there!
You actually can do this, with an extglob. To demonstrate, copy-and-paste the following code:
shopt -s extglob
cd "$(mktemp -d "${TMPDIR:-/tmp}/test.XXXXXX")" || exit
touch hello.txt hello.foo hello.foo.jpg hello.jpg
printf '%q\n' !(*.foo).jpg
Output should be:
hello.jpg
In bash, if a glob pattern has no matches bash will return the pattern itself. You can change this behavior with the nullglob shell option, which can be turned on like this:
shopt -s nullglob
This is described in the section titled Filename Expansion in the bash man page.
As to why it doesn't match, it's simply that you don't have any files that match. This is possibly due to your use of ^ which isn't normally a valid glob meta character. As far as glob is concerned, ^ simply matches a literal ^. Also, [...] probably doesn't do what you think it does either.
For an explanation of valid glob meta-characters, see the Pattern Matching section of the bash man page.
You can't write a glob pattern that returns "all files ending in .jpg but not .foo.jpg.". The easiest thing to do is glob over all jpg files (*.jpg) and then filter out the ones that end in foo.jpg inside the code block.
for filename in *.jpg
do
[[ $filename = *.foo.jpg ]] && continue
echo $filename
done

How to match nothing if a file name glob has no matches [duplicate]

This question already has answers here:
How to skip the for loop when there are no matching files?
(2 answers)
Closed 3 years ago.
I want to loop over all files matching extension jpg or txt. I use:
for file in myDir/*.{jpg,txt}
do
echo "$file"
done
Problem: If the directory contains no jpg file at all, the loop will have one iteration with output myDir/*.jpg. I thought * will be replaced by an arbitrary file (and if no file exists it cannot be expanded). How can I avoid the unwanted iteration?
Use this to avoid the unwanted iteration:
shopt -s nullglob
From man bash:
nullglob: If set, bash allows patterns which match no files (see Pathname Expansion above) to expand to a null string, rather than themselves.
See: help shopt and shopt
This and a duplicate question both were in context of not just pathname-expansion, but also brace-expansion, and a duplicate asked for POSIX.
The compgen -G does bash --posix compatible pathname-expansion (no brace-expansion) and... you guessed it: yields nothing if there are no matches.
Therefore write a bash --posix function to do it. Brief outline: temporarily use set -f to first do brace-expansion (without pathname-expansion) via an echo, then apply compgen -G to each result for pathname-expansion. Full function left as an exercise.

Bash asterisk omits files that start with '.'? [duplicate]

This question already has answers here:
Bash for loop with wildcards and hidden files
(6 answers)
Closed 9 years ago.
I'm using a bash script to iterate over all files in a directory. But the loop is skipping over files that begin with a '.' such as '.bashrc' I'm not sure if .bashrc is failing the file test or is being omitted from the wildcard '*'. I've tried double quotes around "$item" but same result. How can I make this loop include .bashrc files?
id=0
cd $USERDIR
for item in *
do
if [[ -f $item ]]; then
cdir[$id]=$item
id=$(($id+1))
echo $item
fi
done
It's not the loop omitting those files, it's the expansion of * by the shell. If you want the dotfiles as well, use:
for item in .* *
From the bash manpage:
When a pattern is used for pathname expansion, the character "." at the start of a name or immediately following a slash must be matched explicitly, unless the shell option dotglob is set.
That last sentence on the dotglob option may seem to be useful but you should be wary of changing options that may affect later code. The safest way to use them is to ensure you set them back to their original values, something like:
rest_cmd=$(shopt -p dotglob) # Get restoration command
shopt -s dotglob # Set option
for item in * ; do
blah blah blah
done
${rest_cmd} # Restore option
But, in this case, I'd just stick with the explicit use of .* * since that's an easy solution.
You can set dotglob
shopt -s dotglob
for item in *; do echo "$item"; done

Resources