项目地址: [spf13/pflag at v1.0.5 (github.com)](https://github.com/spf13/pflag/tree/v1.0.5)
介绍
Cobra 是用 go 语言编写的一款命令行工具,也是 go 的常用库之一,它提供了一个简单的接口来创建类似于git和go工具的强大现代CLI接口。
项目的作者 Steve Francia 是前 go 团队成员,其用 go 创作了很多热门的作品如 hugo、vim、viper再到我今天要介绍的cobra。作者的地址放在下面:spf13 (Steve Francia) (github.com)
关于 go 的描述在项目主页里有这样一句话——pflag 是Go的 flag 包的替代物,实现了POSIX/ gnu风格的——flag。pflag在与Go语言相同风格的BSD许可下可用,可以在license文件中找到。
这可以从一个小demo中看出区别:
flag包
packagemain
import (
"flag"
"fmt"
)
funcmain() {
flag.Parse()
args :=flag.Args()
iflen(args) <=0 {
fmt.Println("Usage: admin-cli [command]")
return
}
switchargs[0] {
case"help":
// ...
case"export":
//...
iflen(args) ==3 { // 导出到文件
// todo
} elseiflen(args) ==2 { // 导出...
// todo
}
default:
//...
}
}
packagemain
import (
"fmt"
"github.com/spf13/cobra"
"os"
)
// rootCmd represents the base command when called without any subcommands
varrootCmd=&cobra.Command{
Use: "api",
Short: "A brief description of your application",
Long: `A longer description `,
}
// 命令一
varmockMsgCmd=&cobra.Command{
Use: "mockMsg",
Short: "批量发送测试文本消息",
Long: ``,
Run: func(cmd*cobra.Command, args []string) {
fmt.Println("mockMsg called")
},
}
// 命令二
varexportCmd=&cobra.Command{
Use: "export",
Short: "导出数据",
Long: ``,
Run: func(cmd*cobra.Command, args []string) {
fmt.Println("export called")
},
}
funcExecute() {
err :=rootCmd.Execute()
iferr!=nil {
os.Exit(1)
}
}
funcinit() {
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
rootCmd.AddCommand(mockMsgCmd)
rootCmd.AddCommand(exportCmd)
exportCmd.Flags().StringP("out", "k", "./backup", "导出路径")
}
funcmain() {
Execute()
}
我们可以明显得发现,使用了 github.com/spf13/cobra包后,我我们再也不用处理各种参数的组合了,只需要编写自己的业务逻辑代码即可。
概念
Cobra是建立在一个由 commands, arguments & flags 组成的结构上。
Commands 代表 action(行为,具体操作的动作),Args 是事物,Flags是这些方法的修饰语。
最好的应用程序在使用时就像句子一样,因此,用户凭直觉就知道如何与它们互动。
要遵循的模式是 APPNAME VERB NOUN --ADJECTIVE 或者 APPNAME COMMAND ARG --FLAG。
几个好的实际的例子可以更好地说明这一点。
在下面的例子中,'server'是一个 command ,'port'是一个 flag 。
hugo server --port=1313
在这个命令中,我们告诉 Git 克隆直链 url。
git clone URL --bare
Commands
command(命令)是应用程序的中心点。应用程序支持的每一个交互都将包含在一个命令中。一个命令可以有子命令,也可以选择运行一个 action 。
在上面的例子中,"server "是一个命令。
Flags
flag 是一种修改命令行为的方法。Cobra 支持完全符合 POSIX 标准的 flags ,以及 Go 的 flag package 。一个 Cobra 命令可以定义持续到子命令的 flags ,以及只对该命令有效的 flags 。如下:
./learncobra father --help
./learncobra father son --help
在上面的例子中,'help'是 flag 。(需要注意的是,两个'help'分别用于输出 father 命令和 son 命令的说明文档),当然严格意义上来讲 father 命令本身也是一个子命令,其位于 root 命令下。具体的命令父子关系问题稍后再讨论。
flag 功能是由 pflag 库提供的,它是 pflag library 的分叉,保持了相同的接口,同时增加了 POSIX 兼容性。
arguments
表示数值
安装
使用 Cobra 很容易。首先,使用 go get 来安装最新版本的库。
go get -u github.com/spf13/cobra@latest
注:由于库迭代速度比较快,可能会发生库转移,命令改变等等,具体安装命令建议到项目地址处查看
接下来,在你的应用程序中包含 Cobra:
import "github.com/spf13/cobra"
使用方法
cobra-cli是一个命令行程序,用于生成 cobra 应用程序和命令文件。它将引导你的应用程序脚手架,快速开发基于 Cobra 的应用程序。它是将 Cobra 纳入你的应用程序的最简单方法。
它可以通过运行以下指令来安装:
go install github.com/spf13/cobra-cli@latest
Cobra生成器
Cobra 提供了自己的程序,它将创建您的应用程序并添加您想要的任何命令。这是将 Cobra 纳入你的应用程序的最简单方法。
用命令 go install github.com/spf13/cobra-cli@latest 来安装 cobra 生成器。Go 会自动将其安装在你的 $GOPATH/bin 目录中,该目录应该在你的 $PATH 中。
一旦安装完毕,你的 cobra-cli 命令便可以使用。在命令行中输入 cobra-cli 来确认。
目前Cobra生成器只支持两种操作。
cobra-cli init
cobra-cli init [app] 命令将为您创建初始的应用程序代码。这是一个非常强大的应用程序,它将为您的程序填充正确的结构,使您可以立即享受到 Cobra 的所有好处。它还可以将你指定的许可证应用于你的应用程序。
随着 Go 模块的引入,Cobra 生成器已被简化以利用模块的优势。Cobra 生成器在 Go 模块中工作。
启动一个 moudle
如果你已经创建了一个模块,跳过这一步。
如果你想初始化一个新的 Go 模块。
创建一个新的目录
cd进入该目录
运行 go mod init <MODNAME>。
例如
cd $HOME/code
mkdir myapp
cd myapp
go mod init github.com/spf13/myapp
初始化一个Cobra CLI应用程序
在 Go 模块中运行 cobra-cli init 。这将创建一个新的裸机项目供你编辑。
你应该能够立即运行你的新应用程序。用 go run main.go 试试。
你要打开并编辑'cmd/root.go'并提供你自己的描述和逻辑。
例如
cd $HOME/code/myapp
cobra-cli init
go run main.go
cobra-cli init
也可以从一个子目录中运行,这可以根据 cobra 生成器本身的组织结构看出。如果你想把你的应用程序代码和库的代码分开,这很有用。
可选的 flags:
你可以用 --author flag 向它提供作者名字。例如,cobra-cli init --author "Steve Francia spf@spf13.com"
你可以用 --license 来提供使用许可,例如:cobra-cli init --license apache。
使用 --viper 标志来自动设置 viper
Viper 是 Cobra 的一个伙伴,旨在提供对环境变量和配置文件的简单处理,并将它们与应用程序的标志无缝连接。
向项目中添加命令
一旦cobra应用程序被初始化,你就可以继续使用Cobra生成器来为你的应用程序添加额外的命令。做到这一点的命令是 cobra-cli add 。
比方说,你创建了一个应用程序,你想为它添加以下命令。
app serve
app config
app config create
在你的项目目录中(你的 main.go 文件所在的位置),你将运行以下命令。
cobra-cli add serve
cobra-cli add config
cobra-cli add create -p 'configCmd'
cobra-cli add 支持所有与 cobra-cli init 一样的可选 flags(如上所述)。
你会注意到,最后这条命令有一个 -p 标志。这是用来给新添加的命令分配一个父命令的。在这个例子中,我们想把 "create" 命令分配给 "config" 命令。如果没有指定,所有的命令都有一个默认的父命令,即 rootCmd 。
默认情况下,cobra-cli 会将 Cmd 附加到所提供的名称上,并使用这个名称作为内部变量名称。当指定父级时,请确保与代码中使用的变量名称相匹配。
注意:命令名称使用 camelCase(不是 snake_case/kebab-case)。否则,你会遇到错误。例如,cobra-cli add add-user 是不正确的,但 cobra-cli add addUser 是有效的。
一旦你运行了这三个命令,你就会有一个类似以下的应用程序结构。
▾ app/
▾ cmd/
config.go
create.go
serve.go
root.go
main.go
在这一点上,你可以运行 go run main.go ,它将运行你的应用程序。go run main.go serve、go run main.go config、go run main.go config create 以及 go run main.go help serve 等都可以工作。当然,如果你用的是 go build 命令来运行程序,那么 ./moduleName serve、./moduleName create 以及 ./moduleName serve 等都可以工作( moduleName 是当前文件的模块名)。
现在你已经有了一个基于Cobra的基本应用程序,并开始运行。下一步是在cmd中编辑这些文件,为你的应用程序进行定制。
关于使用Cobra库的完整细节,请阅读The Cobra User Guide.。
尽情享受吧!
配置cobra生成器
如果你提供一个简单的配置文件,Cobra 生成器将更容易使用,这将帮助你消除在标志中反复提供的一堆信息。
一个例子~/.cobra.yaml文件。
author: Steve Francia <spf@spf13.com>
license: MIT
useViper: true
你也可以使用内置的许可证。例如,GPLv2、GPLv3、LGPL、AGPL、MIT、2-Clause BSD 或 3-Clause BSD。
你可以通过将 license 设置为 none 来指定没有 license,或者你可以指定一个自定义 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 属性生成的。LICENSE 文件的内容是:
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 属性被用作许可证头文件。没有进行插值。这是 go 文件头的例子。
/*
Copyright © 2020 Steve Francia <spf@spf13.com>
This file is part of CLI application foo.
*/
入门实践
在安装了 cobra-cli 生成器后,指向 init 初始化创建项目
cobra-cli init
将在当前文件夹自动生成以下的目录结构
├── go.mod
├── go.sum
├── LICENSE
├── main.go
│
└───cmd
└── root.go
main.go:
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package main
import "test/cmd"
func main() {
cmd.Execute()
}
root.go(有删减):
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "test",
Short: "A brief description of your application",
Long: `A longer description `,
//Run: func(cmd *cobra.Command, args []string) {
// fmt.Println("api called")
//},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// 全局flag
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.api.yaml)")
// local flag
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
如果此刻运行程序,不指定参数,会默执行 rootCmd ,打印使用说明
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
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.
可见,初始的,Cobra Application 的 RootCmd 不具备任何逻辑功能,仅仅是输出一些描述信息。
新增 command
RootCmd 初始仅有一个 --help flag,下面让我们来新增一个命令试试,这本身也是命令行程序的魅力,即可以通过各种不同的参数执行不同的动作。
语法:
cobra-cli add [command]
实例
cobra-cli add mock-msg
mockMsg created at E:\backend\Go_backend\test
可以看到指令里我们希望创建的 command 名为 mock-msg ,但实际创建的 command 名为 mockMsg(驼峰命名法),这点在上文介绍向项目中添加命令时提到过。
此时,在cmd下会多一个文件(mockMsg.go),内容如下:(有删改)
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var mockMsgCmd = &cobra.Command{
Use: "mockMsg",
Short: "A brief description of your command",
Long: `mock msg command`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("mockMsg called")
},
}
func init() {
rootCmd.AddCommand(mockMsgCmd)
}
再次编译程序,并执行 rootCmd
go build
./test
会发现多了一个命令,且多了 Flags 提示
...
Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
mockMsg A brief description of your command
Flags:
-h, --help help for test
-t, --toggle Help message for toggle
...
执行 mockMsg 命令:
./test mockMsg
mockMsg called
可以发现程序输出了 mockMsg called ,这意味着它正常运行了,此时就可以在生成的 mockMsg.go: Run() 函数中,放你自己的业务代码了。
如何显示自己的命令用法
上面新增了一个命令 mockMsg ,通过 ./test -h 打印了命令的 help 内容,但是 Use 里面指定的内容打印到哪里去了呢?
这个时候,需要针对 Command 再指定 help,此时就能打印这个命令的具体用法了。(./test -h打印的是 rootCmd 的 help 内容,./test mockMsg -h 打印的是 mockMsg 内的 help 内容,两个 command 是父子关系)
新增 flags
让我们在 mockMsg.go: init() 中添加以下代码
func init() {
mockmsgCmd.Flags().Int32P("goroutine", "g", 1, "并发routine数量")
mockmsgCmd.Flags().Int32P("packet", "p", 20, "每个routine一秒写入mq的数量")
rootCmd.AddCommand(mockmsgCmd)
}
注:经试验,rootCmd.AddCommand(mockmsgCmd)和前两句代码的顺序可互换,不影响程序的运行和实现。
在 &cobra.Command 中进行修改
// mockMsgCmd represents the mockMsg command
var mockMsgCmd = &cobra.Command{
Use: "mockMsg",
Short: "Mass produce mq messages",
// Long: `A longer description `,
Run: func(cmd *cobra.Command, args []string) {
// GetXXX() 内要写全名名
g, _ := cmd.Flags().GetInt32("goroutine")
p, _ := cmd.Flags().GetInt32("packet")
fmt.Println("mockmsg called,flags:g=", g, ",p=", p, ",args:", args)
},
}
再次编译程序,并执行 mockMsg
go build
./test mockMsg -h
可以得到
Mass produce mq messages
Usage:
test mockMsg [flags]
Flags:
-g, --goroutine int32 并发routine数量 (default 1)
-h, --help help for mockMsg
-p, --packet int32 每个routine一秒写入mq的数量 (default 20)
其中-g和-p是新增的2个flag,后面紧跟着的是它的接收值类型以及用途
执行 mockMsg 命令
./test mockMsg -p 322 -g 5 args1 args2
mockmsg called,flags:g= 5 ,p= 322 ,args: [args1 args2]
发现回显正常,值被接受收了,并打印了 args 构成的数组。
父子命令
cobra 里实现父子命令非常简单,且有两种方式,下面逐一介绍。
1.通过自动创建生成子命令
自动创建子命令是基于 Cobra 生成器的,通过简单的 cobra-cli add father,就能创建一个命令,该命令一经创建就被默认设为 rootCmd 的子命令,具体的代码可以在 father.go: init() 方法中看到,一般如下:
func init() {
rootCmd.AddCommand(fatherCmd)
}
该句代码是系统默认生成的,让我们通过一个 demo 来更加升入了解父子命令
让我们添加一对父子命令,并输出目录结构
cobra-cli add father
cobra-cli add son
tree /f
可以看到我们的目录结构如下
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── test.exe
│
└───cmd
├── father.go
├── mockMsg.go
├── root.go
└─── son.go
让我们将 father.go 修改为如下:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("father end")
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
rootCmd.AddCommand(fatherCmd)
}
试着编译运行程序,并使用 father 指令
go build
./test father -S 9 arg1
返回结果:
father start
father called,flags:S=9,args:[arg1]
son start
再让我们修改 son.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var sonCmd = &cobra.Command{
Use: "son",
Short: "I'm father's son",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("son start\n\n")
s, _ := cmd.Flags().GetInt32("source2")
t, _ := cmd.Flags().GetString("string2")
fmt.Print("son called,flags:s=", s, ",t=", t, ",args:", args, "\n\n")
fmt.Println("son end")
},
}
func init() {
// 此处对三条语句的顺序并没有先后要求,都可以正常执行
fatherCmd.AddCommand(sonCmd)
sonCmd.Flags().Int32P("source2", "s", 2, "Int32 directory to read from")
sonCmd.Flags().StringP("string2", "t", "", "String directory to read from")
}
试着编译运行程序,并使用 son 指令
go build
./test father son -s 9 -t hahaha arg1 arg2
返回结果:
son start
son called,flags:s=9,t=hahaha,args:[arg1 arg2]
son end
有关 Int32P、StringP、GetInt32、GetString 等方法的描述详情,请到 github.com/spf13/pflag 包查看。
这就是一个简单的自动创建子命令的案例。
手动创建子命令
存在自动创建子命令,当然也可以手动创建子命令,相信通过前面的种种学习,我们对一个 command 的语法格式应该已经很熟悉了。
让我们再对 father.go 稍加修改,在其内手动添加一个子命令 cmdTimes 用于根据 times(次数)打印输入的单词。
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("son end")
},
}
var echoTimes int
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.`,
Run: func(cmd *cobra.Command, args []string) {
for i := 0; i < echoTimes; i++ {
fmt.Println("Echo: " + strings.Join(args, " "))
}
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
// 给 father 子命令添加一个 cmdTimes 子命令
fatherCmd.AddCommand(cmdTimes)
rootCmd.AddCommand(fatherCmd)
}
试着编译运行程序,并使用 son 指令
go build
./test father times -t 10 args1 args2
返回结果:
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
Echo: args1 args2
这样我们就成功手动创建了一个子命令。
子命令的验证
NoArgs //如果存在任何位置参数,该命令将报错
ArbitraryArgs //该命令会接受任何位置参数
OnlyValidArgs //如果有任何位置参数不在命令的 ValidArgs 字段中,该命令将报错
MinimumNArgs(int) //至少要有 N 个位置参数,否则报错
MaximumNArgs(int) //如果位置参数超过 N 个将报错
ExactArgs(int) //必须有 N个位置参数,否则报错
ExactValidArgs(int) //必须有 N 个位置参数,且都在命令的 ValidArgs 字段中,否则报错
RangeArgs(min, max) //如果位置参数的个数不在区间 min 和 max 之中,报错
我们可以通过一个简单的案例来了解
如果我们通过以下指令来运行 father 命令下的 times 子命令
./test father times -t 10
会得到结果:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
Echo:
但我们知道这不是我们想要的,因为我们没有传递任何 arg 的值,我们希望在这种情况下,程序能给予我们提示信息。
现在将 father.go 的内容稍作修改,也就是在 cmdTimes 内添加如下验证
Args: cobra.MinimumNArgs(1)
现在我们的 father.go 变成了如下的样子:
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
// fatherCmd represents the father command
var fatherCmd = &cobra.Command{
Use: "father",
Short: "I'm son's father",
Long: `This is a parent-child command relationship demo.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Print("father start\n\n")
// 此处左侧的变量名必须和 shorthand 或者 name 保持一致
// GetXXX() 内要写全名
source, _ := cmd.Flags().GetInt32("source")
fmt.Print("father called,flags:S=", source, ",args:", args, "\n\n")
fmt.Println("son start")
},
}
var echoTimes int
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, " "))
}
},
}
func init() {
// 需要注意的是此处的 shorthand 只能为单个英文字符(包含大小写)
// func (f *FlagSet) StringP(name, shorthand string, value string, usage string) *string
fatherCmd.Flags().Int32P("source", "S", 8, "Int32 directory to read from")
cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
// 给 father 子命令添加一个 cmdTimes 子命令
fatherCmd.AddCommand(cmdTimes)
rootCmd.AddCommand(fatherCmd)
}
让我们再次试着编译运行程序,并使用 times 命令
go build
./test father times -t 10
返回结果:
Error: requires at least 1 arg(s), only received 0
Usage:
test father times [string to echo] [flags]
Flags:
-h, --help help for times
-t, --times int times to echo the input (default 1)
可以看到,当我们在为传递任何 arg 信息时,程序会报错并给予我们提示,即我们至少需要传递一个 arg 让其接收(因为我们对 args 参数做的验证是至少传递一个参数————Args: cobra.MinimumNArgs(1),若要求为 2,则至少传递 2 个)
而当我们试着传递一个参数的时候
./test father times -t 10 arg1
会返回结果:
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
Echo: arg1
可以发现命令 times 正常执行了。
有关 cobra 的介绍就到这里了,希望能对你带来帮助!
参考文献:
go Cobra命令行工具入门 Go和分布式IM的博客-CSDN博客 go cobra
Go 语言编程 — Cobra 指令行工具 范桂飓的博客-CSDN博客 cobra go语言
Golang学习(二十七)强大的命令行工具cobra 默子昂的博客-CSDN博客 cobra golang
golang常用库之-命令行工具 Cobra(眼镜蛇) 西京刀客的博客-CSDN博客 spf13/cobra