How to deal with `*` expansion when there are no files - bash

I am making a shell script that allows you to select a file from a directory using YAD. I am doing this:
list='';
exc='!'
for f in "$SHOTS_NOT_CONVERTED_DIR"/*;do
f=`basename $f`
list="${list}${exc}${f}"
done
The problem is that if there are no files in that directory, I end up with a selection with *.
What's the easiest, most elegant way to make this work in Bash?
The goal is to have an empty list if there are no files there.

* expansion is called a glob expressions. The bash manual calls it filename expansion.
You need to set the nullglob option. Doing so gives you an empty result if the glob expression does not find files:
shopt -s nullglob
list='';
exc='!'
for f in "$SHOTS_NOT_CONVERTED_DIR"/*;do
# Btw, use $() instead of ``
f=$(basename "$f")
list="${list}${exc}${f}"
done

Related

What is the shortest expression to match all file/dir names (including those beginning with a dot) in Bash?

I have extglob set and dotglob unset.
.* also yields . and .., very evil in conjunction with mv or cp.
I played around a bit and found that *(?(.)+([^.])) and $(ls -A) give the desired result, but I think there should be an easier way...
EDIT: Sorry, I should have mentioned that I am looking for an expression to be used at the prompt, not within a script.
unset GLOBIGNORE # empty-by-default, but let's make sure
shopt -s dotglob # disable special handling for "hidden" files
# ...and with the above items both done:
files=( * ) # just an example use of a glob
...sets the array files to contain all objects in the current directory except . and ..; any other use of * would behave similarly.

How to convert files in Unix using iconv?

I'm new to Bash scripting. I have a requirement to convert multiple input files in UTF-8 encoding to ISO 8859-1.
I am using the below command, which is working fine for the conversion part:
cd ${DIR_INPUT}/
for f in *.txt; do iconv -f UTF-8 -t ISO-8859-1 $f > ${DIR_LIST}/$f; done
However, when I don't have any text files in my input directory ($DIR_INPUT), it still creates an empty .txt file in my output directory ($DIR_LIST).
How can I prevent this from happening?
The empty file *.txt is being created in your output directory because by default, bash expands an unmatched expansions to the literal string that you supplied. You can change this behaviour in a number of ways, but what you're probably looking for is shopt -s nullglob. Observe:
$ for i in a*; do echo "$i"; done
a*
$ shopt -s nullglob
$ for i in a*; do echo "$i"; done
$
You can find documentation about this in the bash man page under Pathname Expansion. Or here or here.
In your case, I'd probably rewrite this in this way:
shopt -s nullglob
for f in "$DIR_INPUT"/*.txt; do
iconv -f UTF-8 -t ISO-8859-1 "$f" > "${DIR_LIST}/${f##*/}"
done
This avoids the need for the initial cd, and uses parameter expansion to strip off the path portion of $f for the output redirection. The nullglob will obviously eliminate the work being done on a nonexistent file.
As #ghoti pointed out, in the absence of files matching the wildcard expression a* the expression itself becomes the result of pathname expansion. By default (when nullglob option is unset), a* is expanded to, literally, a*.
You can set nullglob option, of course. But then you should be aware of the fact that all subsequent pathname expansions will be affected, unless you unset the option after the loop.
I would rather use find command which has a clear interface (and, in my opinion, is less likely to perform implicit conversions as opposed to the Bash globbing). E.g.:
cmd='iconv --verbose -f UTF-8 -t ISO-8859-1 "$0" > "$1"/$(basename "$0")'
find "${DIR_INPUT}/" \
-mindepth 1 \
-maxdepth 1 \
-type f \
-name '*.txt' \
-exec sh -c "$cmd" {} "${DIR_LIST}" \;
In the example above, $0 and $1 are positional arguments for the file path and ${DIR_LIST} respectively. The command is invoked via standard shell (sh) because of the need to refer to the file path {} twice. Although most modern implementations of find may handle multiple occurrences of {} correctly, the POSIX specification states:
If more than one argument containing the two characters "{}" is present, the behavior is unspecified.
As in the for loop, the -name pattern *.txt is evaluated as true if the basename of the current pathname matches the operand (*.txt) using the pattern matching notation. But, unlike the for loop, filename expansion do not apply as this is a matching operation, not an expansion.

glob pattern doesn't expand inside file in zsh

I am trying to exclude a directory from a glob.
This works at the command line:
$ export exclude=BigDir
$ for d in ^$exclude/ ; do echo "$d" ; done
SmallDir/
SmallerDir/
$
But in a file it doesn't work at all
#!/bin/zsh
exclude=BigDir
for d in ^$exclude/ ; do echo "$d" ; done
Running ./test or however I saved it prints the literal string
^BigDir/
How do I get it to correctly expand in the script file?
You are incorrectly using the glob characters ? used by the shell and the regular expression constructs ^, $. The for loop in your example can not undergo a regex match to exclude the directory provided, since it undergoes only pathname expansion (aka. glob expansion)
Unless you let know the shell to treat ^ and $ as special by enabling extended glob options extglob in bash and extendedglob in zsh, you cannot achieve what you wanted to do.
So you probably just need
setopt extendedglob
print -rl ^BigDir*
meaning print anything except the the filenames matching with BigDir.

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

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

Resources