golang 命令行解析库cobra的使用

golang 命令行解析库cobra的使用

​ 关于go语言的命令行解析,标准库flag提供的功能比较少,不能满足我的使用需求,所以就需要寻找第三方解决方案了.我选择cobra是因为它支持 sub command子命令,满足我的使用需求,而且被很多大公司使用(比如github cli),安全性应该没什么问题.

Overview

cobra提供简单的接口用于创建类似于git或者go官方工具的命令行工具

cobra可以快速创建基于cobra的应用的脚手架.

  • 简单的基于子命令的命令行接口: app server, app fetch等等
  • 完全兼容POSIX的flag(包括短和长版本)
  • 嵌套子命令
  • 全局,本地和级联flag
  • 快速生成应用脚手架cobra init appname & cobra add cmdname
  • 智能提示功能 (app srver... did you mean app server?)
  • 自动基于command和flag生成帮助
  • 自动添加帮助flag -h, --help
  • 自动添加shell自动补全功能(bash, zsh, fish, powershell)
  • 自动生成帮助文档
  • command 别名功能
  • 灵活定制help,usage等等
  • 可选 集成viper用于构建12-factor app

Concept

cobra基于结构化的command,arguments和flag进行构建

Commands代表行动

Args表示事物

Flags 是行动的修饰符

最好的应用使用起来像读语句一样,用户会自然而然地知道如何使用这个应用

遵循的原则是

APPNAME VERB NOUN --ADJECTIVE.或者APPNAME COMMAND ARG --FLAG

在接下来的例子里, server是一个command,port是一个flag

hugo server --port=1313

下一个命令我们在告诉git,从目标url 克隆一个裸仓库(只拷贝.git子目录)

git clone URL --bare

Commands

command是应用的核心,每次应用的互动都会包含一个command,一个command可以存在可选的子command,在这个例子hugo server --port=1313里,server就是一个command

More about cobra.Command

Flags

flag是修改command行为的一种方式,cobra提供完全POSIX兼容的flag就像gp官方命令行工具一样.

cobra的command可以定义影响所有子命令的flag或者只影响一个命令的command

.在这个例子里,hugo server --port=1313 port是一个flag

flag功能由pflag库提供.

Installing

使用go get下载最新版

go get -u github.com/spf13/cobra

导入项目:

import "github.com/spf13/cobra"

Getting Started

典型的项目结构

  ▾ appName/
    ▾ cmd/
        add.go
        your.go
        commands.go
        here.go
      main.go

通常main行数比较简单,起到初始化cobra的作用

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

Using the Cobra Generator

cobra自身提供的程序可以帮助你创建app和添加command,这是最简单的添加cobra到你的应用程序的方式

Here you can find more information about it.

Using the Cobra Library

手动实现cobra,你需要创建一个main.go和rootCmd文件,你需要视情况添加额外的command

Create rootCmd

cobra不需要构造函数,简单地创建你的command即可.

通常把rootCmd放到 app/cmd/root.go下:

var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

可以在init函数中定义flag或者修改配置

For example cmd/root.go:

package cmd

