Skip to content

Commit

Permalink
add alias feature
Browse files Browse the repository at this point in the history
  • Loading branch information
sagan committed Dec 29, 2023
1 parent 9fc1d9e commit a47418e
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 18 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,4 +584,16 @@ sites = ["u2", "kamept"]
ptool search acg clannad
```

预置的 ```_all``` 分组可以用来指代所有站点。
预置的 ```_all``` 分组可以用来指代所有站点。

### 命令别名 (Alias) 功能

ptool.toml 里可以使用 `[[alias]]` 区块自定义命令别名,例如:

```
[[aliases]]
name = "st"
cmd = "status local -t"
```

然后可以直接运行 `ptool st`, 等效于运行 `ptool status local -t`。注:定义的别名无法覆盖内置命令。
61 changes: 61 additions & 0 deletions cmd/alias/alias.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package versioncmd

import (
"fmt"
"os"
"strings"

"github.com/google/shlex"
"github.com/spf13/cobra"

"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
)

var command = &cobra.Command{
Use: "alias",
Short: "Run alias.",
Long: `Run alias.`,
DisableFlagParsing: true,
RunE: aliascmd,
}

var (
inAlias = false
)

func init() {
cmd.RootCmd.AddCommand(command)
}

func aliascmd(_ *cobra.Command, args []string) error {
if inAlias {
return fmt.Errorf("recursive alias definition is NOT supported")
}
inAlias = true
defer func() {
inAlias = false
}()
if len(args) == 0 {
return fmt.Errorf("alias name must be provided as the first arg")
}
aliasName := args[0]
args = args[1:]

aliasConfig := config.GetAliasConfig(aliasName)
if aliasConfig == nil {
return fmt.Errorf("command or alias '%s' not found. Run 'ptool --help' for usage", aliasName)
}
argsCmd := strings.TrimSpace(aliasConfig.Cmd)
if argsCmd == "" {
return fmt.Errorf("alias '%s' does have cmd", aliasName)
}
aliasArgs, err := shlex.Split(argsCmd)
if err != nil {
return fmt.Errorf("failed to parse cmd of alias '%s': %v", aliasName, err)
}
aliasArgs = append([]string{os.Args[0]}, aliasArgs...)
os.Args = append(aliasArgs, args...)
fmt.Printf("Run alias '%s': %v\n", aliasName, os.Args[1:])
return cmd.RootCmd.Execute()
}
1 change: 1 addition & 0 deletions cmd/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
_ "github.com/sagan/ptool/cmd/addlocal"
_ "github.com/sagan/ptool/cmd/addtags"
_ "github.com/sagan/ptool/cmd/addtrackers"
_ "github.com/sagan/ptool/cmd/alias"
_ "github.com/sagan/ptool/cmd/batchdl"
_ "github.com/sagan/ptool/cmd/brush"
_ "github.com/sagan/ptool/cmd/clientctl"
Expand Down
3 changes: 0 additions & 3 deletions cmd/brush/brush.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package brush
import (
"fmt"
"math/rand"
"os"
"path/filepath"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -63,7 +62,6 @@ func brush(cmd *cobra.Command, args []string) error {
cntAddTorrents := int64(0)
cntDeleteTorrents := int64(0)
doneSiteFlag := map[string]bool{}
tmpdir, _ := os.MkdirTemp(os.TempDir(), "ptool")
var statDb *stats.StatDb
if config.Get().BrushEnableStats {
statDb, err = stats.NewDb(filepath.Join(config.ConfigDir, config.STATS_FILENAME))
Expand Down Expand Up @@ -299,7 +297,6 @@ func brush(cmd *cobra.Command, args []string) error {

fmt.Printf("Finish brushing %d sites: successSites=%d, skipSites=%d; Added / Deleted torrents: %d / %d to client %s\n",
len(sitenames), cntSuccessSite, cntSkipSite, cntAddTorrents, cntDeleteTorrents, clientInstance.GetName())
os.RemoveAll(tmpdir)
if cntSuccessSite == 0 {
return fmt.Errorf("no sites successed")
}
Expand Down
28 changes: 20 additions & 8 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -24,8 +25,9 @@ var RootCmd = &cobra.Command{
Long: `ptool is a command-line program which facilitate the use of private tracker sites and BitTorrent clients.
It's a free and open-source software, visit https://github.com/sagan/ptool for more infomation.`,
// Run: func(cmd *cobra.Command, args []string) { },
// SilenceErrors: true,
SilenceUsage: true,
SilenceErrors: true,
SilenceUsage: true,
DisableSuggestions: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if config.InShell && config.Get().ShellMaxHistory != 0 {
in := strings.Join(os.Args[1:], " ")
Expand Down Expand Up @@ -84,14 +86,23 @@ func Execute() {
}
ShellHistory = &ShellHistoryStruct{filename: filepath.Join(config.ConfigDir, config.HISTORY_FILENAME)}
})
// see https://github.com/spf13/cobra/issues/914
// Must use RunE to capture error
// See https://github.com/spf13/cobra/issues/914 .
// Must use RunE to capture error.
// Returned errors:
// Unknown command (specified direct subcommand not found), unknown shorthand flag,
err := RootCmd.Execute()
if err != nil {
Exit(1)
} else {
Exit(0)
if strings.HasPrefix(err.Error(), "unknown command ") {
log.Debugf("Unknown command. Try to parse input as alias: %v", os.Args)
os.Args = append([]string{os.Args[0], "alias"}, os.Args[1:]...)
err = RootCmd.Execute()
}
if err != nil {
fmt.Printf("Error: %v.\n", err)
Exit(1)
}
}
Exit(0)
}

