Check if file has been modified - bash

How can I validate that this replace command succeeded:
perl -pi -e 's/contoso/'"$hostname"'/g' /etc/inet/hosts
I have tried checking the return value:
if [ $? -eq 0 ]; then
echo "OK"
else
echo "Error."
fi
But the return value is not being set when the command fails.
Thanks

One option is to check, if file has been modified. You can achieve with adding extension of backup file to -i option:
perl -pi.orig -e 's/contoso/'"$hostname"'/g' /etc/inet/hosts
This command will store original content of /etc/inet/hosts into /etc/inet/hosts.orig. Then run the specified command. Then you can check if the files are different with, for example cmp command:
if ! cmp -s foo.txt foo.txt.orig; then
echo OK
else
echo ERROR
fi
Remove the .orig file after that.
The other option is to modify the script to read the content of the file, replace required entry, check is change actually happened and return proper status at the end to verify in the shell using $?. You have been given solution in this answer.

I don't know Perl, but you can manage multiple case of "error" (no match/no way to write in file) with a little Bash script like that :
#!/bin/bash
FILE="/etc/inet/hosts"
SEARCH="contoso"
REPLACE="$hostname"
NB=$(grep -c $SEARCH $FILE)
if [ $NB -ne 0 ]; then
perl -pi -e 's/${SEARCH}/'${REPLACE}'/g' "$FILE" && echo "${NB} replaced" || echo "Error (permission maybe)"
else
echo "No match in file"
fi
I think there is a better way by improving the Perl code or by using the sed command. But it should works.

If you expect your perl script to return a value that has some meaning, you will need to write your perl script to return a meaningful value. In your case, perhaps something as simple as:
perl -p -e 's/contoso/'"$hostname"'/g; $rv=1 if $&; END{ exit !$rv }'

Generally checksums is a very efficient way to detect changes in files.
md5sum [filename]
root#miaoulis:~# echo 'line 1' >>1.txt
root#miaoulis:~# md5sum 1.txt
5c2ce561e1e263695dbd267271b86fb8 1.txt
root#miaoulis:~# echo 'line 2' >>1.txt
root#miaoulis:~# md5sum 1.txt
c7253b64411b3aa485924efce6494bb5 1.txt
I guess the sum could be extracted from the output with AWK
root#miaoulis:~# echo $(md5sum 1.txt) | awk 'BEGIN{FS=" *"}{print "MD5:",$1}'
MD5: c7253b64411b3aa485924efce6494bb5
root#miaoulis:~# echo $(md5sum 1.txt) | awk 'BEGIN{FS=" *"}{print "filename:",$2}'
filename: 1.txt
FS=" *" instructs AWK to split the string on the occurrence of one or more spaces. $1 will be the MD5, $2 will be the filename.
MD5 checksum works fast for any size of file. The downside is that you don't really detect what exactly changed in the file, only the fact that it has changed. Should be good enough for most scenarios.

Related

In a bash pipe, take the output of the previous command as a variable to the next command (Eg. if statement)

