Arguments check controll logic and syntax confusion - bash

I am trying to validate users input to a small script I am writing that checks that there should be: 2 arguments and the the first argument should be wither 'mount' or ' unmount'
I have the following:
if [ ! $# == 2 ] || [ $1 != "mount" -o $1 != "unmount" ]; then
However it seems to be a bit overzelouse at meeting the conditions I want. For example with the current || operator, nothing gets past the validator but if I use the && operator everything does.
if [ ! $# == 2 ] && [ $1 != "mount" -o $1 != "unmount" ]; then
Can someone help me figure this out?
Here is the whole block and intended usage
if [ ! $# == 2 ] || [ $1 != "mount" -o $1 != "unmount" ]; then
echo "Usage:"
echo "encmount.sh mount remotepoint # mount the remote file system"
echo "encmount.sh unmount remotepoint # unmount the remote file system"
exit
fi

You could do it like this:
if [ "$#" -ne 2 ] || [ "$1" != "mount" -a "$1" != "unmount" ]; then
echo "Usage:"
echo "encmount.sh mount remotepoint # mount the remote file system"
echo "encmount.sh unmount remotepoint # unmount the remote file system"
exit -1
fi
echo "OK"
You have a small logical error in your test, since you should enter the usage branch if $1 is not equal to both "mount" and "unmount". Also you should compare numbers with the -eq and -ne operators (see here), or use the (( )).
Note that you should quote your variables inside a test ([ ])
You can also combine the two expressions like this:
if [ "$#" -ne 2 -o \( "$1" != "mount" -a "$1" != "unmount" \) ]; then
If you have bash, you can also use the [[ ]] syntax:
if [[ $# -ne 2 || ( $1 != "mount" && $1 != "unmount" ) ]]; then

Related

How do I use round brackets in an 'if' condition

I'm creating a bash script and somewhere inside I have this code:
if [ $# -eq 2 -a (! -r "$2" -o ! -f "$2") ]; then
echo "rvf: bestand \""$2"\" bestaat niet of is onleesbaar" 1>&2
exit 2
fi
When i try to run this inside the script I get this error:
Syntax Error (bash -n):
rvf: line 14: syntax error in conditional expression
rvf: line 14: syntax error near `-a'
rvf: line 14: `if [[ $# -eq 2 -a (! -r "$2" -o ! -f "$2") ]]; then'
How does '()' work inside Bash scripts?
[[ doens't support -a, and it is considered obsolete and non portable for [. The correct solution using [ would be
if [ "$#" -eq 2 ] && { [ ! -r "$2" ] || [ ! -f "$2" ]; }; then
Grouping is done with { ... } rather than ( ... ) to avoid creating an unnecessary subshell.
Using [[ is simplifies to
if [[ "$#" -eq 2 && ( ! -r "$2" || ! -f "$2" ) ]]; then
Parentheses can be used for grouping inside [[; as a compound command, it uses separate parsing and evaluation rules, compared to an ordinary command like [ (which is just an alias for test, not syntax of any kind).
In either case, De Morgan's laws lets you refactor this to something a little simpler:
if [ "$#" -eq 2 ] && ! { [ -r "$2" ] && [ -f "$2" ] }; then
if [[ "$#" -eq 2 && ! ( -r "$2" && -f "$2" ) ]]; then
There are multiple points of confusion here.
[ can (as an optional XSI extension to the standard) support ( as a separate word (meaning there needs to be spaces around it), but the POSIX sh specification marks it (like -a and -o) as "obsolescent" and advises against its use.
[[ does support (, but again, it needs to always be a separate word.
Don't do that at all, though. You're using only well-supported and portable functionality if you keep each test its own simple command and combine them only with the shell's boolean logic support.
That is:
if [ "$#" -eq 2 ] && { [ ! -r "$2" ] || [ ! -f "$2" ]; }; then
echo "rvf: bestand \"$2\" bestaat niet of is onleesbaar" >&2
exit 2
fi
Restructure your logic.
"Not A or Not B" is just a more complicated way to say "not (A and B)".
In bash, try
if [[ "$#" == 2 ]] && ! [[ -r "$2" && -f "$2" ]]; then
Better,
if [[ "$#" == 2 && -r "$2" && -f "$2" ]]
then : all good code
else : nope code
fi
Even better,
if [[ "$#" == 2 ]] # correct args
then if [[ -r "$2" ]] # is readable
then if [[ if -f "$2" ]] # is a file
then echo "all good"
: do all good stuff
else echo "'$2' not a file"
: do not a file stuff
fi
else echo "'$2' not readable"
: do not readable stuff
fi
else echo "Invalid number of args"
: do wrong args stuff
fi
Clear error logging is worth breaking the pieces out.
Even better, imho -
if [[ "$#" != 2 ]]
then : wrong args stuff
fi
if [[ ! -r "$2" ]]
then : unreadable stuff
fi
if [[ ! -f "$2" ]]
then : do not a file stuff
fi
: do all good stuff

UNIX how to make my script delete multiple files and wildcards?

I was given the task of making a remove script that imitates the rm command. As you know, the rm command deletes all files if you were to type something like rm file1 file2. Using this example, my script would only delete file2. Can anyone help me on how to make it so my remove script would delete all files listed? My script is below. I apologise if its a little messy, I am new to coding.
#!/bin/bash
function directory(){
#Checks if deleted directory & .restore.info file exists
#If they don't exist, it creates them
if [ ! -d ~/deleted ]
then
mkdir ~/deleted
fi
if [ ! -f ~/.restore.info ]
then
touch ~/.restore.info
fi
}
function movefile(){
#not currently using
mv "$1" ~/deleted/$1
echo "file moved to recycle bin"
}
function error_conditions(){
#not currently using
#Prints error messages and checks if file is in project directory
if [ ! -f ~/project ]
then
echo "cannot remove $filename: no such file or directory"
elif [ -d ~/project ]
then
echo "cannot remove $filename: is a directory"
else
echo "missing operand"
fi
}
function delete_file(){
#Gets inode for filename
#Takes user input and puts file wherever based on user input
inode=$(stat -c%i "$filename")
pwd=$(readlink -e $filename)
if "$interactive"
then
if [ "$verbose" = true ]; then
read -p "Are you sure you want to delete $filename? " user_input
if [ $user_input == "y" ] || [ $user_input == "Y" ] || [ $user_input == "yes" ] || [ $user_input == "Yes" ];
then
mv $filename ~/deleted/${filename}_$inode
#moves deleted file to deleted directory (with inode at end)
echo ${filename}_$inode:$pwd>>~/.restore.info
#stores info of removed file in .restore.info (with path)
echo "removed '$filename'"
else
echo "Nothing has been deleted"
fi
else
read -p "Are you sure you want to delete $filename? " user_input
if [ $user_input == "y" ] || [ $user_input == "Y" ] || [ $user_input == "yes" ] || [ $user_input == "Yes"];
then
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
else
echo "Aborted"
fi
fi
elif "$verbose"
then
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$inode:pwd>>~/.restore.info
echo "removed '$filename'"
else
mv "$filename" ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo "Executed"
fi
}
#Setting all flags to false
interactive=false
verbose=false
recursive=false
while getopts :ivr optionvar
do
case "$optionvar" in
i) interactive=true;;
v) verbose=true;;
r) recursive=true;;
esac
done
shift $((OPTIND-1)) #process arguments.
#doing error commands with help of recursive
for i in $*
do
filename=$i
basefile=$(basename $i)
if [ "$filename" == " " ];
then
echo "No filename provcided"
elif [ -d $filename ];
then
if [ ! $recursive = true ];
then
echo "Directory name provided, please provide a file"
fi
elif [ ! -f $filename ];
then
echo "File does not exist"
# elif [ "$basefile" == "safe_rm" ]
# then
# echo "Attempting to delete safe_rm"
fi
done
#################################M A I N###############################
directory
delete_file $*
#error_conditions $* #- this gives me duplicate output lines
#movefile "$#" - this gives me an unnecessary "mv: cannot stat" output line
I'm not going to do a detailed code review of your whole script, but here are a few notes.
You are looping over the arguments in the main part of your script, but then you're calling the delete function with multiple arguments. That function has no looping in it. Move the loop from main() to delete_files() (and note that I pluralized its name for clarity).
And speaking of main(), you might as well encapsulate that code (option processing, function dispatch, etc.) in a function of that name, then at the bottom of your script have a line that calls it: main "$#"
Don't use $* unless you need what it does and understand its use - instead use "$#" almost always and always quote it (with very rare exceptions)
Use indentation consistently
If your script doesn't need to be portable to shells other than Bash, then use Bash-specific features such as [[ ]] instead of [ ]
You're using both methods of naming a function at the same time (function f()). Use one or the other - parens are preferred over using function - so f () { ...; }
Use more quotes, some examples:
pwd=$(readlink -e "$filename")
mv "$filename" ~/deleted/"${filename}_$inode"
echo "${filename}_$inode:$pwd" >> ~/.restore.info
But I don't recommend using tilde (~) in scripts - use $HOME instead. And if you need to look up a user's home directory, use getent instead of other methods.

capture output using expect

I want compare the number of files on the remote server and my local directory. I ssh into the server and I was able to capture the output of "ls somewhere/*something | wc -l" using $expect_out(buffer) and store it as a variable. Now my problem is that how do I come back to my local computer and count the files here and compare them. After this comparison, I need to go back to the server and continue the job if the result of the comparison is acceptable.
The easiest thing to do -- including from a correctness perspective -- is to not try to have a single long-running SSH session, but multiple shorter-lived ones (potentially using SSH multiplexing to reuse a single transport between such sessions):
count_remote_files() {
local host=$1 dirname=$2 dirname_q
printf -v dirname_q '%q' "$dirname"
remote_files=$(ssh "$host" "bash -s ${dirname_q}" <<EOF
cd "$1" || exit
set -- *
if [ "$#" -eq 1 ] && [ ! -e "$1" ] && [ ! -L "$1" ]; then
echo "0"
else
echo "$#"
fi
EOF
)
}
count_local_files() {
local dirname=$1
cd "$dirname" || return
set -- *
if [ "$#" -eq 1 ] && [ ! -e "$1" ] && [ ! -L "$1" ]; then
echo "0"
else
echo "$#"
fi
}
if (( $(count_remote_files "$host" "$remote_dir") ==
$(count_local_files "$local_dir") )); then
echo "File count is identical"
else
echo "File count differs"
ssh "$host" 'echo "doing something else now"'
fi
Since you are using Expect, you can easily count the local files by using Tcl commands, since Expect is built on top of Tcl:
set num_local_files [llength [glob somewhere/*something]]
For more info see http://nikit.tcl.tk/page/Tcl+Tutorial+Lesson+25

Chosing "input" in an AND (&&) and OR (||) list of commands

I have to find a way to have my script read from one of these three options:
a file argument
standard input
a previously established environment variable
Here's what I currently have:
#!/bin/bash
key=$1
[ $# -ge 1 -a -f "$2" ] && input="$2" || [ -f "$INPUT" ] && input="$INPUT" || input="-"
echo $input
Only the environment variable refuses to work, the rest works fine.
I've tried using the export INPUT="pathnametofile" before but it doesn't make any difference, I end up with the shell asking me to enter info as if I called on cat.
The problem in your script
Your attemp is not working due to the way the shell processes a Lists of Commands:
‘&&’ and ‘||’ have equal precedence.
AND and OR lists are executed with left associativity.
Your sentence:
[ $# -ge 1 -a -f "$2" ] && input="$2" || [ -f "$INPUT" ] && input="$INPUT" || input="-"
does the same as follows:
[ $# -ge 1 -a -f "$2" ] && input="$2"
[ $? -eq 0 ] || [ -f "$INPUT" ]
[ $? -eq 0 ] && input="$INPUT"
[ $? -eq 0 ] || input="-"
Now yo may see why your unexpected behaviour.
A better attempt grouping commands
{ [ $# -ge 1 -a -f "$2" ] && input="$2"; } || { [ -f "$INPUT" ] && input="$INPUT"; } || input="-"
Now, due to precedence, the first group is not needed at all:
[ $# -ge 1 -a -f "$2" ] && input="$2" || { [ -f "$INPUT" ] && input="$INPUT"; } || input="-"
Furthermore, unless you have set the positional parameters by hand, you can remove the first check (after all, if $2 is emtpy, -f "" fails the same).
[ -f "$2" ] && input="$2" || { [ -f "$INPUT" ] && input="$INPUT"; } || input="-"
An alternative with the if conditional construct
if [ -f "$2" ]; then
input=$2
elif [ -f "$INPUT" ]; then
input=$INPUT
fi
echo "${input:=-}"
untested, but you'll probably have better luck with if commands, and test that the variable is not empty:
if [ $# -ge 1 -a -f "$2" ]; then
input="$2"
elif [ -n "$INPUT" -a -f "$INPUT" ]; then
input="$INPUT"
else
input="-"
fi

Test multiple file conditions in one swoop (BASH)?

Often when writing for the bash shell, one needs to test if a file (or Directory) exists (or doesn't exist) and take appropriate action. Most common amongst these test are...
-e - file exists, -f - file is a regular file (not a directory or device file), -s - file is not zero size, -d - file is a directory, -r - file has read permission, -w - file has write, or -x execute permission (for the user running the test)
This is easily confirmed as demonstrated on this user-writable directory....
#/bin/bash
if [ -f "/Library/Application Support" ]; then
echo 'YES SIR -f is fine'
else echo 'no -f for you'
fi
if [ -w "/Library/Application Support" ]; then
echo 'YES SIR -w is fine'
else echo 'no -w for you'
fi
if [ -d "/Library/Application Support" ]; then
echo 'YES SIR -d is fine'
else echo 'no -d for you'
fi
➝ no -f for you ✓
➝ YES SIR -w is fine ✓
➝ YES SIR -d is fine ✓
My question, although seemingly obvious, and unlikely to be impossible - is how to simply combine these tests, without having to perform them separately for each condition... Unfortunately...
if [ -wd "/Library/Application Support" ]
▶ -wd: unary operator expected
if [ -w | -d "/Library/Application Support" ]
▶ [: missing `]'
▶ -d: command not found
if [ -w [ -d "/Library.... ]] & if [ -w && -d "/Library.... ]
▶ [: missing `]'
➝ no -wd for you ✖
➝ no -w | -d for you ✖
➝ no [ -w [ -d .. ]] for you ✖
➝ no -w && -d for you ✖
What am I missing here?
You can use logical operators to multiple conditions, e.g. -a for AND:
MYFILE=/tmp/data.bin
if [ -f "$MYFILE" -a -r "$MYFILE" -a -w "$MYFILE" ]; then
#do stuff
fi
unset MYFILE
Of course, you need to use AND somehow as Kerrek(+1) and Ben(+1) pointed it out. You can do in in few different ways. Here is an ala-microbenchmark results for few methods:
Most portable and readable way:
$ time for i in $(seq 100000); do [ 1 = 1 ] && [ 2 = 2 ] && [ 3 = 3 ]; done
real 0m2.583s
still portable, less readable, faster:
$ time for i in $(seq 100000); do [ 1 = 1 -a 2 = 2 -a 3 = 3 ]; done
real 0m1.681s
bashism, but readable and faster
$ time for i in $(seq 100000); do [[ 1 = 1 ]] && [[ 2 = 2 ]] && [[ 3 = 3 ]]; done
real 0m1.285s
bashism, but quite readable, and fastest.
$ time for i in $(seq 100000); do [[ 1 = 1 && 2 = 2 && 3 = 3 ]]; done
real 0m0.934s
Note, that in bash, "[" is a builtin, so bash is using internal command not a symlink to /usr/bin/test exacutable. The "[[" is a bash keyword. So the slowest possible way will be:
time for i in $(seq 100000); do /usr/bin/\[ 1 = 1 ] && /usr/bin/\[ 2 = 2 ] && /usr/bin/\[ 3 = 3 ]; done
real 14m8.678s
You want -a as in -f foo -a -d foo (actually that test would be false, but you get the idea).
You were close with & you just needed && as in [ -f foo ] && [ -d foo ] although that runs multiple commands rather than one.
Here is a manual page for test which is the command that [ is a link to. Modern implementations of test have a lot more features (along with the shell-builtin version [[ which is documented in your shell's manpage).
check-file(){
while [[ ${#} -gt 0 ]]; do
case $1 in
fxrsw) [[ -f "$2" && -x "$2" && -r "$2" && -s "$2" && -w "$2" ]] || return 1 ;;
fxrs) [[ -f "$2" && -x "$2" && -r "$2" && -s "$2" ]] || return 1 ;;
fxr) [[ -f "$2" && -x "$2" && -r "$2" ]] || return 1 ;;
fr) [[ -f "$2" && -r "$2" ]] || return 1 ;;
fx) [[ -f "$2" && -x "$2" ]] || return 1 ;;
fe) [[ -f "$2" && -e "$2" ]] || return 1 ;;
hf) [[ -h "$2" && -f "$2" ]] || return 1 ;;
*) [[ -e "$1" ]] || return 1 ;;
esac
shift
done
}
check-file fxr "/path/file" && echo "is valid"
check-file hf "/path/folder/symlink" || { echo "Fatal error cant validate symlink"; exit 1; }
check-file fe "file.txt" || touch "file.txt" && ln -s "${HOME}/file.txt" "/docs/file.txt" && check-file hf "/docs/file.txt" || exit 1
if check-file fxrsw "${HOME}"; then
echo "Your home is your home from the looks of it."
else
echo "You infected your own home."
fi
Why not write a function to do it?
check_file () {
local FLAGS=$1
local PATH=$2
if [ -z "$PATH" ] ; then
if [ -z "$FLAGS" ] ; then
echo "check_file: must specify at least a path" >&2
exit 1
fi
PATH=$FLAGS
FLAGS=-e
fi
FLAGS=${FLAGS#-}
while [ -n "$FLAGS" ] ; do
local FLAG=`printf "%c" "$FLAGS"`
if [ ! -$FLAG $PATH ] ; then false; return; fi
FLAGS=${FLAGS#?}
done
true
}
Then just use it like:
for path in / /etc /etc/passwd /bin/bash
{
if check_file -dx $path ; then
echo "$path is a directory and executable"
else
echo "$path is not a directory or not executable"
fi
}
And you should get:
/ is a directory and executable
/etc is a directory and executable
/etc/passwd is not a directory or not executable
/bin/bash is not a directory or not executable
This seems to work (notice the double brackets):
#!/bin/bash
if [[ -fwd "/Library/Application Support" ]]
then
echo 'YES SIR -f -w -d are fine'
else
echo 'no -f or -w or -d for you'
fi

Resources