func init() {
Expand All @@ -110,11 +121,12 @@ func init() {
break
}
}
config.DefaultConfigFile = configFile

// global flags
RootCmd.PersistentFlags().BoolVarP(&config.Fork, "fork", "", false, "Enables a daemon mode that runs the ptool process in the background (detached from current terminal). The current stdout / stderr will still be used so you may want to redirect them to files using pipe. It only works on Linux platform")
RootCmd.PersistentFlags().BoolVarP(&config.LockOrExit, "lock-or-exit", "", false, "Used with --lock flag. If failed to acquire lock, exit 1 immediately instead of waiting")
RootCmd.PersistentFlags().StringVarP(&config.ConfigFile, "config", "", configFile, "Config file ([ptool.toml])")
RootCmd.PersistentFlags().StringVarP(&config.ConfigFile, "config", "", config.DefaultConfigFile, "Config file ([ptool.toml])")
RootCmd.PersistentFlags().StringVarP(&config.LockFile, "lock", "", "", "Lock filename. If set, ptool will acquire the lock on the file before executing command. It is intended to be used to prevent multiple invocations of ptool process at the same time. If the lock file does not exist, it will be created automatically. However, it will NOT be deleted after ptool process exits")
RootCmd.PersistentFlags().CountVarP(&config.VerboseLevel, "verbose", "v", "verbose (-v, -vv, -vvv)")
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ func delete(cmd *cobra.Command, args []string) error {
if !force {
client.PrintTorrents(torrents, "", 1)
fmt.Printf("\n")
if !util.AskYesNoConfirm(fmt.Sprintf("Above %d torrents will be deteled", len(torrents))) {
if !util.AskYesNoConfirm(fmt.Sprintf("Above %d torrents will be deteled (Preserve disk files = %t)",
len(torrents), preserve)) {
return fmt.Errorf("abort")
}
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/versioncmd/versioncmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"

"github.com/sagan/ptool/cmd"
"github.com/sagan/ptool/config"
"github.com/sagan/ptool/util"
"github.com/sagan/ptool/version"
)
Expand All @@ -28,6 +29,7 @@ func versioncmd(cmd *cobra.Command, args []string) error {
fmt.Printf("- os/type: %s\n", runtime.GOOS)
fmt.Printf("- os/arch: %s\n", runtime.GOARCH)
fmt.Printf("- go/version: %s\n", runtime.Version())
fmt.Printf("- config_file: %s\n", config.DefaultConfigFile)
fmt.Printf("- config/default_tls_ja3: %s\n", util.CHROME_JA3)
fmt.Printf("- config/default_h2_fingerprint: %s\n", util.CHROME_H2FINGERPRINT)
fmt.Printf("- config/default_http_request_headers:\n")
Expand Down
48 changes: 44 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ type GroupConfigStruct struct {
Comment string `yaml:"comment"`
}

type AliasConfigStruct struct {
Name string `yaml:"name"`
Cmd string `yaml:"cmd"`
}

type ClientConfigStruct struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Expand Down Expand Up @@ -167,6 +172,7 @@ type ConfigStruct struct {
Clients []*ClientConfigStruct `yaml:"clients"`
Sites []*SiteConfigStruct `yaml:"sites"`
Groups []*GroupConfigStruct `yaml:"groups"`
Aliases []*AliasConfigStruct `yaml:"aliases"`
Cookieclouds []*CookiecloudConfigStruct `yaml:"cookieclouds"`
Comment string `yaml:"comment"`
ClientsEnabled []*ClientConfigStruct
Expand All @@ -179,6 +185,7 @@ var (
Initialized = false
ConfigDir = "" // "/root/.config/ptool"
ConfigFile = "" // "ptool.toml"
DefaultConfigFile = "" // set when start
ConfigName = "" // "ptool"
ConfigType = "" // "toml"
LockFile = ""
Expand All @@ -187,6 +194,7 @@ var (
configData *ConfigStruct = &ConfigStruct{}
clientsConfigMap = map[string]*ClientConfigStruct{}
sitesConfigMap = map[string]*SiteConfigStruct{}
aliasesConfigMap = map[string]*AliasConfigStruct{}
groupsConfigMap = map[string]*GroupConfigStruct{}
cookiecloudsConfigMap = map[string]*CookiecloudConfigStruct{}
once sync.Once
Expand Down Expand Up @@ -306,22 +314,46 @@ func Get() *ConfigStruct {
if client.Name == "" {
log.Fatalf("Invalid config file: client name can not be empty")
}

if clientsConfigMap[client.Name] != nil {
log.Fatalf("Invalid config file: duplicate client name %s found", client.Name)
}
clientsConfigMap[client.Name] = client
}
for _, site := range configData.Sites {
if sitesConfigMap[site.GetName()] != nil {
log.Fatalf("Invalid config file: duplicate site name %s found", site.GetName())
}
site.Register()
}
for _, group := range configData.Groups {
if group.Name == "" {
log.Fatalf("Invalid config file: group name can not be empty")
log.Fatalf("Invalid config file: group name can not be empty for %v", group)
}
if groupsConfigMap[group.Name] != nil {
log.Fatalf("Invalid config file: duplicate group name %s found", group.Name)
}
groupsConfigMap[group.Name] = group
}
for _, alias := range configData.Aliases {
if alias.Name == "" {
log.Fatalf("Invalid config file: alias name can not be empty for %v", alias)
}
if alias.Name == "alias" {
log.Fatalf("Invalid config file: alias name can not be 'alias' itself")
}
if aliasesConfigMap[alias.Name] != nil {
log.Fatalf("Invalid config file: duplicate alias name %s found", alias.Name)
}
aliasesConfigMap[alias.Name] = alias
}
for _, cookiecloud := range configData.Cookieclouds {
if cookiecloud.Name != "" {
cookiecloudsConfigMap[cookiecloud.Name] = cookiecloud
if cookiecloud.Name == "" {
continue
}
if cookiecloudsConfigMap[cookiecloud.Name] != nil {
log.Fatalf("Invalid config file: duplicate cookiecloud name %s found", cookiecloud.Name)
}
cookiecloudsConfigMap[cookiecloud.Name] = cookiecloud
}
configData.ClientsEnabled = util.Filter(configData.Clients, func(c *ClientConfigStruct) bool {
return !c.Disabled
Expand Down Expand Up @@ -355,6 +387,14 @@ func GetGroupConfig(name string) *GroupConfigStruct {
return groupsConfigMap[name]
}

func GetAliasConfig(name string) *AliasConfigStruct {
Get()
if name == "" {
return nil
}
return aliasesConfigMap[name]
}

func GetCookiecloudConfig(name string) *CookiecloudConfigStruct {
Get()
if name == "" {
Expand Down
8 changes: 7 additions & 1 deletion ptool.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,13 @@ cookie = 'cookie_here'

# 站点分组功能
# 定义分组后,大部分命令中 <site> 类型的参数可以使用分组名代替以指代多个站点,例如:
# 在 acg 分组的所有站点中搜索 'clannad' 关键词的种子: ptool search acg clannad
# 在 acg 分组的所有站点中搜索 'clannad' 关键词的种子: "ptool search acg clannad"
[[groups]]
name = 'acg'
sites = ['u2', 'kamept']

# 命令别名功能
# 定义以下别名后,运行 "ptool st" 等效于运行 "ptool status local -t"
[[aliases]]
name = "st"
cmd = "status local -t"

0 comments on commit a47418e

Please sign in to comment.