import (
    "fmt"
    "os"

    homedir "github.com/mitchellh/go-homedir"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var (
    // Used for flags.
    cfgFile     string
    userLicense string

    rootCmd = &cobra.Command{
        Use:   "cobra",
        Short: "A generator for Cobra based Applications",
        Long: `Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    }
)

// Execute executes the root command.
func Execute() error {
    return rootCmd.Execute()
}

func init() {
    cobra.OnInitialize(initConfig)

    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra.yaml)")
    rootCmd.PersistentFlags().StringP("author", "a", "YOUR NAME", "author name for copyright attribution")
    rootCmd.PersistentFlags().StringVarP(&userLicense, "license", "l", "", "name of license for the project")
    rootCmd.PersistentFlags().Bool("viper", true, "use Viper for configuration")
    viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
    viper.BindPFlag("useViper", rootCmd.PersistentFlags().Lookup("viper"))
    viper.SetDefault("author", "NAME HERE <EMAIL ADDRESS>")
    viper.SetDefault("license", "apache")

    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(initCmd)
}

func er(msg interface{}) {
    fmt.Println("Error:", msg)
    os.Exit(1)
}

func initConfig() {
    if cfgFile != "" {
        // Use config file from the flag.
        viper.SetConfigFile(cfgFile)
    } else {
        // Find home directory.
        home, err := homedir.Dir()
        if err != nil {
            er(err)
        }

        // Search config in home directory with name ".cobra" (without extension).
        viper.AddConfigPath(home)
        viper.SetConfigName(".cobra")
    }

    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

Create your main.go

定义好root command后,你需要在main函数里执行execute

在cobra app中,通常main.go文件中的内容比较少,只用于初始化cobra

In a Cobra app, typically the main.go file is very bare. It serves, one purpose, to initialize Cobra.

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

Create additional commands

可以添加新的command,通常我们把每个command放到一个文件里,放在cmd/目录下

如果你想添加一个version command,你可以创建cmd/version.go,然后像下面这个例子一样填充内容

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

func init() {
  rootCmd.AddCommand(versionCmd)
}

var versionCmd = &cobra.Command{
  Use:   "version",
  Short: "Print the version number of Hugo",
  Long:  `All software has versions. This is Hugo's`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("Hugo Static Site Generator v0.9 -- HEAD")
  },
}

Returning and handling errors

If you wish to return an error to the caller of a command, RunE can be used.

如果你想针对command的调用返回一个error,可以使用RunE

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

func init() {
  rootCmd.AddCommand(tryCmd)
}

var tryCmd = &cobra.Command{
  Use:   "try",
  Short: "Try and possibly fail at something",
  RunE: func(cmd *cobra.Command, args []string) error {
    if err := someFunc(); err != nil {
    return err
    }
    return nil
  },
}

这样在execute函数调用的时候,error就会被捕获

Working with Flags

通过flag传入参数可以控制comand的执行行为

Assign flags to a command

由于flag定义后,可以在很多不同的地方被使用,我们需要定义一个变量来接受flag

var Verbose bool
var Source string

Persistent Flags

persistent(持久性) flag,可以用于分配给他的命令以及所有子命令.

下面例子里的verbose作用是让命令行输出的信息更详细,所有的子命令都会支持 verbos flag

rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

Local Flags

local flag ,本地分配,只会分配给那个特定的command

localCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")

Local Flag on Parent Commands

默认情况下 cobra只会在目标command的基础上解析local flag,它的parent command会忽略解析.开启Command.TraverseChildren选项后,cobra就会执行目标command之前,在每个command上解析local command

command := cobra.Command{
  Use: "print [OPTIONS] [COMMANDS]",
  TraverseChildren: true,
}

Bind Flags with Config

你也可以使用viper绑定flag

var author string

func init() {
  rootCmd.PersistentFlags().StringVar(&author, "author", "YOUR NAME", "Author name for copyright attribution")
  viper.BindPFlag("author", rootCmd.PersistentFlags().Lookup("author"))
}

在这个persistent flag的例子中persistent flag author ,用viper进行了绑定, 注意,当用户没有使用 --author flag时,就不会从配置文件中读取值

More in viper documentation.

Required flags

Flags are optional by default. If instead you wish your command to report an error when a flag has not been set, mark it as required:

默认情况下flag时可选的,如果你希望缺失flag的时候报错,可以标记为Required flags,如下

rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")

或者对于PersistentFlag来说

rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkPersistentFlagRequired("region")

Positional and Custom Arguments

位置参数的验证,可以使用commnad的 args字段

下列验证器是内置的:

  • NoArgs - 如果没有任何参数,command会报错
  • ArbitraryArgs - command接受任何参数
  • OnlyValidArgs - 如果有任何位置参数不在 validArgs的范围里面, command会报错
  • MinimumNArgs(int) - 当位置参数的个数少于n时,command 会报错
  • MaximumNArgs(int) - 当位置参数的个数大于n时,,command会报错
  • ExactArgs(int) - 当位置参数的个数不正好是n个时,command会报错
  • ExactValidArgs(int) - 当不是正好有n个位置参数或者有任何位置参数不属于ValidArgs,command会报错.
  • RangeArgs(min, max) - 如果参数的个数不是在min和max之间,command会报错

关于自定义验证器的一个例子:

var cmd = &cobra.Command{
  Short: "hello",
  Args: func(cmd *cobra.Command, args []string) error {
    if len(args) < 1 {
      return errors.New("requires a color argument")
    }
    if myapp.IsValidColor(args[0]) {
      return nil
    }
    return fmt.Errorf("invalid color specified: %s", args[0])
  },
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("Hello, World!")
  },
}

