zsh script parser error for nested if/else - bash

I have the following humble zsh function:
function remember()
{
if [ "$1" != "" ]
then
if[ "$1" != "clean" ]
then
echo "Why";
#echo $1 >> {~/.remember_info};
else
rm -r ~/.remember_info;
touch ~/.remember_info;
fi
else
cat .remember_info;
fi
}
When I try to source it I get:
parse error near `echo' (The echo being the line with echo "Why";)
The error is quite non descriptive and I assume its related to part of the loop's logic (since no matter what instruction I give after then it error out there).
Is there any way to "debug" this kind of thing ? zsh -n doesn't help much (at all)

You forgot the space between if and [ when comparing to clean.
This is case, though, where your function can be made simpler by handling the = case first.
function remember()
{
if [ "$1" = "" ]; then
cat ~/.remember_info
elif [ "$1" = clean ]; then
rm -r ~/.remember_info
touch ~/.remember_info
else
echo "$1" >> ~/.remember_info;
fi
}
Or, use a case statement.
remember () {
f=~/.remember_info
case $1 in
"")
cat "$f"
;;
clean)
rm -r "$f"
touch "$f"
;;
*)
print "$1" >> "$f"
;;
esac
}

You are missing a whitespace after [. It should be:
function remember()
{
if [ "$1" != "" ]
then
if [ "$1" != "clean" ]
then
echo "Why";
#echo $1 >> {~/.remember_info};
else
rm -r ~/.remember_info;
touch ~/.remember_info;
fi
else
cat .remember_info;
fi
}
[ is the same as test. It is a separate command, described in man test:
TEST(1)
NAME
test - check file types and compare values
SYNOPSIS
test EXPRESSION
test
[ EXPRESSION ]
[ ]
[ OPTION

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 do delete my unnecessary/excessive lines in my output?

My script is a mimic of the rm command, long story short. Can anyone point out the errors/unnecessary lines I have in my remove script that causes my output to produce excessive/irrelevant lines? The code works as intended but it produces all these unnecessary/excessive/duplicate lines. Output below is what looks like when I try to remove 2 files in the same line and do some other simple commands. Thank you in advance. I appreciate any help.
input: sh remove file2 file4
output:
Executed
Executed
cannot remove file4: no such file or directory
stat: cannot stat 'file4': No such file or directory
mv: cannot stat 'file4': No such file or directory
Executed
mv: cannot stat 'file4': No such file or directory
File moved to recycle bin
#/bin/bash
function directory(){
if [ ! -d ~/deleted ]
then
mkdir ~/deleted
fi
if [ ! -f ~/.restore.info ]
then
touch ~/.restore.info
fi
}
function movefile(){
mv $1 ~/deleted/$1
echo "file moved to recycle bin"
}
function error_conditions(){
#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
inode=$(stat -c%i $filename)
filename=$1
#pwd=$(readlink -e$filename)
if $interactive
then
if [ $verbose = true ]; then
read -p "Are you sure you want to delete $filename?" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ]
then
mv $filename ~/delete/${filename}_inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo "$filename has been deleted"
else
echo "Nothing has been deleted"
fi
else
read -p "Are you sure you want to delete $filename?" i_input
if [ $i_input == "y" ] || [ $i_input == "Y" ];
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 "$filename has been deleted."
else
mv $filename ~/deleted/${filename}_$inode
echo ${filename}_$inode:$pwd>>~/.restore.info
echo Executed
fi
}
interactive=false
verbose=false
while getopts iv option
do
case $option in
i) interactive=true;;
v) verbose=true;;
esac
done
shift $[OPTIND-1]
for i in $*
do
filename=$i
baseline=$(basename $i)
if [ "$i" == "" ];
then
echo "No filename provided"
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 [ "$basefule" == "safe_rm" ]
then
echo "Attempting to delete safe_rm"
else
delete_file $filename
fi
done
#################################M A I N###############################
directory
error_conditions $*
delete_file $*
movefile $*
Please indent properly.
for i in $* should be for i in "$#" or simply for i.
In general, variables should be quoted
(e.g., "$1", "$i",
"$filename",
"$verbose", etc.)
$[expression] is obsolete. 
Use $((expression)).
Your main loop calls
delete_file $filename
(line 100).
Your delete_file function sets
filename=$1
(line 35),
which is somewhat redundant and therefore confusing.
You set baseline but never use it. 
You test (i.e., reference) $basefule without ever setting it. 
Are these meant to be the same variable?
The code says
if [ ! -f ~/project ]
then
echo "cannot remove $filename: no such file or directory"
︙
This is a very misleading message.
You have a big comment that says “M A I N”,
but the “main” code begins about 33 lines earlier.
The code doesfor i in $*
do
filename=$i # This is an example of terrible indenting.
︙
delete_file $filename
︙
donebut then, five lines later,delete_file $*
so you’re processing the files twice. 
So, even if delete_file succeeds the first time you call it,
the file will be gone when you call it a second time.
And, if you want to call a function (e.g., delete_file)
with all the arguments to the script,
you should use "$#" rather than $*.
And, if you’re going to call delete_file with a list of filenames,
then delete_file needs to iterate (loop) over those arguments. 
Your delete_file function only looks at $1.

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.

update a file with the content of a variable, if need be, in bash

In order to update some files only if their desired content has changed, I have been using this script:
updatefile()
{
# First parameter is file name.
# Second parameter is desired content.
local FILENAME
local DIRNAME
local FILEALREADYMATCHING
local CURRENTFILECONTENT
if [ $# -ne 2 ] ; then
return 99
fi
FILENAME=$(basename "$1")
DIRNAME=$(dirname "$FILENAME")
FILECONTENT="$2"
mkdir -p "$DIRNAME"
if [ -d "$DIRNAME" ] ; then
FILEALREADYMATCHING=true
if ! [ -f "$FILENAME" ] ; then
FILEALREADYMATCHING=false
else
CURRENTFILECONTENT="$(IFS= cat "$FILENAME")X"
CURRENTFILECONTENT=${CURRENTFILECONTENT%?}
if [ "$CURRENTFILECONTENT" != "$FILECONTENT" ] ; then
FILEALREADYMATCHING=false
fi
fi
if [ "$FILEALREADYMATCHING" != "true" ] ; then
printf '%s' "$2" > "$FILENAME"
fi
fi
}
But I found out that it carries on rewriting the file even when its current content is already matching the desired content. All the X appending and removing gymnastics did not help. Debug-printing the current and desired content shows no difference. What is wrong with the comparison I am using?
Alternatively, is there a standard way of changing a file's content without wearing off the drive? If it matters, I am using the most recent version of Bash on OpenWRT, but the needless overwriting also occurs on Debian testing, amd64.
Your code actually works perfectly fine when I tried it, however;
This may be where it's falling apart:
FILENAME=$(basename "$1") # <-- Path is REMOVED here
DIRNAME=$(dirname "$FILENAME") # <-- No point running, path is already gone
FILECONTENT="$2"
Say i have a file: /tmp/testfile
If i run basename /tmp/testfile I get: testfile
If I then run dirname testfile it doesn't magically find the original path again.
Reverse the order of argument assignment and try again:
DIRNAME=$(dirname "$1") # <-- No point running, path is already gone
FILENAME=$(basename "$1") # <-- Path is REMOVED here
FILECONTENT="$2"
Sidenote
Why don't you add some debug code to it to see where it's falling apart.
I find this sometimes help when bash -x is just a bit busy
#!/bin/bash
# First parameter is file name.
# Second parameter is desired content.
decho () {
if [ "$debug" == "1" ]; then
echo "$#"
return
fi
}
updatefile () {
local FILENAME
local DIRNAME
local FILEALREADYMATCHING
local CURRENTFILECONTENT
if [ $# -ne 2 ] ; then
decho returning #echo if debug=1
return 99
fi
FILENAME=$(basename "$1")
DIRNAME=$(dirname "$FILENAME")
FILECONTENT="$2"
mkdir -p "$DIRNAME"
if [ -d "$DIRNAME" ] ; then
FILEALREADYMATCHING=true
if ! [ -f "$FILENAME" ] ; then
FILEALREADYMATCHING=false
decho "file doesn't exist" #echo if debug=1
else
CURRENTFILECONTENT="$(IFS= cat "$FILENAME")X"
CURRENTFILECONTENT=${CURRENTFILECONTENT%?}
decho "file exists" #echo if debug=1
if [ "$CURRENTFILECONTENT" != "$FILECONTENT" ] ; then
decho "content is different" #echo if debug=1
FILEALREADYMATCHING=false
fi
fi
if [ "$FILEALREADYMATCHING" != "true" ] ; then
decho "file doesnt match, making it match now" #echo if debug=1
decho "file is $FILENAME by the way" #echo if debug=1
printf '%s' "$2" > "$FILENAME"
fi
fi
}
updatefile "$1" "$2"
So then, instead of invoking the script like this:
./script filename "Content"
invoke it like this:
debug=1 ./script filename "Content"

Bash script "Syntax Error: Unexpected end of file"

The goal is to create a simple trash utility using a Bourne shell (it's part of an assignment). I am receiving the following error: "line 17: Syntax Error: Unexpected end of file"
I have been staring at the code for a few hours now and I can't see the mistake (probably something simple I am overlooking)
#!/bin/sh
if [$# == 0] ;then
echo "Usage: trash -l | -p | { filename }*"
else
if $1 == '-l'; then
dir $HOME/.trash
else if $1=='-p'; then
rm $HOME/.trash/*
else
for i in ${} ;do
mv i $HOME/.trash
done
fi
fi
Thanks!
This is what I achieved using shellcheck:
#!/bin/sh
if [ $# -eq 0 ] ;then
echo "Usage: trash -l | -p | { filename }*"
else
if [ "$1" = '-l' ]; then
dir "$HOME"/.trash
elif "$1"=='-p'; then
rm "$HOME"/.trash/*
else
for i in ${} ;do
mv "$i" "$HOME"/.trash
done
fi

Resources