Bash, confusing results for different file tests (test -f) - file

I am confused in bash by this expression:
$ var="" # empty var
$ test -f $var; echo $? # test if such file exists
0 # and this file exists, amazing!
$ test -f ""; echo $? # let's try doing it without var
1 # and all ok
I can't understand such bash behaviour, maybe anybody can explain?

It's because the empty expansion of $var is removed before test sees it. You are actually running test -f and thus there's only one arg to test, namely -f. According to POSIX, a single arg like -f is true because it is not empty.
From POSIX test(1) specification:
1 argument:
Exit true (0) if `$1` is not null; otherwise, exit false.
There's never a test for a file with an empty file name. Now with an explicit test -f "" there are two args and -f is recognized as the operator for "test existence of path argument".

When var is empty, $var will behave differently when if quoted or not.
test -f $var # <=> test -f ==> $? is 0
test -f "$var" # <=> test -f "" ==> $? is 1
So this example tells us: we should quote the $var.

Related

check multiple directories and output status

Hello I am trying run a script that checks for a list of directories. then output the status of each directory into new fields or an array.
Thanks in Advance!
#!/bin/sh
dir00="/tmp/Apple"
dir01="/tmp/Banana"
dir02="/tmp/Carrot"
dirList=("$dir00" "$dir01" "$dir02")
dirName00="Apple"
dirName01="Banana"
dirName02="Carrot"
dirNames=("$dirName00" "$dirName01" "$dirName02")
for i in "${dirList[#]}"; do
if [ -d "$i" ]; then
echo "Directory Not Missing:$i"
# write to a new arrary (dirStatus) either 0 or 1
else
echo "Directory Missing: $i"
# write to a new arrary (dirStatus) either 0 or 1
fi
done
# then I can do the following:
echo dirName[0] dirStatus[0]
# expected output:
echo Apple 1 # if Apple is missing
The test [ -d path ] sets the exit status $?. Instead of using that status implicitly in an if statement you can append it explicitly to an array.
An exit status of 0 means "yes" and everything else (usually 1) means "no".
Since you didn't specify how exactly you'd like to store the results, here are two alternatives that could be useful:
Two Regular Arrays
#! /bin/bash
path=(tmp/DIR_0{0..2})
isDir=()
for p in "${path[#]}"; do
[ -d "$p" ]
isDir+=($?)
done
declare -p path isDir
One Associative Array
#! /bin/bash
declare -A isDir
for p in tmp/DIR_0{0..2}; do
[ -d "$p" ]
isDir["$p"]=$?
done
declare -p isDir

How to return an array from a function and get the exit code returned from the function at the same time

