Skip to content

Commit ad52e8c

Browse files
authored
Merge pull request bitfield#139 from bitfield/http
Add HTTP support via Do, Get, Post
2 parents 5f9e087 + 18ed0db commit ad52e8c

File tree

3 files changed

+724
-372
lines changed

3 files changed

+724
-372
lines changed

README.md

+101-29
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,29 @@ If you're already familiar with shell scripting and the Unix toolset, here is a
2525

2626
| Unix / shell | `script` equivalent |
2727
| ------------------ | ------------------- |
28-
| (any program name) | [`Exec()`](https://pkg.go.dev/github.com/bitfield/script#Exec) |
29-
| `[ -f FILE ]` | [`IfExists()`](https://pkg.go.dev/github.com/bitfield/script#IfExists) |
30-
| `>` | [`WriteFile()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WriteFile) |
31-
| `>>` | [`AppendFile()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.AppendFile) |
32-
| `$*` | [`Args()`](https://pkg.go.dev/github.com/bitfield/script#Args) |
33-
| `basename` | [`Basename()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) |
34-
| `cat` | [`File()`](https://pkg.go.dev/github.com/bitfield/script#File) / [`Concat()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) |
35-
| `cut` | [`Column()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Column) |
36-
| `dirname` | [`Dirname()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Dirname) |
37-
| `echo` | [`Echo()`](https://pkg.go.dev/github.com/bitfield/script#Echo) |
38-
| `grep` | [`Match()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Match) / [`MatchRegexp()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.MatchRegexp) |
39-
| `grep -v` | [`Reject()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Reject) / [`RejectRegexp()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.RejectRegexp) |
40-
| `head` | [`First()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.First) |
28+
| (any program name) | [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Exec) |
29+
| `[ -f FILE ]` | [`IfExists`](https://pkg.go.dev/github.com/bitfield/script#IfExists) |
30+
| `>` | [`WriteFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.WriteFile) |
31+
| `>>` | [`AppendFile`](https://pkg.go.dev/github.com/bitfield/script#Pipe.AppendFile) |
32+
| `$*` | [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) |
33+
| `basename` | [`Basename`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Basename) |
34+
| `cat` | [`File`](https://pkg.go.dev/github.com/bitfield/script#File) / [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) |
35+
| `curl` | [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) / [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get) / [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
36+
| `cut` | [`Column`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Column) |
37+
| `dirname` | [`Dirname`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Dirname) |
38+
| `echo` | [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Echo) |
39+
| `grep` | [`Match`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Match) / [`MatchRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.MatchRegexp) |
40+
| `grep -v` | [`Reject`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Reject) / [`RejectRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.RejectRegexp) |
41+
| `head` | [`First`](https://pkg.go.dev/github.com/bitfield/script#Pipe.First) |
4142
| `find -type f` | [`FindFiles`](https://pkg.go.dev/github.com/bitfield/script#FindFiles) |
4243
| `jq` | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
43-
| `ls` | [`ListFiles()`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) |
44-
| `sed` | [`Replace()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Replace) / [`ReplaceRegexp()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ReplaceRegexp) |
45-
| `sha256sum` | [`SHA256Sum()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sum) / [`SHA256Sums()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sums) |
46-
| `tail` | [`Last()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Last) |
47-
| `uniq -c` | [`Freq()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Freq) |
48-
| `wc -l` | [`CountLines()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.CountLines) |
49-
| `xargs` | [`ExecForEach()`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecForEach) |
44+
| `ls` | [`ListFiles`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) |
45+
| `sed` | [`Replace`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Replace) / [`ReplaceRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ReplaceRegexp) |
46+
| `sha256sum` | [`SHA256Sum`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sum) / [`SHA256Sums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sums) |
47+
| `tail` | [`Last`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Last) |
48+
| `uniq -c` | [`Freq`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Freq) |
49+
| `wc -l` | [`CountLines`](https://pkg.go.dev/github.com/bitfield/script#Pipe.CountLines) |
50+
| `xargs` | [`ExecForEach`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecForEach) |
5051

5152
# Some examples
5253

@@ -62,7 +63,7 @@ That looks straightforward enough, but suppose you now want to count the lines i
6263
numLines, err := script.File("test.txt").CountLines()
6364
```
6465

65-
For something a bit more challenging, let's try counting the number of lines in the file that match the string "Error":
66+
For something a bit more challenging, let's try counting the number of lines in the file that match the string `Error`:
6667

6768
```go
6869
numErrors, err := script.File("test.txt").Match("Error").CountLines()
@@ -92,31 +93,91 @@ Maybe we're only interested in the first 10 matches. No problem:
9293
script.Args().Concat().Match("Error").First(10).Stdout()
9394
```
9495

95-
What's that? You want to append that output to a file instead of printing it to the terminal? _You've got some attitude, mister_.
96+
What's that? You want to append that output to a file instead of printing it to the terminal? *You've got some attitude, mister*. But okay:
9697

9798
```go
9899
script.Args().Concat().Match("Error").First(10).AppendFile("/var/log/errors.txt")
99100
```
100101

101-
If the data is JSON, we can do better than simple string-matching. We can use [JQ](https://stedolan.github.io/jq/) queries:
102+
We're not limited to getting data only from files or standard input. We can get it from HTTP requests too:
102103

103104
```go
104-
script.File("commits.json").JQ(".[0] | {message: .commit.message, name: .commit.committer.name}").Stdout()
105+
script.Get("https://wttr.in/London?format=3").Stdout()
106+
// Output:
107+
// London: 🌦 +13°C
108+
```
109+
110+
That's great for simple GET requests, but suppose we want to *send* some data in the body of a POST request, for example. Here's how that works:
111+
112+
```go
113+
script.Echo(data).Post(URL).Stdout()
114+
```
115+
116+
If we need to customise the HTTP behaviour in some way, such as using our own HTTP client, we can do that:
117+
118+
```go
119+
script.NewPipe().WithHTTPClient(&http.Client{
120+
Timeout: 10 * time.Second,
121+
}).Get("https://example.com").Stdout()
122+
```
123+
124+
Or maybe we need to set some custom header on the request. No problem. We can just create the request in the usual way, and set it up however we want. Then we pass it to `Do`, which will actually perform the request:
125+
126+
```go
127+
req, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
128+
req.Header.Add("Authorization", "Bearer "+token)
129+
script.Do(req).Stdout()
130+
```
131+
132+
The HTTP server could return some non-okay response, though; for example, “404 Not Found”. So what happens then?
133+
134+
In general, when any pipe stage (such as `Do`) encounters an error, it produces no output to subsequent stages. And `script` treats HTTP response status codes outside the range 200-299 as errors. So the answer for the previous example is that we just won't *see* any output from this program if the server returns an error response.
135+
136+
Instead, the pipe “remembers” any error that occurs, and we can retrieve it later by calling its `Error` method, or by using a *sink* method such as `String`, which returns an `error` value along with the result.
137+
138+
`Stdout` also returns an error, plus the number of bytes successfully written (which we don't care about for this particular case). So we can check that error, which is always a good idea in Go:
139+
140+
```go
141+
_, err := script.Do(req).Stdout()
142+
if err != nil {
143+
log.Fatal(err)
144+
}
105145
```
106146

107-
Suppose we want to execute some external program instead of doing the work ourselves. We can do that too:
147+
If, as is common, the data we get from an HTTP request is in JSON format, we can use [JQ](https://stedolan.github.io/jq/) queries to interrogate it:
148+
149+
```go
150+
data, err := script.Do(req).JQ(".[0] | {message: .commit.message, name: .commit.committer.name}").String()
151+
```
152+
153+
We can also run external programs and get their output:
108154

109155
```go
110156
script.Exec("ping 127.0.0.1").Stdout()
111157
```
112158

113-
But maybe we don't know the arguments yet; we might get them from the user, for example. We'd like to be able to run the external command repeatedly, each time passing it the next line of input. No worries:
159+
Note that `Exec` runs the command concurrently: it doesn't wait for the command to complete before returning any output. That's good, because this `ping` command will run forever (or until we get bored).
160+
161+
Instead, when we read from the pipe using `Stdout`, we see each line of output as it's produced:
162+
163+
```
164+
PING 127.0.0.1 (127.0.0.1): 56 data bytes
165+
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.056 ms
166+
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.054 ms
167+
...
168+
```
169+
170+
In the `ping` example, we knew the exact arguments we wanted to send the command, and we just needed to run it once. But what if we don't know the arguments yet? We might get them from the user, for example.
171+
172+
We might like to be able to run the external command repeatedly, each time passing it the next line of data from the pipe as an argument. No worries:
114173

115174
```go
116175
script.Args().ExecForEach("ping -c 1 {{.}}").Stdout()
117176
```
118177

119-
If there isn't a built-in operation that does what we want, we can just write our own:
178+
That `{{.}}` is standard Go template syntax; it'll substitute each line of data from the pipe into the command line before it's executed. You can write as fancy a Go template expression as you want here (but this simple example probably covers most use cases).
179+
180+
If there isn't a built-in operation that does what we want, we can just write our own, using `Filter`:
120181

121182
```go
122183
script.Echo("hello world").Filter(func (r io.Reader, w io.Writer) error {
@@ -129,7 +190,11 @@ script.Echo("hello world").Filter(func (r io.Reader, w io.Writer) error {
129190
// filtered 11 bytes
130191
```
131192

132-
Notice that the "hello world" appeared before the "filtered n bytes". Filters run concurrently, so the pipeline can start producing output before the input has been fully read.
193+
The `func` we supply to `Filter` takes just two parameters: a reader to read from, and a writer to write to. The reader reads the previous stages of the pipe, as you might expect, and anything written to the writer goes to the *next* stage of the pipe.
194+
195+
If our `func` returns some error, then, just as with the `Do` example, the pipe's error status is set, and subsequent stages become a no-op.
196+
197+
Filters run concurrently, so the pipeline can start producing output before the input has been fully read, as it did in the `ping` example. In fact, most built-in pipe methods, including `Exec`, are implemented *using* `Filter`.
133198

134199
If we want to scan input line by line, we could do that with a `Filter` function that creates a `bufio.Scanner` on its input, but we don't need to:
135200

@@ -193,12 +258,15 @@ These are functions that create a pipe with a given contents:
193258
| Source | Contents |
194259
| -------- | ------------- |
195260
| [`Args`](https://pkg.go.dev/github.com/bitfield/script#Args) | command-line arguments
261+
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Do) | HTTP response
196262
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Echo) | a string
197263
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Exec) | command output
198264
| [`File`](https://pkg.go.dev/github.com/bitfield/script#File) | file contents
199265
| [`FindFiles`](https://pkg.go.dev/github.com/bitfield/script#FindFiles) | recursive file listing
266+
| [`Get`](https://pkg.go.dev/github.com/bitfield/script#Get) | HTTP response
200267
| [`IfExists`](https://pkg.go.dev/github.com/bitfield/script#IfExists) | do something only if some file exists
201268
| [`ListFiles`](https://pkg.go.dev/github.com/bitfield/script#ListFiles) | file listing (including wildcards)
269+
| [`Post`](https://pkg.go.dev/github.com/bitfield/script#Post) | HTTP response
202270
| [`Slice`](https://pkg.go.dev/github.com/bitfield/script#Slice) | slice elements, one per line
203271
| [`Stdin`](https://pkg.go.dev/github.com/bitfield/script#Stdin) | standard input
204272

@@ -212,6 +280,7 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
212280
| [`Column`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Column) | Nth column of input |
213281
| [`Concat`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Concat) | contents of multiple files |
214282
| [`Dirname`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Dirname) | removes filename from each line, leaving only leading path components |
283+
| [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do) | response to supplied HTTP request |
215284
| [`Echo`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Echo) | all input replaced by given string |
216285
| [`Exec`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Exec) | filtered through external command |
217286
| [`ExecForEach`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ExecForEach) | execute given command template for each line of input |
@@ -220,18 +289,20 @@ Filters are methods on an existing pipe that also return a pipe, allowing you to
220289
| [`FilterScan`](https://pkg.go.dev/github.com/bitfield/script#Pipe.FilterScan) | user-supplied function filtering each line to a writer |
221290
| [`First`](https://pkg.go.dev/github.com/bitfield/script#Pipe.First) | first N lines of input |
222291
| [`Freq`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Freq) | frequency count of unique input lines, most frequent first |
292+
| [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get) | response to HTTP GET on supplied URL |
223293
| [`Join`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Join) | replace all newlines with spaces |
224294
| [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) | result of `jq` query |
225295
| [`Last`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Last) | last N lines of input|
226296
| [`Match`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Match) | lines matching given string |
227297
| [`MatchRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.MatchRegexp) | lines matching given regexp |
298+
| [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) | response to HTTP POST on supplied URL |
228299
| [`Reject`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Reject) | lines not matching given string |
229300
| [`RejectRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.RejectRegexp) | lines not matching given regexp |
230301
| [`Replace`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Replace) | matching text replaced with given string |
231302
| [`ReplaceRegexp`](https://pkg.go.dev/github.com/bitfield/script#Pipe.ReplaceRegexp) | matching text replaced with given string |
232303
| [`SHA256Sums`](https://pkg.go.dev/github.com/bitfield/script#Pipe.SHA256Sums) | SHA-256 hashes of each listed file |
233304

234-
Note that filters run concurrently, rather than producing nothing until each stage has fully read its input. This is convenient for executing long-running comands, for example. If you do need to wait for the pipeline to complete, call `Wait`.
305+
Note that filters run concurrently, rather than producing nothing until each stage has fully read its input. This is convenient for executing long-running comands, 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).
235306

236307
## Sinks
237308

@@ -254,6 +325,7 @@ Sinks are methods that return some data from a pipe, ending the pipeline and ext
254325

255326
| Version | New |
256327
| ----------- | ------- |
328+
| v0.21.0 | HTTP support: [`Do`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Do), [`Get`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Get), [`Post`](https://pkg.go.dev/github.com/bitfield/script#Pipe.Post) |
257329
| v0.20.0 | [`JQ`](https://pkg.go.dev/github.com/bitfield/script#Pipe.JQ) |
258330

259331
# Contributing

0 commit comments

Comments
 (0)