// zhmakeindex: 带中文支持的 makeindex 实现 package main // import "github.com/leo-liu/zhmakeindex" import ( "flag" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "runtime/pprof" "strings" "golang.org/x/text/encoding" "golang.org/x/text/encoding/simplifiedchinese" "golang.org/x/text/encoding/traditionalchinese" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" ) var ( ProgramAuthor = "刘海洋" // Build with option: -ldflags "-x main.Version ? -X main.Revision ?" Version = "???" Revision = "???" ) var debug = log.New(os.Stderr, "DEBUG: ", log.Lshortfile) func init() { log.SetFlags(0) log.SetPrefix("") } func main() { option := NewOptions() option.parse() setupLog(option) log.Printf("zhmakeindex 版本:%s-%s\t作者:%s\n", Version, Revision, ProgramAuthor) if option.cpuprofile != "" { f, err := os.Create(option.cpuprofile) if err != nil { log.Fatal(err) } if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("无法启动 CPU 性能调试:", err) } defer pprof.StopCPUProfile() } if option.style != "" { log.Printf("正在读取格式文件 %s……", option.style) } instyle, outstyle := NewStyles(&option.StyleOptions) in := NewInputIndex(&option.InputOptions, instyle) log.Printf("合并后共 %d 项。\n", len(*in)) log.Println("正在排序……") out := NewOutputIndex(in, &option.OutputOptions, outstyle) log.Println("正在输出……") out.Output(&option.OutputOptions) if option.output != "" { log.Printf("输出文件写入 %s\n", option.output) } if option.log != "" { log.Printf("日志文件写入 %s\n", option.log) } } type Options struct { InputOptions OutputOptions StyleOptions encoding string style_encoding string log string quiet bool cpuprofile string } type InputOptions struct { compress bool stdin bool decoder transform.Transformer // 由 encoding 生成 input []string } type OutputOptions struct { encoder transform.Transformer // 由 encoding 生成 output string sort string page string strict bool disable_range bool } type StyleOptions struct { style string style_decoder transform.Transformer // 由 style_encoding 生成 } func NewOptions() *Options { o := new(Options) flag.BoolVar(&o.compress, "c", false, "忽略条目首尾空格") flag.BoolVar(&o.stdin, "i", false, "从标准输入读取") flag.StringVar(&o.output, "o", "", "输出文件") flag.StringVar(&o.sort, "z", "pinyin", "中文分组排序方式,可以使用 pinyin (reading)、bihua (stroke) 或 bushou (radical)") // flag.StringVar(&o.page, "p", "", "设置起始页码") // 未实现 flag.BoolVar(&o.quiet, "q", false, "静默模式,不输出错误信息") flag.BoolVar(&o.disable_range, "r", false, "禁用自动生成页码区间") flag.BoolVar(&o.strict, "strict", false, "严格区分不同 encapsulated 命令的页码") flag.StringVar(&o.style, "s", "", "格式文件名") flag.StringVar(&o.log, "t", "", "日志文件名") flag.StringVar(&o.encoding, "enc", "utf-8", "读写索引文件的编码") flag.StringVar(&o.style_encoding, "senc", "utf-8", "格式文件的编码") flag.StringVar(&o.cpuprofile, "cpuprofile", "", "将 CPU 性能调试信息写入文件") return o } func (o *Options) parse() { // 设置自定义帮助信息 flag.Usage = Usage flag.Parse() // 整理输入文件 o.input = flag.Args() for i := range o.input { o.input[i] = filepath.Clean(o.input[i]) } // 错误的参数组合 if len(o.input) > 0 && o.stdin { log.Fatalln("不能同时从文件和标准输入流读取输入") } else if len(o.input) == 0 && !o.stdin { // 没有输入文件 flag.Usage() os.Exit(0) } // 不指定输出文件且不使用标准输入时,使用第一个输入文件的主文件名 + ".ind" 后缀 if o.output == "" && !o.stdin { o.output = stripExt(o.input[0]) + ".ind" } // 不指定输入文件且不使用标准输入时,使用第一个输入文件的主文件名 + ".ilg" 后缀 if o.log == "" && !o.stdin { o.log = stripExt(o.input[0]) + ".ilg" } // 不指定格式文件且只有一个输入文件时,尝试使用第一个输入文件的主文件名 + ".mst" 后缀 if o.style == "" && len(o.input) == 1 { // 这里只在当前目录下查找 .mst 文件而不调用 kpathsea 搜索,预先判断文件存在 mst := stripExt(o.input[0]) + ".mst" if _, err := os.Stat(mst); err == nil { o.style = mst } } // 检查并设置 IO 编码 encoding := checkEncoding(o.encoding) o.encoder = encoding.NewEncoder() o.decoder = encoding.NewDecoder() // 检查并设置格式文件编码 styleEncoding := checkEncoding(o.style_encoding) o.style_decoder = styleEncoding.NewDecoder() } // 检查编码名,返回编码 func checkEncoding(encodingName string) encoding.Encoding { var encodingMap = map[string]encoding.Encoding{ "utf-8": encoding.Nop, "utf-16": unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM), "gb18030": simplifiedchinese.GB18030, "gbk": simplifiedchinese.GBK, "big5": traditionalchinese.Big5, } encoding, ok := encodingMap[strings.ToLower(encodingName)] if !ok { log.Printf("编码 '%s' 是无效编码\n", encodingName) var supported string for enc := range encodingMap { supported += enc + " " } log.Fatalf("支持的编码有(不区分大小写):%s\n", supported) } return encoding } func setupLog(option *Options) { var stderr io.Writer = os.Stderr if option.quiet { stderr = ioutil.Discard } if option.log == "" { // 只使用标准错误流 return } flog, err := os.Create(option.log) if err != nil { log.Fatalln(err) } log.SetOutput(io.MultiWriter(stderr, flog)) } // 删除文件后缀名 func stripExt(fpath string) string { ext := filepath.Ext(fpath) return fpath[:len(fpath)-len(ext)] } // 帮助信息 func Usage() { fmt.Fprintln(os.Stderr, `用法: zhmakeindex [-c] [-i] [-o ] [-q] [-r] [-s ] [-t ] [-enc ] [-senc ] [-strict] [-z ] [<输入文件1> <输入文件2> ...]`) fmt.Fprintln(os.Stderr, "\n中文索引处理程序") fmt.Fprintf(os.Stderr, "\n %-10s %-5s %s\n", "选项", "默认值", "说明") flag.VisitAll(func(f *flag.Flag) { if f.Value.String() == "" { fmt.Fprintf(os.Stderr, " -%-11s %-7s %s\n", f.Name, "无", f.Usage) } else { fmt.Fprintf(os.Stderr, " -%-11s %-8s %s\n", f.Name, f.DefValue, f.Usage) } }) fmt.Fprintf(os.Stderr, "\n版本:%s-%s\t作者:%s\n", Version, Revision, ProgramAuthor) }