I have many functions which return an array.
function myfunction() {
local -i status=0
local -a statusmsg=()
... do ....
statusmsg=($(do something ...))
if [[ ${status} -eq 0 ]]; then
....
return 0
else
for (( statusmsgline=0; statusmsgline<${#statusmsg[#]}; statusmsgline++ ))
do
printf "%s\n" "${statusmsg[${statusmsgline}]}"
done
return 1
fi
}
in the script I use mapfile as suggested here How to return an array in bash without using globals?
mapfile -t returnmsg <<< "$(myfunction "${param1}" "${param2}" "${paramX}" ...)"
if [ $? -eq 1 ]] ; then
... do something
fi
Using mapfile the array is well returned as it was generated but return code is ever and ever 0 (mapfile exit code) and can't retrieve the status code returned by the function.
I tried to use shopt -s lastpipe and shopt -so pipefail but without any success.
Is there any way to retrieve the array from function and the exit code at the same time ?
Kind Regards
Read the whole array into one string variable, evaluate the status, then pass the string variable on to mapfile.
output=$(myfunction ...)
if [ $? -eq 1 ] ; then
# do something
fi
mapfile -t array <<< "$output"
Warning: if the output of myfunction is very long the script may become slow or may even run out of memory. In this case you should write to a file or file descriptor instead.
The $() removes trailing empty lines. If those are important to you, then you can write either the exit status or the output to a file. Here we write the exit status at it is always short. Writing a long output to the file system and reading it afterwards would have more overhead.
mapfile -t array <(myfunction ...; echo $? > /tmp/status)
if [ "$(< /tmp/status; rm -f /tmp/status)" -eq 1 ] ; then
# do something
fi
There's also a way to work without these temporary variables/files by leveraging bash's options:
shopt -s lastpipe
myfunction ... | mapfile -t array
if [ "${PIPESTATUS[0]}" -eq 1 ] ; then
# do something
fi
# you may want to do `shopt -u lastpipe` here

shell mock --define from array: ERROR: Bad option for '--define' ("dist). Use --define 'macro expr'

I am currently writing a script which should make it more easy for me to build some RPMs using mock.
The plan is to make it possible to add values for the mock (and therefor rpmbuild) --define parameter.
The error I get if I add such a define value is
ERROR: Bad option for '--define' ("dist). Use --define 'macro expr'
When I execute the script with as simple as ./test.sh --define "dist .el7" the "debug" output is as follows:
/usr/bin/mock --init -r epel-7-x86_64 --define "dist .el7"
If I copy this and execute it in the shell directly it is actually working. Does anybody have an idea why this is the case?
My script can be cut down to the following:
#!/bin/sh
set -e
set -u
set -o pipefail
C_MOCK="/usr/bin/mock"
MOCK_DEFINES=()
_add_mock_define() {
#_check_parameters_count_strict 1 ${#}
local MOCK_DEFINE="${1}"
MOCK_DEFINES+=("${MOCK_DEFINE}")
}
_print_mock_defines_parameter() {
if [ ${#MOCK_DEFINES[#]} -eq 0 ]; then
return 0
fi
printf -- "--define \"%s\" " "${MOCK_DEFINES[#]}"
}
_mock_init() {
local MOCK_DEFINES_STRING="$(_print_mock_defines_parameter)"
local MOCK_PARAMS="--init"
MOCK_PARAMS="${MOCK_PARAMS} -r epel-7-x86_64"
[ ! "${#MOCK_DEFINES_STRING}" -eq 0 ] && MOCK_PARAMS="${MOCK_PARAMS} ${MOCK_DEFINES_STRING}"
echo "${C_MOCK} ${MOCK_PARAMS}"
${C_MOCK} ${MOCK_PARAMS}
local RC=${?}
if [ ${RC} -ne 0 ]; then
_exit_error "Error while mock initializing ..." ${RC}
fi
}
while (( ${#} )); do
case "${1}" in
-s|--define)
shift 1
_add_mock_define "${1}"
;;
esac
shift 1
done
_mock_init
exit 0
After asking this question a coworker I was pointed to this question on unix stackexchange: Unix Stackexchange question
The way this problem was solved can be broken down to following lines:
DEFINES=()
DEFINES+=(--define "dist .el7")
DEFINES+=(--define "foo bar")
/usr/bin/mock --init -r epel-7-x86_64 "${DEFINES[#]}"
Just in case somebody else stumbles upon this kind of issue.

Storing Command Line Arguments in an Array , Arguments can be Integers OR a file containing integers delimited by a space

Requirement :
Able to send a file containing numbers (433 434 435) as a parameter
sh Test.sh myFile.txt
Parameters can be numbers directly if not a file (433 434 434)
sh Test.sh 434 435 436
So , it has to support both file and numbers as the parameters
Below is the code i ve tried writing but in the for loop below , all numbers are getting printed as a string , but i need the for loop to run thrice as the input values are 3.
How to have it as a part of an array in shell script
Iam relatively new to shell script
OutPut:
In either case for loop has to run the number of parameter times(filedata determinies the parameters or direct input)
Please advice if any unforeseen bugs exist
#!/bin/bash
echo -e $# 2>&1 ;
myFile=$1 ; // As the first parameter will be a file
#[ -f "$myFile" ] && echo "$myFile Found" || echo "$myFile Not found"
if [ -f "$myFile" ]; then
tcId=`cat $#`;
echo $tcId;
else
tcId=$#;
echo $tcId;
fi
# Execute each of the given tests
for testCase in "$tcId"
do
echo "Test Case is "$testCase ;
done
I'd use a process substitution to "pretend" the explicit arguments are in a file.
while IFS= read -r testCase; do
echo "Test case is $testCase"
done < <( if [ -f "$1" ]; then
cat "$1"
else
printf "%s\n" "$#"
fi
)
If you are flexible in how your script is called, I would simplify it to only read test cases from standard input
while IFS= read -r testCase; do
echo "Test case is $testCase"
done
and call it one of two ways, neither using command line arguments:
sh Test.sh < myFile.txt
or
sh Test.sh <<TESTCASES
433
434
434
TESTCASES

Bash: rm with an array of filenames

So I'm working on making an advanced delete script. The idea is the user inputs a grep regex for what needs to be deleted, and the script does an rm operation for all of it. Basically eliminates the need to write all the code directly in the command line each time.
Here is my script so far:
#!/bin/bash
# Script to delete files passed to it
if [ $# -ne 1 ]; then
echo "Error! Script needs to be run with a single argument that is the regex for the files to delete"
exit 1
fi
IFS=$'\n'
files=$(ls -a | grep $1 | awk '{print "\"" $0 "\"" }')
## TODO ensure directory support
echo "This script will delete the following files:"
for f in $files; do
echo " $f"
done
valid=false
while ! $valid ; do
read -p "Do you want to proceed? (y/n): "
case $REPLY in
y)
valid=true
echo "Deleting, please wait"
echo $files
rm ${files}
;;
n)
valid=true
;;
*)
echo "Invalid input, please try again"
;;
esac
done
exit 0
My problem is when I actually do the "rm" operation. I keep getting errors saying No such file or directory.
This is the directory I'm working with:
drwxr-xr-x 6 user staff 204 May 9 11:39 .
drwx------+ 51 user staff 1734 May 9 09:38 ..
-rw-r--r-- 1 user staff 10 May 9 11:39 temp two.txt
-rw-r--r-- 1 user staff 6 May 9 11:38 temp1.txt
-rw-r--r-- 1 user staff 6 May 9 11:38 temp2.txt
-rw-r--r-- 1 user staff 10 May 9 11:38 temp3.txt
I'm calling the script like this: easydelete.sh '^tem'
Here is the output:
This script will delete the following files:
"temp two.txt"
"temp1.txt"
"temp2.txt"
"temp3.txt"
Do you want to proceed? (y/n): y
Deleting, please wait
"temp two.txt" "temp1.txt" "temp2.txt" "temp3.txt"
rm: "temp two.txt": No such file or directory
rm: "temp1.txt": No such file or directory
rm: "temp2.txt": No such file or directory
rm: "temp3.txt": No such file or directory
If I try and directly delete one of these files, it works fine. If I even pass that whole string that prints out before I call "rm", it works fine. But when I do it with the array, it fails.
I know I'm handling the array wrong, just not sure exactly what I'm doing wrong. Any help would be appreciated. Thanks.
Consider instead:
# put all filenames containing $1 as literal text in an array
#files=( *"$1"* )
# ...or, use a grep with GNU extensions to filter contents into an array:
# this passes filenames around with NUL delimiters for safety
#files=( )
#while IFS= read -r -d '' f; do
# files+=( "$f" )
#done < <(printf '%s\0' * | egrep --null --null-data -e "$1")
# ...or, evaluate all files against $1, as regex, and add them to the array if they match:
files=( )
for f in *; do
[[ $f =~ $1 ]] && files+=( "$f" )
done
# check that the first entry in that array actually exists
[[ -e $files || -L $files ]] || {
echo "No files containing $1 found; exiting" >&2
exit 1
}
# warn the user
echo "This script will delete the following files:" >&2
printf ' %q\n' "${files[#]}" >&2
# prompt the user
valid=0
while (( ! valid )); do
read -p "Do you want to proceed? (y/n): "
case $REPLY in
y) valid=1; echo "Deleting; please wait" >&2; rm -f "${files[#]}" ;;
n) valid=1 ;;
esac
done
I'll go into the details below:
files has to be explicitly created as an array to actually be an array -- otherwise, it's just a string with a bunch of files in it.
This is an array:
files=( "first file" "second file" )
This is not an array (and, in fact, could be a single filename):
files='"first file" "second file"'
A proper bash array is expanded with "${arrayname[#]}" to get all contents, or "$arrayname" to get only the first entry.
[[ -e $files || -L $files ]]
...thus checks the existence (whether as a file or a symlink) of the first entry in the array -- which is sufficient to tell if the glob expression did in fact expand, or if it matched nothing.
A boolean is better represented with numeric values than a string containing true or false: Running if $valid has potential to perform arbitrary activity if the contents of valid could ever be set to a user-controlled value, whereas if (( valid )) -- checking whether $valid is a positive numeric value (true) or otherwise (false) -- has far less room for side effects in presence of bugs elsewhere.
There's no need to loop over array entries to print them in a list: printf "$format_string" "${array[#]}" will expand the format string additional times whenever it has more arguments (from the array expansion) than its format string requires. Moreover, using %q in your format string will quote nonprintable values, whitespace, newlines, &c. in a format that's consumable by both human readers and the shell -- whereas otherwise a file created with touch $'evil\n - hiding' will appear to be two list entries, whereas in fact it is only one.

Resources