您的浏览器过于古老 & 陈旧。为了更好的访问体验, 请 升级你的浏览器
二周 发布于2020年03月30日 17:40

原创 Golang 命令行界面(CLI)的简单使用(apk-info)

4106 次浏览 读完需要≈ 21 分钟 GolangShell

内容目录

命令行界面(CLI)是纯文本的,比如最常用的ls,tar等。而go就是设计这种程序的理想选择,比如我们熟知的KubernetesDocker等。Go可以非常快速地编译为一个二进制文件,可以以一致的风格跨平台工作。用Go编写的程序可以在任何系统上运行,而无需任何现有的库,运行时或依赖项。

当然了,我们不会从零开始,有很多强大的库供我们使用,比如:

  • spf13/cobra,用于创建功能强大的现代CLI应用程序的库,以及用于在Go中生成应用程序和CLI应用程序的程序
  • spf13/viper, 是Go应用程序的完整配置解决方案,旨在在应用程序内工作以处理配置需求和格式
  • urfave/cli,这是用于创建和组织命令行Go应用程序的最小框架
  • delve,一个简单而强大的工具,为程序员习惯于以编译语言使用源代码级调试器
  • chzyer/readline,一个纯Golang实现,在GNU Readline中提供了大多数功能(在MIT许可下)
  • dixonwille/wmenu,是CLI应用程序的一种易于使用的菜单结构,提示用户做出选择
  • spf13/pflag,Go的flag包的替代品,实现POSIX/GNU样式的标志

这里我们使用urface/cli来实现一个简单的命令:apk-info -p ./x.apk -lm来查看apk的信息

说明,这里借助了aapt工具,这个请自行搜索安装哈。

package main

import (
	"fmt"
	"github.com/urfave/cli/v2"
	"log"
	"os"
)

var (
	path  string
	long  bool
	human bool
)

func main() {
	app := &cli.App{
		Name:                   "apk-info",      // 应用名称
		Usage:                  "show apk info", // 功能说明
		UseShortOptionHandling: true,            // 使用短参数处理,比如 ls -lh 中的-lh形式
		// 参数列表
		Flags: []cli.Flag{
			&cli.PathFlag{
				Name:        "path",        // 参数名称,这里是apk的路径
				Aliases:     []string{"p"}, // 参数的别名
				Usage:       "apk `path`",  // 参数说明
				Required:    true,          // 是否必须得
				Destination: &path,         // 参数赋值给path变量
			},
			&cli.BoolFlag{
				Name:        "long",
				Aliases:     []string{"l"},
				Value:       false,
				DefaultText: "false", // 默认值
				Usage:       "show apk more `info`",
				Destination: &long,
			},
			&cli.BoolFlag{
				Name:        "human",
				Aliases:     []string{"m"},
				Value:       false,
				DefaultText: "false",          // 默认值
				Usage:       "human-readable", // 以人类刻度的形式展示,比如12MB
				Destination: &human,
			},
		},
		// 应用执行函数
		Action: func(context *cli.Context) error {
			info, err := aaptApkInfo(path)
			if err != nil {
				log.Fatal(err)
			}
			// 打印执行结果
			fmt.Print(info.String(human, long))
			return nil
		},
	}
	// 运行应用
	if err := app.Run(os.Args); err != nil {
		log.Fatal(err)
	}
}
package main

import (
	"bufio"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strconv"
	"strings"
)

type fileSize int64

const (
	pkgNamePrefix    = "package: name='"
	sdkVersionPrefix = "sdkVersion:'"
	targetSdkVersion = "targetSdkVersion:'"
	permissionPrefix = "uses-permission: name='"
	cpName1Prefix    = "application: label='"
	cpNamePrefix     = "application-label:'"
	cpNameZhPrefix   = "application-label-zh:'"
)

type apkInfo struct {
	CpName           string
	PkgName          string
	VersionCode      int
	VersionName      string
	SdkVersion       int
	TargetSdkVersion int
	Permissions      map[string]struct{}
	Size             fileSize
	Md5              string
}

// 字节的单位转换 保留一位小数
func (fs fileSize) format() string {
	fileSize := float64(fs)
	switch {
	case fileSize < 1024:
		return fmt.Sprintf("%.1fB", fileSize)
	case fileSize < (1024 * 1024):
		return fmt.Sprintf("%.1fKB", fileSize/1024)
	case fileSize < (1024 * 1024 * 1024):
		return fmt.Sprintf("%.1fMB", fileSize/(1024*1024))
	default:
		return fmt.Sprintf("%.1fGB", fileSize/(1024*1024*1024))
	}
}

func (info *apkInfo) permissionStr() string {
	elems := info.Permissions
	switch len(elems) {
	case 0:
		return ""
	case 1:
		for key, _ := range elems {
			return key
		}
	}
	const sep = "|"
	n := len(sep) * (len(elems) - 1)
	for key, _ := range elems {
		n += len(key)
	}

	var b strings.Builder
	b.Grow(n)
	first := true
	for key, _ := range elems {
		if first {
			first = false
			b.WriteString(key)
			continue
		}
		b.WriteString(sep)
		b.WriteString(key)
	}
	return b.String()
}

