493 lines
16 KiB
Bash
Executable File
493 lines
16 KiB
Bash
Executable File
#!/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 "状态" "未检测到 IP(ip 命令不可用)"
|
||
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 "$@"
|