I wanted to write a command to compare the hash of a file. I wrote the below single line command. Wanted to understand as to how I can take the output of the previous command as a variable for the current command, in a pipe.
Eg. below command I wanted to compare the output of 1st command "Calculated hash" to the original hash. In the last command, I wanted to refer to the output of the previous command. How do I do that in the if statement? (Instead of $0)
sha256sum abc.txt | awk '{print $1}' | if [ "$0" = "8237491082roieuwr0r9812734iur" ]; then
echo "match"
fi
Following your narrow request looks like:
sha256sum abc.txt |
awk '{print $1}' |
if [ "$(cat)" = "8237491082roieuwr0r9812734iur" ]; then echo "match"; fi
...as cat with no arguments reads the command's stdin, and in a pipeline, content generated from prior stages are streamed into their successors.
Alternately:
sha256sum abc.txt |
awk '{print $1}' |
if read -r line && [ "$line" = "8237491082roieuwr0r9812734iur" ]; then echo "match"; fi
...wherein we read only a single line from stdin instead of using cat. (To instead loop over all lines given on stdin, see BashFAQ #1).
However, I would strongly suggest writing this instead as:
if [ "$(sha256sum abc.txt | awk '{print $1}')" = "8237491082roieuwr0r9812734iur" ]; then
echo "match"
fi
...which, among other things, keeps your logic outside the pipeline, so your if statement can set variables that remain set after the pipeline exits. See BashFAQ #24 for more details on the problems inherent in running code in pipelines.
Consider using sha256sum's check mode. If you save the output of sha256sum to a file, you can check it with sha256sum -c.
$ echo foo > file
$ sha256sum file > hash.txt
$ cat hash.txt
b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c file
$ sha256sum -c hash.txt
file: OK
$ if sha256sum -c --quiet hash.txt; then echo "match"; fi
If you don't want to save the hashes to a file you could pass them in via a here-string:
if sha256sum -c --quiet <<< 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c file'; then
echo "match"
fi

Why am I getting this file error and a cat error even though I am passing the text file in the argument?

The program calls for us to read in the directory full of text files, parse data from those files into their respective attributes.
Then once the data is set, load a general template which has those attributes in the text.
I'm using a sed command to replace the specific attributes, only if the number of students is greater than 50. If so it runs the sed command and writes to a file, and into a directory.
But i am getting this error when I'm passing
test3.sh ./data assign4.template 12/16/2021 ./output
Error
cat: assign4.template: No such file or directory
test3.sh: line 62: output/MAT3103.crs: No such file or directory
The current file is MAT4353.crs
Now what I am thinking is that, for the file or directory error, it is looking in that folder and searching for a file named that
But Not entirely sure how to resolve that.
As for the cat: template error, I don't get that since I am passing the template in the terminal
As for the other paramaters being passed, the Date which is also substituted in the sed command, All output files should be written to the directory defined by the last argument. This directory may or may not already exist. Each file should be named by the course’s department code and number,and with the extension.warn
Here is the total code
#!/bin/bash
# checking if user has passed atleast four arguments are passed
if [ $# -ne 4 ]
then
echo "Atleast 4 argument should be passed"
exit 1
fi
# if output directory exits check
if [ -d output ]
then
# if output directory exists will get deleted
echo "output directory already exists. So removing its contents"
rm -f output/*
else
# output directory does not exist, so gets created here
echo "output directory does not exist. So creating a new directory"
mkdir output
fi
max_students=50
template=$2
dt=$3
cd $1
for i in *; do
echo The current file is ${i}
dept_code=$(awk 'NR==2
{print $1 ; exit}' $i)
echo $dept_code
dept_name=$(awk 'NR==2
{print $2 ; exit}' $i)
echo $dept_name
course_name=$(awk 'FNR==2' $i)
echo $course_name
course_sched=$(awk 'FNR==3' $i | awk '{print $1}')
course_sched=$(awk 'FNR==3' $i | awk '{print $1}')
echo $course_sched
course_start=$(awk 'FNR==3' $i | awk '{print $2}')
echo $course_start
course_end=$(awk 'FNR==3' $i | awk '{print $3}')
echo $course_end
credit_hours=$(awk 'FNR==4' $i)
echo $credit_hours
num_students=$(awk 'FNR==5' $i)
echo $num_students
# checking if number of students currently enrolled > max students
if (( $(echo "$num_students > $max_students" |bc -l) ))
then
# output filename creation
out_file=${i}
# using example Template and sed command to replace the variables
cat $template | sed -e "s/\[\\[\dept_code\]\]/$dept_code/" | sed -e "s/\[\\[\dept_name\]\]/$dept_name/" | sed -e "s|\[\[course_name\]\]|$course_name|" | sed -e "s|\[\[course_start\]\]|$$
fi
done
You define the variable as
template=$2
and since your second parameter is assign4.template, this is what the variable template is set to. Then you do a
cat $template
which is, first of all, unnecessary, since you can do an input redirection on sed instead, but most of all requires, that the file exists in your working directory. Since you have done before a
cd $1
it means that the file data/assign4.template does not exist. You have to create this file before you can use your script.
use single quotes in your positional arguments.
test3.sh './data' 'assign4.template' '12/16/2021' './output'
or
test3.sh data assign4.template '12/16/2021' output

Extract a line from a text file using grep?

I have a textfile called log.txt, and it logs the file name and the path it was gotten from. so something like this
2.txt
/home/test/etc/2.txt
basically the file name and its previous location. I want to use grep to grab the file directory save it as a variable and move the file back to its original location.
for var in "$#"
do
if grep "$var" log.txt
then
# code if found
else
# code if not found
fi
this just prints out to the console the 2.txt and its directory since the directory has 2.txt in it.
thanks.
Maybe flip the logic to make it more efficient?
f=''
while read prev
do case "$prev" in
*/*) f="${prev##*/}"; continue;; # remember the name
*) [[ -e "$f" ]] && mv "$f" "$prev";;
done < log.txt
That walks through all the files in the log and if they exist locally, move them back. Should be functionally the same without a grep per file.
If the name is always the same then why save it in the log at all?
If it is, then
while read prev
do f="${prev##*/}" # strip the path info
[[ -e "$f" ]] && mv "$f" "$prev"
done < <( grep / log.txt )
Having the file names on the same line would significantly simplify your script. But maybe try something like
# Convert from command-line arguments to lines
printf '%s\n' "$#" |
# Pair up with entries in file
awk 'NR==FNR { f[$0]; next }
FNR%2 { if ($0 in f) p=$0; else p=""; next }
p { print "mv \"" p "\" \"" $0 "\"" }' - log.txt |
sh
Test it by replacing sh with cat and see what you get. If it looks correct, switch back.
Briefly, something similar could perhaps be pulled off with printf '%s\n' "$#" | grep -A 1 -Fxf - log.txt but you end up having to parse the output to pair up the output lines anyway.
Another solution:
for f in `grep -v "/" log.txt`; do
grep "/$f" log.txt | xargs -I{} cp $f {}
done
grep -q (for "quiet") stops the output

