my script deletes user accounts that have not been logged in for more than 30 days, in fact it works, but it deletes some system accounts - bash

It works, but also deletes some system accounts...i don't know why
#!/bin/bash
# This script takes everyone with id>1000 from /etc/passwd and removes every user account in case if it hasn't been used for the last 30 days.
# Make sure that script is being executed with root priviligies.
if [[ "${UID}" -ne 0 ]]
then
echo "You should run this script as a root!"
exit 1
fi
# First of all we need to know id limit (min & max)
USER_MIN=$(grep "^UID_MIN" /etc/login.defs)
USER_MAX=$(grep "^UID_MAX" /etc/login.defs)
# Print all users accounts with id>=1000 and <=6000 (default).
awk -F':' -v "min=${USER_MIN##UID_MIN}" -v "max=${USER_MAX##UID_MAX}" ' { if ( $3 >= min && $3 <= max ) print $0}' /etc/passwd
# This function deletes users which hasn't log in in the last 30 days
# Make a color output message
for accounts in ` lastlog -b 30 | sed "1d" | awk ' { print $1 } '`
do
userdel $accounts 2>/dev/null
done
echo -e "\e[36mYou have successfully deleted all user's account which nobody logged in in the past 30 days.\e[0,"
exit 0

You do not apply the awk -F':' -v "min=${USER_MIN##UID_MIN}" -v "max=${USER_MAX##UID_MAX}" ' { if ( $3 >= min && $3 <= max ) print $0}' /etc/passwd filter to lastlog output....
Join the list from lastlog with the list of users from /etc/passwd and filter UID with proper number range.
lastlog -b30 | sed 1d | awk '{print $1}' |
sort | join -t: -11 -21 -o2.1,2.3 - <(sort /etc/passwd) |
awk -F: -v "min=${USER_MIN##UID_MIN}" -v "max=${USER_MAX##UID_MAX}" '$2 >= min && $2 <= max {print $1}' |
xargs -n1 echo userdel
Notes:
Do not use backticks `. Use $(...) instead. Obsolete and deprecated syntax
how to read a stream line by line in bash bashfaq

With bash and GNU grep:
#!/bin/bash
# I left out the check for root user
# get only the numbers from each line
# \K removes the matching part before \K (from left to right)
USER_MIN=$(grep -Po "^UID_MIN *\K.*" /etc/login.defs)
USER_MAX=$(grep -Po "^UID_MAX *\K.*" /etc/login.defs)
# create an associative array, see: help declare
declare -A users
# read all users with its uid to this associative array
while IFS=":" read -r user x uid x; do users[$user]="$uid"; done </etc/passwd
# see output of: declare -p users
# remove all unwanted lines including headline. NR is line number
lastlog -b 30 | awk '! /Never logged in/ && NR>1 {print $1}' |
while read -r user; do
# -ge: greater-than-or-equal
# -le: less-than-or-equal
if [[ ${users[$user]} -ge $USER_MIN ]] && [[ ${users[$user]} -le $USER_MAX ]]; then
echo "delete user $user with uid ${users[$user]}"
# add your code here to delete user $user
fi
done

Related

How to Assign User-Account Name and Directory to Array in Bash/sh?

I am attempting to create a bash script for the STIG test with the vulnerability ID V-72017 on a Red Hat Enterprise Linux (RHEL) system. I am tasked with making sure all user permissions have the octal value of 0750 or less
I have the ability to gather the permission octal value a user by using
stat -c "%a" /home/$username
I am trying to create a $username (or directory) array by utilizing the command (outputs name of each user on the system):
eval getent passwd {$(awk '/^UID_MIN/ {print $2}' /etc/login.defs)..$(awk '/^UID_MAX/ {print $2}' /etc/login.defs)} | cut -d: -f1
I plan to map this output to an array, possibly a while loop. Is this a possible solution?
Syntax Error from the following:
(eval getent passwd {$(awk '/^UID_MIN/ {print $2}' /etc/login.defs)..$(awk '/^UID_MAX/ {print $2}' /etc/login.defs)} | cut -d: -f1) | while read -r line
do
myarray+=line
stat -c "%a" /home/$line
done
Desired Output Case 1:
Users:
rob
bob
Exit Fail: bob has permission octal value 0755.
Desired Output Case 2:
Users:
rob
bob
Exit Pass: All users have permission octal value of 0750 or less.
You have found all login users. Regexp can be used to check home dir's permissions.
echo "Users: "
(eval getent passwd {$(awk '/^UID_MIN/ {print $2}' /etc/login.defs)..$(awk '/^UID_MAX/ {print $2}' /etc/login.defs)} | cut -d: -f1) | while read -r line
do
echo $line
perm=$(stat -c "%a" /home/$line)
[[ "$perm" =~ [0-7][0,1,4,5][0] ]] || echo "Exit fail: $line has permission octal value $perm"
done
Maybe you want to adjust the output form.
It is suggested to avoid using eval as much as possible. All the more
if you are investigating the system security status. Please try the
following instead:
#!/bin/bash
perm=0750 # system policy
uid_min=$(sed -n '/^UID_MIN/ s/[^0-9]*\([0-9]\+\).*/\1/p' "/etc/login.defs")
uid_max=$(sed -n '/^UID_MAX/ s/[^0-9]*\([0-9]\+\).*/\1/p' "/etc/login.defs")
# read /etc/passwd and process line by line
while IFS=: read -ra a; do
# now ${a[0]} holds username and ${a[2]} holds uid
if (( ${a[2]} >= uid_min && ${a[2]} <= uid_max )); then
# narrow down the users whose uid is within the range
users+=("${a[0]}")
# check the user's permission
userperm="0$(stat -c "%a" "/home/${a[0]}")"
if (( (~ perm) & userperm )); then
# the user's permission exceeds the limitation $perm
fail+=("$(printf "%s has permission octal value 0%o." "${a[0]}" "$userperm")")
fi
fi
done < "/etc/passwd"
echo "Users:"
for i in "${users[#]}"; do
echo "$i"
done
if (( ${#fail[#]} == 0 )); then
printf "Exit Pass: All users have permission octal value of 0%o or less.\n" "$perm"
else
for i in "${fail[#]}"; do
printf "Exit Fail: %s\n" "$i"
done
fi
Hope this helps.

UNIX average of specific employee as per designation

This is an example of a text file to be given as input
Name,Designation,Salary
Hari,Engineer,35000
Suresh,Consultant,80000
Umesh,Engineer,45500
Maya,Analyst,50000
Guru,Consultant,100000
Sushma,Engineer,30000
Mohan,Engineer,30000
My code should be able to run find the average salary of particular employee's designation. For example,
bash script.sh employees.txt Analyst
Then my output should be
50000
My current code to find just the average of all employees doesn't work. I am new to shell. This is my current code
count="$(tail -n 1 salary.txt | grep -o '^[^\s]\+')"
echo "$count"
salary="$(grep -o '[^ ]\+$' salary.txt | paste -sd+)"
echo "$salary"
echo "($salary)/$count" | bc
I get empty values as results.
This is better done in awk:
awk -F, -v dgn='Engineer' '$2 == dgn{s += $3; ++c} END{printf "%.2f\n", s/c}' file.csv
35125.00
Could you please try following(since OP requested for script way, so adding it in a script way where passing 1st argument as Input_file name and 2nd argument as string whose avg is needed).
cat script.ksh
file="$1"
name="$2"
awk -F, -v field="$name" '{a[$2]+=$3;b[$2]++} END{for(i in a){if(i == field){print a[i]/b[i]}}}' "$file"
Now run the script as follwos.
./script.ksh Input_file Analyst
50000
GNU datamash is a useful tool for calculating this kind of thing:
$ datamash -sHt, groupby 2 mean 3 < employees.txt
Combine with grep to limit it to just the title you're interested in.
If you want to do this in the shell:
#!/bin/bash
file=$1
designation=$2
# code to validate user input here ...
sum=0
count=0
while IFS=, read -r n d s; do
if [[ ${designation,,} == "${d,,}" ]]; then
(( sum += s ))
(( count++ ))
fi
done < "$file"
if (( count == 0 )); then
echo "No $designation found in $file"
else
echo $((sum / count))
fi
Using Perl
perl -F, -lane ' if(/Engineer/) { $dsg+=$F[2];$c++ } END { print $dsg/$c } ' file
with your given inputs
$ cat john.txt
Name,Designation,Salary
Hari,Engineer,35000
Suresh,Consultant,80000
Umesh,Engineer,45500
Maya,Analyst,50000
Guru,Consultant,100000
Sushma,Engineer,30000
Mohan,Engineer,30000
$ perl -F, -lane ' if(/Engineer/) { $dsg+=$F[2];$c++ } END { print $dsg/$c } ' john.txt
35125
$

Shell Script : Assign the outputs to different variables

In a shell script I need to assign the output of few values to different varialbes, need help please.
cat file1.txt
uid: user1
cn: User One
employeenumber: 1234567
absJobAction: HIRED
I need to assign the value of each attribute to different variables so that I can call them them in script. For example uid should be assigned to a new variable name current_uid and when $current_uid is called it should give user1 and so forth for all other attributes.
And if the output does not contain any of the attributes then that attribute value should be considered as "NULL". Example if the output does not have absJobAction then the value of $absJobAction should be "NULL"
This is what I did with my array
#!/bin/bash
IFS=$'\n'
array=($(cat /tmp/file1.txt | egrep -i '^uid:|^cn:|^employeenumber|^absJobAction'))
current_uid=`echo ${array[0]} | grep -w uid | awk -F ': ' '{print $2}'`
current_cn=`echo ${array[1]} | grep -w cn | awk -F ': ' '{print $2}'`
current_employeenumber=`echo ${array[2]} | grep -w employeenumber | awk -F ': ' '{print $2}'`
current_absJobAction=`echo ${array[3]} | grep -w absJobAction | awk -F ': ' '{print $2}'`
echo $current_uid
echo $current_cn
echo $current_employeenumber
echo $current_absJobAction
Output from sh /tmp/testscript.sh follows:
user1
User One
1234567
HIRED
#!/usr/bin/env bash
# assuming bash 4.0 or newer: create an associative array
declare -A vars=( )
while IFS= read -r line; do ## See http://mywiki.wooledge.org/BashFAQ/001
if [[ $line = *": "* ]]; then ## skip lines not containing ": "
key=${line%%": "*} ## strip everything after ": " for key
value=${line#*": "} ## strip everything before ": " for value
vars[$key]=$value
else
printf 'Skipping unrecognized line: <%s>\n' "$line" >&2
fi
done <file1.txt # or < <(ldapsearch ...)
# print all variables read, just to demonstrate
declare -p vars >&2
# extract and print a single variable by name
echo "Variable uid has value ${vars[uid]}"
Note that this must be run with bash yourscript, not sh yourscript.
By the way -- if you don't have bash 4.0, you might consider a different approach:
while IFS= read -r line; do
if [[ $line = *": "* ]]; then
key=${line%%": "*}
value=${line#*": "}
printf -v "ldap_$key" %s "$value"
fi
done <file1.txt # or < <(ldapsearch ...)
will create separate variables of the form "$ldap_cn" or "$ldap_uid", as opposed to putting everything in a single associative array.
Here's a simple example of what you are trying to do that should get you started. It assumes 1 set of data in the file. Although a tad brute-force, I believe its easy to understand.
Given a file called file.txt in the current directory with the following contents (absJobAction intentionally left out):
$ cat file1.txt
uid: user1
cn: User One
employeenumber: 1234567
$
This script gets each value into a local variable and prints it out:
# Use /bin/bash to run this script
#!/bin/bash
# Make SOURCEFILE a readonly variable. Make it uppercase to show its a constant. This is the file the LDAP values come from.
typeset -r SOURCEFILE=./file1.txt
# Each line sets a variable using awk.
# -F is the field delimiter. It's a colon and a space.
# Next is the value to look for. ^ matches the start of the line.
# When the above is found, return the second field ($2)
current_uid="$(awk -F': ' '/^uid/ {print $2}' ${SOURCEFILE})"
current_cn="$(awk -F': ' '/^cn/ {print $2}' ${SOURCEFILE})"
current_enbr="$(awk -F': ' '/^employeenumber/ {print $2}' ${SOURCEFILE})"
current_absja="$(awk -F': ' '/^absJobAction/ {print $2}' ${SOURCEFILE})"
# Print the contents of the variables. Note since absJobAction was not in the file,
# it's value is NULL.
echo "uid: ${current_uid}"
echo "cn: ${current_cn}"
echo "EmployeeNumber: ${current_enbr}"
echo "absJobAction: ${current_absja}"
~
When run:
$ ./test.sh
uid: user1
cn: User One
EmployeeNumber: 1234567
absJobAction:
$

How to get the line number of a match?

My aim is to get the line number ($lineof) of a string which matches a line in /etc/crontab.
To give 0 8 * * * Me echo "start working please" and get this is the line number 13 from /etc/crontab.
Given this file /tmp/crontab :
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
#
0 17 * * * Me echo "end of work"
0 8 * * * Me echo "start working please"
1 3 2 4 2 Me ls -la
I do something like that for the moment:
cat /etc/crontab | grep -v "#" | grep "Me" > /tmp/task.cron
i=1
while read -r content
do
line=$content
# lineof=$LINENO
nbline=${i}
minute=$(echo "$line" | awk '{print $1}') #0-59
hour=$(echo "$line" | awk '{print $2}') #0-23
dom=$(echo "$line" | awk '{print $3}') #1-31
month=$(echo "$line" | awk '{print $4}') #1-12
dow=$(echo "$line" | awk '{print $5}') #0-6 (0=Sunday)
cmd=$(echo "$line" | awk '{$1=$2=$3=$4=$5=$6=""; print $0}') #command
cmd=$(echo "$cmd" | tr ' ' _)
str=$str' '$nbline' "'$minute'" "'$hour'" "'$dom'" "'$month'" "'$dow'" "'$user'" "'$cmd'" '
i=$(($i+1))
done < /tmp/task.cron
$nbline give me the line of the content in /tmp/task.cron
$LINENO give me the line of the current script (which execute the program)
I want $lineof give me the number of the line in /etc/crontab
To print the line number of your match, use the -n option of grep. Since the pattern contains some special characters, use -F to make them be interpreted as fixed strings and not a regular expression:
grep -Fn 'your_line' /etc/crontab
However, since you want to print some message together with the line number, you may want to use awk instead:
awk -v line='your_line' '$0 == line {print "this is the line number", NR, "from", FILENAME}' /etc/crontab
Test
$ cat a
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# m h dom mon dow user command
#
0 17 * * * Me echo "end of work"
0 8 * * * Me echo "start working please"
1 3 2 4 2 Me ls -la
With awk:
$ awk -v line='0 8 * * * Me echo "start working please"' '$0 == line {print "this is the line number", NR, "from", FILENAME}' a
this is the line number 13 from a
With grep:
$ grep -Fn '0 8 * * * Me echo "start working please"' a13:0 8 * * * Me echo "start working please"
13:0 8 * * * Me echo "start working please"
grep --fixed-strings --line-number "${match}" | cut --delimiter=":" --fields=1
I finally did like this, found alone :
nbline=1
while read -r content
do
line=$content
if [ "${line:0:1}" != "#" ]; then #if this is not a comment
line=$(echo -e "$line" | grep "$user") #$line keep only lines with the $user choose
if [ ! -z "$line" ];then #if this is not a void $line
minute=$(echo -e "$line" | awk '{print $1}') #0-59
hour=$(echo -e "$line" | awk '{print $2}') #0-23
dom=$(echo -e "$line" | awk '{print $3}') #1-31
month=$(echo -e "$line" | awk '{print $4}') #1-12
dow=$(echo -e "$line" | awk '{print $5}') #0-6 (0=Sunday)
cmd=$(echo -e "$line" | awk '{$1=$2=$3=$4=$5=$6=""; print $0}') #command
cmd=$(echo -e "$cmd" | tr ' ' _) #replace space by '_' because it's annoying later
str=$str' "'$nbline'" "'$minute'" "'$hour'" "'$dom'" "'$month'" "'$dow'" "'$user'" "'$cmd'" '
fi
fi
nbline=$(($nbline+1))
done < /etc/crontab
I don't need to create an other file and get in $nbline the number of current line in loop. And count all line, even if they are void or commented. That's what I wanted.
'#' is the line number of the right content in /etc/crontab.

BASH script - print sorted contents from all files in directory with no rep's

In the current directory there are files with names of the form "gradesXXX" (where XXX is a course number) which look like this:
ID GRADE (this line is not contained in the files)
123456789 56
213495873 84
098342362 77
. .
. .
. .
I want to write a BASH script that prints all the IDs that have a grade above a certain number, which is given as the first parameter to said script.
The requirements are that an ID must be printed once at most, and that no intermediate files are used.
I was guided to use two scripts - the first with length of one line, and the second with length of up to six lines (not including the "#!" line).
I'm quite lost with this one so any suggestions will be appreciated.
Cheers.
The answer I was looking for was
// internal script
#!/bin/bash
while read line; do
line_split=( $line )
if (( ${line_split[1]} > $1 )); then
echo ${line_split[0]}
fi
done
// external script
#!/bin/bash
cat grades* | sort -r -n -k 1 | internalScript $1 | cut -f1 -d" " | uniq
OK, a simple solution.
cat grades[0-9][0-9][0-9] | sort -nurk 2 | while read ID GRADE ; do if [ $GRADE -lt 60 ] ; then break ; fi ; echo $ID ; done | sort -u
I'm not sure why two scripts should be necessary. All in a script:
#!/bin/bash
threshold=$1
cat grades[0-9][0-9][0-9] | sort -nurk 2 | while read ID GRADE ; do if [ $GRADE -lt $threshold ] ; then break ; fi ; echo $ID ; done | sort -u
We first cat all the grade files, the sort them by grade in reverse order. The while loop breaks if grade is below threshold, so that only lines with higher grades get their ID printed. sort -u makes sure that every ID is sent only once.
You can use awk:
awk '{ if ($2 > 70) print $1 }' grades777
It prints the first column of every line which seconds column is greater than 70. If you need to change the threshold:
N=71
awk '{ if ($2 > '$N') print $1 }' grades777
That ' are required to pass shell variables in AWK. To work with all grade??? files in the current directory and remove duplicated lines:
awk '{ if ($2 > '$N') print $1 }' grades??? | sort -u
A simple one-line solution.
Yet another solution:
cat grades[0-9][0-9][0-9] | awk -v MAX=70 '{ if ($2 > MAX) foo[$1]=1 }END{for (id in foo) print id }'
Append | sort -n after that if you want the IDs in sorted order.
In pure bash :
N=60
for file in /path/*; do
while read id grade; do ((grade > N)) && echo "$id"; done < "$file"
done
OUTPUT
213495873
098342362

Resources