18 Production-Ready Shell Scripts for Daily Sysadmin Tasks
This article presents 18 practical shell scripts for common system administration tasks including file consistency checks, log rotation, network traffic monitoring, FTP downloads, user input validation, Nginx 502 error auto-recovery, port scanning, batch file renaming, IP blocking, and IP address validation.
1. Detect File Consistency Between Two Servers
Compares files in a specified directory ( /data/web) on two servers by generating MD5 checksums locally and remotely via SSH, then copying the remote checksum file with SCP. A loop iterates over each file from the local list, checks existence on the remote server, and compares MD5 values. Outputs "changed" if checksums differ or "deleted" if the file is missing on the remote host.
#!/bin/bash
#####################################
#检测两台服务器指定目录下的文件一致性
#####################################
#通过对比两台服务器上文件的md5值,达到检测一致性的目的
dir=/data/web
b_ip=192.168.88.10
#将指定目录下的文件全部遍历出来并作为md5sum命令的参数,进而得到所有文件的md5值,并写入到指定文件中
find $dir -type f|xargs md5sum > /tmp/md5_a.txt
ssh $b_ip "find $dir -type f|xargs md5sum > /tmp/md5_b.txt"
scp $b_ip:/tmp/md5_b.txt /tmp
#将文件名作为遍历对象进行一一比对
for f in `awk '{print $2} /tmp/md5_a.txt`
do
#以a机器为标准,当b机器不存在遍历对象中的文件时直接输出不存在的结果
if grep -qw "$f" /tmp/md5_b.txt
then
md5_a=`grep -w "$f" /tmp/md5_a.txt|awk '{print $1}'`
md5_b=`grep -w "$f" /tmp/md5_b.txt|awk '{print $1}'`
#当文件存在时,如果md5值不一致则输出文件改变的结果
if [ $md5_a != $md5_b ]
then
echo "$f changed."
fi
else
echo "$f deleted."
fi
done2. Scheduled File Content Clearing and Size Logging
Runs hourly via cron. At 00:00 and 12:00, it truncates all files under /data/log/ (including subdirectories) without deleting them. At other hours, it records each file's size in a log file named with the current hour and date (e.g., /tmp/00-2024-01-15.log). Uses find to traverse files and du -sh for human-readable sizes.
#!/bin/bash
################################################################
#每小时执行一次脚本(任务计划),当时间为0点或12点时,将目标目录下的所有文件内
#容清空,但不删除文件,其他时间则只统计各个文件的大小,一个文件一行,输出以时#间和日期命名的文件中,需要考虑目标目录下二级、三级等子目录的文件
################################################################
logfile=/tmp/`date +%H-%F`.log
n=`date +%H`
if [ $n -eq 00 ] || [ $n -eq 12 ]
then
#通过for循环,以find命令作为遍历条件,将目标目录下的所有文件进行遍历并做相应操作
for i in `find /data/log/ -type f`
do
true > $i
done
else
for i in `find /data/log/ -type f`
do
du -sh $i >> $logfile
done
fi3. Network Interface Traffic Logging
Logs traffic for interface ens33 every minute in a daily log file ( /tmp/DD.log). Sets LANG=en to ensure English output. Uses sar -n DEV 1 59 to collect 59 seconds of statistics, then extracts average input/output in kb/s, converts to bps (multiply by 1000*8), and appends formatted lines with a separator. The sar command's 59-second run eliminates the need for an explicit sleep.
#!/bin/bash
#######################################################
#检测网卡流量,并按规定格式记录在日志中
#规定一分钟记录一次
#日志格式如下所示:
#2019-08-12 20:40
#ens33 input: 1234bps
#ens33 output: 1235bps
######################################################3
while :
do
#设置语言为英文,保障输出结果是英文,否则会出现bug
LANG=en
logfile=/tmp/`date +%d`.log
#将下面执行的命令结果输出重定向到logfile日志中
exec >> $logfile
date +"%F %H:%M"
#sar命令统计的流量单位为kb/s,日志格式为bps,因此要*1000*8
sar -n DEV 1 59|grep Average|grep ens33|awk '{print $2,"\t","input:","\t",$5*1000*8,"bps
",$2,"\t","output:","\t",$6*1000*8,"bps"}'
echo "####################"
#因为执行sar命令需要59秒,因此不需要sleep
done4. Count Digits Per Line and Total in a File
Reads a.txt line by line using sed to extract each line, removes non-digits with sed 's/[^0-9]//g', then uses wc -L to count the length of the longest resulting line (which equals the number of digits in that line). Accumulates a running total and prints each line's digit count followed by the sum.
#!/bin/bash
#########################################################
#计算文档每行出现的数字个数,并计算整个文档的数字总数
########################################################
#使用awk只输出文档行数(截取第一段)
n=`wc -l a.txt|awk '{print $1}'`
sum=0
#文档中每一行可能存在空格,因此不能直接用文档内容进行遍历
for i in `seq 1 $n`
do
#输出的行用变量表示时,需要用双引号
line=`sed -n "$i"p a.txt`
#wc -L选项,统计最长行的长度
n_n=`echo $line|sed s/'[^0-9]//'g|wc -L`
echo $n_n
sum=$[$sum+$n_n]
done
echo "sum:$sum"5. Download File from FTP Server
Takes a remote file path as an argument, splits it into directory and filename, then uses an FTP heredoc to connect to 192.168.1.10 with user admin and password password. Sets binary mode to avoid corruption, changes to the remote directory, and downloads the file.
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 filename"
fi
dir=$(dirname $1)
file=$(basename $1)
ftp -n -v << EOF # -n 自动登录
open 192.168.1.10 # ftp服务器
user admin password
binary # 设置ftp传输模式为二进制,避免MD5值不同或.tar.gz压缩包格式错误
cd $dir
get "$file"
EOF6. Sum, Min, Max of Five User-Input Numbers (1-100)
Prompts the user to enter five integers between 1 and 100. Validates each input: must be numeric and ≤100. Updates running sum, minimum, and maximum. After five valid entries, prints SUM, MIN, MAX.
#!/bin/bash
COUNT=1
SUM=0
MIN=0
MAX=100
while [ $COUNT -le 5 ]; do
read -p "请输入1-10个整数:" INT
if [[ ! $INT =~ ^[0-9]+$ ]]; then
echo "输入必须是整数!"
exit 1
elif [[ $INT -gt 100 ]]; then
echo "输入必须是100以内!"
exit 1
fi
SUM=$(($SUM+$INT))
[ $MIN -lt $INT ] && MIN=$INT
[ $MAX -gt $INT ] && MAX=$INT
let COUNT++
done
echo "SUM: $SUM"
echo "MIN: $MIN"
echo "MAX: $MAX"7. Number Guessing Game
Generates a random number between 1 and 100 using $[RANDOM%100+1]. Loops until the user guesses correctly, giving hints "猜大了" (too high) or "猜小了" (too low). Exits on correct guess.
#!/bin/bash # 脚本生成一个 100 以内的随机数,提示用户猜数字,根据用户的输入,提示用户猜对了,
# 猜小了或猜大了,直至用户猜对脚本结束。
# RANDOM 为系统自带的系统变量,值为 0‐32767的随机数
# 使用取余算法将随机数变为 1‐100 的随机数num=$[RANDOM%100+1]echo "$num"
# 使用 read 提示用户猜数字
# 使用 if 判断用户猜数字的大小关系:‐eq(等于),‐ne(不等于),‐gt(大于),‐ge(大于等于),
# ‐lt(小于),‐le(小于等于)
while :
do
read -p "计算机生成了一个 1‐100 的随机数,你猜: " cai
if [ $cai -eq $num ]
then
echo "恭喜,猜对了"
exit
elif [ $cai -gt $num ]
then
echo "Oops,猜大了"
else
echo "Oops,猜小了"
fi
done8. Monitor Nginx 502 Errors and Auto-Restart PHP-FPM
Assumes an LNMP stack where 502 errors disappear after restarting php-fpm. Checks the last 300 lines of /data/log/access.log every 10 seconds. If 502 count ≥ 30 (10% threshold), restarts php-fpm via /etc/init.d/php-fpm restart, then sleeps 60 seconds to prevent rapid reboots.
#场景:
#1.访问日志文件的路径:/data/log/access.log
#2.脚本死循环,每10秒检测一次,10秒的日志条数为300条,出现502的比例不低于10%(30条)则需要重启php-fpm服务
#3.重启命令为:/etc/init.d/php-fpm restart
#!/bin/bash
###########################################################
#监测Nginx访问日志502情况,并做相应动作
###########################################################
log=/data/log/access.log
N=30 #设定阈值
while :
do
#查看访问日志的最新300条,并统计502的次数
err=`tail -n 300 $log |grep -c '502' `
if [ $err -ge $N ]
then
/etc/init.d/php-fpm restart 2> /dev/null
#设定60s延迟防止脚本bug导致无限重启php-fpm服务
sleep 60
fi
sleep 10
done9. Assign Command Results to Variables
Demonstrates three methods to assign values to dynamically named variables: Method 1: Loop over a list, use eval to create variables a4, a5, a6. Method 2: Expand brace expression 192.168.1.1{1,2} via eval, assign to node1, node2. Method 3: Use array indexing to assign INDEX1, INDEX2, INDEX3 from array arr=(4 5 6).
for i in $(echo "4 5 6"); do
eval a$i=$i
done
echo $a4$a5$a6 num=0
for i in $(eval echo $*);do #eval将{1,2}分解为1 2
let num+=1
eval node${num}="$i"
done
echo $node1$node2$node3
# bash a.sh 192.168.1.1{1,2}
192.168.1.11 192.168.1.12 arr=(4 5 6)
INDEX1=$(echo ${arr[0]})
INDEX2=$(echo ${arr[1]})
INDEX3=$(echo ${arr[2]})10. Batch Rename Files (article_*.html → bbs_*.html)
Shows three approaches: Method 1: Loop over ls *html, use parameter expansion ${file#*_} to strip prefix. Method 2: Use find with -maxdepth 1 for safety. Method 3: Use rename command: rename article bbs *.html.
for file in $(ls *html); do
mv $file bbs_${file#*_}
done for file in $(find . -maxdepth 1 -name "*html"); do
mv $file bbs_${file#*_}done # rename article bbs *.html11. Delete Lines with Letters in First 5 Lines; Strip Letters from Lines 6-10
Uses sed to process 2.txt in three segments: lines 1-5 delete any line containing a letter ( sed '/[a-zA-Z]/d'); lines 6-10 remove all letters globally ( sed 's/[a-zA-Z]//g'); lines 11-end print unchanged. Output is to stdout; to modify file in-place, redirect to temp file or use sed -i.
#!/bin/bash
###############################################################
把一个文档前五行中包含字母的行删掉,同时删除6到10行包含的所有字母
##############################################################
sed -n '1,5'p 2.txt |sed '/[a-zA-Z]/d'
sed -n '6,10'p 2.txt |sed s/'[a-zA-Z]//'g
sed -n '11,$'p 2.txt
#最终结果只是在屏幕上打印结果,如果想直接更改文件,可将输出结果写入临时文件中,再替换2.txt或者使用-i选项12. Total Size of .html Files in Current Directory
Method 1:
find . -name "*.html" -exec du -k {} \; | awk '{sum+=$1}END{print sum}'— sums kilobyte sizes. Method 2: Loop over ls -l *.html, extract 5th field (size in bytes) with awk, accumulate in variable sum.
# find . -name "*.html" -exec du -k {} \; |awk '{sum+=$1}END{print sum}' for size in $(ls -l *.html |awk '{print $5}'); do
sum=$(($sum+$size))
done
echo $sum13. Scan Host Port Status
Takes target host as first argument. Checks ports 22, 25, 80, 8080 using bash's /dev/tcp pseudo-device. For each port, attempts a connection; prints "open" or "close".
#!/bin/bash
HOST=$1
PORT="22 25 80 8080"
for PORT in $PORT; do
if echo &>/dev/null > /dev/tcp/$HOST/$PORT; then
echo "$PORT open"
else
echo "$PORT close"
fi
done14. Print Words with Fewer Than 6 Characters
Iterates over words in a sample sentence. For each word, counts characters with wc -c (includes newline), prints if count < 6.
#!/bin/bash
##############################################################
#shell打印示例语句中字母数小于6的单词
##############################################################
for s in Bash also interprets a number of multi-character options.
do
n=`echo $s|wc -c`
if [ $n -lt 6 ]
then
echo $s
fi
done15. Interactive Command Menu
Displays a menu (date, ls, who, pwd, exit). Loops reading user input, validates numeric input, rejects non-digits or empty input. Uses case to execute corresponding command. Exits on 0.
#!/bin/bash
##############################################################
#输入数字运行相应命令
##############################################################
echo "*cmd menu* 1-date 2-ls 3-who 4-pwd 0-exit "
while :
do
#捕获用户键入值
read -p "please input number :" n
n1=`echo $n|sed s/'[0-9]//'g`
#空输入检测
if [ -z "$n" ]
then
continue
fi
#非数字输入检测
if [ -n "$n1" ]
then
exit 0
fi
break
done
case $n in
1)
date
;;
2)
ls
;;
3)
who
;;
4)
pwd
;;
0)
break
;;
#输入数字非1-4的提示
*)
echo "please input number is [1-4]"
esac16. Expect for SSH Non-Interactive Command Execution
Three methods to automate SSH login with Expect: Method 1: Heredoc feeding Expect script via stdin. Method 2: expect -c with inline script. Method 3: Separate Expect script ( login.exp) taking IP, user, password, command as arguments; a wrapper Bash script reads host info from user_info.txt and loops over IPs.
#!/bin/bash
USER=root
PASS=123.com
IP=192.168.1.120
expect << EOF
set timeout 30
spawn ssh $USER@$IP
expect {
"(yes/no)" {send "yes\r"; exp_continue}
"password:" {send "$PASS\r"}
}
expect "$USER@*" {send "$1\r"}
expect "$USER@*" {send "exit\r"}
expect eof
EOF #!/usr/bin/expect
set ip [lindex $argv 0]
set user [lindex $argv 1]
set passwd [lindex $argv 2]
set cmd [lindex $argv 3]
if { $argc != 4 } {
puts "Usage: expect login.exp ip user passwd"
exit 1
}
set timeout 30
spawn ssh $user@$ip
expect {
"(yes/no)" {send "yes\r"; exp_continue}
"password:" {send "$passwd\r"}
}
expect "$user@*" {send "$cmd\r"}
expect "$user@*" {send "exit\r"}
expect eof17. Create 10 Users with Random 10-Character Passwords
Requires mkpasswd. Generates usernames user_00 to user_09 via seq -w 0 09. For each, creates user, generates password with mkpasswd -s 0 -l 10 (10 chars, letters+numbers), sets password via passwd --stdin (or interactive), logs credentials to /tmp/userpassword.
#!/bin/bash
##############################################################
#创建10个用户,并分别设置密码,密码要求10位且包含大小写字母以及数字
#最后需要把每个用户的密码存在指定文件中
#前提条件:安装mkpasswd命令
##############################################################
#生成10个用户的序列(00-09)
for u in `seq -w 0 09`
do
#创建用户
useradd user_$u
#生成密码
p=`mkpasswd -s 0 -l 10`
#从标准输入中读取密码进行修改(不安全)
echo $p|passwd --stdin user_$u
#常规修改密码
echo -e "$p
$p"|passwd user_$u
#将创建的用户及对应的密码记录到日志文件中
echo "user_$u$p" >> /tmp/userpassword
done18. Monitor httpd Process Count with Auto-Restart and Alerting
Every 10 seconds, counts httpd processes with pgrep -l httpd | wc -l. If ≥500, attempts restart via /usr/local/apache2/bin/apachectl restart. On failure, calls check_service function which retries up to 5 times (logging errors to /var/log/httpderr.log). If all retries fail, runs mail.py and exits. On successful restart, waits 60 seconds, re-checks process count; if still ≥500, alerts and exits. Otherwise resumes normal 10-second checks.
#!/bin/bash
###############################################################################################################################
#需求:
#1.每隔10s监控httpd的进程数,若进程数大于等于500,则自动重启Apache服务,并检测服务是否重启成功
#2.若未成功则需要再次启动,若重启5次依旧没有成功,则向管理员发送告警邮件,并退出检测
#3.如果启动成功,则等待1分钟后再次检测httpd进程数,若进程数正常,则恢复正常检测(10s一次),否则放弃重启并向管理员发送告警邮件,并退出检测
###############################################################################################################################
#计数器函数
check_service()
{
j=0
for i in `seq 1 5`
do
#重启Apache的命令
/usr/local/apache2/bin/apachectl restart 2> /var/log/httpderr.log
#判断服务是否重启成功
if [ $? -eq 0 ]
then
break
else
j=$[$j+1]
fi
#判断服务是否已尝试重启5次
if [ $j -eq 5 ]
then
mail.py
exit
fi
done
}
while :
do
n=`pgrep -l httpd|wc -l`
#判断httpd服务进程数是否超过500
if [ $n -gt 500 ]
then
/usr/local/apache2/bin/apachectl restart
if [ $? -ne 0 ]
then
check_service
else
sleep 60
n2=`pgrep -l httpd|wc -l`
#判断重启后是否依旧超过500
if [ $n2 -gt 500 ]
then
mail.py
exit
fi
fi
fi
#每隔10s检测一次
sleep 10
done19. Batch Change Passwords on Multiple Servers via SSH
Reads old_pass.txt (IP, user, old password, port). For each host, generates a new random 8-char password with mkpasswd -l 8, records new credentials to new_pass.txt, then uses Expect to SSH in, accept host key, authenticate with old password, and run echo 'newpass' | passwd --stdin user.
#!/bin/bash
OLD_INFO=old_pass.txt
NEW_INFO=new_pass.txt
for IP in $(awk '/^[^#]/{print $1}' $OLD_INFO); do
USER=$(awk -v I=$IP 'I==$1{print $2}' $OLD_INFO)
PASS=$(awk -v I=$IP 'I==$1{print $3}' $OLD_INFO)
PORT=$(awk -v I=$IP 'I==$1{print $4}' $OLD_INFO)
NEW_PASS=$(mkpasswd -l 8) # 随机密码
echo "$IP $USER $NEW_PASS $PORT" >> $NEW_INFO
expect -c "
spawn ssh -p$PORT $USER@$IP
set timeout 2
expect {
\"(yes/no)\" {send \"yes\r\";exp_continue}
\"password:\" {send \"$PASS\r\";exp_continue}
\"$USER@*\" {send \"echo '$NEW_PASS' |passwd --stdin $USER\r exit\r\";exp_continue}
}"
done20. Auto-Block High-Frequency Web Access IPs with iptables
Two methods to detect abusive IPs: Method 1 (access log): Examines last 5000 lines of Nginx access.log for current minute, counts requests per IP with awk, blocks IPs >100 requests/minute if not already in iptables. Method 2 (netstat): Counts established connections on port 80 per client IP, blocks >100 connections.
Also includes SSH brute-force blocking: parses lastb or /var/log/auth.log for failed logins in current minute/hour, blocks IPs with >10 (lastb) or >5 (auth.log) attempts, logs actions to ~/ssh-login-limit.log.
#!/bin/bash
DATE=$(date +%d/%b/%Y:%H:%M)
ABNORMAL_IP=$(tail -n5000 access.log |grep $DATE |awk '{a[$1]++}END{for(i in a)if(a[i]>100)print i}')
#先tail防止文件过大,读取慢,数字可调整每分钟最大的访问量。awk不能直接过滤日志,因为包含特殊字符。
for IP in $ABNORMAL_IP; do
if [ $(iptables -vnL |grep -c "$IP") -eq 0 ];
then
iptables -I INPUT -s $IP -j DROP
fi
done #!/bin/bash
ABNORMAL_IP=$(netstat -an |awk '$4~/:80$/ && $6~/ESTABLISHED/{gsub(/:[0-9]+/,"",$5);{a[$5]++}}END{for(i in a)if(a[i]>100)print i}')
#gsub是将第五列(客户端IP)的冒号和端口去掉
for IP in $ABNORMAL_IP; do
if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then
iptables -I INPUT -s $IP -j DROP
fi
done21. Block/Unblock Abnormal IPs Based on Web Log with Half-Hour Recovery
Runs periodically (e.g., via cron). block() extracts IPs from previous minute's log ( /data/log/access.log), counts requests, rejects (REJECT) those >100 requests, logs to /tmp/badip.log. unblock() checks iptables packet/byte counters for rules matching 0.0.0.0/0, deletes rules with packet count <10 (indicating no recent traffic), then zeroes counters with iptables -Z. At minute 00 and 30, runs unblock then block; otherwise only block.
#!/bin/bash
####################################################################################
#根据web访问日志,封禁请求量异常的IP,如IP在半小时后恢复正常,则解除封禁
####################################################################################
logfile=/data/log/access.log
#显示一分钟前的小时和分钟
d1=`date -d "-1 minute" +%H%M`
d2=`date +%M`
ipt=/sbin/iptables
ips=/tmp/ips.txt
block()
{
#将一分钟前的日志全部过滤出来并提取IP以及统计访问次数
grep '$d1:'$logfile|awk '{print $1}'|sort -n|uniq -c|sort -n > $ips
#利用for循环将次数超过100的IP依次遍历出来并予以封禁
for i in `awk '$1>100 {print $2}' $ips`
do
$ipt -I INPUT -p tcp --dport 80 -s $i -j REJECT
echo "`date +%F-%T` $i" >> /tmp/badip.log
done
}
unblock()
{
#将封禁后所产生的pkts数量小于10的IP依次遍历予以解封
for a in `$ipt -nvL INPUT --line-numbers |grep '0.0.0.0/0'|awk '$2<10 {print $1}'|sort -nr`
do
$ipt -D INPUT $a
done
$ipt -Z
}
#当时间在00分以及30分时执行解封函数
if [ $d2 -eq "00" ] || [ $d2 -eq "30" ]
then
#要先解再封,因为刚刚封禁时产生的pkts数量很少
unblock
block
else
block
fi22. Validate IPv4 Address Input
Three implementations of a check_ip function: Method 1: Uses regex to verify four octets, then awk to ensure each ≤255. Method 2: Bash regex match, then splits with cut and tests each field ≤255. Enhanced version: Wraps validation in a loop; prompts until valid IP entered, returns 0 on success, 1 on failure.
#!/bin/bash
function check_ip(){
IP=$1
VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >/dev/null; then
if [ $VALID_CHECK == "yes" ]; then
echo "$IP available."
else
echo "$IP not available!"
fi
else
echo "Format error!"
fi
}
check_ip 192.168.1.1
check_ip 256.1.1.1 #!/bin/bash
function check_ip(){
IP=$1
if [[ $IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
FIELD1=$(echo $IP|cut -d. -f1)
FIELD2=$(echo $IP|cut -d. -f2)
FIELD3=$(echo $IP|cut -d. -f3)
FIELD4=$(echo $IP|cut -d. -f4)
if [ $FIELD1 -le 255 -a $FIELD2 -le 255 -a $FIELD3 -le 255 -a $FIELD4 -le 255 ]; then
echo "$IP available."
else
echo "$IP not available!"
fi
else
echo "Format error!"
fi
}
check_ip 192.168.1.1
check_ip 256.1.1.1 #!/bin/bash
function check_ip(){
local IP=$1
VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >/dev/null; then
if [ $VALID_CHECK == "yes" ]; then
return 0
else
echo "$IP not available!"
return 1
fi
else
echo "Format error! Please input again."
return 1
fi
}
while true; do
read -p "Please enter IP: " IP
check_ip $IP
[ $? -eq 0 ] && break || continue
doneSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Linux Tech Enthusiast
Focused on sharing practical Linux technology content, covering Linux fundamentals, applications, tools, as well as databases, operating systems, network security, and other technical knowledge.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
