Comments (15)
@bitfield commented on Sep 1, 2020, 9:59 PM GMT+4:30:
Okay, please supply a short test program that uses ExecWithSlice in the way you want to use it, and that can't easily be refactored to work with Exec by calling
strings.Join
. We'll hash out the design a bit, and then we can go do it!
(I am not sure what you expect by a test program, so sorry if this is not that)
/// 2>/dev/null ; exec gorun "$0" "$@"
package main
import (
"log"
. "github.com/bitfield/script"
"os"
)
func main() {
url := os.Args[1]
out := os.Args[2]
_, err := Exec("youtube-dl --no-playlist --prefer-ffmpeg --extract-audio --audio-format mp3 " + url + " --output " + out ).Stdout()
if err != nil {
log.Fatalln(err.Error())
}
}
~/tmp
❯ scripttest.go https://overcast.fm/+C7LmyTtQ test1.mp3
[generic] +C7LmyTtQ: Requesting header
WARNING: Falling back on generic information extractor.
[generic] +C7LmyTtQ: Downloading webpage
[generic] +C7LmyTtQ: Extracting information
[download] Downloading playlist: How to Destroy Surveillance Capitalism — Podcast – Cory Doctorow's craphound.com
[generic] playlist How to Destroy Surveillance Capitalism — Podcast – Cory Doctorow's craphound.com: Collected 1 video ids (downloading 1 of them)
[download] Downloading video 1 of 1
[download] Destination: test1.mp3
[download] 100% of 26.53MiB in 01:20.90KiB/s ETA 00:00nown ETA
[ffmpeg] Post-process file test1.mp3 exists, skipping
[download] Finished downloading playlist: How to Destroy Surveillance Capitalism — Podcast – Cory Doctorow's craphound.com
~/tmp
❯ scripttest.go https://overcast.fm/+C7LmyTtQ "Alice's test \"HAPPY\".mp3"
2020/09/01 22:19:04 unbalanced quotes or backslashes in [youtube-dl --no-playlist --prefer-ffmpeg --extract-audio --audio-format mp3 https://overcast.fm/+C7LmyTtQ --output Alice's test "HAPPY".mp3]
The same program with ExecWithSlice
(Though this name is too lengthy and I suggest just using Cmd
for it):
/// 2>/dev/null ; exec gorun "$0" "$@"
package main
import (
"log"
. "github.com/bitfield/script"
"os"
)
func main() {
url := os.Args[1]
out := os.Args[2]
_, err := ExecWithSlice("youtube-dl", "--no-playlist", "--prefer-ffmpeg", "--extract-audio", "--audio-format", "mp3", url, "--output", out).Stdout()
if err != nil {
log.Fatalln(err.Error())
}
}
As I said, I am not at all proficient with go, so there might be better design choices. I solved a variant of this problem in Python using its metaprogramming API, achieving easy, safe interpolation of variables.
from script.
Hey @NightMachinary, thanks for the question!
Is it invoking a shell, or is it parsing the string itself?
It uses Go's standard library exec.Command
to run the external command directly (not via a shell). Though you can, of course, exec a shell if you like and give it a command to run.
https://github.com/bitfield/script/blob/master/filters.go#L120
how can we use an explicit argument list and not a string?
Can you clarify the question slightly for me? Are you asking if there's a way to execute a command with a slice of arguments, instead of a space-separated string? If so, something like this might work:
args := []string{"hello", "world"}
p := script.Exec("echo " + strings.Join(args, " "))
It's usually more convenient to start with a string, which is why Exec takes a string instead of a slice. If you have a use case where it's more convenient to pass a slice, I'd like to hear about it. We could provide that as an alternative.
Generally, I think a major shortcoming of the readme is that it does not clarify the security considerations of using this library.
Fair enough. What do you think needs to be mentioned in the README about this? I'm happy to accept PRs or to make the updates myself.
from script.
Can you clarify the question slightly for me? Are you asking if there's a way to execute a command with a slice of arguments, instead of a space-separated string? If so, something like this might work:
args := []string{"hello", "world"} p := script.Exec("echo " + strings.Join(args, " "))It's usually more convenient to start with a string, which is why Exec takes a string instead of a slice. If you have a use case where it's more convenient to pass a slice, I'd like to hear about it. We could provide that as an alternative.
Yes, having a slice is sometimes more convenient; Generally, whenever the command is going to use some dynamic variable, there would be a danger of that variable containing spaces. For example, consider a fictional program get-movie-rating
which takes exactly one argument, the name of a movie. script.Exec("get-movie-rating " + movie_name)
would fail if the name contains whitespace.
Fair enough. What do you think needs to be mentioned in the README about this? I'm happy to accept PRs or to make the updates myself.
There are a lot of injection attack vectors in the shell; E.g., you get a string from the user to grep through the logs, and the user uses ... ; do-evil
. It's not exactly clear that this library is or is not vulnerable to these kinds of attacks. It seems to me that the only bit of shell magic you have included is this splitting the command by whitespace into separate args (which doesn't seem that dangerous), but I am not sure. Anyhow, mentioning that this is the only bit of shell magic and that the library is safe, will reassure the potential users.
from script.
script.Exec("get-movie-rating " + movie_name) would fail if the name contains whitespace.
Would it? Can you give a more detailed example which shows this?
It's not exactly clear that this library is or is not vulnerable to these kinds of attacks.
Fair point. Why don't you add an Exec
test case which demonstrates this, one way or the other (I'm pretty sure a semicolon would simply be passed to the exec'ed binary, in which case there's no risk, but of course you can exec the shell as a binary, in which case there would be, but that's hardly script
's fault, I think you'll agree.
from script.
@bitfield commented on Aug 31, 2020, 5:12 PM GMT+4:30:
Would it? Can you give a more detailed example which shows this?
I'm new to Go, so I don't know how to write a test case for it, but testing this manually is very easy. Here I use it with a script of mine that just echoes each argument back in a new line:
package main
import (
. "github.com/bitfield/script"
)
func main() {
test2 := "test 2"
_, _ = Exec("brishzq.zsh arger hi 'test 1' " + test2 ).Stdout()
}
The result of running this:
arg: hi
arg: test 1
arg: test
arg: 2
As you see, test 2
has gotten broken into two separate arguments. You might think we could do Exec("brishzq.zsh arger hi 'test 1' '" + test2 + "'")
, but then we would need to escape '
in test2
. In fact, the program completely breaks if test2
should contain '
:
package main
import (
"log"
. "github.com/bitfield/script"
)
func main() {
test2 := "Alice's pet"
_, err := Exec("brishzq.zsh arger hi 'test 1' " + test2 ).Stdout()
if err != nil {
log.Fatalln(err.Error())
}
}
2020/08/31 19:52:05 unbalanced quotes or backslashes in [brishzq.zsh arger hi 'test 1' Alice's pet]
Process finished with exit code 1
So the current design can only handle constant input, and anything dynamic can easily break it.
from script.
As you see, test 2 has gotten broken into two separate arguments
Right, as it would with the shell. If you run:
cat filename with spaces
you'll find the shell thinks you're trying to cat
three separate files. You'd need to put quotes round the name if you want the shell to interpret it as a single argument, and it's exactly the same here.
In fact, the program completely breaks if test2 should contain ':
As does the shell. Because quotes can be used to protect spaces, unbalanced quotes are not valid input.
If your issue report is that script
is vulnerable to ... ; do-evil
-type attacks, are you now satisfied that it's not? If so, I'll document this in the README.
from script.
@bitfield commented on Sep 1, 2020, 1:51 PM GMT+4:30:
Right, as it would with the shell. If you run:
cat filename with spaces
you'll find the shell thinks you're trying to
cat
three separate files. You'd need to put quotes round the name if you want the shell to interpret it as a single argument, and it's exactly the same here.
First of all, I hope we agree that the library is to be better than the shell in something because the shell beats it handsomely in the syntax part.
Other than that, the shell's quoting is different from this library, and some newer shells (e.g., zsh
) don't even require quoting. When you do echo "$sth"
, you do NOT need to escape anything in sth
. That "$sth"
is shell syntax for insert variable here quoted. However, this library has no concept of variables, and only works with strings, so you'll need to manually escape the inserted variable.
In fact, the program completely breaks if test2 should contain ':
As does the shell. Because quotes can be used to protect spaces, unbalanced quotes are not valid input.
The shell does not break under this circumstance:
❯ a="Alice's pet said \"Hello\!\"" # The escaping here is simply to feed the data into the shell. If we read this same string from a file, we do not need to escape it.
❯ echo "$a"
Alice's pet said "Hello!"
If your issue report is that
script
is vulnerable to... ; do-evil
-type attacks, are you now satisfied that it's not? If so, I'll document this in the README.
Yes, I am now satisfied that that attack vector doesn't exist (to the best of my very rudimentary knowledge).
While ExecWithSlice
is so critical that I can't use the library without it, a nice feature to have is to have short aliases for the methods. For example, script.E
that just calls script.Exec
. These help make the boilerplate more palatable.
from script.
ExecWithSlice is so critical that I can't use the library without it
Okay, please supply a short test program that uses ExecWithSlice in the way you want to use it, and that can't easily be refactored to work with Exec by calling strings.Join
. We'll hash out the design a bit, and then we can go do it!
from script.
Yes, that's just the sort of thing I had in mind. I find a very helpful way to design features is to write the code that you'd like to write, using the feature as though it exists, and then once you're happy with that, implementing the feature is a lot easier!
I'm not sure that being able to supply a slice is really your problem here, I think interpolating values containing quotes is the problem, and using a slice is one potential solution, is that fair to say? If there were an easy way to solve your use case without adding ExecWithSlice
, would you be happy with that?
from script.
Yes, that's just the sort of thing I had in mind. I find a very helpful way to design features is to write the code that you'd like to write, using the feature as though it exists, and then once you're happy with that, implementing the feature is a lot easier!
I'm not sure that being able to supply a slice is really your problem here, I think interpolating values containing quotes is the problem, and using a slice is one potential solution, is that fair to say? If there were an easy way to solve your use case without adding
ExecWithSlice
, would you be happy with that?
Your assessment is right on the money; Interpolating variables is the problem, and any way to do that that works reliably is fine by me.
from script.
Great! Here's what I suggest. Fork the repo and create a work branch for this issue. Add a test case to TestExec, based on your demo program, that fails because it tries to supply a command argument containing quotes. We can then decide how best to solve it, and the test case will demonstrate that we've succeeded!
(If you don't have the time or inclination to do this, that's absolutely fine; I will do it sooner or later. But it will be a lot sooner if you can do it.)
from script.
I would also find extremely useful to have something like ExecWithSlice
(hopefuly with a nicer name).
Interpolation of variables is not necessarily the only problem because the sliced version would essentially avoid trying to implement the arguments list splitting based on white spaces only (which creates this kind of problems).
Specifically, I'm trying to replace this code (which uses Mage's sh package):
out, err := sh.Output("go", "build", "-ldflags", "-X main.version="+version, "-o", filepath.Join(BuildDir, binary), filepath.Join(".", binary, "main.go"))
With something like this:
out, err := script.Exec("go", "build", "-ldflags", "-X main.version="+version, "-o", filepath.Join(BuildDir, binary), filepath.Join(".", binary, "main.go")).String()
Being able to specify the arguments as a slice saves us from dealing and worrying about quoting, shell syntax, etc.
That use case, using the suggested strings.Join() would look much worse because it would no only be more verbose but also the coder would need to be careful about what needs to be quoted and quote it appropriately, etc:
out, err := script.Exec("go build -ldflags '-X main.version="+version+"' -o '"+ filepath.Join(BuildDir, binary) +"' '" + filepath.Join(".", binary, "main.go")).String()
Which is not only harder to write but also to read!
I will gladly send a PR if this makes sense.
from script.
I'm not sure I follow this, @lalloni. What code is it you want to write? The example you gave doesn't use a slice, but variadic arguments.
from script.
Hi @bitfield! Well... you are right about the slice vs varargs... I joined late into this conversation and if you check the previous discussion it was already called "slice" while the example ExecWithSlice
actually received varargs. Nevertheless I feel the varargs approach is the most ergonomic, that's why I naturally used that.
Generally speaking, I think the fact that variadic arguments are received as slices is also contributing to the terminology mix up.
One more data important data point for me, coming from abundant previous experience using other command execution APIs, is that receiving the command line as a string array/slice (and varargs variants to produce nicer API) is pretty ubiquitous. Kubernetes, Docker, os/exec, juju, cilium, etc.
I get that an alternative could be to use yet another library (like github.com/keegancsmith/shell) to render shell command strings before calling script... but that seems suboptimal since this design would mean to render data we may have on diverse structures into a shell syntax string (which can be tricky) to pass into shell.Exec for it to parse the shell syntax and re-split into the constituent parts to finally call exec... I think it would more direct if we could simply pass the strings to shell.Exec.
from script.
BTW, as you can see on my PR #76 I went ahead and implemented this using ExecArgs(cmd string, args ...string) and I used that for the purposes I was needing.
I am now not so sure about the cmd-args separation. In many cases it got in the way, making the API slightly harder to use compared with how it would have been if having just ExecArgs(args ...string).
So possibly receiving only the variadic form would be better from a usage perspective.
from script.
Related Issues (20)
- gitignore entry for Intellij IDEA HOT 4
- Feature Request: Add FilterFunc HOT 2
- Basename introduces a line feed HOT 1
- How can I check the progress of a script.Exec() command? HOT 2
- Interaction with bubbletea HOT 10
- Propose adding a delimeter to Column()
- Add Base64 encode and decode methods HOT 6
- Suggestion: `./go-script -c 'script.Stdin()...'` tool for running these HOT 5
- Should `ExecForEach` sanitize or escape arguments differently? HOT 9
- Parameterize a script.Pipe with a user data struct ? HOT 5
- Execv function HOT 1
- how to get the stdout of the Exec result? HOT 3
- Race condition in Pipe.Exec HOT 4
- Methods for mkdir and touch HOT 2
- Conditinally execute statements. HOT 2
- How to create folder, delete folder and list folder contents ? HOT 4
- Feature Suggestion: Fields HOT 4
- Makefile replacement HOT 2
- Expand prompt logic for PR #199
- Easy Utility Functions to Create Symbolic/Hard Links HOT 5
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from script.