Why can "$path"/{,.}* include ".*" in its results? - shell

I have a bash script where I loop over all files in a directory using
for x in "$path/"{.,}*; do
do some stuff here
done
This does loop over all the files and directories (this is what I want),
but it also gives me the file .* which does not exist.
I am using a terminal emulator on android, so it could be an error there.
I am looking for a shell solution as I don't have most of the "normal Linux" commands such as sed.

If Your Shell Is Really Bash (as the question originally stated)
Running the following command:
shopt -s nullglob
will disable the default behavior of leaving globs with no matches unexpanded. Note that this can have surprising side effects: With nullglob set, ls -l *.NotFilenamesHaveThisSuffix will be exactly the same as ls -l.
That said, you can do even better:
shopt -s dotglob
for x in "$path/"*; do
printf 'Processing file: %q\n' "$x"
done
will, on account of setting dotglob, find all hidden files without needing the {.,} idiom at all, and will also have the benefit of consistently ignoring . and ...
If Your Shell Is ash Or mksh
It's actually quite usual to find bash on an Android device. If you have a baseline POSIX shell, the following will be more robust.
for x in "$path"/* "$path"/.*; do
[ -e "$x" ] || continue # skip files that don't exist
basename=${x##*/} # find filename w/o path
case $basename in "."|"..") continue ;; esac # skip "." and ".."
echo "Processing $x" >&2
: ...put your logic here...
done

Related

How to `command file.txt OR another_file.txt` in CLI (bash/zsh)

