背景

在 linux 环境下,需要经常下载源码包、部署包之类的压缩包,一般是 tar、tar.gz 压缩包,大部分下载到的压缩包里面会有一层根目录,但免不了有一些压缩包里面没有一个根目录,而是直接就是各种文件

在 windows 系统的时候,我都会先打开压缩包看看里面的目录结构,再决定需不需要新建一个文件夹来存放,但在 linux 系统中,暂时还没找到可以按层级展示压缩包里面目录结构的工具,而 tar -tf 这种命令输出的都是文件的全路径,不直观

所以啊,自己写了个 shell 脚本,在不真正解压的情况下,将压缩包里面的文件目录按树形结构打印出来

脚本

功能

脚本支持 tar、tar.gz、tar.bz2、tar.lzma、tar.lz、tar.zst、zip、7z、rar 这几种压缩包,其中,zip、rar 这两种压缩包,需要自己先安装 unzip、unrar 这两个工具

unzip:apt、yum 可以安装

unrar:需要从官网下载安装

wget https://www.rarlab.com/rar/rarlinux-x64-6.0.2.tar.gz
tar -zxf rarlinux-x64-6.0.2.tar.gz
cd rar
sudo make

使用步骤

  • 在 /usr/local/bin 下面新建一个文件 tartree

  • 给 tartree 可执行权限,sudo chmod +x /usr/local/bin/tartree

  • 将文章后面的脚本内容粘贴到 tartree.sh 就能愉快的使用了

用法

用法: tartree [选项] -f <压缩包路径>
示例:
  查看tar包的目录结构:tartree -f ./test.tar
  查看tar.gz包的目录结构:tartree -z -f./test.tar.gz
  查看zip包的目录结构:tartree --zip -f./test.zip

选项:
  -z            处理 tar.gz 格式的压缩包
  -j            处理 tar.bz2 格式的压缩包
  -J            处理 tar.xz 格式的压缩包
  --tar         处理 tar 格式的压缩包,这也是默认的压缩类型
  --lzma        处理 tar.lzma 格式的压缩包
  --lzip        处理 tar.lz 格式的压缩包
  --zstd        处理 tar.zst 格式的压缩包
  --zip         处理 zip 格式的压缩包
  --7z          处理 7z 格式的压缩包
  --rar         处理 rar 格式的压缩包
  --depth <n>   显示的最大层级,n 为正整数
  -h, --help    显示此帮助信息

脚本代码

#!/bin/bash
# 作者:kk

# 初始化变量
compression_type="tar"
tar_file=""
max_depth=0

# 定义帮助信息
help_info="
用法: $0 [选项] -f <压缩包路径>
示例:
  查看tar包的目录结构:$0 -f ./test.tar
  查看tar.gz包的目录结构:$0 -z -f./test.tar.gz
  查看zip包的目录结构:$0 --zip -f./test.zip

选项:
  -z            处理 tar.gz 格式的压缩包
  -j            处理 tar.bz2 格式的压缩包
  -J            处理 tar.xz 格式的压缩包
  --tar         处理 tar 格式的压缩包,这也是默认的压缩类型
  --lzma        处理 tar.lzma 格式的压缩包
  --lzip        处理 tar.lz 格式的压缩包
  --zstd        处理 tar.zst 格式的压缩包
  --zip         处理 zip 格式的压缩包
  --7z          处理 7z 格式的压缩包
  --rar         处理 rar 格式的压缩包
  --depth <n>   显示的最大层级,n 为正整数
  -h, --help    显示此帮助信息
"

# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help)
            echo "$help_info"
            exit 0
            ;;
        -z)
            compression_type="gz"
            shift
            ;;
        -j)
            compression_type="bz2"
            shift
            ;;
        -J)
            compression_type="xz"
            shift
            ;;
        --tar)
            compression_type="tar"
            shift
            ;;
        --lzma)
            compression_type="lzma"
            shift
            ;;
        --lzip)
            compression_type="lz"
            shift
            ;;
        --zstd)
            compression_type="zst"
            shift
            ;;
        --zip)
            if [ -z "`command -v unzip`" ]; then
                echo "错误: 未安装 unzip 工具。"
                exit 250
            fi
            compression_type="zip"
            shift
            ;;
        --7z)
            if [ -z "`command -v 7z`" ]; then
                echo "错误: 未安装 7z 工具。"
                exit 250
            fi
            compression_type="7z"
            shift
            ;;
        --rar)
            if [ -z "`command -v unrar`" ]; then
                echo "错误: 未安装 unrar 工具。"
                exit 250
            fi
            compression_type="rar"
            shift
            ;;
        -f)
            if [[ -n "$2" ]]; then
                tar_file="$2"
                shift 2
            else
                echo "错误: -f 参数后需要跟压缩包路径。"
                exit 1
            fi
            ;;
        --depth)
            if [[ -n "$2" && "$2" =~ ^[1-9][0-9]*$ ]]; then
                max_depth="$2"
                shift 2
            else
                echo "错误: --depth 参数后需要跟一个正整数。"
                exit 1
            fi
            ;;
        *)
            echo "错误: 未知参数 $1。"
            exit 1
            ;;
    esac