Example

在下面的例子里,定义了3个command,其中有两个时最高级别的command,另一个(cmdTimes)时其中一个的子command.在这个例子里,root不能执行意味着需要subcommand,这是通过不给rootCmd提供Run参数来实现的.

More documentation about flags is available at https://github.com/spf13/pflag

执行go run main.go echo dasda dasdaa dadsa, 执行go run main.go print dasada dasdafesf fasfdasf

times是echo的子命令所以执行命令是这样的go run main.go echo times -t 10 dsadas dasda dasda

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  var echoTimes int

  var cmdPrint = &cobra.Command{
    Use:   "print [string to print]",
    Short: "Print anything to the screen",
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Echo: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

  var rootCmd = &cobra.Command{Use: "app"}
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

For a more complete example of a larger application, please checkout Hugo.

Help Command

当你有一个新的subcommand,cobra会自动为你的应用添加help command.当用户执行app help时,会调用默认的help command.并且help支持所有其他输入的command.每个command都会自动添加--helpflag

Example

下面的输出是由cobra自动生成的. 除了command和flag你不需要其他东西就能生成help command

$ cobra help

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

help就和其他command一样,没有特殊的逻辑或行为.如果你想的话也可以自定义help.

Defining your own help

你可以提供自己的和help command或者你自己的模板,只需要使用下面的函数

cmd.SetHelpCommand(cmd *Command)
cmd.SetHelpFunc(f func(*Command, []string))
cmd.SetHelpTemplate(s string)

The latter two will also apply to any children commands.

后两个,还可以提供给子command

Usage Message

当用户提供了一个无效的flag或者command,cobra会回复一份usage

Example

默认的help,也嵌入了一份usage作为输出

下面是遇到无效flag,输出usage文档的例子

$ cobra --invalid
Error: unknown flag: --invalid
Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration (default true)

Use "cobra [command] --help" for more information about a command.

Defining your own usage

你可以提供自己的usage函数或者模板,这样就会覆盖公用模板了.

cmd.SetUsageFunc(f func(*Command) error)
cmd.SetUsageTemplate(s string)

Version Flag

如果你的root command上有一个version字段,cobra会添加一个--versionflag. 使用--versionflag会使用version模板打印版本到输出.模板可以通过cmd.SetVersionTemplate(s string)函数来自定义

PreRun and PostRun Hooks

在你main函数里的run函数运行之前和之后,也是可以运行函数的.PersistentPreRunPreRun 函数会在Run之前执行,PersistentPostRunPostRun会在Run之后执行. 符合Persistent*Run格式的函数如果子命令没有定义自己的Run,就会继承.这些函数的执行顺序如下:

  • PersistentPreRun
  • PreRun
  • Run
  • PostRun
  • PersistentPostRun

下面是一个两个comand运用了以上所有feature的例子.当子command执行的时候,会执行root command的PersistentPreRun函数而不是root command的PersistentPostRun函数.

package main

import (
  "fmt"

  "github.com/spf13/cobra"
)

func main() {

  var rootCmd = &cobra.Command{
    Use:   "root [sub]",
    Short: "My root command",
    PersistentPreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args)
    },
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside rootCmd PersistentPostRun with args: %v\n", args)
    },
  }

  var subCmd = &cobra.Command{
    Use:   "sub [no options!]",
    Short: "My subcommand",
    PreRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PreRun with args: %v\n", args)
    },
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd Run with args: %v\n", args)
    },
    PostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PostRun with args: %v\n", args)
    },
    PersistentPostRun: func(cmd *cobra.Command, args []string) {
      fmt.Printf("Inside subCmd PersistentPostRun with args: %v\n", args)
    },
  }

  rootCmd.AddCommand(subCmd)

  rootCmd.SetArgs([]string{""})
  rootCmd.Execute()
  fmt.Println()
  rootCmd.SetArgs([]string{"sub", "arg1", "arg2"})
  rootCmd.Execute()
}