Is it possible to run a command with either file.txt as an argument or, if file.txt doesn't exist, with another_file.txt?
To make my request more realistic, I'd like to run VSCode with workspace file by default or, if workspace file doesn't exist, with . (current folder), something like:
code *.code-workspace OR .
If *.code-workspace exists, then the command should be equivalent to code *.code-workspace, otherwise the commend should be equivalent to code .
Is it possible in bash or zsh? (I'm using zsh + oh-my-zsh)
In bash you can use nullglob to expand wildcards (like *) to nothing if there is no match. Then put everything into an array and retrieve the first entry. Note that $array is the same as the first array entry ${array[0]}.
#! /bin/bash
shopt -s nullglob
files=(*.code-workspace .)
code "$files"
Above code starts code firstMatchOf.code-workspace if there is a file ending with .code-workspace and . if there is no such file.
For zsh you can do the same by replacing shopt -s nullglob with setopt null_glob.
Note that above approach only works with wildcards. files=(a b); code "$files" will call code a even if a does not exist. Here you could use the following function instead, which should work in every case:
#! /bin/sh
firstExisting() {
set -- "$#" . # ensure termination
while ! [ -e "$1" ]; do
shift
done
printf %s\\n "$1"
}
Usage example:
code "$(firstExisting *.code-workspace .)"
or just
code "$(firstExisting *.code-workspace)"
… as . is the default in case none of the arguments existed. shopt -s nullglob is not needed here.

How can I use getopts in a script that appends lines from files in a separate directory to a new file?

I am trying to write a bash script that takes in a directory, reads each file in the directory, and then appends the first line of each file in that directory to a new file. When I hard-code the variables in my script, it works fine.
This works:
#!/bin/bash
rm /local/SomePath/multigene.firstline.btab
touch /local/SomePath/multigene.firstline.btab
btabdir=/local/SomePath/test/*
outfile=/local/SomePath/multigene.firstline.btab
for f in $btabdir
do
head -1 $f >> $outfile
done
This does not work:
#!/bin/bash
while getopts ":d:o:" opt; do
case ${opt} in
d) btabdir=$OPTARG;;
o) outfile=$OPTARG;;
esac
done
rm $outfile
touch $outfile
for f in $btabdir
do
head -1 $f >> $outfile
done
Here is how I call the script:
bash /local/SomePath/Scripts/btab.besthits.wBp-q_wBm-r.sh -d /local/SomePath/test/* -o /local/SomePath/out.test/multigene.firstline.btab
And here is what I get when I run it:
rm: missing operand
Try 'rm --help' for more information.
touch: missing file operand
Try 'touch --help' for more information.
/local/SomePath/Scripts/btab.besthits.wBp-q_wBm-r.sh: line 23: $outfile: ambiguous redirect
Any suggestions? I'd like to be able to use getopts so I can make the script more generic. Thanks!
You have to pay extra attention to quoting and globbing when writing bash scripts.
When you call the script with a glob (* here) it gets expanded and split into words by your shell. This happends before your script even gets executed.
If you for example do cat *.txt cat will get all .txt files in the directory as its arguments. It will be the same as calling cat afile.txt nextfile.txt (and so on). Cat will never see the asterisk.
In your script it means that the input -d /local/SomePath/test/* gets expanded som something like /local/SomePath/test/someFile /local/SomePath/test/someOtherFile /test/someThirdFile.
Subsequently getopts only takes the first file after -d as for $btabdir and the -o doesn't get handled in the case switch.
I suggest you start by quoting every variable, preferable in the "${name}" style, and only invoke the script with quoted input.
It might also be send in a directory path, test that it is a directory (test -d), and change your for loop to for f in "${btabdir}"/*
This also works:
head -n1 -q /local/SomePath/test/* >> /local/SomePath/out.test/multigene.firstline.btab
I think the right answer here is "don't do it that way." :-)
The reason your current script isn't working may be that the wildcard is expanded by your interactive shell, not by your script. Try running your command with an echo at the beginning of the line for a hint at what's really happening. Once getopts sees the second of the matched files in the glob, it stops processing options, so -o never gets read, and $outfile remains unset. And since you don't quote your variable in rm $outfile, it's as if you're running rm without options. Test the difference in your shell between rm alone and rm "".
Also, what happens to your for loop if there's a space in a filename? Since you have bash, you have arrays. And arrays are much better for processing lists of files.
Perhaps use something like this instead:
#!/bin/bash
# initialize an array
files=()
while getopts :d:o: opt; do
case "$opt" in
d)
if [[ ! -d "$OPTARG" ]]; then
printf 'ERROR: not a directory: %s\n' "$OPTARG" >&2
exit 65
fi
# add to the array
files+=( "$OPTARG"/* )
;;
o) outfile="$OPTARG" ;;
*)
printf 'ERROR: unknown option: %s\n' "$opt" >&2
exit 64
;;
esac
done
if ! rm -f "$outfile" && touch "$outfile"; then
printf 'ERROR: cannot create %s\n' "$outfile" >&2
exit 73
fi
for f in "${files[#]}"; do
read -r < "$f"
printf '%s\n' "$REPLY"
done > "$outfile"
Here are some highlights of the changes....
We're using arrays, of course. The array ${files[#]} will contain one-file-per-record, without relying on whitespace, so with proper quoting you'll avoid problems with special characters in filenames.
We test for more error conditions, and actually show errors and exit if we see them. (The exit values are sysexits.)
Instead of using head, we use read and a single redirect to $outfile. This saves multiple forks to an external program, and multiple fopen() calls to your output file.
Note that the argument to -d should be a directory, not a glob. And you can specify options multiple times. Multiple -d options will be added together, but only the last -o option will be used.

"for" loop wildcard evaluated to variable if no such files present

$ for f in /etc/shell*; do echo $f; done
/etc/shells
$
good!
$ for f in /etc/no_such*; do echo $f; done
/etc/no_such*
$
BAD!
How can I reap off wildcard evaluation if no files present?
There is a specific shell option to enable this behaviour with globs, called nullglob. To enable it, use shopt -s nullglob.
When this option is enabled, a pattern with no matches evaluates to nothing, rather than to itself.
This is non-standard feature provided by bash, so if you're using another shell or are looking for a more widely compatible option you can add a condition to the loop body:
for f in /etc/no_such*; do [ -e "$f" ] && echo "$f"; done
This will only echo if the file exists.

Iterating through files gives odd results when no files [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'm trying to iterate through all zip files in a bash script (I'm using Cygwin, but I kind of doubt this is a bug with Cygwin).
It looks like this right now:
for z in *.zip
do
echo $z
done
which works well when there are zip files in the folder, and it echos exactly the zip files and nothing but the zip files. However, when I do it on a folder that's empty, it echos *.zip, when I'd rather it echo nothing.
What should I be doing? I don't think the right solution is if [ $z != "*.zip ]... but is it?
This is the expected behavior. From the documentation:
If no matching filenames are found, and the shell option nullglob is disabled, the word is left unchanged. If the nullglob option is set, and no matches are found, the word is removed.
So the solution is to set the nullglob option before the loop:
shopt -s nullglob
for z in *.zip
...
As one of its steps for executing a command, the shell may perform path expansion in which case it will replace a string, such as *.zip with the list of files that match that glob. If there are no such files, then the string is left unchanged. A reasonable solution is:
for z in *.zip
do
[ -f "$z" ] && echo $z
done
[ -f "$z" ] verifies that the file exists and is a regular file. The && means that echo will be executed only if that test is passed.
Either that or turning on the nullglob option.
$ set -x
$ echo *.none
+ echo '*.none'
*.none
$ shopt -s nullglob
+ shopt -s nullglob
$ echo *.none
$ echo *.none
+ echo

bash - recursive script can't see files in sub directory

I got a recursive script which iterates a list of names, some of which are files and some are directories.
If it's a (non-empty) directory, I should call the script again with all of the files in the directory and check if they are legal.
The part of the code making the recursive call:
if [[ -d $var ]] ; then
if [ "$(ls -A $var)" ]; then
./validate `ls $var`
fi
fi
The part of code checking if the files are legal:
if [[ -f $var ]]; then
some code
fi
But, after making the recursive calls, I can no longer check any of the files inside that directory, because they are not in the same directory as the main script, the -f $var if cannot see them.
Any suggestion how can I still see them and use them?
Why not use find? Simple and easy solution to the problem.
Always quote variables, you never known when you will find a file or directory name with spaces
shopt -s nullglob
if [[ -d "$path" ]] ; then
contents=( "$path"/* )
if (( ${#contents[#]} > 0 )); then
"$0" "${contents[#]}"
fi
fi
you're re-inventing find
of course, var is a lousy variable name
if you're recursively calling the script, you don't need to hard-code the script name.
you should consider putting the logic into a function in the script, and the function can recursively call itself, instead of having to spawn an new process to invoke the shell script each time. If you do this, use $FUNCNAME instead of "$0"
A few people have mentioned how find might solve this problem, I just wanted to show how that might be done:
find /yourdirectory -type f -exec ./validate {} +;
This will find all regular files in yourdirectory and recursively in all its sub-directories, and return their paths as arguments to ./validate. The {} is expanded to the paths of the files that find locates within yourdirectory. The + at the end means that each call to validate will be on a large number of files, instead of calling it individually on each file (wherein the + is replaced with a \), this provides a huge speedup sometimes.
One option is to change directory (carefully) into the sub-directory:
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ./validate $(ls))
fi
fi
The outer parentheses start a new shell so the cd command does not affect the main shell. The exec replaces the original shell with (a new copy of) the validate script. Using $(...) instead of back-ticks is sensible. In general, it is sensible to enclose variable names in double quotes when they refer to file names that might contain spaces (but see below). The $(ls) will list the files in the directory.
Heaven help you with the ls commands if any file names or directory names contain spaces; you should probably be using * glob expansion instead. Note that a directory containing a single file with a name such as -n would trigger a syntax error in your script.
Corrigendum
As Jens noted in a comment, the location of the shell script (validate) has to be adjusted as you descend the directory hierarchy. The simplest mechanism is to have the script on your PATH, so you can write exec validate or even exec $0 instead of exec ./validate. Failing that, you need to adjust the value of $0 — assuming your shell leaves $0 as a relative path and doesn't mess around with converting it to an absolute path. So, a revised version of the code fragment might be:
# For validate on PATH or absolute name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec $0 $(ls))
fi
fi
or:
# For validate not on PATH and relative name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ../$0 $(ls))
fi
fi

Resources