Deleting a PARTICULAR word from a file using shell script

Hi I am having a problem in deleting a particular set of words from a file using Shell script. Here goes my problem,
My file: group.dat
Sample lines
ADMIN
ADMINISTRATION
ADMINISTRATOR
My Script: groupdel.sh
#!/bin/sh
groupf="<pathtofile>/group.dat"
tmp="<pathtofile>/te"
delgrp()
{
echo "Enter the group to be deleted"
read gname
echo "-------------------"
for gn in `cat $groupf`
do
if [ "$gname" = "$gn" ]
then
sed -e "s/$gname//g" $groupf > $tmp&&mv $tmp $groupf
echo "deleted group"
cat $groupf
exit 1
fi
done
}
echo "Welcome to Group delete wizard"
delgrp
Output:
Enter the group to be deleted
ADMIN
deleted group
ISTRATION
ISTRATOR
Problem: My problem is I dont want the script to delete ADMINISTRATION or ADMINISTRATOR but to delete only ADMIN, any help how to achieve it.
Thanks in Advance
#!/bin/sh
groupf="<pathtofile>/group.dat"
tmp="<pathtofile>/te"
delgrp()
{
echo "Enter the group to be deleted"
read gname
echo "-------------------"
sed -e "/^$gname[[:blank:]]/d" "$groupf" > "$tmp" && mv "$tmp" "$groupf"
echo "deleted group $gname"
cat "$groupf"
return 0
}
echo "Welcome to Group delete wizard"
delgrp
Assuming that the group name is at the beginning of the line and there are other things on the line and you want to delete the whole line, use the regular expression and command as shown.
There's no need for a loop since sed will iterate over the lines of the file for free.
You should return from a function rather than exit from it. Zero means success. One indicates an error or failure.
Always quote variable names that contain filenames.
If the file is one group per line, and the group name is the only thing on the line, use anchors in your regular expression:
s/^$gname:.*//g
If you have Perl installed, you can probably simplify this a bit with something like this:
if grep -q "^${gname}:" $groupf ; then
perl -ni -e "print unless /^${gname}:/" $groupf
echo "Group deleted."
else
echo "No such group $gname."
fi
Or even
grep -v "^${gname}:" $groupf > $tmp && \
cp -f $tmp $groupf && rm -f $tmp
which will copy all lines except the matching one to the temporary file, and then copy the tempfile over the original file, replacing it.
Note that I suggest using a cp rather than a mv in order to retain the permissions of the original file; mv will result in the edited file having permissions set according to your umask with no concern for the original permissions.
So, for the complete script:
#!/bin/sh
groupf="<pathtofile>/group.dat"
tmp="<pathtofile>/te"
delgrp()
{
echo -n "Enter the group to be deleted: "
read gname
echo "-------------------"
if grep -q "^${gname}:" $groupf ; then
grep -v "^${gname}:" $groupf > $tmp
cp -f $tmp $groupf
rm -f $tmp
else
echo "No such group '$gname'"
fi
}
echo "Welcome to Group delete wizard"
delgrp
That should work reliably.
You can use \W to denote the start and end of a word, if they are separated properly:
sed -e "s/\(\W\)$gname\(\W\)/\1\2/g" $groupf > $tmp&&mv $tmp $groupf
Awk is a readable alternative to sed:
awk -v to_delete="$gname" -F: '$1 == to_delete {next} {print}'
Why you don't use sed ?
sed 's/^word$//g'
Also you can use regex to specify multiple words
sed 's/word1|word2//g'
I didn't try this, but this is what you need. Just take a look on Internet on the sed syntax.
Regards

How to search an expression in a file from a bash script?

I have a bash script.
I need to look if "text" exists in the file and do something if it exists.
If you need to execute a command on all files containing the text, you can combine grep with xargs. For example, this would remove all files containing "yourtext":
grep -l "yourtext" * | xargs rm
To search a single file, use if grep ...
if grep -q "yourtext" yourfile ; then
# Found
fi
Something like the following would do what you need.
grep -w "text" file > /dev/null
if [ $? -eq 0 ]; then
#Do something
else
#Do something else
fi
grep is your friend here
You can put the grep inside the if statement, and you can use the -q flag to silence it.
if grep -q "text" file; then
:
else
:
fi
cat <file> | grep <"text"> and check the return code with test $?
Check out the excellent:
Advanced Bash-Scripting Guide
just use the shell
while read -r line
do
case "$line" in
*text* )
echo "do something here"
;;
* ) echo "text not found"
esac
done <"file"

Resources