diff --git a/.github/dependabot.yml b/.github/dependabot.yaml similarity index 100% rename from .github/dependabot.yml rename to .github/dependabot.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yaml similarity index 93% rename from .github/workflows/build.yml rename to .github/workflows/build.yaml index b53c4c0..75cb88a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yaml @@ -5,12 +5,12 @@ on: paths: - '**.go' - 'go.mod' - - 'build.yml' + - 'build.yaml' pull_request: paths: - '**.go' - 'go.mod' - - 'build.yml' + - 'build.yaml' jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yaml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/release.yaml diff --git a/.goreleaser.yml b/.goreleaser.yaml similarity index 100% rename from .goreleaser.yml rename to .goreleaser.yaml diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..181c9a5 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,5 @@ +- id: xq + name: xq + entry: xq --overwrite + language: golang + types: [ "xml" ] diff --git a/cmd/root.go b/cmd/root.go index 3786447..97e23da 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "os" "path" "strings" + "sync" "github.com/antchfx/xmlquery" "github.com/sibprogrammer/xq/internal/utils" @@ -28,31 +29,20 @@ func NewRootCmd() *cobra.Command { SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { var err error - var reader io.Reader var indent string + overwrite, _ := cmd.Flags().GetBool("overwrite") if indent, err = getIndent(cmd.Flags()); err != nil { return err } - if len(args) == 0 { - fileInfo, _ := os.Stdin.Stat() - - if (fileInfo.Mode() & os.ModeCharDevice) != 0 { - _ = cmd.Help() - return nil - } - - reader = os.Stdin - } else { - var err error - if reader, err = os.Open(args[len(args)-1]); err != nil { - return err - } - } - xPathQuery, singleNode := getXpathQuery(cmd.Flags()) withTags, _ := cmd.Flags().GetBool("node") - colors := getColorMode(cmd.Flags()) + var colors int + if overwrite { + colors = utils.ColorsDisabled + } else { + colors = getColorMode(cmd.Flags()) + } options := utils.QueryOptions{ WithTags: withTags, @@ -67,45 +57,133 @@ func NewRootCmd() *cobra.Command { } jsonOutputMode, _ := cmd.Flags().GetBool("json") - pr, pw := io.Pipe() - errChan := make(chan error, 1) + var pagerPR *io.PipeReader + var pagerPW *io.PipeWriter + if !overwrite { + pagerPR, pagerPW = io.Pipe() + } + + var totalFiles int + if len(args) == 0 { + totalFiles = 1 + } else { + totalFiles = len(args) + } + // The “totalFiles * 2” part comes from the fact that there’s two goroutines + // inside the for loop and the fact that those two goroutines send a maximum + // of one value to errChan. The “+ 1” part comes from the fact that there’s + // one goroutine outside of the for loop and the fact that that goroutine + // sends a maximum of one value to errChan. + errChan := make(chan error, totalFiles * 2 + 1) + var wg sync.WaitGroup + for i := 0; i < totalFiles; i++ { + var path string + var reader io.Reader + if len(args) == 0 { + fileInfo, _ := os.Stdin.Stat() + + if (fileInfo.Mode() & os.ModeCharDevice) != 0 { + _ = cmd.Help() + return nil + } - go func() { - defer close(errChan) - defer pw.Close() - - var err error - if xPathQuery != "" { - err = utils.XPathQuery(reader, pw, xPathQuery, singleNode, options) - } else if cssQuery != "" { - err = utils.CSSQuery(reader, pw, cssQuery, cssAttr, options) + if overwrite { + return errors.New("--overwrite was used but no filenames were specified") + } + + reader = os.Stdin } else { - var contentType utils.ContentType - contentType, reader = detectFormat(cmd.Flags(), reader) - if jsonOutputMode { - err = processAsJSON(cmd.Flags(), reader, pw, contentType) + path = args[i] + if reader, err = os.Open(path); err != nil { + return err + } + } + + formattedPR, formattedPW := io.Pipe() + + wg.Add(1) + go func(reader io.Reader, formattedPW *io.PipeWriter) { + defer wg.Done() + defer formattedPW.Close() + + var err error + if xPathQuery != "" { + err = utils.XPathQuery(reader, formattedPW, xPathQuery, singleNode, options) + } else if cssQuery != "" { + err = utils.CSSQuery(reader, formattedPW, cssQuery, cssAttr, options) } else { - switch contentType { - case utils.ContentHtml: - err = utils.FormatHtml(reader, pw, indent, colors) - case utils.ContentXml: - err = utils.FormatXml(reader, pw, indent, colors) - case utils.ContentJson: - err = utils.FormatJson(reader, pw, indent, colors) - default: - err = fmt.Errorf("unknown content type: %v", contentType) + var contentType utils.ContentType + contentType, reader = detectFormat(cmd.Flags(), reader) + if jsonOutputMode { + err = processAsJSON(cmd.Flags(), reader, formattedPW, contentType) + } else { + switch contentType { + case utils.ContentHtml: + err = utils.FormatHtml(reader, formattedPW, indent, colors) + case utils.ContentXml: + err = utils.FormatXml(reader, formattedPW, indent, colors) + case utils.ContentJson: + err = utils.FormatJson(reader, formattedPW, indent, colors) + default: + err = fmt.Errorf("unknown content type: %v", contentType) + } } } - } - errChan <- err + errChan <- err + }(reader, formattedPW) + + wg.Add(1) + go func(formattedPR *io.PipeReader, path string) { + defer wg.Done() + defer formattedPR.Close() + + var err error + var allData []byte + if allData, err = io.ReadAll(formattedPR); err != nil { + errChan <- err + return + } + if overwrite { + if err = os.WriteFile(path, allData, 0666); err != nil { + errChan <- err + return + } + } else { + if _, err = pagerPW.Write(allData); err != nil { + errChan <- err + return + } + } + }(formattedPR, path) + } + + go func() { + wg.Wait() + if !overwrite { + if err := pagerPW.Close(); err != nil { + errChan <- err + } + } + close(errChan) }() - if err := utils.PagerPrint(pr, cmd.OutOrStdout()); err != nil { - return err + if !overwrite { + if err = utils.PagerPrint(pagerPR, cmd.OutOrStdout()); err != nil { + return err + } + if err = pagerPR.Close(); err != nil { + return err + } + } + + for err = range errChan { + if err != nil { + return err + } } - return <-errChan + return nil }, } } @@ -140,6 +218,7 @@ func InitFlags(cmd *cobra.Command) { cmd.PersistentFlags().BoolP("json", "j", false, "Output the result as JSON") cmd.PersistentFlags().Bool("compact", false, "Compact JSON output (no indentation)") cmd.PersistentFlags().IntP("depth", "d", -1, "Maximum nesting depth for JSON output (-1 for unlimited)") + cmd.PersistentFlags().Bool("overwrite", false, "Instead of printing the formatted file, replace the original with the formatted version") } func Execute() { @@ -209,6 +288,7 @@ func detectFormat(flags *pflag.FlagSet, origReader io.Reader) (utils.ContentType buf := make([]byte, 10) length, err := origReader.Read(buf) if err != nil { + print(err.Error()) return utils.ContentText, origReader } diff --git a/codecov.yml b/codecov.yaml similarity index 100% rename from codecov.yml rename to codecov.yaml diff --git a/docker-compose.yml b/docker-compose.yaml similarity index 100% rename from docker-compose.yml rename to docker-compose.yaml diff --git a/docs/xq.man b/docs/xq.man index 71b1b61..31b1e07 100644 --- a/docs/xq.man +++ b/docs/xq.man @@ -3,7 +3,7 @@ .SH NAME xq - command-line XML and HTML beautifier and content extractor .SH SYNOPSIS -xq [\fIoptions...\fR] [\fIfile\fR] +xq [\fIoptions...\fR] [\fIfiles...\fR] .SH DESCRIPTION Formats the provided \fIfile\fR and outputs it in the colorful mode. The file can be provided as an argument or via stdin. @@ -68,6 +68,11 @@ Uses HTML formatter instead of XML. .RS 4 Returns the node content instead of text. .RE +.PP +\fB--overwrite\fR +.RS 4 +Instead of printing the formatted file, replace the original with the formatted version. +.RE .SH EXAMPLES .PP Format an XML file and highlight the syntax: