最近在学习go语言,把之前picbed的命令行cli.py用go重写,在编写过程中,go实现相应功能走了些 弯路,在本文拎出来分享给大家(新手)。

三端复制剪贴板

三端指的是Windows、Linux、macOS,实现用命令复制内容到系统剪贴板。

Windows

cmd内置的 clip 命令实现复制剪贴板,示例:

echo hello world | clip

Linux

需要安装第三方软件包 xclip ,用于桌面环境, 测试支持Deepin、Fedora、Manjaro、Ubuntu、CentOS, 其他系统未测试,示例:

echo "hello world" | xclip -selection clipboard

macOS

内置的 pbcopy 命令可以实现复制到系统剪贴板,示例:

echo "hello world" | pbcopy

使用golang调用带有管道符的命令

go执行系统命令使用 os/exec 标准库,不过当有管道符的时候就有点麻烦了,这时候google找到了 捷径,Linux、macOS使用 sh -c ,而Windows使用 cmd.exe /C ,示例:

Linux/macOS

package main

import "fmt"
import "os/exec"

func main() {
    content := "hello world"
    // if macOS
    // cmd := fmt.Sprintf(`echo "%s" | pbcopy`, content)
	cmd := fmt.Sprintf(`echo "%s" | xclip -selection clipboard`, content)
    err := exec.Command("bash", "-c", cmd).Run()
    if err != nil {
        fmt.Println("exec error")
    }
}

Windows

package main

import (
    "fmt"
    "strings"
    "os/exec"
)

func main() {
    content := "hello world"
    cmd := fmt.Sprintf(`echo %s | clip`, strings.ReplaceAll(content, "\n", "\\n"))
	err := exec.Command("cmd.exe", "/C", cmd).Run()
	if err != nil {
        fmt.Println("exec error")
	}
}

使用golang调用powershell触发Windows10桌面消息通知

找到一个Win10桌面通知的脚本不容易,得用powershell调起,以下内容保存为xxx.ps1,执行它 通过 powershell xxx.ps1 <Title> <Sub-Title>

param(
    [String] $Title,
    [String] $SubTitle
)

[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null

$APP_ID = '110366bd-56e2-47ed-9bdf-3ce1fa408b6c'

$template = @"
<toast>
    <visual>
        <binding template="ToastText02">
            <text id="1">$($Title)</text>
            <text id="2">$($SubTitle)</text>
        </binding>
    </visual>
</toast>
"@

$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)

在go程序中想调用它,我一时没想到别的方法,暂时思路是把内容保存为临时文件,再用 os/exec 调用, 如下示例(没有错误处理):

package main

import (
	"fmt"
	"os"
    "os/exec"
	"io/ioutil"
)

func main() {
    sf := genTmpPS1()
	exec.Command(
		"powershell", "-ExecutionPolicy", "Unrestricted", sf,
		"上传成功", "已复制到剪贴板",
	).Run()
}

func genTmpPS1() (filepath string) {
	tpl := []byte(`
param(
    [String] $Title,
    [String] $SubTitle
)

[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null

$APP_ID = '110366bd-56e2-47ed-9bdf-3ce1fa408b6c'

$template = @"
<toast>
    <visual>
        <binding template="ToastText02">
            <text id="1">$($Title)</text>
            <text id="2">$($SubTitle)</text>
        </binding>
    </visual>
</toast>
"@

$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
`)

	tmpfile, err := ioutil.TempFile(os.TempDir(), "*.ps1")
	tmpfile.Write(tpl)
	tmpfile.Close()

	return tmpfile.Name()
}

使用homebrew安装golang编译的可执行程序

go程序编译完成后就一个二进制的可执行文件,在macOS系统中,可以自建一个Tap方便homebrew安装。

相关概念可以参考 使用 Homebrew 维护自己的软件仓库 这篇文章,这里不再赘述。

新发布程序

当你的程序是第一次发布时,需要先创建一个 homebrew-XXX 的git仓库,比如我的 staugur/homebrew-tap

picbed-cli的发行版为例, 我已经编译了macOS版本的压缩包,在release中有附件,压缩包里只有一个picbed-cli可执行文件。

打开macOS终端,执行命令:

brew create 程序包网络下载地址

下载包,然后提示ENTER回车直接使用编辑器进入编辑 xxx.rb 文件,修改下desc、homepage两个 描述信息,修改 def installsystem ./configure 那几行注释,增加 bin.install, 参考示例:

class PicbedCli < Formula
  desc "picbed client cli"
  homepage "https://github.com/staugur/picbed-cli"
  url "https://static.saintic.com/download/picbed-cli/picbed-cli.0.4.2-darwin-amd64.tar.gz"
  sha256 "c33ae9aae32273e9ea681eff904bfbae912753e7afa0175ad69765fe17b002ff"

  def install
    bin.install "picbed-cli"
  end

  test do
    system "false"
  end
end

其实也可以不需要 brew create 命令,参照上述示例填写,把压缩包的地址和sha256填上即可。

示例内容保存为xxx.rb提交到你的git仓库 homebrew-XXX 中

更新程序版本

只需要编辑git仓库homebrew-XXX下的xxx.rb,修改url为新版本压缩包路径、sha256为新版本压缩包 sha256值,再提交后即可。

使用 brew update && brew upgrade picbed-cli 执行版本更新。

其他经验补充说明

1、使用ldflags -X设置额外信息

在picbed-cli开发中有一个 -i/--info 选项使用的变量是编译时 ldflag -X 动态传入的:

go build -ldflags "-s -w -X main.commitID=xxx -X main.built=xxx"

这个程序比较简单,就一个main包,传入没什么问题。

不过有时候你可能想往子包中传值,参考这篇文章:使用ldflags设置Go应用程序的版本信息

假如你的程序目录结构如下:

rtfd
├── cmd
│   └── root.go
├── go.mod
├── main.go
├── Makefile
└── VERSION

其中main.go是main包,cmd下go文件属于cmd包,按照上面文章应该这么传值:

$ go build -ldflags "-X cmd.commitID=xxx -X cmd.built=xxx"
$ go build -ldflags "-X rtfd/cmd.commitID=xxx -X rtfd/cmd.built=xxx"

均失败!

ldflag格式是:-ldflags="-X 'package_path.variable_name=new_value'"

可能是新手,没搞明白package_path含义,google也是没找到教新的分享, 捣鼓好久突然灵机一动,改为这么传值:

go build -ldflags "-X tcw.im/rtfd/cmd.commitID=xxx -X tcw.im/rtfd/cmd.built=xxx"

行了!

因为我的go项目全都是go module模式!

此刻,好想吐槽(自己?)

废话太多了,分享的大概意思就是:

使用go module模式时,go build -ldflag -X传递给子包变量时package_path需要添加module前缀:

go build -ldflag -X module/package_path.variable_name=value

·End·