Skip to content

Commit 6fff62a

Browse files
mahadzaryab1terrorbytewishdev
authored
Add Hash / HashSums (#215)
Co-authored-by: poptart <[email protected]> Co-authored-by: John W Higgins <[email protected]> Co-authored-by: [email protected]
1 parent 0296fd2 commit 6fff62a

File tree

4 files changed

+170
-27
lines changed

4 files changed

+170
-27
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
4848
| `jq` | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
4949
| `ls` | [`ListFiles`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) |
5050
| `sed` | [`Replace`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Replace) / [`ReplaceRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ReplaceRegexp) |
51-
| `sha256sum` | [`SHA256Sum`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sum) / [`SHA256Sums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sums) |
51+
| `sha256sum` | [`Hash`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Hash) / [`HashSums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.HashSums) |
5252
| `tail` | [`Last`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Last) |
5353
| `tee` | [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee) |
5454
| `uniq -c` | [`Freq`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Freq) |
@@ -317,6 +317,7 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
317317
| [`First`](https://pkg.go.dev/github.com/bitfield/script#Pipe.First) | first N lines of input |
318318
| [`Freq`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Freq) | frequency count of unique input lines, most frequent first |
319319
| [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get) | response to HTTP GET on supplied URL |
320+
| [`HashSums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.HashSums) | hashes of each listed file |
320321
| [`Join`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Join) | replace all newlines with spaces |
321322
| [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) | result of `jq` query |
322323
| [`Last`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Last) | last N lines of input|
@@ -327,7 +328,6 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
327328
| [`RejectRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.RejectRegexp) | lines not matching given regexp |
328329
| [`Replace`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Replace) | matching text replaced with given string |
329330
| [`ReplaceRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ReplaceRegexp) | matching text replaced with given string |
330-
| [`SHA256Sums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sums) | SHA-256 hashes of each listed file |
331331
| [`Tee`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Tee) | input copied to supplied writers |
332332

333333
Note that filters run concurrently, rather than producing nothing until each stage has fully read its input. This is convenient for executing long-running commands, for example. If you do need to wait for the pipeline to complete, call [`Wait`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Wait).
@@ -340,9 +340,9 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
340340
| ---- | ----------- | ------- |
341341
| [`AppendFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.AppendFile) | appended to file, creating if it doesn't exist | bytes written, error |
342342
| [`Bytes`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Bytes) | | data as `[]byte`, error
343+
| [`Hash`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Hash) | | hash, error |
343344
| [`CountLines`](https://pkg.go.dev/github.com/bitfield/script#Pipe.CountLines) | |number of lines, error |
344345
| [`Read`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Read) | given `[]byte` | bytes read, error |
345-
| [`SHA256Sum`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sum) | | SHA-256 hash, error |
346346
| [`Slice`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Slice) | | data as `[]string`, error |
347347
| [`Stdout`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Stdout) | standard output | bytes written, error |
348348
| [`String`](https://pkg.go.dev/github.com/bitfield/script#Pipe.String) | | data as `string`, error |

script.go

+41-23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"encoding/hex"
99
"encoding/json"
1010
"fmt"
11+
"hash"
1112
"io"
1213
"math"
1314
"net/http"
@@ -650,6 +651,40 @@ func (p *Pipe) Get(url string) *Pipe {
650651
return p.Do(req)
651652
}
652653

654+
// Hash returns the hex-encoded hash of the entire contents of the
655+
// pipe based on the provided hasher, or an error.
656+
// To perform hashing on files, see [Pipe.HashSums].
657+
func (p *Pipe) Hash(hasher hash.Hash) (string, error) {
658+
if p.Error() != nil {
659+
return "", p.Error()
660+
}
661+
_, err := io.Copy(hasher, p)
662+
if err != nil {
663+
p.SetError(err)
664+
return "", err
665+
}
666+
return hex.EncodeToString(hasher.Sum(nil)), nil
667+
}
668+
669+
// HashSums reads paths from the pipe, one per line, and produces the
670+
// hex-encoded hash of each corresponding file based on the provided hasher,
671+
// one per line. Any files that cannot be opened or read will be ignored.
672+
// To perform hashing on the contents of the pipe, see [Pipe.Hash].
673+
func (p *Pipe) HashSums(hasher hash.Hash) *Pipe {
674+
return p.FilterScan(func(line string, w io.Writer) {
675+
f, err := os.Open(line)
676+
if err != nil {
677+
return // skip unopenable files
678+
}
679+
defer f.Close()
680+
_, err = io.Copy(hasher, f)
681+
if err != nil {
682+
return // skip unreadable files
683+
}
684+
fmt.Fprintln(w, hex.EncodeToString(hasher.Sum(nil)))
685+
})
686+
}
687+
653688
// Join joins all the lines in the pipe's contents into a single
654689
// space-separated string, which will always end with a newline.
655690
func (p *Pipe) Join() *Pipe {
@@ -816,36 +851,19 @@ func (p *Pipe) SetError(err error) {
816851

817852
// SHA256Sum returns the hex-encoded SHA-256 hash of the entire contents of the
818853
// pipe, or an error.
854+
// Deprecated: SHA256Sum has been deprecated by [Pipe.Hash]. To get the SHA-256
855+
// hash for the contents of the pipe, call `Hash(sha256.new())`
819856
func (p *Pipe) SHA256Sum() (string, error) {
820-
if p.Error() != nil {
821-
return "", p.Error()
822-
}
823-
hasher := sha256.New()
824-
_, err := io.Copy(hasher, p)
825-
if err != nil {
826-
p.SetError(err)
827-
return "", err
828-
}
829-
return hex.EncodeToString(hasher.Sum(nil)), p.Error()
857+
return p.Hash(sha256.New())
830858
}
831859

832860
// SHA256Sums reads paths from the pipe, one per line, and produces the
833861
// hex-encoded SHA-256 hash of each corresponding file, one per line. Any files
834862
// that cannot be opened or read will be ignored.
863+
// Deprecated: SHA256Sums has been deprecated by [Pipe.HashSums]. To get the SHA-256
864+
// hash for each file path in the pipe, call `HashSums(sha256.new())`
835865
func (p *Pipe) SHA256Sums() *Pipe {
836-
return p.FilterScan(func(line string, w io.Writer) {
837-
f, err := os.Open(line)
838-
if err != nil {
839-
return // skip unopenable files
840-
}
841-
defer f.Close()
842-
h := sha256.New()
843-
_, err = io.Copy(h, f)
844-
if err != nil {
845-
return // skip unreadable files
846-
}
847-
fmt.Fprintln(w, hex.EncodeToString(h.Sum(nil)))
848-
})
866+
return p.HashSums(sha256.New())
849867
}
850868

851869
// Slice returns the pipe's contents as a slice of strings, one element per

script_test.go

+126-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ package script_test
33
import (
44
"bufio"
55
"bytes"
6+
"crypto/sha256"
7+
"crypto/sha512"
68
"errors"
79
"fmt"
10+
"hash"
811
"io"
912
"log"
1013
"net/http"
@@ -1127,7 +1130,7 @@ func TestSHA256Sums_OutputsCorrectHashForEachSpecifiedFile(t *testing.T) {
11271130
want string
11281131
}{
11291132
// To get the checksum run: openssl dgst -sha256 <file_name>
1130-
{"testdata/sha256Sum.input.txt", "1870478d23b0b4db37735d917f4f0ff9393dd3e52d8b0efa852ab85536ddad8e\n"},
1133+
{"testdata/hashSum.input.txt", "1870478d23b0b4db37735d917f4f0ff9393dd3e52d8b0efa852ab85536ddad8e\n"},
11311134
{"testdata/hello.txt", "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9\n"},
11321135
{"testdata/multiple_files", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n"},
11331136
}
@@ -2013,6 +2016,110 @@ func TestWithStdErr_IsConcurrencySafeAfterExec(t *testing.T) {
20132016
}
20142017
}
20152018

2019+
func TestHash_OutputsCorrectHash(t *testing.T) {
2020+
t.Parallel()
2021+
tcs := []struct {
2022+
name, input, want string
2023+
hasher hash.Hash
2024+
}{
2025+
{
2026+
name: "for no data",
2027+
input: "",
2028+
hasher: sha256.New(),
2029+
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
2030+
},
2031+
{
2032+
name: "for short string with SHA 256 hasher",
2033+
input: "hello, world",
2034+
hasher: sha256.New(),
2035+
want: "09ca7e4eaa6e8ae9c7d261167129184883644d07dfba7cbfbc4c8a2e08360d5b",
2036+
},
2037+
{
2038+
name: "for short string with SHA 512 hasher",
2039+
input: "hello, world",
2040+
hasher: sha512.New(),
2041+
want: "8710339dcb6814d0d9d2290ef422285c9322b7163951f9a0ca8f883d3305286f44139aa374848e4174f5aada663027e4548637b6d19894aec4fb6c46a139fbf9",
2042+
},
2043+
{
2044+
name: "for string containing newline with SHA 256 hasher",
2045+
input: "The tao that can be told\nis not the eternal Tao",
2046+
hasher: sha256.New(),
2047+
want: "788542cb92d37f67e187992bdb402fdfb68228a1802947f74c6576e04790a688",
2048+
},
2049+
}
2050+
for _, tc := range tcs {
2051+
t.Run(tc.name, func(t *testing.T) {
2052+
got, err := script.Echo(tc.input).Hash(tc.hasher)
2053+
if err != nil {
2054+
t.Fatal(err)
2055+
}
2056+
if got != tc.want {
2057+
t.Errorf("want %q, got %q", tc.want, got)
2058+
}
2059+
})
2060+
}
2061+
}
2062+
2063+
func TestHashSums_OutputsCorrectHashForEachSpecifiedFile(t *testing.T) {
2064+
t.Parallel()
2065+
tcs := []struct {
2066+
testFileName string
2067+
hasher hash.Hash
2068+
want string
2069+
}{
2070+
// To get the checksum run: openssl dgst -sha256 <file_name>
2071+
{
2072+
testFileName: "testdata/hashSum.input.txt",
2073+
hasher: sha256.New(),
2074+
want: "1870478d23b0b4db37735d917f4f0ff9393dd3e52d8b0efa852ab85536ddad8e\n",
2075+
},
2076+
{
2077+
testFileName: "testdata/hashSum.input.txt",
2078+
hasher: sha512.New(),
2079+
want: "3543bd0d68129e860598ccabcee1beb6bb90d91105cea74a8e555588634ec6f6d6d02033139972da2dc4929b1fb61bd24c91c8e82054e9ae865cf7f70909be8c\n",
2080+
},
2081+
{
2082+
testFileName: "testdata/hello.txt",
2083+
hasher: sha256.New(),
2084+
want: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9\n",
2085+
},
2086+
{
2087+
testFileName: "testdata/multiple_files",
2088+
hasher: sha256.New(),
2089+
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n",
2090+
},
2091+
}
2092+
for _, tc := range tcs {
2093+
got, err := script.ListFiles(tc.testFileName).HashSums(tc.hasher).String()
2094+
if err != nil {
2095+
t.Fatal(err)
2096+
}
2097+
if got != tc.want {
2098+
t.Errorf("%q: want %q, got %q", tc.testFileName, tc.want, got)
2099+
}
2100+
}
2101+
}
2102+
2103+
func TestHash_ReturnsErrorGivenReadErrorOnPipe(t *testing.T) {
2104+
t.Parallel()
2105+
brokenReader := iotest.ErrReader(errors.New("oh no"))
2106+
_, err := script.NewPipe().WithReader(brokenReader).Hash(sha256.New())
2107+
if err == nil {
2108+
t.Fatal(nil)
2109+
}
2110+
}
2111+
2112+
func TestHashSums_OutputsEmptyStringForFileThatCannotBeHashed(t *testing.T) {
2113+
got, err := script.Echo("file_does_not_exist.txt").HashSums(sha256.New()).String()
2114+
if err != nil {
2115+
t.Fatal(err)
2116+
}
2117+
want := ""
2118+
if got != want {
2119+
t.Errorf("want %q, got %q", want, got)
2120+
}
2121+
}
2122+
20162123
func ExampleArgs() {
20172124
script.Args().Stdout()
20182125
// prints command-line arguments
@@ -2276,6 +2383,24 @@ func ExamplePipe_Get() {
22762383
// You said: hello
22772384
}
22782385

2386+
func ExamplePipe_Hash() {
2387+
sum, err := script.Echo("hello world").Hash(sha512.New())
2388+
if err != nil {
2389+
panic(err)
2390+
}
2391+
fmt.Println(sum)
2392+
// Output:
2393+
// 309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f
2394+
}
2395+
2396+
func ExamplePipe_HashSums() {
2397+
script.ListFiles("testdata/multiple_files").HashSums(sha256.New()).Stdout()
2398+
// Output:
2399+
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
2400+
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
2401+
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
2402+
}
2403+
22792404
func ExamplePipe_Join() {
22802405
script.Echo("hello\nworld\n").Join().Stdout()
22812406
// Output:
File renamed without changes.

0 commit comments

Comments
 (0)