done

# 检查 -f 参数是否提供了压缩包路径
if [[ -z "$tar_file" ]]; then
    echo "错误: 必须提供 -f 参数指定压缩包路径。"
    exit 1
fi

# 检查压缩包是否存在
if [ ! -f "$tar_file" ]; then
    echo "错误: 指定的压缩包 $tar_file 不存在。"
    exit 1
fi

# 根据压缩类型选择合适的命令
case "$compression_type" in
    tar)
        command="tar -tf"
        ;;
    gz)
        command="tar -ztf"
        ;;
    bz2)
        command="tar -jtf"
        ;;
    xz)
        command="tar -Jtf"
        ;;
    lzma)
        command="tar --lzma -tf"
        ;;
    lz)
        command="tar --lzip -tf"
        ;;
    zst)
        command="tar --zstd -tf"
        ;;
    zip)
        command="unzip -l"
        ;;
    7z)
        command="7z l"
        ;;
    rar)
        command="unrar l"
        ;;
    *)
        echo "错误: 不支持的压缩类型。"
        exit 1
        ;;
esac

current_row=0
_zip_7z_rar_status=0
# 定义一个map存放出现过的前缀
declare -A prefix_map
$command "$tar_file" | while IFS= read -r line; do
    ((current_row++))
    # zip, 7z, rar 特殊处理
    if [[ "$compression_type" =~ ^(zip|7z|rar)$ ]]; then
        # zip, 7z, rar 真正有用的是 ------ ... ------ 包裹着的内容,所以要去掉前面几行和后面几行
        if [[ $_zip_7z_rar_status -eq 0 ]]; then
            if [[ "${line:0:6}" == "------" ]]; then
                _zip_7z_rar_status=1
            fi
            continue
        fi
        if [[ $_zip_7z_rar_status -eq 1 ]]; then
            if [[ "${line:0:6}" == "------" ]]; then
                exit 0
            fi
        fi

        # 针对 zip、7z、rar 输出格式,提取文件名部分
        if [[ "$compression_type" == "zip" ]]; then
            line=${line:30}
        elif [[ "$compression_type" == "7z" ]]; then
            line=${line:53}
        elif [[ "$compression_type" == "rar" ]]; then
            line=${line:41}
        fi
    fi

    # 如果这一行里没有 / ,则手动加上,避免下面逻辑无法按 / 切割字符串
    if [[ $line != *"/"* ]]; then
        line="/${line}"
    fi
    # 如果最后一个字符是 /,则手动再拼接上一个空格,避免下面逻辑无法按 / 切割字符串
    if [[ "${line: -1}" == "/" ]]; then
        line="${line} "
    fi
    # 按 / 切割字符串
    IFS='/' read -r -a parts <<< "$line"
    current_prefix=""
    indent="|"
    current_depth=0
    part_index=-1
    for part in "${parts[@]}"; do
        ((part_index++))
        # 是否是最后一个元素
        is_last_part=$((part_index == ${#parts[@]} - 1))
        # 去掉前后空白符号
        part="${part#"${part%%[![:space:]]*}"}"
        part="${part%"${part##*[![:space:]]}"}"
        # 只有不是空字符串才处理
        if [[ -n $part ]]; then
            # 当前深度加 1
            ((current_depth++))
            if [ $max_depth -gt 0 ] && [ $current_depth -gt $max_depth ]; then
                continue
            fi
            # 拼接出当前前缀
            current_prefix="${current_prefix}/${part}"
            tmp_value=${prefix_map[$current_prefix]}
            # 前缀没出现过,则将当前层级打印出来
            if [[ "$tmp_value" = "" ]]; then
                tmp_echo_part="${part}"
                # 如果当前并不是最后一层,那这一层肯定是目录,则输出的时候加上 /
                if [[ ! $is_last_part -eq 1 ]]; then
                    tmp_echo_part="${tmp_echo_part}/"
                fi
                echo "${indent}--${tmp_echo_part}"
                # 标记一下当前层级出现过了
                prefix_map[$current_prefix]="1"
            fi
            # 增加一层缩进
            indent="${indent}   |"
        fi
    done
done