Backup the first argument on bash script - linux

I wrote a script to backup the first argument that the user input with the script:
#!/bin/bash
file=$1/$(date +"_%Y-%m-%d").tar.gz
if [ $1 -eq 0 ]
then
echo "We need first argument to backup"
else
if [ ! -e "$file" ]; then
tar -zcvf $1/$(date +"_%Y-%m-%d").tar.gz $1
else
exit
fi
fi
The result that i want from the script is
backup folder the first argument that user input
save the backup file into folder that user input with date time format.
but the script is not running when I try to input the argument. What's wrong with the script?

The backup part of your script seem to be working well, but not the part where you check that $1 is not empty.
Firstly you would need quotes around $1, to prevent that it expends to nothing. Without the quotes the shell sees it as
if [ -eq 0 ]
and throws an error.
Secondly it would be better to use the -z operator to test if the variable exists:
if [ -z "$1" ]
Now you script should work as expected

I see several problems:
As H. Gourlé pointed out, the test for whether an argument was passed is wrong. Use if [ -z "$1" ] to check for a missing/blank argument.
Also, it's almost always a good idea to wrap variable references in double-quotes, as in "$1" above. You do this in the test for whether $file exists, but not in the tar command. There are places where it's safe to leave the double-quotes off, but the rules are complicated; it's easier to just always double-quote.
In addition to checking whether $1 was passed, I'd recommend checking whether it corresponds to a directory (or possibly file) that actually exists. Use something like:
if [ -z "$1" ]; then
echo "$0: We need first argument to backup" >&2
elif [ ! -d "$1" ]; then
echo "$0: backup source $1 not found or is not a directory" >&2
BTW, note how the error messages start with $0 (the name the script was run as) and are directed to error output (the >&2 part)? These are both standard conventions for error messages.
This isn't serious, but it really bugs me: you calculate $1/$(date +"_%Y-%m-%d").tar.gz, store it in the file variable, test to see whether something by that name exists, and then calculate it again when creating the backup file. There's no reason to do that; just use the file variable again. The reason it bugs me is partly that it violates the DRY ("Don't Repeat Yourself") principle, partly that if you ever change the naming convention you have to change it consistently in two places or the script will not work, and partly because in principle it's possible that the script will run just at midnight, and the first calculation will get one day and the second will get a different day.
Speaking of naming conventions, there's a problem with how you store the backup file. If you put it in the directory that's being backed up, then the first day you'll get a .tar.gz file containing the previous contents of the directory. The second day you'll get a file containing the regular contents plus the first backup file. Thus, the second day's backup will be about twice as big. The third day's backup will contain the regular contents, plus the first two backup files, so it'll be four times as big. And the fourth day's will be eight times as big, then 16 times, then 32 times, etc.
You need to either store the backup file somewhere outside the directory being backed up, or add something like --exclude="*.tar.gz" to the arguments to tar. The disadvantage of the --exclude option is that it may exclude other .tar.gz files from the backup, so I'd really recommend the first option. And if you followed my advice about using "$file" everywhere instead of recalculating the name, you only need to make a change in one place to change where the backup goes.
One final note: run your scripts through shellcheck.net. It'll point out a lot of common errors and bad practices before you discover them the hard way.
Here's a corrected version of the script (storing the backup in the directory, and excluding .tar.gz files; again, I recommend the other option):
#!/bin/bash
file="$1/$(date +"_%Y-%m-%d").tar.gz"
if [ -z "$1" ]; then
echo "$0: We need first argument to backup" >&2
elif [ ! -d "$1" ]; then
echo "$0: backup source $1 not found or is not a directory" >&2
elif [ -e "$file" ]; then
echo "$0: A backup already exists for today" >&2
else
tar --exclude="*.tar.gz" -zcvf "$file" "$1"
fi

Related

Shell Scripting to Compress directory [duplicate]