Output:

Inside rootCmd PersistentPreRun with args: []
Inside rootCmd PreRun with args: []
Inside rootCmd Run with args: []
Inside rootCmd PostRun with args: []
Inside rootCmd PersistentPostRun with args: []

Inside rootCmd PersistentPreRun with args: [arg1 arg2]
Inside subCmd PreRun with args: [arg1 arg2]
Inside subCmd Run with args: [arg1 arg2]
Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun with args: [arg1 arg2]

Suggestions when "unknown command" happens

当unknow command错误发生时,cobra会自动打印建议.这使得cobra拥有和git命令行类似的效果

$ hugo srever
Error: unknown command "srever" for "hugo"

Did you mean this?
        server

Run 'hugo --help' for usage.

建议会基于每个子command的注册自动生成,实现基于 Levenshtein distance.每个注册的command匹配到最小两个字母的时候,就会显示建议

如果你想要取消建议功能,或者调整字符的距离:

command.DisableSuggestions = true

or

command.SuggestionsMinimumDistance = 1

你也可以设置可能会被建议的command的名字,使用SuggestFor属性.这对那些字符串距离不太近的单词可以起到效果,但是注意不要使用那些你会用来作为别名的名字

$ kubectl remove
Error: unknown command "remove" for "kubectl"

Did you mean this?
        delete

Run 'kubectl help' for usage.

Generating documentation for your command

cobra会基于你的 subcommand和flag生成文档.Read more about it in the docs generation documentation.

Generating shell completions

cobra能生成shell自动补全,Bash, Zsh, Fish, Powershell.Read more about it in Shell Completions.

关于cobra generator的使用

cobra用于创建命令行应用的脚手架工具

执行下面的命令安装

go get github.com/spf13/cobra/cobra

cobra init

创建应用初始代码,提供正确的项目结构,并且自动应用你给定的license

可以在当前应用运行,或者你可以指定一个相对路径,如果目录不存在,他会自己创建一个.

执行需要提供--pkg-name,在非空的目录中也能成功执行

mkdir -p newApp && cd newApp
cobra init --pkg-name github.com/spf13/newApp

or

cobra init --pkg-name github.com/spf13/newApp path/to/newApp

cobra add

用于添加新的command,举个例子

  • app serve
  • app config
  • app config create

在项目目录上执行以下命令即可

cobra add serve
cobra add config
cobra add create -p 'configCmd'

command采用驼峰式命名,不然可能会遇到错误.For example, cobra add add-user is incorrect, but cobra add addUser is valid.

Once you have run these three commands you would have an app structure similar to the following:

执行以上命令后的项目结构

  ▾ app/
    ▾ cmd/
        serve.go
        config.go
        create.go
      main.go

配置文件

提供配置文件可以避免每次使用提供一堆信息.

举个例子 ~/.cobra.yaml :

author: Steve Francia <spf@spf13.com>
license: MIT

你也可以使用其他内置license,比如GPLv2, GPLv3, LGPL, AGPL, MIT, 2-Clause BSD or 3-Clause BSD.

也可以不使用证书,把license设置为none即可,或者你也可以自定义license

author: Steve Francia <spf@spf13.com>
year: 2020
license:
  header: This file is part of CLI application foo.
  text: |
    {{ .copyright }}

    This is my license. There are many like it, but this one is mine.
    My license is my best friend. It is my life. I must master it as I must
    master my life.

上面的copyright部分是用author和year两个属性生成的,

上面的例子生成的内容是:

Copyright © 2020 Steve Francia <spf@spf13.com>

This is my license. There are many like it, but this one is mine.
My license is my best friend. It is my life. I must master it as I must
master my life.

header也会在license头部被使用.

/*
Copyright © 2020 Steve Francia <spf@spf13.com>
This file is part of CLI application foo.
*/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342