Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to stay aligned with GitHub docs, Dependabot default configuration, and thousands of other repos.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So are you saying that I should change this pull request so that it doesn’t change .github/dependabot.yml’s filename to .github/dependabot.yaml?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, thank you. I'll rework some parts based on your ideas. For example, I prefer to go with the "-i" option for conformity with sed and Perl. Also, I would like to cover changes with tests to avoid regression issues.

File renamed without changes.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml → .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- id: xq
name: xq
entry: xq --overwrite
language: golang
types: [ "xml" ]
174 changes: 127 additions & 47 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"path"
"strings"
"sync"

"github.com/antchfx/xmlquery"
"github.com/sibprogrammer/xq/internal/utils"
Expand All @@ -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,
Expand All @@ -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
},
}
}
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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
}

Expand Down
File renamed without changes.
File renamed without changes.
7 changes: 6 additions & 1 deletion docs/xq.man
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down