Files
linux-bash/login-info/login-info.sh
2026-04-04 13:24:58 +08:00

493 lines
16 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
#===============================================================================
# 登录信息展示脚本 (Login Info)
# 用途: 快速查看系统/资源/网络/Docker 等信息,适合作为 SSH 登录后提示。
#
# 手动运行:
# bash login-info/login-info.sh
#
# 安装为登录自动展示:
# sudo bash login-info/install_login-info.sh
#
# 环境变量:
# LOGIN_INFO_MODE=summary|full # 默认 summary
# LOGIN_INFO_COLOR=auto|always|never # 默认 auto
#===============================================================================
usage() {
local prog
prog=${0##*/}
cat <<EOF
用法:
${prog} [--summary|--full] [--no-docker] [--no-color] [--help]
说明:
--summary 快速模式(默认),适合登录时自动展示
--full 详细模式(包含 GPU/磁盘明细/软件包统计等)
--no-docker 不显示 Docker 信息
--no-color 禁用颜色输出
EOF
}
has_cmd() { command -v "$1" >/dev/null 2>&1; }
init_colors() {
local color_mode=${LOGIN_INFO_COLOR:-auto}
case "$color_mode" in
auto|always|never) ;;
*) color_mode=auto ;;
esac
local enable_color=0
if [ "$color_mode" = "always" ]; then
enable_color=1
elif [ "$color_mode" = "never" ] || [ -n "${NO_COLOR:-}" ]; then
enable_color=0
else
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
enable_color=1
fi
fi
if [ "$enable_color" -eq 1 ]; then
C_RESET='\033[0m'
C_BOLD='\033[1m'
C_TITLE='\033[1;36m'
C_INFO='\033[1;33m'
C_VALUE='\033[0;37m'
C_LABEL='\033[0;32m'
C_SUBTLE='\033[1;30m'
C_BLUE='\033[1;34m'
C_RED='\033[0;31m'
else
C_RESET=''
C_BOLD=''
C_TITLE=''
C_INFO=''
C_VALUE=''
C_LABEL=''
C_SUBTLE=''
C_BLUE=''
C_RED=''
fi
HR='───────────────────────────────────────────────────────────────────────────────'
}
print_hr() { printf '%b%s%b\n' "${C_SUBTLE}" "${HR}" "${C_RESET}"; }
print_title() { printf '%b%s%b\n' "${C_TITLE}${C_BOLD}" "$1" "${C_RESET}"; }
print_kv() { printf ' %b%s:%b %b%s%b\n' "${C_LABEL}" "$1" "${C_RESET}" "${C_VALUE}" "$2" "${C_RESET}"; }
print_kv_info() { printf ' %b%s:%b %b%s%b\n' "${C_LABEL}" "$1" "${C_RESET}" "${C_INFO}" "$2" "${C_RESET}"; }
format_kb() {
local kb=${1:-}
case "$kb" in
''|*[!0-9]*) printf 'N/A'; return 0 ;;
esac
if [ "$kb" -ge 1048576 ]; then
awk -v v="$kb" 'BEGIN{printf "%.1fGiB", v/1048576}'
elif [ "$kb" -ge 1024 ]; then
awk -v v="$kb" 'BEGIN{printf "%.1fMiB", v/1024}'
else
printf '%sKiB' "$kb"
fi
}
get_os_pretty_name() {
local os=''
if has_cmd lsb_release; then
os=$(lsb_release -ds 2>/dev/null || true)
os=${os#\"}
os=${os%\"}
fi
if [ -z "$os" ] && [ -r /etc/os-release ]; then
os=$(
awk -F= '
$1=="PRETTY_NAME"{
gsub(/^"/,"",$2); gsub(/"$/,"",$2);
print $2; exit
}' /etc/os-release 2>/dev/null || true
)
fi
printf '%s' "${os:-N/A}"
}
get_boot_time() {
local boot=''
if has_cmd uptime; then
boot=$(uptime -s 2>/dev/null || true)
fi
if [ -z "$boot" ] && has_cmd who; then
boot=$(who -b 2>/dev/null | awk '{print $3" "$4}' || true)
fi
printf '%s' "${boot:-N/A}"
}
get_uptime_pretty() {
local up=''
if has_cmd uptime; then
up=$(uptime -p 2>/dev/null | sed 's/^up //')
fi
if [ -z "$up" ] && [ -r /proc/uptime ]; then
local seconds
seconds=$(awk '{print int($1)}' /proc/uptime 2>/dev/null || echo 0)
if [ "$seconds" -gt 0 ] 2>/dev/null; then
local days hours minutes
days=$((seconds / 86400))
hours=$(((seconds % 86400) / 3600))
minutes=$(((seconds % 3600) / 60))
if [ "$days" -gt 0 ]; then
up="${days}${hours}小时 ${minutes}分钟"
elif [ "$hours" -gt 0 ]; then
up="${hours}小时 ${minutes}分钟"
else
up="${minutes}分钟"
fi
fi
fi
printf '%s' "${up:-N/A}"
}
get_load_avg() {
local load=''
if [ -r /proc/loadavg ]; then
load=$(awk '{print $1" "$2" "$3}' /proc/loadavg 2>/dev/null || true)
fi
if [ -z "$load" ] && has_cmd uptime; then
load=$(uptime 2>/dev/null | awk -F'load average: ' '{print $2}' || true)
fi
printf '%s' "${load:-N/A}"
}
get_cpu_model() {
local model=''
if [ -r /proc/cpuinfo ]; then
model=$(awk -F': *' '/model name/{print $2; exit}' /proc/cpuinfo 2>/dev/null || true)
[ -n "$model" ] || model=$(awk -F': *' '/Hardware/{print $2; exit}' /proc/cpuinfo 2>/dev/null || true)
[ -n "$model" ] || model=$(awk -F': *' '/Processor/{print $2; exit}' /proc/cpuinfo 2>/dev/null || true)
fi
if [ -z "$model" ] && has_cmd lscpu; then
model=$(lscpu 2>/dev/null | awk -F': *' '/Model name:/{print $2; exit}' || true)
fi
printf '%s' "${model:-N/A}"
}
get_cpu_cores() {
local cores=''
if has_cmd nproc; then
cores=$(nproc 2>/dev/null || true)
fi
if [ -z "$cores" ] && [ -r /proc/cpuinfo ]; then
cores=$(awk -F': *' '/^processor/{c++} END{print c+0}' /proc/cpuinfo 2>/dev/null || true)
fi
printf '%s' "${cores:-N/A}"
}
get_cpu_temp_c() {
local temp=''
if has_cmd sensors; then
temp=$(
sensors 2>/dev/null | awk '
BEGIN{IGNORECASE=1}
/(package id 0|tctl|tdie|cpu temperature|core 0|composite|temp1)/{
for(i=1;i<=NF;i++){
if($i ~ /^[+]?[0-9]+([.][0-9]+)?°C$/){
gsub(/[+°C]/,"",$i);
print $i; exit
}
}
}' || true
)
fi
if [ -z "$temp" ]; then
local zone
for zone in /sys/class/thermal/thermal_zone*; do
[ -r "$zone/temp" ] || continue
local temp_raw
temp_raw=$(cat "$zone/temp" 2>/dev/null || true)
case "$temp_raw" in
''|*[!0-9]*) continue ;;
esac
if [ "$temp_raw" -gt 0 ] 2>/dev/null; then
if [ "$temp_raw" -ge 1000 ]; then
temp=$(awk -v t="$temp_raw" 'BEGIN{printf "%.1f", t/1000}')
else
temp=$temp_raw
fi
break
fi
done
fi
printf '%s' "$temp"
}
section_system() {
print_hr
print_title "系统概览"
print_kv "主机名" "$(hostname 2>/dev/null || echo N/A)"
print_kv "操作系统" "$(get_os_pretty_name)"
print_kv "内核版本" "$(uname -sr 2>/dev/null || echo N/A)"
print_kv "系统架构" "$(uname -m 2>/dev/null || echo N/A)"
print_kv "启动时间" "$(get_boot_time)"
print_kv "运行时间" "$(get_uptime_pretty)"
print_kv "平均负载" "$(get_load_avg)"
print_kv "当前时间" "$(date '+%F %T %Z' 2>/dev/null || echo N/A)"
print_hr
}
section_resources() {
print_title "硬件与资源"
local cpu_model cpu_cores cpu_temp
cpu_model=$(get_cpu_model)
cpu_cores=$(get_cpu_cores)
cpu_temp=$(get_cpu_temp_c)
if [ -n "$cpu_temp" ]; then
print_kv_info "CPU" "${cpu_model} (${cpu_cores} 核, ${cpu_temp}°C)"
else
print_kv "CPU" "${cpu_model} (${cpu_cores} 核)"
fi
local mem_total_kb mem_avail_kb mem_used_percent
mem_total_kb=$(awk '/MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || true)
mem_avail_kb=$(awk '/MemAvailable:/ {print $2; exit}' /proc/meminfo 2>/dev/null || true)
mem_used_percent=$(
awk -v t="${mem_total_kb:-0}" -v a="${mem_avail_kb:-0}" '
BEGIN{
if(t>0 && a>0){
printf "%.1f%%", (t-a)*100/t
} else {
print "N/A"
}
}' 2>/dev/null || true
)
print_kv "内存" "总计: $(format_kb "$mem_total_kb") | 已用: ${mem_used_percent} | 可用: $(format_kb "$mem_avail_kb")"
local disk_type disk_size disk_used disk_avail disk_percent
disk_type=$(df -hPT / 2>/dev/null | awk 'NR==2 {print $2}' || true)
disk_size=$(df -hPT / 2>/dev/null | awk 'NR==2 {print $3}' || true)
disk_used=$(df -hPT / 2>/dev/null | awk 'NR==2 {print $4}' || true)
disk_avail=$(df -hPT / 2>/dev/null | awk 'NR==2 {print $5}' || true)
disk_percent=$(df -hPT / 2>/dev/null | awk 'NR==2 {print $6}' || true)
if [ -n "$disk_size" ] && [ -n "$disk_percent" ]; then
print_kv "磁盘(/)" "${disk_type:-N/A} 总:${disk_size} 已用:${disk_used} (${disk_percent}) 可用:${disk_avail}"
else
print_kv "磁盘(/)" "N/A"
fi
}
section_storage_full() {
print_title "存储明细"
if ! has_cmd df; then
print_kv "df" "未安装"
return 0
fi
df -hPT \
-x tmpfs -x devtmpfs -x devpts -x proc -x sysfs -x cgroup -x cgroup2 \
-x securityfs -x pstore -x efivarfs -x autofs -x debugfs -x mqueue \
-x hugetlbfs -x tracefs -x fusectl -x squashfs 2>/dev/null \
| awk 'NR==1{next} {printf "%s|%s|%s|%s|%s|%s\n",$7,$2,$3,$4,$6,$1}' \
| while IFS='|' read -r mount fs_type size used percent device; do
case "$mount" in
/var/lib/docker/*|/run/*|/snap/*) continue ;;
esac
printf ' %b%s%b (%b%s%b - %s)\n' "${C_INFO}" "$mount" "${C_RESET}" "${C_VALUE}" "$device" "${C_RESET}" "$fs_type"
printf ' %b总:%b %b%s%b, %b已用:%b %b%s%b (%b%s%b)\n' \
"${C_LABEL}" "${C_RESET}" "${C_BLUE}" "$size" "${C_RESET}" \
"${C_LABEL}" "${C_RESET}" "${C_BLUE}" "$used" "${C_RESET}" \
"${C_VALUE}" "$percent" "${C_RESET}"
done
}
section_network() {
print_title "网络连接"
if has_cmd ip; then
local default_route default_iface default_gw
default_route=$(ip route show default 2>/dev/null | head -n 1 || true)
if [ -n "$default_route" ]; then
default_gw=$(printf '%s\n' "$default_route" | awk '{print $3}' || true)
default_iface=$(printf '%s\n' "$default_route" | awk '{for(i=1;i<=NF;i++) if($i=="dev"){print $(i+1); exit}}' || true)
[ -n "$default_iface" ] && print_kv "默认路由" "dev=${default_iface} gw=${default_gw:-N/A}"
fi
local has_ip=0
local iface
while IFS= read -r iface; do
[ -n "$iface" ] || continue
case "$iface" in
lo|docker*|veth*|br-*|virbr* ) continue ;;
esac
local ipv4 ipv6 mac
ipv4=$(ip -o -4 addr show dev "$iface" scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | paste -sd' ' - || true)
ipv6=$(ip -o -6 addr show dev "$iface" scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | paste -sd' ' - || true)
mac=$(ip link show dev "$iface" 2>/dev/null | awk '/link\/ether/ {print $2; exit}' || true)
if [ -n "$ipv4" ] || [ -n "$ipv6" ]; then
has_ip=1
printf ' %b%s%b\n' "${C_INFO}${C_BOLD}" "$iface" "${C_RESET}"
[ -n "$ipv4" ] && printf ' %bIPv4:%b %b%s%b\n' "${C_LABEL}" "${C_RESET}" "${C_VALUE}" "$ipv4" "${C_RESET}"
[ -n "$ipv6" ] && printf ' %bIPv6:%b %b%s%b\n' "${C_LABEL}" "${C_RESET}" "${C_VALUE}" "$ipv6" "${C_RESET}"
[ -n "$mac" ] && printf ' %bMAC:%b %b%s%b\n' "${C_LABEL}" "${C_RESET}" "${C_VALUE}" "$mac" "${C_RESET}"
fi
done < <(ip -o link show up 2>/dev/null | awk -F': ' '{print $2}' | cut -d'@' -f1)
if [ "$has_ip" -eq 0 ]; then
print_kv "状态" "未检测到活动的网络连接或 IP 地址"
fi
else
local ips
ips=$(hostname -I 2>/dev/null | tr -s ' ' ' ' | sed 's/ $//' || true)
if [ -n "$ips" ]; then
print_kv "IP" "$ips"
else
print_kv "状态" "未检测到 IPip 命令不可用)"
fi
fi
}
section_docker() {
local mode=$1
if ! has_cmd docker; then
return 0
fi
if ! docker ps >/dev/null 2>&1; then
return 0
fi
local running total
running=$(docker ps -q 2>/dev/null | wc -l | awk '{print $1}' || true)
total=$(docker ps -aq 2>/dev/null | wc -l | awk '{print $1}' || true)
print_title "Docker 容器"
print_kv "容器数量" "运行中: ${running:-0} / 总计: ${total:-0}"
if [ "$mode" = "full" ] && [ "${running:-0}" -gt 0 ] 2>/dev/null; then
local names
names=$(docker ps --format '{{.Names}}' 2>/dev/null | paste -sd', ' - || true)
[ -n "$names" ] && print_kv "运行列表" "$names"
fi
}
section_activity() {
local mode=$1
local user from_ip tty last_login
user=${USER:-}
[ -n "$user" ] || user=$(id -un 2>/dev/null || echo N/A)
from_ip=''
if [ -n "${SSH_CONNECTION:-}" ]; then
from_ip=$(printf '%s\n' "$SSH_CONNECTION" | awk '{print $1}' || true)
fi
if [ -z "$from_ip" ] && has_cmd who; then
from_ip=$(who -m 2>/dev/null | awk -F'[()]' '{print $2}' | awk '{print $1}' || true)
fi
tty=$(tty 2>/dev/null || true)
print_title "登录与统计"
print_kv "当前用户" "$user"
[ -n "$from_ip" ] && print_kv "来源地址" "$from_ip"
[ -n "$tty" ] && print_kv "终端" "$tty"
last_login=$(
last -n 5 -w "$user" 2>/dev/null \
| awk -v u="$user" '$1==u && $0 !~ /wtmp begins/ && $0 !~ /still logged in/ {print; exit}' \
|| true
)
[ -n "$last_login" ] && print_kv "上次登录" "$last_login"
if [ "$mode" = "full" ]; then
local package_count='N/A'
if has_cmd dpkg-query; then
package_count=$(dpkg-query -f '${Package}\n' -W 2>/dev/null | wc -l | awk '{print $1}' || true)
print_kv "软件包数" "${package_count:-N/A} (Debian/APT)"
elif has_cmd rpm; then
package_count=$(rpm -qa 2>/dev/null | wc -l | awk '{print $1}' || true)
print_kv "软件包数" "${package_count:-N/A} (RPM/Yum/Dnf)"
elif has_cmd pacman; then
package_count=$(pacman -Qq 2>/dev/null | wc -l | awk '{print $1}' || true)
print_kv "软件包数" "${package_count:-N/A} (Pacman)"
fi
fi
}
section_gpu() {
if ! has_cmd lspci; then
return 0
fi
local gpus
gpus=$(lspci 2>/dev/null | awk -F': ' '/VGA compatible controller|3D controller|Display controller/{print $2}' || true)
[ -n "$gpus" ] || return 0
print_title "图形处理器 (GPU)"
while IFS= read -r line; do
[ -n "$line" ] || continue
printf ' %b%s%b\n' "${C_VALUE}" "$line" "${C_RESET}"
done <<<"$gpus"
}
main() {
local mode=${LOGIN_INFO_MODE:-summary}
local show_docker=1
while [ $# -gt 0 ]; do
case "$1" in
--summary|--fast) mode=summary ;;
--full) mode=full ;;
--no-docker) show_docker=0 ;;
--no-color) LOGIN_INFO_COLOR=never ;;
-h|--help) usage; return 0 ;;
*) printf '错误: 未知参数: %s\n' "$1" >&2; usage; return 2 ;;
esac
shift
done
init_colors
section_system
section_resources
if [ "$mode" = "full" ]; then
section_gpu
section_storage_full
fi
section_network
if [ "$show_docker" -eq 1 ]; then
section_docker "$mode"
fi
section_activity "$mode"
print_hr
if [ "$mode" = "summary" ]; then
printf '%b提示:%b 运行 %blogin-info --full%b 查看更完整信息\n' \
"${C_SUBTLE}" "${C_RESET}" "${C_INFO}" "${C_RESET}"
fi
}
main "$@"