Browse Source

refine api, run command and merger (#766)

* mergers code optimize

* api log work with pipe

* remove inbounds/outbounds by tags
*fix rmo flag parse

* cmds description and message optimize
Jebbs 4 years ago
parent
commit
7a0af318df

+ 2 - 2
infra/conf/mergers/extensions.go

@@ -8,7 +8,7 @@ func GetExtensions(formatName string) ([]string, error) {
 	if lowerName == "auto" {
 		return GetAllExtensions(), nil
 	}
-	f, found := mergeLoaderByName[lowerName]
+	f, found := mergersByName[lowerName]
 	if !found {
 		return nil, newError(formatName+" not found", formatName).AtWarning()
 	}
@@ -18,7 +18,7 @@ func GetExtensions(formatName string) ([]string, error) {
 // GetAllExtensions get all extensions supported
 func GetAllExtensions() []string {
 	extensions := make([]string, 0)
-	for _, f := range mergeLoaderByName {
+	for _, f := range mergersByName {
 		extensions = append(extensions, f.Extensions...)
 	}
 	return extensions

+ 0 - 40
infra/conf/mergers/formats.go

@@ -1,40 +0,0 @@
-package mergers
-
-//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
-
-import (
-	"strings"
-)
-
-// MergeableFormat is a configurable mergeable format of V2Ray config file.
-type MergeableFormat struct {
-	Name       string
-	Extensions []string
-	Loader     MergeLoader
-}
-
-// MergeLoader is a utility to merge V2Ray config from external source into a map and returns it.
-type MergeLoader func(input interface{}, m map[string]interface{}) error
-
-var (
-	mergeLoaderByName = make(map[string]*MergeableFormat)
-	mergeLoaderByExt  = make(map[string]*MergeableFormat)
-)
-
-// RegisterMergeLoader add a new MergeLoader.
-func RegisterMergeLoader(format *MergeableFormat) error {
-	if _, found := mergeLoaderByName[format.Name]; found {
-		return newError(format.Name, " already registered.")
-	}
-	mergeLoaderByName[format.Name] = format
-
-	for _, ext := range format.Extensions {
-		lext := strings.ToLower(ext)
-		if f, found := mergeLoaderByExt[lext]; found {
-			return newError(ext, " already registered to ", f.Name)
-		}
-		mergeLoaderByExt[lext] = format
-	}
-
-	return nil
-}

+ 11 - 11
infra/conf/mergers/merge.go

@@ -12,15 +12,15 @@ import (
 
 // MergeAs load input and merge as specified format into m
 func MergeAs(formatName string, input interface{}, m map[string]interface{}) error {
-	f, found := mergeLoaderByName[formatName]
+	f, found := mergersByName[formatName]
 	if !found {
-		return newError("format loader not found for: ", formatName)
+		return newError("format merger not found for: ", formatName)
 	}
-	return f.Loader(input, m)
+	return f.Merge(input, m)
 }
 
 // Merge loads inputs and merges them into m
-// it detects extension for loader selecting, or try all loaders
+// it detects extension for merger selecting, or try all mergers
 // if no extension found
 func Merge(input interface{}, m map[string]interface{}) error {
 	switch v := input.(type) {
@@ -49,7 +49,7 @@ func Merge(input interface{}, m map[string]interface{}) error {
 			return err
 		}
 	case io.Reader:
-		// read to []byte incase it tries different loaders
+		// read to []byte incase it tries different mergers
 		bs, err := ioutil.ReadAll(v)
 		if err != nil {
 			return err
@@ -69,24 +69,24 @@ func mergeSingleFile(input interface{}, m map[string]interface{}) error {
 		ext := getExtension(file)
 		if ext != "" {
 			lext := strings.ToLower(ext)
-			f, found := mergeLoaderByExt[lext]
+			f, found := mergersByExt[lext]
 			if !found {
 				return newError("unmergeable format extension: ", ext)
 			}
-			return f.Loader(file, m)
+			return f.Merge(file, m)
 		}
 	}
-	// no extension, try all loaders
-	for _, f := range mergeLoaderByName {
+	// no extension, try all mergers
+	for _, f := range mergersByName {
 		if f.Name == core.FormatAuto {
 			continue
 		}
-		err := f.Loader(input, m)
+		err := f.Merge(input, m)
 		if err == nil {
 			return nil
 		}
 	}
-	return newError("tried all loaders but failed for: ", input).AtWarning()
+	return newError("tried all mergers but failed for: ", input).AtWarning()
 }
 
 func getExtension(filename string) string {

+ 6 - 4
infra/conf/mergers/merger_base.go

@@ -11,15 +11,17 @@ import (
 
 type jsonConverter func(v []byte) ([]byte, error)
 
-func makeLoader(name string, extensions []string, converter jsonConverter) *MergeableFormat {
-	return &MergeableFormat{
+// makeMerger makes a merger who merge the format by converting it to JSON
+func makeMerger(name string, extensions []string, converter jsonConverter) *Merger {
+	return &Merger{
 		Name:       name,
 		Extensions: extensions,
-		Loader:     makeConvertToJSONLoader(converter),
+		Merge:      makeToJSONMergeFunc(converter),
 	}
 }
 
-func makeConvertToJSONLoader(converter func(v []byte) ([]byte, error)) MergeLoader {
+// makeToJSONMergeFunc makes a merge func who merge the format by converting it to JSON
+func makeToJSONMergeFunc(converter func(v []byte) ([]byte, error)) MergeFunc {
 	return func(input interface{}, target map[string]interface{}) error {
 		if target == nil {
 			panic("merge target is nil")

+ 43 - 6
infra/conf/mergers/mergers.go

@@ -1,32 +1,69 @@
 package mergers
 
+//go:generate go run github.com/v2fly/v2ray-core/v4/common/errors/errorgen
+
 import (
+	"strings"
+
 	core "github.com/v2fly/v2ray-core/v4"
 	"github.com/v2fly/v2ray-core/v4/common"
 	"github.com/v2fly/v2ray-core/v4/infra/conf/json"
 )
 
 func init() {
-	common.Must(RegisterMergeLoader(makeLoader(
+	common.Must(RegisterMerger(makeMerger(
 		core.FormatJSON,
 		[]string{".json", ".jsonc"},
 		nil,
 	)))
-	common.Must(RegisterMergeLoader(makeLoader(
+	common.Must(RegisterMerger(makeMerger(
 		core.FormatTOML,
 		[]string{".toml"},
 		json.FromTOML,
 	)))
-	common.Must(RegisterMergeLoader(makeLoader(
+	common.Must(RegisterMerger(makeMerger(
 		core.FormatYAML,
 		[]string{".yml", ".yaml"},
 		json.FromYAML,
 	)))
-	common.Must(RegisterMergeLoader(
-		&MergeableFormat{
+	common.Must(RegisterMerger(
+		&Merger{
 			Name:       core.FormatAuto,
 			Extensions: nil,
-			Loader:     Merge,
+			Merge:      Merge,
 		}),
 	)
 }
+
+// Merger is a configurable format merger for V2Ray config files.
+type Merger struct {
+	Name       string
+	Extensions []string
+	Merge      MergeFunc
+}
+
+// MergeFunc is a utility to merge V2Ray config from external source into a map and returns it.
+type MergeFunc func(input interface{}, m map[string]interface{}) error
+
+var (
+	mergersByName = make(map[string]*Merger)
+	mergersByExt  = make(map[string]*Merger)
+)
+
+// RegisterMerger add a new Merger.
+func RegisterMerger(format *Merger) error {
+	if _, found := mergersByName[format.Name]; found {
+		return newError(format.Name, " already registered.")
+	}
+	mergersByName[format.Name] = format
+
+	for _, ext := range format.Extensions {
+		lext := strings.ToLower(ext)
+		if f, found := mergersByExt[lext]; found {
+			return newError(ext, " already registered to ", f.Name)
+		}
+		mergersByExt[lext] = format
+	}
+
+	return nil
+}

+ 1 - 1
infra/conf/mergers/names.go

@@ -3,7 +3,7 @@ package mergers
 // GetAllNames get names of all formats
 func GetAllNames() []string {
 	names := make([]string, 0)
-	for _, f := range mergeLoaderByName {
+	for _, f := range mergersByName {
 		names = append(names, f.Name)
 	}
 	return names

+ 8 - 4
main/commands/all/api/inbounds_add.go

@@ -10,15 +10,18 @@ import (
 
 var cmdAddInbounds = &base.Command{
 	CustomFlags: true,
-	UsageLine:   "{{.Exec}} api adi [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
+	UsageLine:   "{{.Exec}} api adi [--server=127.0.0.1:8080] [c1.json] [dir1]...",
 	Short:       "add inbounds",
 	Long: `
 Add inbounds to V2Ray.
 
+> Make sure you have "HandlerService" set in "config.api.services" 
+of server config.
+
 Arguments:
 
 	-format <format>
-		Specify the input format.
+		The input format.
 		Available values: "auto", "json", "toml", "yaml"
 		Default: "auto"
 
@@ -33,7 +36,8 @@ Arguments:
 
 Example:
 
-    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
+    {{.Exec}} {{.LongName}} dir
+    {{.Exec}} {{.LongName}} c1.json c2.yaml
 `,
 	Run: executeAddInbounds,
 }
@@ -44,7 +48,7 @@ func executeAddInbounds(cmd *base.Command, args []string) {
 	cmd.Flag.Parse(args)
 	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
 	if err != nil {
-		base.Fatalf("%s", err)
+		base.Fatalf("failed to load: %s", err)
 	}
 	if len(c.InboundConfigs) == 0 {
 		base.Fatalf("no valid inbound found")

+ 29 - 10
main/commands/all/api/inbounds_remove.go

@@ -10,21 +10,27 @@ import (
 
 var cmdRemoveInbounds = &base.Command{
 	CustomFlags: true,
-	UsageLine:   "{{.Exec}} api rmi [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
+	UsageLine:   "{{.Exec}} api rmi [--server=127.0.0.1:8080] [c1.json] [dir1]...",
 	Short:       "remove inbounds",
 	Long: `
 Remove inbounds from V2Ray.
 
+> Make sure you have "HandlerService" set in "config.api.services" 
+of server config.
+
 Arguments:
 
 	-format <format>
-		Specify the input format.
+		The input format.
 		Available values: "auto", "json", "toml", "yaml"
 		Default: "auto"
 
 	-r
 		Load folders recursively.
 
+	-tags
+		The input are tags instead of config files
+
 	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
@@ -33,7 +39,9 @@ Arguments:
 
 Example:
 
-    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
+    {{.Exec}} {{.LongName}} dir
+    {{.Exec}} {{.LongName}} c1.json c2.yaml
+    {{.Exec}} {{.LongName}} -tags tag1 tag2
 `,
 	Run: executeRemoveInbounds,
 }
@@ -41,12 +49,23 @@ Example:
 func executeRemoveInbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
 	setSharedConfigFlags(cmd)
+	isTags := cmd.Flag.Bool("tags", false, "")
 	cmd.Flag.Parse(args)
-	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
-	if err != nil {
-		base.Fatalf("%s", err)
+
+	var tags []string
+	if *isTags {
+		tags = cmd.Flag.Args()
+	} else {
+		c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+		if err != nil {
+			base.Fatalf("failed to load: %s", err)
+		}
+		tags = make([]string, 0)
+		for _, c := range c.InboundConfigs {
+			tags = append(tags, c.Tag)
+		}
 	}
-	if len(c.InboundConfigs) == 0 {
+	if len(tags) == 0 {
 		base.Fatalf("no inbound to remove")
 	}
 
@@ -54,10 +73,10 @@ func executeRemoveInbounds(cmd *base.Command, args []string) {
 	defer close()
 
 	client := handlerService.NewHandlerServiceClient(conn)
-	for _, c := range c.InboundConfigs {
-		fmt.Println("removing:", c.Tag)
+	for _, tag := range tags {
+		fmt.Println("removing:", tag)
 		r := &handlerService.RemoveInboundRequest{
-			Tag: c.Tag,
+			Tag: tag,
 		}
 		_, err := client.RemoveInbound(ctx, r)
 		if err != nil {

+ 9 - 4
main/commands/all/api/log.go

@@ -3,6 +3,7 @@ package api
 import (
 	"io"
 	"log"
+	"os"
 
 	logService "github.com/v2fly/v2ray-core/v4/app/log/command"
 	"github.com/v2fly/v2ray-core/v4/main/commands/base"
@@ -15,6 +16,9 @@ var cmdLog = &base.Command{
 	Long: `
 Follow and print logs from v2ray.
 
+> Make sure you have "LoggerService" set in "config.api.services" 
+of server config.
+
 > It ignores -timeout flag while following logs
 
 Arguments:
@@ -33,10 +37,10 @@ Example:
     {{.Exec}} {{.LongName}}
     {{.Exec}} {{.LongName}} --restart
 `,
-	Run: executeRestartLogger,
+	Run: executeLog,
 }
 
-func executeRestartLogger(cmd *base.Command, args []string) {
+func executeLog(cmd *base.Command, args []string) {
 	var restart bool
 	cmd.Flag.BoolVar(&restart, "restart", false, "")
 	setSharedFlags(cmd)
@@ -69,7 +73,8 @@ func followLogger() {
 	if err != nil {
 		base.Fatalf("failed to follow logger: %s", err)
 	}
-
+	// work with `v2ray api log | grep expr`
+	log.SetOutput(os.Stdout)
 	for {
 		resp, err := stream.Recv()
 		if err == io.EOF {
@@ -78,6 +83,6 @@ func followLogger() {
 		if err != nil {
 			base.Fatalf("failed to fetch log: %s", err)
 		}
-		log.Print(resp.Message)
+		log.Println(resp.Message)
 	}
 }

+ 8 - 4
main/commands/all/api/outbounds_add.go

@@ -10,15 +10,18 @@ import (
 
 var cmdAddOutbounds = &base.Command{
 	CustomFlags: true,
-	UsageLine:   "{{.Exec}} api ado [--server=127.0.0.1:8080] <c1.json> [c2.json]...",
+	UsageLine:   "{{.Exec}} api ado [--server=127.0.0.1:8080] [c1.json] [dir1]...",
 	Short:       "add outbounds",
 	Long: `
 Add outbounds to V2Ray.
 
+> Make sure you have "HandlerService" set in "config.api.services" 
+of server config.
+
 Arguments:
 
 	-format <format>
-		Specify the input format.
+		The input format.
 		Available values: "auto", "json", "toml", "yaml"
 		Default: "auto"
 
@@ -33,7 +36,8 @@ Arguments:
 
 Example:
 
-    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json c2.json
+    {{.Exec}} {{.LongName}} dir
+    {{.Exec}} {{.LongName}} c1.json c2.yaml
 `,
 	Run: executeAddOutbounds,
 }
@@ -44,7 +48,7 @@ func executeAddOutbounds(cmd *base.Command, args []string) {
 	cmd.Flag.Parse(args)
 	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
 	if err != nil {
-		base.Fatalf("%s", err)
+		base.Fatalf("failed to load: %s", err)
 	}
 	if len(c.OutboundConfigs) == 0 {
 		base.Fatalf("no valid outbound found")

+ 30 - 11
main/commands/all/api/outbounds_remove.go

@@ -10,21 +10,27 @@ import (
 
 var cmdRemoveOutbounds = &base.Command{
 	CustomFlags: true,
-	UsageLine:   "{{.Exec}} api rmo [--server=127.0.0.1:8080] <json_file|tag> [json_file] [tag]...",
+	UsageLine:   "{{.Exec}} api rmo [--server=127.0.0.1:8080] [c1.json] [dir1]...",
 	Short:       "remove outbounds",
 	Long: `
 Remove outbounds from V2Ray.
 
+> Make sure you have "HandlerService" set in "config.api.services" 
+of server config.
+
 Arguments:
 
 	-format <format>
-		Specify the input format.
+		The input format.
 		Available values: "auto", "json", "toml", "yaml"
 		Default: "auto"
 
 	-r
 		Load folders recursively.
 
+	-tags
+		The input are tags instead of config files
+
 	-s, -server <server:port>
 		The API server address. Default 127.0.0.1:8080
 
@@ -33,20 +39,33 @@ Arguments:
 
 Example:
 
-    {{.Exec}} {{.LongName}} --server=127.0.0.1:8080 c1.json "tag name"
+    {{.Exec}} {{.LongName}} dir
+    {{.Exec}} {{.LongName}} c1.json c2.yaml
+    {{.Exec}} {{.LongName}} -tags tag1 tag2
 `,
 	Run: executeRemoveOutbounds,
 }
 
 func executeRemoveOutbounds(cmd *base.Command, args []string) {
 	setSharedFlags(cmd)
-	cmd.Flag.Parse(args)
 	setSharedConfigFlags(cmd)
-	c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
-	if err != nil {
-		base.Fatalf("%s", err)
+	isTags := cmd.Flag.Bool("tags", false, "")
+	cmd.Flag.Parse(args)
+
+	var tags []string
+	if *isTags {
+		tags = cmd.Flag.Args()
+	} else {
+		c, err := helpers.LoadConfig(cmd.Flag.Args(), apiConfigFormat, apiConfigRecursively)
+		if err != nil {
+			base.Fatalf("failed to load: %s", err)
+		}
+		tags = make([]string, 0)
+		for _, c := range c.OutboundConfigs {
+			tags = append(tags, c.Tag)
+		}
 	}
-	if len(c.OutboundConfigs) == 0 {
+	if len(tags) == 0 {
 		base.Fatalf("no outbound to remove")
 	}
 
@@ -54,10 +73,10 @@ func executeRemoveOutbounds(cmd *base.Command, args []string) {
 	defer close()
 
 	client := handlerService.NewHandlerServiceClient(conn)
-	for _, c := range c.OutboundConfigs {
-		fmt.Println("removing:", c.Tag)
+	for _, tag := range tags {
+		fmt.Println("removing:", tag)
 		r := &handlerService.RemoveOutboundRequest{
-			Tag: c.Tag,
+			Tag: tag,
 		}
 		_, err := client.RemoveOutbound(ctx, r)
 		if err != nil {

+ 4 - 1
main/commands/all/api/stats.go

@@ -19,13 +19,16 @@ var cmdStats = &base.Command{
 	Long: `
 Query statistics from V2Ray.
 
+> Make sure you have "StatsService" set in "config.api.services" 
+of server config.
+
 Arguments:
 
 	-regexp
 		The patterns are using regexp.
 
 	-reset
-		Fetch values then reset statistics counters to 0.
+		Reset counters to 0 after fetching their values.
 
 	-runtime
 		Get runtime statistics.

+ 17 - 17
main/commands/all/convert.go

@@ -23,17 +23,17 @@ var cmdConvert = &base.Command{
 	Short:       "convert config files",
 	Long: `
 Convert config files between different formats. Files are merged 
-before convert if multiple assigned.
+before convert.
 
 Arguments:
 
 	-i, -input <format>
-		Specify the input format.
+		The input format.
 		Available values: "auto", "json", "toml", "yaml"
 		Default: "auto"
 
 	-o, -output <format>
-		Specify the output format
+		The output format
 		Available values: "json", "toml", "yaml", "protobuf" / "pb"
 		Default: "json"
 
@@ -42,15 +42,15 @@ Arguments:
 
 Examples:
 
-	{{.Exec}} {{.LongName}} -output=protobuf config.json           (1)
-	{{.Exec}} {{.LongName}} -input=toml config.toml                (2)
-	{{.Exec}} {{.LongName}} "path/to/dir"                          (3)
-	{{.Exec}} {{.LongName}} -i yaml -o protobuf c1.yaml <url>.yaml (4)
+	{{.Exec}} {{.LongName}} -output=protobuf "path/to/dir"   (1)
+	{{.Exec}} {{.LongName}} -o=yaml config.toml              (2)
+	{{.Exec}} {{.LongName}} c1.json c2.json                  (3)
+	{{.Exec}} {{.LongName}} -output=yaml c1.yaml <url>.yaml  (4)
 
-(1) Convert json to protobuf
-(2) Convert toml to json
-(3) Merge json files in dir
-(4) Merge yaml files and convert to protobuf
+(1) Merge all supported files in dir and convert to protobuf
+(2) Convert toml to yaml
+(3) Merge json files
+(4) Merge yaml files
 
 Use "{{.Exec}} help config-merge" for more information about merge.
 `,
@@ -81,11 +81,11 @@ func executeConvert(cmd *base.Command, args []string) {
 
 	m, err := helpers.LoadConfigToMap(cmd.Flag.Args(), inputFormat, confDirRecursively)
 	if err != nil {
-		base.Fatalf(err.Error())
+		base.Fatalf("failed to merge: %s", err)
 	}
 	err = merge.ApplyRules(m)
 	if err != nil {
-		base.Fatalf(err.Error())
+		base.Fatalf("failed to apply merge rules: %s", err)
 	}
 
 	var out []byte
@@ -93,17 +93,17 @@ func executeConvert(cmd *base.Command, args []string) {
 	case core.FormatJSON:
 		out, err = json.Marshal(m)
 		if err != nil {
-			base.Fatalf("failed to marshal json: %s", err)
+			base.Fatalf("failed to convert to json: %s", err)
 		}
 	case core.FormatTOML:
 		out, err = toml.Marshal(m)
 		if err != nil {
-			base.Fatalf("failed to marshal json: %s", err)
+			base.Fatalf("failed to convert to toml: %s", err)
 		}
 	case core.FormatYAML:
 		out, err = yaml.Marshal(m)
 		if err != nil {
-			base.Fatalf("failed to marshal json: %s", err)
+			base.Fatalf("failed to convert to yaml: %s", err)
 		}
 	case core.FormatProtobuf, core.FormatProtobufShort:
 		data, err := json.Marshal(m)
@@ -121,7 +121,7 @@ func executeConvert(cmd *base.Command, args []string) {
 		}
 		out, err = proto.Marshal(pbConfig)
 		if err != nil {
-			base.Fatalf("failed to marshal proto config: %s", err)
+			base.Fatalf("failed to convert to protobuf: %s", err)
 		}
 	default:
 		base.Errorf("invalid output format: %s", outputFormat)

+ 6 - 6
main/commands/all/format_doc.go

@@ -8,12 +8,12 @@ var docFormat = &base.Command{
 	UsageLine: "{{.Exec}} format-loader",
 	Short:     "config formats and loading",
 	Long: `
-{{.Exec}} supports different config formats:
+{{.Exec}} is equipped with multiple loaders to support different 
+config formats:
 
 	* auto
-	  The default loader, supports all extensions below.
-	  It loads config by format detecting, with mixed 
-	  formats support.
+	  The default loader, supports all formats listed below, with 
+	  format detecting, and mixed fomats support.
 
 	* json (.json, .jsonc)
 	  The json loader, multiple files support, mergeable.
@@ -21,14 +21,14 @@ var docFormat = &base.Command{
 	* toml (.toml)
 	  The toml loader, multiple files support, mergeable.
 
-	* yaml (.yml)
+	* yaml (.yml, .yaml)
 	  The yaml loader, multiple files support, mergeable.
 
 	* protobuf / pb (.pb)
 	  Single file support, unmergeable.
 
 
-The following explains how format loaders behave with examples.
+The following explains how format loaders behaves.
 
 Examples:
 

+ 10 - 2
main/commands/run.go

@@ -25,19 +25,27 @@ var CmdRun = &base.Command{
 	Long: `
 Run V2Ray with config.
 
+{{.Exec}} will also use the config directory specified by environment 
+variable "v2ray.location.confdir". If no config found, it tries 
+to load config from one of below:
+
+	1. The default "config.json" in the current directory
+	2. The config file from ENV "v2ray.location.config"
+	3. The stdin if all failed above
+
 Arguments:
 
 	-c, -config <file>
 		Config file for V2Ray. Multiple assign is accepted.
 
 	-d, -confdir <dir>
-		A dir with config files. Multiple assign is accepted.
+		A directory with config files. Multiple assign is accepted.
 
 	-r
 		Load confdir recursively.
 
 	-format <format>
-		Format of input files. (default "json")
+		Format of config input. (default "auto")
 
 Examples:
 

+ 10 - 2
main/commands/test.go

@@ -16,19 +16,27 @@ var CmdTest = &base.Command{
 	Long: `
 Test config files, without launching V2Ray server.
 
+{{.Exec}} will also use the config directory specified by environment 
+variable "v2ray.location.confdir". If no config found, it tries 
+to load config from one of below:
+
+	1. The default "config.json" in the current directory
+	2. The config file from ENV "v2ray.location.config"
+	3. The stdin if all failed above
+
 Arguments:
 
 	-c, -config <file>
 		Config file for V2Ray. Multiple assign is accepted.
 
 	-d, -confdir <dir>
-		A dir with config files. Multiple assign is accepted.
+		A directory with config files. Multiple assign is accepted.
 
 	-r
 		Load confdir recursively.
 
 	-format <format>
-		Format of input files. (default "json")
+		Format of config input. (default "auto")
 
 Examples: