Skip to content

Commit

Permalink
add -c and -t command line options
Browse files Browse the repository at this point in the history
  • Loading branch information
jftuga committed Oct 9, 2022
1 parent 3ae8aa6 commit 1265d58
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 6 deletions.
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,33 @@ Binaries for Windows, macOS, Linux and FreeBSD are provided on the
* For help, run `chars -h`

```
chars v2.3.0
chars v2.4.0
Determine the end-of-line format, tabs, bom, and nul
https://github.com/jftuga/chars
Usage:
chars [filename or file-glob 1] [filename or file-glob 2] ...
-F when used with -f, only display a list of failed files, one per line
-b examine binary files
-c add comma thousands separator to numeric values
-e string
exclude based on regular expression; use .* instead of *
-f string
fail with OS exit code=100 if any of the included characters exist; ex: -f crlf,nul,bom8
-j output results in JSON format; can't be used with -l
-j output results in JSON format; can't be used with -l; does not honor -t or -c
-l int
shorten files names to a maximum of this length
shorten files names to a maximum of this length
-t append a row which includes a total for each column
-v display version and then exit
Notes:
Use - to read a file from STDIN
On Windows, try: chars * -or- chars */* -or- chars */*/*
```

___

## Example 1

* Run `chars` with no additional cmd-line switches
Expand Down Expand Up @@ -138,6 +143,27 @@ $ chars -e '^go' -j * | jq -r '.[] | select(.tab > 0) | [.filename,.tab] | @csv'
"chars.go",475
```

## Example 7
* Output totals, with `-t`
* Output commas in numeric values, with `-c`
* Exclude files containing `.g*`, with `-e`

```shell
PS C:\chars> .\chars.exe -t -c -e "\.g.*" *
+-----------------+------+-----+-----+-----+------+-------+-----------+
| FILENAME | CRLF | LF | TAB | NUL | BOM8 | BOM16 | BYTESREAD |
+-----------------+------+-----+-----+-----+------+-------+-----------+
| LICENSE | 0 | 21 | 0 | 0 | 0 | 0 | 1,068 |
| README.md | 0 | 178 | 4 | 0 | 0 | 0 | 6,656 |
| STATUS.md | 0 | 50 | 0 | 0 | 0 | 0 | 3,055 |
| go.mod | 0 | 11 | 3 | 0 | 0 | 0 | 214 |
| go.sum | 0 | 9 | 0 | 0 | 0 | 0 | 795 |
| TOTALS: 5 files | 0 | 269 | 7 | 0 | 0 | 0 | 11,788 |
+-----------------+------+-----+-----+-----+------+-------+-----------+
```

___

## Reading from STDIN on Windows
* **YMMV when piping to `STDIN` under Windows**
* * Under `cmd`, instead of `type input.txt | chars`, use `<` redirection when possible: `chars < input.txt`
Expand Down
39 changes: 37 additions & 2 deletions chars.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
const PgmName string = "chars"
const PgmDesc string = "Determine the end-of-line format, tabs, bom, and nul"
const PgmUrl string = "https://github.com/jftuga/chars"
const PgmVersion string = "2.3.0"
const PgmVersion string = "2.4.0"
const BlockSize int = 4096

type SpecialChars struct {
Expand Down Expand Up @@ -176,7 +176,7 @@ func searchForSpecialChars(filename string, rdr *bufio.Reader, examineBinary boo
}*/

// OutputTextTable - display a text table with each filename and the number of special characters
func OutputTextTable(allStats []SpecialChars, maxLength int) error {
func OutputTextTable(allStats []SpecialChars, maxLength int, wantTotals, wantCommas bool) error {
if len(allStats) == 0 {
return nil
}
Expand All @@ -187,6 +187,7 @@ func OutputTextTable(allStats []SpecialChars, maxLength int) error {
table.SetHeader([]string{"filename", "crlf", "lf", "tab", "nul", "bom8", "bom16", "bytesRead"})

var name string
var crlf, lf, tab, nul, bom8, bom16, bytesRead uint64
for _, s := range allStats {
if maxLength == 0 {
name = s.Filename
Expand All @@ -196,6 +197,40 @@ func OutputTextTable(allStats []SpecialChars, maxLength int) error {
row := []string{name, strconv.FormatUint(s.Crlf, 10), strconv.FormatUint(s.Lf, 10),
strconv.FormatUint(s.Tab, 10), strconv.FormatUint(s.Nul, 10), strconv.FormatUint(s.Bom8, 10),
strconv.FormatUint(s.Bom16, 10), strconv.FormatUint(s.BytesRead, 10)}
if wantCommas {
row[1] = RenderInteger("#,###.", int64(s.Crlf))
row[2] = RenderInteger("#,###.", int64(s.Lf))
row[3] = RenderInteger("#,###.", int64(s.Tab))
row[4] = RenderInteger("#,###.", int64(s.Nul))
row[5] = RenderInteger("#,###.", int64(s.Bom8))
row[6] = RenderInteger("#,###.", int64(s.Bom16))
row[7] = RenderInteger("#,###.", int64(s.BytesRead))
}
if wantTotals {
crlf += s.Crlf
lf += s.Lf
tab += s.Tab
nul += s.Nul
bom8 += s.Bom8
bom16 += s.Bom16
bytesRead += s.BytesRead
}
table.Append(row)
}
if wantTotals {
totals := fmt.Sprintf("TOTALS: %d files", len(allStats))
row := []string{totals, strconv.FormatUint(crlf, 10), strconv.FormatUint(lf, 10),
strconv.FormatUint(tab, 10), strconv.FormatUint(nul, 10), strconv.FormatUint(bom8, 10),
strconv.FormatUint(bom16, 10), strconv.FormatUint(bytesRead, 10)}
if wantCommas {
row[1] = RenderInteger("#,###.", int64(crlf))
row[2] = RenderInteger("#,###.", int64(lf))
row[3] = RenderInteger("#,###.", int64(tab))
row[4] = RenderInteger("#,###.", int64(nul))
row[5] = RenderInteger("#,###.", int64(bom8))
row[6] = RenderInteger("#,###.", int64(bom16))
row[7] = RenderInteger("#,###.", int64(bytesRead))
}
table.Append(row)
}
table.Render()
Expand Down
6 changes: 4 additions & 2 deletions cmd/chars/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ func main() {
argsBinary := flag.Bool("b", false, "examine binary files")
argsExclude := flag.String("e", "", "exclude based on regular expression; use .* instead of *")
argsMaxLength := flag.Int("l", 0, "shorten files names to a maximum of this length")
argsJSON := flag.Bool("j", false, "output results in JSON format; can't be used with -l")
argsJSON := flag.Bool("j", false, "output results in JSON format; can't be used with -l; does not honor -t or -c")
argsVersion := flag.Bool("v", false, "display version and then exit")
argsFail := flag.String("f", "", "fail with OS exit code=100 if any of the included characters exist; ex: -f crlf,nul,bom8")
argsFailedFileList := flag.Bool("F", false, "when used with -f, only display a list of failed files, one per line")
argsTotals := flag.Bool("t", false, "append a row which includes a total for each column")
argsComma := flag.Bool("c", false, "add comma thousands separator to numeric values")

flag.Usage = Usage
flag.Parse()
Expand Down Expand Up @@ -99,7 +101,7 @@ func main() {
} else if *argsFailedFileList && len(*argsFail) > 0 && failed > 0 {
chars.OutputFailedFileList(allStats)
} else {
err := chars.OutputTextTable(allStats, *argsMaxLength)
err := chars.OutputTextTable(allStats, *argsMaxLength, *argsTotals, *argsComma)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(5)
Expand Down
196 changes: 196 additions & 0 deletions render_number.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
Author: https://github.com/gorhill
Source: https://gist.github.com/gorhill/5285193
A Go function to render a number to a string based on
the following user-specified criteria:
* thousands separator
* decimal separator
* decimal precision
Usage: s := RenderFloat(format, n)
The format parameter tells how to render the number n.
http://play.golang.org/p/LXc1Ddm1lJ
Examples of format strings, given n = 12345.6789:
"#,###.##" => "12,345.67"
"#,###." => "12,345"
"#,###" => "12345,678"
"#\u202F###,##" => "12 345,67"
"#.###,###### => 12.345,678900
"" (aka default format) => 12,345.67
The highest precision allowed is 9 digits after the decimal symbol.
There is also a version for integer number, RenderInteger(),
which is convenient for calls within template.
I didn't feel it was worth to publish a library just for this piece
of code, hence the snippet. Feel free to reuse as you wish.
*/

package chars

import (
"math"
"strconv"
)

var renderFloatPrecisionMultipliers = [10]float64{
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
}

var renderFloatPrecisionRounders = [10]float64{
0.5,
0.05,
0.005,
0.0005,
0.00005,
0.000005,
0.0000005,
0.00000005,
0.000000005,
0.0000000005,
}

// RenderFloat - add thousands separator
func RenderFloat(format string, n float64) string {
// Special cases:
// NaN = "NaN"
// +Inf = "+Infinity"
// -Inf = "-Infinity"
if math.IsNaN(n) {
return "NaN"
}
if n > math.MaxFloat64 {
return "Infinity"
}
if n < -math.MaxFloat64 {
return "-Infinity"
}

// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
negativeStr := "-"

if len(format) > 0 {
// If there is an explicit format directive,
// then default values are these:
precision = 9
thousandStr = ""

// collect indices of meaningful formatting directives
formatDirectiveChars := []rune(format)
formatDirectiveIndices := make([]int, 0)
for i, char := range formatDirectiveChars {
if char != '#' && char != '0' {
formatDirectiveIndices = append(formatDirectiveIndices, i)
}
}

if len(formatDirectiveIndices) > 0 {
// Directive at index 0:
// Must be a '+'
// Raise an error if not the case
// index: 0123456789
// +0.000,000
// +000,000.0
// +0000.00
// +0000
if formatDirectiveIndices[0] == 0 {
if formatDirectiveChars[formatDirectiveIndices[0]] != '+' {
panic("RenderFloat(): invalid positive sign directive")
}
positiveStr = "+"
formatDirectiveIndices = formatDirectiveIndices[1:]
}

// Two directives:
// First is thousands separator
// Raise an error if not followed by 3-digit
// 0123456789
// 0.000,000
// 000,000.00
if len(formatDirectiveIndices) == 2 {
if (formatDirectiveIndices[1] - formatDirectiveIndices[0]) != 4 {
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
}
thousandStr = string(formatDirectiveChars[formatDirectiveIndices[0]])
formatDirectiveIndices = formatDirectiveIndices[1:]
}

// One directive:
// Directive is decimal separator
// The number of digit-specifier following the separator indicates wanted precision
// 0123456789
// 0.00
// 000,0000
if len(formatDirectiveIndices) == 1 {
decimalStr = string(formatDirectiveChars[formatDirectiveIndices[0]])
precision = len(formatDirectiveChars) - formatDirectiveIndices[0] - 1
}
}
}

// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}

// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])

// generate integer part string
intStr := strconv.FormatInt(int64(intf), 10)

// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}

// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}

// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}

return signStr + intStr + decimalStr + fracStr
}

// RenderInteger - add thousands separator
func RenderInteger(format string, n int64) string {
return RenderFloat(format, float64(n))
}

0 comments on commit 1265d58

Please sign in to comment.