Logical OR in my shell script - bash

My script:
#!/bin/bash
for file in *.ats;
do
if [[ ("${file}" = THx) || ("${file}" = THy)]]
then cp $file /home/milenko/procmt
fi
done
Files in directory
262_V01_C00_R000_TEx_BL_128H.ats
262_V01_C01_R000_TEy_BL_128H.ats
262_V01_C02_R000_THx_BL_128H.ats
262_V01_C03_R000_THy_BL_128H.ats
What I wanted is to copy the files that contain THx or THy,but files are not copied.
Why?

I think you can avoid a loop entirely here:
cp *TH[xy]*.ats /home/milenko/procmt
There's no need to loop through the results and then do a separate comparison; a single glob will expand to the list of files that you want.
There were a couple of problems with your original approach:
Firstly, you were trying to test for exact matches, so the condition would never be true.
Also, take care with spaces: ]] is a keyword in the compound command [[, so it needs to be a separate word (i.e. surrounded by spaces).

What about using extglob for extended globbing? This way you can use the for itself to get the required extensions:
shopt -s extglob
for file in *TH?(x|y)*.ats; do
# do things with "$file" ...
done
*TH?(x|y)*.ats expands to those files containing <something> + TH + either x or y + <something> + .ats
Your script fails because you have a typo in it:
if [[ ("${file}" = THx) || ("${file}" = THy)]]
# ^
# missing space
This is fine:
$ d="hi"
$ [[ ($d == hi) || ($d == ha) ]] && echo "yes"
yes
Although the parentheses are superfluous:
$ [[ $d == hi || $d == ha ]] && echo "yes"
yes

Your question specifies "files that contain THx or THy" ... but your code specifies that the file name is THx or THy.

You can use character class in glob i.e. *TH[xy]* to check if $file contains THx or THy:
for file in *.ats; do
if [[ $file == *TH[xy]* ]]; then
cp "$file" /home/milenko/procmt
fi
done

Related

Why is empty string changed into -n expression in bash

Taken this snippet:
$ [[ ""=="foo" ]] && echo yes || echo no
+ [[ -n ==foo ]]
+ echo yes
yes
How does [[ ""=="foo" ]] turn into [[ -n ==foo ]] ?
The RC was of course missing spaces around == - after adding them, it works as expected:
$ [[ "" == "foo" ]] && echo yes || echo no
+ [[ '' == \f\o\o ]]
+ echo no
no
But still i cannot understand why it behaved like this?
It's not changing the empty string into -n.
The string ""=="foo" is equivalent to the string ==foo. The trace output always shows strings in their simplest format, without unnecessary quotes.
A conditional expression that just contains a single string with no operators is true if the string is not empty. That's what the -n operator tests, so the -x expansion shows it that way.
Any operand that isn't preceded or followed by an operator is treated to have an equal operation as -n <operand>. Operators also need to be isolated with spaces to be distinguished. For a list of operators run help test. Also run help [[ to see how the keyword is different from the [ and test builtins.

How to remove certain items from an array

I am trying to get a list of all Apps plus their versions using mdfind:
function get_mac_apps_info {
local list_apps=()
local app_version
local plist_info_app_path
local plist_field="CFBundleName"
readarray -d '' list_apps < <(mdfind -0 "kMDItemContentType == com.apple.application-bundle")
for index in "${!list_apps[#]}"
do [[ ${list_apps[$index]} =~ '^(?!.*\(Parallels\)).*' ]] && unset -v 'list_apps[$index]'
done
for app_path in "${list_apps[#]}"; do
plist_info_app_path="$app_path/Contents/Info.plist"
if [[ -f "$plist_info_app_path" ]]; then
app_version="$(get_version_from_plist "$app_path" 2>/dev/null)"
app_name="$(get_field_from_plist "$app_path" "$plist_field" 2>/dev/null)"
if [[ $app_version ]]; then
echo "$app_version;$app_name"
fi
fi
done
}
Thing is Parallels Desktop is installed and gets a lot of entries like these when populating the mdfind array:
/Users/user-test/Applications (Parallels)/{8dcf6541-4642-4aa0-b6ef-f73b59c0005e} Applications.localized/Command Prompt.app
/Users/user-test/Applications (Parallels)/{9bfd84de-a9b0-445d-afd5-c95690c3d1ea} Applications.localized/Command Prompt.app
I am trying to filter this out (unsuccessfully):
for index in "${!list_apps[#]}"
do [[ ${list_apps[$index]} =~ '^(?!.*\(Parallels\)).*' ]] && unset -v 'list_apps[$index]'
done
Any suggestions?
Thanks!
Try:
for index in "${!list_apps[#]}"; do
[[ ${list_apps[index]} == *'(Parallels)'* ]] && unset 'list_apps[index]'
done
Because of the single quotes, =~ '^(?!.*\(Parallels\)).*' only matches strings that contain the literal string '^(?!.*\(Parallels\)).*' (no special meaning is given to ^, (, ., etc.). If you remove the quotes it still doesn't work because it uses regular expression features that aren't supported by Bash.
The code above uses Bash glob pattern matching to match strings that contain the literal string (Parallels).

How to check filetype in if statement bash using wildecard and -f

subjects_list=$(ls -l /Volumes/Backup_Plus/PPMI_10 | awk '{ print $NF }')
filepath="/Volumes/Backup_Plus/PPMI_10/$subjects/*/*/S*/"
for subjects in $subjects_list; do
if [[ -f "${filepath}/*.bval" && -f "${filepath}/*.bvec" && -f "${filepath}/*.json" && -f "${filepath}/*.nii.gz" ]]; then
echo "${subjects}" >> /Volumes/Backup_Plus/PPMI_10/keep_subjects.txt
else
echo "${subjects}" >> /Volumes/Backup_Plus/PPMI_10/not_keep_subjects.txt
fi
done
problem is supposedly in the if statement, I tried this...
bvalfile = (*.bval)
bvecfile =(*.bvec)
jsonfile =(*.json)
niigzfile =(*.nii.gz)
if [[ -f "$bvalfile" && -f "$bvecfile" && -f "$jsonfile" && -f "$niigzfile" ]]; then
however that didn't work. Any help with syntax or errors or does it need to be changed completely. Trying to separate the files that have .^file types from those that don't by making two lists.
thanks
You're assigning filepath outside the for-subject loop but using the unset variable $subjects in it. You want to move that inside the loop.
Double-quoted wildcards aren't expanded, so both $filepath and your -f test will be looking for filenames with literal asterisks in them.
-f only works on a single file, so even if you fix the quotes, you'll have a syntax error if there's more than one file matching the pattern.
So I think what you want is something like this:
# note: array assignment -
# shell does the wildcard expansion, no ls required
prefix_list=( /Volumes/Backup_Plus/PPMI_10/* )
# and array expansion
for prefix in "${prefix_list[#]}"; do
# the subject is just the last component of the path
subject=${prefix##*/}
# start by assuming we're keeping this one
decision=keep
# in case filepath pattern matches more than one directory, loop over them
for filepath in "$prefix"/*/*/S*/; do
# if any of the files don't exist, switch to not keeping it
for file in "$filepath"/{*.bval,*.bvec,*.json,*.nii.gz}; do
if [[ ! -f "$file" ]]; then
decision=not_keep
# we have our answer and can stop looping now
break 2
fi
done
done
# now append to the correct list
printf '%s\n' "$subject" >>"/Volumes/Backup_Plus/PPMI_10/${decision}_subjects.txt"
done

How can I check if exists file with name according to "template" in the directory?

Given variable with name template , for example: template=*.txt.
How can I check if files with name like this template exist in the current directory?
For example, according to the value of the template above, I want to know if there is files with the suffix .txt in the current directory.
I would do it like this with just built-ins:
templcheck () {
for f in * .*; do
[[ -f $f ]] && [[ $f = $1 ]] && return 0
done
return 1
}
This takes the template as an argument (must be quoted to prevent premature expansion) and returns success if there was a match in the current directory. This should work for any filenames, including those with spaces and newlines.
Usage would look like this:
$ ls
file1.txt 'has space1.txt' script.bash
$ templcheck '*.txt' && echo yes
yes
$ templcheck '*.md' && echo yes || echo no
no
To use with the template contained in a variable, that expansion has to be quoted as well:
templcheck "$template"
Use find:
: > found.txt # Ensure the file is empty
find . -prune -exec find -name "$template" \; > found.txt
if [ -s found.txt ]; then
echo "No matching files"
else
echo "Matching files found"
fi
Strictly speaking, you can't assume that found.txt contains exactly one file name per line; a filename with an embedded newline will look the same as two separate files. But this does guarantee that an empty file means no matching files.
If you want an accurate list of matching file names, you need to disable field splitting while keeping pathname expansion.
[[ -v IFS ]] && OLD_IFS=$IFS
IFS=
shopt -s nullglob
files=( $template )
[[ -v OLD_IFS ]] && IFS=$OLD_IFS
printf "Found: %s\n" "${files[#]}"
This requires several bash extensions (the nullglob option, arrays, and the -v operator for convenience of restoring IFS). Each element of the array is exactly one match.

Recursively list hidden files without ls, find or extendedglob

As an exercise I have set myself the task of recursively listing files using bash builtins. I particularly don't want to use ls or find and I would prefer not to use setopt extendedglob. The following appears to work but I cannot see how to extend it with /.* to list hidden files. Is there a simple workaround?
g() { for k in "$1"/*; do # loop through directory
[[ -f "$k" ]] && { echo "$k"; continue; }; # echo file path
[[ -d "$k" ]] && { [[ -L "$k" ]] && { echo "$k"; continue; }; # echo symlinks but don't follow
g "$k"; }; # start over with new directory
done; }; g "/Users/neville/Desktop" # original directory
Added later: sorry - I should have said: 'bash-3.2 on OS X'
Change
for k in "$1"/*; do
to
for k in "$1"/* "$1"/.[^.]* "$1"/..?*; do
The second glob matches all files whose names start with a dot followed by anything other than a dot, while the third matches all files whose names start with two dots followed by something. Between the two of them, they will match all hidden files other than the entries . and ...
Unfortunately, unless the shell option nullglob is set, those (like the first glob) could remain as-is if there are no files whose names match (extremely likely in the case of the third one) so it is necessary to verify that the name is actually a file.
An alternative would be to use the much simpler glob "$1"/.*, which will always match the . and .. directory entries, and will consequently always be substituted. In that case, it's necessary to remove the two entries from the list:
for k in "$1"/* "$1"/.*; do
if ! [[ $k =~ /\.\.?$ ]]; then
# ...
fi
done
(It is still possible for "$1"/* to remain in the list, though. So that doesn't help as much as it might.)
Set the GLOBIGNORE file to exclude . and .., which implicitly turns on "shopt -u dotglob". Then your original code works with no other changes.
user#host [/home/user/dir]
$ touch file
user#host [/home/user/dir]
$ touch .dotfile
user#host [/home/user/dir]
$ echo *
file
user#host [/home/user/dir]
$ GLOBIGNORE=".:.."
user#host [/home/user/dir]
$ echo *
.dotfile file
Note that this is bash-specific. In particular, it does not work in ksh.
You can specify multiple arguments to for:
for k in "$1"/* "$1"/.*; do
But if you do search for .* in directories , you should be aware that it also gives you the . and .. files. You may also be given a nonexistent file if the "$1"/* glob matches, so I would check that too.
With that in mind, this is how I would correct the loop:
g() {
local k subdir
for k in "$1"/* "$1"/.*; do # loop through directory
[[ -e "$k" ]] || continue # Skip missing files (unmatched globs)
subdir=${k##*/}
[[ "$subdir" = . ]] || [[ "$subdir" = .. ]] && continue # Skip the pseudo-directories "." and ".."
if [[ -f "$k" ]] || [[ -L "$k" ]]; then
printf %s\\n "$k" # Echo the paths of files and symlinks
elif [[ -d "$k" ]]; then
g "$k" # start over with new directory
fi
done
}
g ~neville/Desktop
Here the funky-looking ${k##*/} is just a fast way to take the basename of the file, while local was put in so that the variables don't modify any existing variables in the shell.
One more thing I've changed is echo "$k" to printf %s\\n "$k", because echo is irredeemably flawed in its argument handling and should be avoided for the purpose of echoing an unknown variable. (See Rich's sh tricks for an explanation of how; it boils down to -n and -e throwing a spanner in the works.)
By the way, this will NOT print sockets or fifos - is that intentional?

Resources