453 lines
10 KiB
Markdown
453 lines
10 KiB
Markdown
# Fyne + Go 命令执行与输出捕获教程
|
||
|
||
## 1. Fyne 快速入门
|
||
|
||
### 安装 Fyne
|
||
|
||
```bash
|
||
go get fyne.io/fyne/v2@latest
|
||
```
|
||
|
||
### 最小 Fyne 窗口
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/app"
|
||
"fyne.io/fyne/v2/widget"
|
||
)
|
||
|
||
func main() {
|
||
myApp := app.New()
|
||
myWindow := myApp.NewWindow("Hello Fyne")
|
||
|
||
myWindow.SetContent(widget.NewLabel("Hello, World!"))
|
||
myWindow.Resize(fyne.NewSize(400, 300))
|
||
myWindow.ShowAndRun()
|
||
}
|
||
```
|
||
|
||
### 常用控件
|
||
|
||
| 控件 | 说明 |
|
||
|------|------|
|
||
| `widget.NewLabel(text)` | 文本标签 |
|
||
| `widget.NewButton(text, callback)` | 按钮 |
|
||
| `widget.NewEntry()` | 单行输入框 |
|
||
| `widget.NewMultiLineEntry()` | 多行输入框 |
|
||
| `widget.NewCheck(text, callback)` | 复选框 |
|
||
| `widget.NewSelect(options, callback)` | 下拉选择 |
|
||
| `widget.NewProgressBar()` | 进度条 |
|
||
|
||
### 布局容器
|
||
|
||
| 容器 | 说明 |
|
||
|------|------|
|
||
| `container.NewVBox(...)` | 垂直排列 |
|
||
| `container.NewHBox(...)` | 水平排列 |
|
||
| `container.NewBorder(top, bottom, left, right, center)` | 边框布局 |
|
||
| `container.NewGridWithColumns(cols, ...)` | 网格布局 |
|
||
|
||
---
|
||
|
||
## 2. Go 执行外部命令并获取输出
|
||
|
||
### 核心包: `os/exec`
|
||
|
||
Go 标准库 `os/exec` 包提供了执行外部命令的功能。
|
||
|
||
### 2.1 基础方法
|
||
|
||
#### 获取命令输出 (一次性)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"os/exec"
|
||
)
|
||
|
||
func main() {
|
||
// cmd := exec.Command("cmd", "/c", "dir") // Windows
|
||
cmd := exec.Command("ls", "-l") // Linux/macOS
|
||
|
||
output, err := cmd.Output() // 返回 []byte
|
||
if err != nil {
|
||
fmt.Println("执行出错:", err)
|
||
return
|
||
}
|
||
fmt.Println(string(output))
|
||
}
|
||
```
|
||
|
||
#### 合并 stdout + stderr
|
||
|
||
```go
|
||
output, err := cmd.CombinedOutput() // stdout + stderr 合并
|
||
```
|
||
|
||
#### 分别获取 stdout 和 stderr
|
||
|
||
```go
|
||
var stdout, stderr bytes.Buffer
|
||
cmd.Stdout = &stdout
|
||
cmd.Stderr = &stderr
|
||
|
||
err := cmd.Run()
|
||
fmt.Println("stdout:", stdout.String())
|
||
fmt.Println("stderr:", stderr.String())
|
||
```
|
||
|
||
### 2.2 实时读取输出 (逐行)
|
||
|
||
适用于长时间运行的命令或需要实时显示进度的场景。
|
||
|
||
```go
|
||
func runCommandWithLiveOutput(name string, args ...string) error {
|
||
cmd := exec.Command(name, args...)
|
||
|
||
stdout, err := cmd.StdoutPipe()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
stderr, err := cmd.StderrPipe()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if err := cmd.Start(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 并发读取 stdout
|
||
go func() {
|
||
scanner := bufio.NewScanner(stdout)
|
||
for scanner.Scan() {
|
||
fmt.Println("[OUT]", scanner.Text())
|
||
}
|
||
}()
|
||
|
||
// 并发读取 stderr
|
||
go func() {
|
||
scanner := bufio.NewScanner(stderr)
|
||
for scanner.Scan() {
|
||
fmt.Println("[ERR]", scanner.Text())
|
||
}
|
||
}()
|
||
|
||
return cmd.Wait()
|
||
}
|
||
```
|
||
|
||
### 2.3 设置超时
|
||
|
||
```go
|
||
func runWithTimeout(timeout time.Duration, name string, args ...string) (string, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||
defer cancel()
|
||
|
||
cmd := exec.CommandContext(ctx, name, args...)
|
||
output, err := cmd.Output()
|
||
if ctx.Err() == context.DeadlineExceeded {
|
||
return "", fmt.Errorf("命令执行超时 (%v)", timeout)
|
||
}
|
||
return string(output), err
|
||
}
|
||
```
|
||
|
||
### 2.4 设置工作目录和环境变量
|
||
|
||
```go
|
||
cmd := exec.Command("make", "build")
|
||
cmd.Dir = "/path/to/project" // 设置工作目录
|
||
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64") // 追加环境变量
|
||
```
|
||
|
||
### 2.5 Windows 特殊处理
|
||
|
||
Windows 下 `cmd` 命令需要 `/c` 前缀:
|
||
|
||
```go
|
||
// Windows
|
||
cmd := exec.Command("cmd", "/c", "dir")
|
||
// 或者使用 PowerShell
|
||
cmd := exec.Command("powershell", "-Command", "Get-ChildItem")
|
||
```
|
||
|
||
通用跨平台封装:
|
||
|
||
```go
|
||
func shellCommand(command string) *exec.Cmd {
|
||
if runtime.GOOS == "windows" {
|
||
return exec.Command("cmd", "/c", command)
|
||
}
|
||
return exec.Command("sh", "-c", command)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Fyne + 命令执行完整示例
|
||
|
||
### 3.1 简单的命令执行 GUI
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/app"
|
||
"fyne.io/fyne/v2/container"
|
||
"fyne.io/fyne/v2/widget"
|
||
"os/exec"
|
||
"runtime"
|
||
)
|
||
|
||
func main() {
|
||
myApp := app.New()
|
||
myWindow := myApp.NewWindow("命令执行器")
|
||
myWindow.Resize(fyne.NewSize(600, 400))
|
||
|
||
cmdEntry := widget.NewEntry()
|
||
cmdEntry.SetPlaceHolder("输入命令,如: ping 127.0.0.1")
|
||
|
||
outputLabel := widget.NewMultiLineEntry()
|
||
outputLabel.Disable() // 只读
|
||
outputLabel.Wrapping = fyne.TextWrapWord
|
||
|
||
runBtn := widget.NewButton("执行", func() {
|
||
command := cmdEntry.Text
|
||
if command == "" {
|
||
return
|
||
}
|
||
|
||
var cmd *exec.Cmd
|
||
if runtime.GOOS == "windows" {
|
||
cmd = exec.Command("cmd", "/c", command)
|
||
} else {
|
||
cmd = exec.Command("sh", "-c", command)
|
||
}
|
||
|
||
var out bytes.Buffer
|
||
var errOut bytes.Buffer
|
||
cmd.Stdout = &out
|
||
cmd.Stderr = &errOut
|
||
|
||
err := cmd.Run()
|
||
if err != nil {
|
||
outputLabel.SetText("错误: " + err.Error() + "\n" + errOut.String())
|
||
return
|
||
}
|
||
outputLabel.SetText(out.String())
|
||
})
|
||
|
||
content := container.NewVBox(
|
||
widget.NewLabel("输入要执行的命令:"),
|
||
cmdEntry,
|
||
runBtn,
|
||
widget.NewLabel("输出:"),
|
||
outputLabel,
|
||
)
|
||
|
||
myWindow.SetContent(content)
|
||
myWindow.ShowAndRun()
|
||
}
|
||
```
|
||
|
||
### 3.2 带实时输出的进阶版本
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/app"
|
||
"fyne.io/fyne/v2/container"
|
||
"fyne.io/fyne/v2/widget"
|
||
"os/exec"
|
||
"runtime"
|
||
"sync"
|
||
)
|
||
|
||
func main() {
|
||
myApp := app.New()
|
||
myWindow := myApp.NewWindow("实时命令输出")
|
||
myWindow.Resize(fyne.NewSize(700, 500))
|
||
|
||
cmdEntry := widget.NewEntry()
|
||
cmdEntry.SetPlaceHolder("输入命令...")
|
||
|
||
output := widget.NewMultiLineEntry()
|
||
output.Disable()
|
||
output.Wrapping = fyne.TextWrapWord
|
||
|
||
var mu sync.Mutex
|
||
|
||
appendOutput := func(text string) {
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
current := output.Text
|
||
if len(current) > 0 {
|
||
current += "\n"
|
||
}
|
||
output.SetText(current + text)
|
||
}
|
||
|
||
runBtn := widget.NewButton("执行", func() {
|
||
command := cmdEntry.Text
|
||
if command == "" {
|
||
return
|
||
}
|
||
|
||
var cmd *exec.Cmd
|
||
if runtime.GOOS == "windows" {
|
||
cmd = exec.Command("cmd", "/c", command)
|
||
} else {
|
||
cmd = exec.Command("sh", "-c", command)
|
||
}
|
||
|
||
stdout, _ := cmd.StdoutPipe()
|
||
stderr, _ := cmd.StderrPipe()
|
||
|
||
cmd.Start()
|
||
|
||
go func() {
|
||
scanner := bufio.NewScanner(stdout)
|
||
for scanner.Scan() {
|
||
appendOutput(scanner.Text())
|
||
}
|
||
}()
|
||
go func() {
|
||
scanner := bufio.NewScanner(stderr)
|
||
for scanner.Scan() {
|
||
appendOutput("[ERR] " + scanner.Text())
|
||
}
|
||
}()
|
||
|
||
cmd.Wait()
|
||
})
|
||
|
||
clearBtn := widget.NewButton("清空", func() {
|
||
output.SetText("")
|
||
})
|
||
|
||
content := container.NewBorder(
|
||
container.NewVBox(
|
||
widget.NewLabelWithStyle("命令执行器 (实时输出)", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||
widget.NewSeparator(),
|
||
cmdEntry,
|
||
container.NewHBox(runBtn, clearBtn),
|
||
),
|
||
nil, nil, nil,
|
||
output,
|
||
)
|
||
|
||
myWindow.SetContent(content)
|
||
myWindow.ShowAndRun()
|
||
}
|
||
```
|
||
|
||
### 3.3 带进度条的长时间任务
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"fyne.io/fyne/v2"
|
||
"fyne.io/fyne/v2/app"
|
||
"fyne.io/fyne/v2/container"
|
||
"fyne.io/fyne/v2/widget"
|
||
"os/exec"
|
||
"time"
|
||
)
|
||
|
||
func main() {
|
||
myApp := app.New()
|
||
myWindow := myApp.NewWindow("带超时的命令执行")
|
||
myWindow.Resize(fyne.NewSize(500, 300))
|
||
|
||
progress := widget.NewProgressBar()
|
||
statusLabel := widget.NewLabel("就绪")
|
||
|
||
go func() {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
cmd := exec.CommandContext(ctx, "ping", "-n", "5", "127.0.0.1") // Windows
|
||
// cmd := exec.CommandContext(ctx, "ping", "-c", "5", "127.0.0.1") // Linux/macOS
|
||
|
||
// 模拟进度更新
|
||
go func() {
|
||
for i := 0; i <= 100; i++ {
|
||
progress.SetValue(float64(i) / 100.0)
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
}()
|
||
|
||
output, err := cmd.Output()
|
||
if ctx.Err() == context.DeadlineExceeded {
|
||
statusLabel.SetText("执行超时!")
|
||
return
|
||
}
|
||
if err != nil {
|
||
statusLabel.SetText("错误: " + err.Error())
|
||
return
|
||
}
|
||
statusLabel.SetText("完成! 输出: " + string(output))
|
||
}()
|
||
|
||
content := container.NewVBox(
|
||
widget.NewLabel("任务进度:"),
|
||
progress,
|
||
statusLabel,
|
||
)
|
||
|
||
myWindow.SetContent(content)
|
||
myWindow.ShowAndRun()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. `os/exec` 关键方法对比
|
||
|
||
| 方法 | 用途 | 特点 |
|
||
|------|------|------|
|
||
| `cmd.Run()` | 执行命令并等待完成 | stdout/stderr 需手动设置 |
|
||
| `cmd.Output()` | 执行并返回 stdout | stderr 为空时出错 |
|
||
| `cmd.CombinedOutput()` | 执行并返回 stdout+stderr | stderr 合并到输出 |
|
||
| `cmd.Start()` + `cmd.Wait()` | 手动控制启停 | 可配合 Pipe 实现实时输出 |
|
||
| `cmd.StdoutPipe()` | 获取 stdout 管道 | 需在 Start 前调用 |
|
||
| `cmd.StderrPipe()` | 获取 stderr 管道 | 需在 Start 前调用 |
|
||
| `exec.CommandContext()` | 带 context 的命令 | 支持超时/取消 |
|
||
|
||
---
|
||
|
||
## 5. 常见问题
|
||
|
||
### Q: `cmd.Output()` 报 "exit status 1"
|
||
**A:** 命令执行失败且 stderr 有内容时,`Output()` 会直接报错。改用 `CombinedOutput()` 或手动设置 `cmd.Stdout` / `cmd.Stderr`。
|
||
|
||
### Q: 如何执行 `cd` 命令?
|
||
**A:** `cd` 是 shell 内置命令,不能用 `exec.Command` 直接执行。应使用 `cmd.Dir` 设置工作目录。
|
||
|
||
### Q: Fyne 编译后文件太大?
|
||
**A:** 使用 `go build -ldflags="-s -w"` 压缩,或使用 `fyne package` 打包。
|
||
|
||
### Q: Fyne 不支持 Windows 7?
|
||
**A:** Fyne v2.4+ 需要 Windows 10 1809+。
|
||
|
||
---
|
||
|
||
## 6. 参考资料
|
||
|
||
- [Fyne 官方文档](https://developer.fyne.io/)
|
||
- [Go os/exec 文档](https://pkg.go.dev/os/exec)
|
||
- [Fyne GitHub](https://github.com/fyne-io/fyne)
|