This question already has an answer here:
Shell spacing in square brackets [duplicate]
(1 answer)
Closed 4 years ago.
$1 is file / folder that want to compressed
Output filename is the same name, plus current date and ext
if output name exist, then just give warning
Example:
./cmp.sh /home/user
It will be /home/user to /home/user_2018-03-11.tar.bz2
i already have lead, but i'm stuck
#!/bin/bash
if ["$1" == ""]; then
echo "Help : To compress file use argument with directory"
exit 0
fi
if [[ -f "$1" || -d "$1" ]]; then
tar -cvjSf $1"_"$(date '+%d-%m-%y').tar.bz2 $1
fi
but the output is _22-04-2018.tar.bz2
I see that you're using quotes to avoid the problem the underscore getting used as part of the variable name. So while $1 is a positional paramater, $1_ is a variable that you have not set in your script. You can avoid this issue by using curly braces, like ${1}. Anything inside the braces is part of the variable name, so ${1}_ works. This notation would be preferable to $1"_" which leaves a user-provided variable outside of quotes. (Of course, "$1"_ would do the job as wel.)
Also, it's probably safer to set the filename in a variable, then use that for all your needs:
#!/bin/bash
if [ -z "$1" ]; then
echo "Help : To compress file use argument with directory"
exit 0
fi
filename="${1}_$(date '+%F').tar.bz2"
if [ -e "$filename" ]; then
echo "WARNING: file exists: $filename" >&2
else
tar -cvjSf "$filename" "$#"
fi
Changes:
you need spaces around your square brackets in an if condition,
while you can test for equivalence to a null string, -z is cleaner, though you could also test for [ $# -eq 0 ], counting the parameters provided,
using $filename makes sure that your test and your tar will always use the same name, even if the script runs over midnight, and is way more readable,
variables should always be quoted.
Also, are you sure about the -S option for tar? On my system, that option extracts sparse files, and is only useful in conjunction with -x.
ALSO, I should note that as I've rewritten it, there's nothing in this script which is specific to bash, and it should be portable to POSIX shells as well (ash/dash/etc). Bash is great, but it's not universal, and if through your learning journey you can learn both, it will give you useful skills across multiple operating systems and environments.
Use -z switch to check if blank
#!/bin/bash
if [[ -z "$1" ]]; then
echo "Help : To compress file use argument with directory"
exit 0
fi
if [[ -f "$1" || -d "$1" ]]; then
tar -cvjSf $1"_"$(date '+%d-%m-%y').tar.bz2 $1
fi

why if expression is always true in bash script

I'm very new in shell script and I wrote this code to copy an input file from directory new1 to directory new2 if the file doesn't exist in second directory.
the problem is that the first if expression is always true and the code always print "file copied successfully" even if the file exists in second directory.
here is my code:
while true; do
echo "enter a file name from directory new1 to copy it to directory new2 "
echo "or enter ctrl+c to exit: "
read input
i=0
cd ~/new2
if [ -f ~/new1/$input ]; then
i=1
fi
if [ $i -eq 0 ];then
cp ~/new1/$input ~/new2/
echo "####" $input "copied successfully ####"
else
echo "#### this file exist ####"
fi
done
I will be appreciated if any one tell me how to fix this problem
You are comparing the wrong file. In addition, you probably want to refactor your logic. There is no need to keep a separate variable to remember what you just did.
while true; do
echo "enter a file name from directory new1 to copy it to directory new2 "
echo "or enter ctrl+c to exit: "
read input
#i=0 # no use
#cd ~/new2 # definitely no use
if [ -f ~/new2/"$input" ]; then # fix s/new1/new2/
# diagnostics to stderr; prefix messages with script's name
echo "$0: file ~/new2/$input already exists" >&2
else
cp ~/new1/"$input" ~/new2/
echo "$0: ~/new1/$input copied to ~/new2 successfully" >&2
fi
done
Take care to make your diagnostic messages specific enough to be useful. Too many beginner scripts tell you "file not found" 23 times but you don't know which of the 50 files you tried to access were not found. Similarly, including the name of the script or tool which produces a diagnostic in the diagnostic message helps identify the culprit and facilitate debugging as you start to build scripts which call scripts which call scripts ...
As you learn to use the command line, you will find that scripts which require interactive input are a dog to use because they don't offer command history, tab completion of file names, and other niceties which are trivially available to any tool which accepts a command-line file argument.
cp -u already does what this script attempts to implement, so the script isn't particularly useful per se.
Note also that ~ is a Bash-only feature which does not work with sh. Your script otherwise seems to be compatible with POSIX sh and could actually benefit from some Bash extensions such as [[ if you are going to use Bash features anyway.

Bash script to iterate contents of directory moving only the files not currently open by other process

I have people uploading files to a directory on my Ubuntu Server.
I need to move those files to the final location (another directory) only when I know these files are fully uploaded.
Here's my script so far:
#!/bin/bash
cd /var/uploaded_by_users
for filename in *; do
lsof $filename
if [ -z $? ]; then
# file has been closed, move it
else
echo "*** File is open. Skipping..."
fi
done
cd -
However it's not working as it says some files are open when that's not true. I supposed $? would have 0 if the file was closed and 1 if it wasn't but I think that's wrong.
I'm not linux expert so I'm looking to know how to implement this simple script that will run on a cron job every 1 minute.
[ -z $? ] checks if $? is of zero length or not. Since $? will never be a null string, your check will always fail and result in else part being executed.
You need to test for numeric zero, as below:
lsof "$filename" >/dev/null; lsof_status=$?
if [ "$lsof_status" -eq 0 ]; then
# file is open, skipping
else
# move it
fi
Or more simply (as Benjamin pointed out):
if lsof "$filename" >/dev/null; then
# file is open, skip
else
# move it
fi
Using negation, we can shorten the if statement (as dimo414 pointed out):
if ! lsof "$filename" >/dev/null; then
# move it
fi
You can shorten it even further, using &&:
for filename in *; do
lsof "$filename" >/dev/null && continue # skip if the file is open
# move the file
done
You may not need to worry about when the write is complete, if you are moving the file to a different location in the same file system. As long as the client is using the same file descriptor to write to the file, you can simply create a new hard link for the upload file, then remove the original link. The client's file descriptor won't be affected by one of the links being removed.
cd /var/uploaded_by_users
for f in *; do
ln "$f" /somewhere/else/"$f"
rm "$f"
done

linux device which don't allow to write on it

I have a script which write some warnings to separate file (it's name is passed as an argument). I want to make this script fail if there is a warning.
So, I need to pass some file name which must raise an error if someone try to write there.
I want to use some more or less idiomatic name (to include it to man page of my script).
So, let say my script is
# myScript.sh
echo "Hello" > $1
If I call it with
./myScript.sh /dev/stdin
it is not fail because /dev/stdin is not read-only device (surprisingly!)
After
./myScript.sh /
it is failed, as I want it (because / is a directory, you can't write there). But is is not idiomatic.
Is there some pretty way to do it?
if [ -w "$1" ]
then
echo "$Hello" > "$1" # Mind the double-quotes
fi
is what you're looking for. Below would even be better in case you've only
one argument.
if [ -w "$*" ]
then
echo "$Hello" > "$*" # Mind the double-quotes
fi
$* is used to accommodate nonstandard file names. "$*" combines multiple arguments into a single word. Check [ this ].

Is the directory NOT writable

Can anyone tell me why this is always saying that the directory is not writable, when it absolutely is?
$dnam="/home/bryan/renametest/C D"
# Is the directory writable
err=0
if [ ! -w $dnam ]
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
You need double-quotes around $dnam -- without them, it's interpreted as two separate shell words, "/home/bryan/renametest/C" and "D", which makes an invalid test expression and hence fails. This should work:
if [ ! -w "$dnam" ]
#tink's suggestion of [[ ]] is a cleaner way of doing tests like this, but is only available in bash (and some other shells with extended syntax). The fact that you get [[: not found means you're using a fairly basic shell, not bash.
I see multiple problems:
You are using a space inside your variable. This is not illegal, but in combination line you use the variable unescaped and generate the following command:
if [ ! -w /home/bryan/renametest/C D ]
This is not a valid syntax. The simplest way to fix this is changing the line to
if [ ! -w "$dnam" ]
The next problem is worse: On my system, help test returns the text:
-w FILE True if the file is writable by you.
Which means, the command doesn't support directories but only files. If you want to check if a directory is writable, you will have to use a different command
As everyone else said, the $dnam variable needs double quotes. Here's why:
The [ ... ] is an alias to the test command. If you look in your system, you will see a file called /bin/[ or maybe /bin/usr/[. On some systems, this is a hard link to /bin/test or /bin/usr/test. The if statement executes what comes after the if, and if that command returns a zero exit status, the if statement will execute the then clause. Otherwise, if there is an else clause, that will execute instead.
To allow for boolean testing, Unix included the test command, so you could do this:
if test -d "$directory"
then
echo "Directory $directory exists!"
fi
Later on, the /bin/[ was added as syntactic sugar. This is identical to the above:
if [ -d "$directory" ]
then
echo "Directory $directory exists!"
fi
Now, both [ and test are builtin commands, but they are *still commands. This means that the shell interpolates the command and then executes it.
Try executing the following:
$ set -xv # Turns on shell debugging
$ dnam="/home/bryan/renametest/C D"
dnam="/home/bryan/renametest/C D"
+ dnam='/home/bryan/renametest/C D'
$ test -d $dnam
test -d $dnam
+ test -d /home/bryan/renametest/C D
$ echo $?
echo $?
+ echo 1
1
$ test -d "$dnam" # Now with quotes
test -d $dnam
+ test -d "/home/bryan/renametest/C D"
$ echo $?
echo $?
+ echo 0
0
$ set +xv # Turn off the debuggin
Each command is echoed twice. The first time as written, and the second time after the line is interpolated. As part of the interpolation, the shell splits parameters on white space. As you can see, the test command is testing the presence of /home/bryan/renamtest/C which doesn't exist and thus not writable. I'm actually surprised that the test command didn't print an error message because you passed it an extra parameter.
In the second attempt, you added quotes. These quotes prevented the shell from splitting your parameters on the space and keep the directory name as a single parameter.
Since [ ... ] is a command, you have to take into account the shell's interpolation of variables and other issues. And, if you're not absolutely careful, you can end up with errors.
Even worse, sometimes the [ ... ] might work and sometimes it might not. If your directory name didn't contain spaces, it will work as expected. Imagine you're writing a program, and you test it and everything works because all directories you've tried don't have spaces. Then, someone uses your program, but has a space in the directory. A substantial number of shell script bugs are do to this type of issue in if statements.
This is why Bash introduced the [[ ... ]] tests. The [[ isn't a command but a statement. This means that the shell doesn't directly interpolate the results. Instead, the parameters are parsed, and then any interpolation is done. Thus, this would have worked:
dnam="/home/bryan/renametest/C D" # No "$" in front of the variable!
# Is the directory writable
if [[ ! -w $dnam ]] # No quotation marks needed!
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
It's almost always better to use the [[ ... ]] test rather than the [ ... ] test, so go ahead and get into the habit.
One more minor error, you had:
$dnam="/home/bryan/renametest/C D"
This gets interpolated by the shell, so the variable being set is whatever the value of $dnam just happens to be. If $dnam happened to equal "foo", you would been doing this:
foo="/home/bryan/renametest/C D"
Not what you want.
You want to leave the $ off when you set variables:
dnam="/home/bryan/renametest/C D"

Resources