func (info *apkInfo) String(human, long bool) string {
	const format = "%-16s: %v\n"
	var b strings.Builder
	b.WriteString(fmt.Sprintf(format, "appName", info.CpName))
	b.WriteString(fmt.Sprintf(format, "pkgName", info.PkgName))
	b.WriteString(fmt.Sprintf(format, "md5", info.Md5))
	if human {
		b.WriteString(fmt.Sprintf(format, "size", info.Size.format()))
	} else {
		b.WriteString(fmt.Sprintf(format, "size", info.Size))
	}
	b.WriteString(fmt.Sprintf(format, "versionName", info.VersionName))
	b.WriteString(fmt.Sprintf(format, "versionCode", info.VersionCode))
	b.WriteString(fmt.Sprintf(format, "sdkVersion", info.SdkVersion))
	b.WriteString(fmt.Sprintf(format, "targetSdkVersion", info.TargetSdkVersion))
	if long {
		b.WriteString(fmt.Sprintf(format, "permissions", info.permissionStr()))
	}
	return b.String()
}

func aaptApkInfo(apkPath string) (*apkInfo, error) {
	file, err := os.Open(apkPath)
	if err != nil {
		return nil, err
	}
	cmd := exec.Command("aapt", "d", "badging", apkPath)
	out, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	if err := cmd.Start(); err != nil {
		return nil, err
	}
	reader := bufio.NewReader(out)
	info := new(apkInfo)
	md5 := md5.New()
	size, err := io.Copy(md5, file)
	if err != nil {
		return nil, err
	}
	info.Size = fileSize(size)
	info.Md5 = hex.EncodeToString(md5.Sum(nil))
	info.Permissions = make(map[string]struct{})
	var cpNameZh, cpName1 string
	for {
		line, _, err := reader.ReadLine()
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, err
		}
		s := string(line)
		switch {
		case strings.HasPrefix(s, pkgNamePrefix): // 包名 版本 版本号
			s = s[len(pkgNamePrefix):]
			info.PkgName = s[:strings.Index(s, "'")]
			s = s[len(info.PkgName)+15:]
			versionCodeS := s[:strings.Index(s, "'")]
			info.VersionCode, err = strconv.Atoi(versionCodeS)
			if err != nil {
				return nil, err
			}
			s = s[len(versionCodeS)+15:]
			info.VersionName = s[:strings.Index(s, "'")]
		case strings.HasPrefix(s, sdkVersionPrefix): // SDK版本
			s = s[len(sdkVersionPrefix):]
			sdkVersionS := s[:strings.Index(s, "'")]
			info.SdkVersion, err = strconv.Atoi(sdkVersionS)
			if err != nil {
				return nil, err
			}
		case strings.HasPrefix(s, targetSdkVersion): // SDK版本
			s = s[len(targetSdkVersion):]
			targetSdkVersion := s[:strings.Index(s, "'")]
			info.TargetSdkVersion, err = strconv.Atoi(targetSdkVersion)
			if err != nil {
				return nil, err
			}
		case strings.HasPrefix(s, permissionPrefix): // 权限
			s = s[len(permissionPrefix):]
			s = s[:strings.Index(s, "'")]
			info.Permissions[s] = struct{}{}
		case strings.HasPrefix(s, cpNamePrefix): // 应用名称
			s = s[len(cpNamePrefix):]
			info.CpName = s[:strings.Index(s, "'")]
		case strings.HasPrefix(s, cpNameZhPrefix): // 应用名称
			s = s[len(cpNameZhPrefix):]
			cpNameZh = s[:strings.Index(s, "'")]
		case strings.HasPrefix(s, cpName1Prefix): // 应用名称
			s = s[len(cpName1Prefix):]
			cpName1 = s[:strings.Index(s, "'")]
		}
		if cpNameZh != "" {
			info.CpName = cpNameZh
		}
		if info.CpName == "" && cpName1 != "" {
			info.CpName = cpName1
		}
	}
	return info, nil
}

go build 后咱们看下使用结果:

apk-info -h
NAME:
   apk-info - show apk info

USAGE:
   apk-info [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --path path, -p path  apk path
   --long info, -l info  show apk more info (default: false)
   --human, -m           human-readable (default: false)
   --help, -h            show help (default: false)

apk-info -p ./20191101142847_745_2588.apk
appName         : 源来生活
pkgName         : com.orangedream.sourcelife
md5             : 5bdd79ce23f6c1406f04d37fd393ddb8
size            : 12020323
versionName     : 1.0.1
versionCode     : 101
sdkVersion      : 18
targetSdkVersion: 28

apk-info -p ./20191101142847_745_2588.apk -lm
appName         : 源来生活
pkgName         : com.orangedream.sourcelife
md5             : 5bdd79ce23f6c1406f04d37fd393ddb8
size            : 11.5MB
versionName     : 1.0.1
versionCode     : 101
sdkVersion      : 18
targetSdkVersion: 28
permissions     : android.permission.SYSTEM_ALERT_WINDOW|android.REQUEST_INSTALL_PACKAGES.GET_TASKS|android.permission.READ_SETTINGS

  • CodePlayer技术交流群1
  • CodePlayer技术交流群2

0 条评论

撰写评论