mirror of
https://github.com/gechandesu/runcmd.git
synced 2026-01-23 23:04:13 +03:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a4262dc1a | |||
| 3cd5e40ac8 | |||
| 055dab663e | |||
| b790cfef0a |
48
.github/workflows/docs.yaml
vendored
Normal file
48
.github/workflows/docs.yaml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Docs
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup V
|
||||||
|
run: |
|
||||||
|
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
|
||||||
|
unzip -q /tmp/v.zip -d /tmp
|
||||||
|
echo /tmp/v >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Build docs
|
||||||
|
run: |
|
||||||
|
v doc -f html -m .
|
||||||
|
pushd _docs
|
||||||
|
ln -vs ${{ github.event.repository.name }}.html index.html
|
||||||
|
ls -alFh
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Upload static files as artifact
|
||||||
|
id: deployment
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: _docs/
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
27
.github/workflows/test.yaml
vendored
Normal file
27
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Lint and test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup V
|
||||||
|
run: |
|
||||||
|
wget -qO /tmp/v.zip https://github.com/vlang/v/releases/latest/download/v_linux.zip
|
||||||
|
unzip -q /tmp/v.zip -d /tmp
|
||||||
|
echo /tmp/v >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
v fmt -verify .
|
||||||
|
v vet -v -W -I -F -r .
|
||||||
|
v missdoc -r --verify .
|
||||||
|
v -stats test .
|
||||||
35
README.md
35
README.md
@@ -1,25 +1,23 @@
|
|||||||
# Run External Commands
|
# Run External Commands
|
||||||
|
|
||||||
`runcmd` module implements high-level interface for running external commands.
|
`runcmd` module implements a high-level interface for running external commands.
|
||||||
|
|
||||||
## Why not vlib `os`?
|
## Why not vlib `os`?
|
||||||
|
|
||||||
The standard V `os` module already contains tools for a similar tasks — `os.Process`,
|
The standard V `os` module already contains many tools for running external commands,
|
||||||
`os.Command`, but I don't like any of them. There is also many functions for executing
|
but I don't like any of them. So let's overview.
|
||||||
external commands:
|
|
||||||
|
|
||||||
* `os.execvp()`, `os.execve()` — cross-platform versions of the C functions of the same name.
|
* `os.execvp()`, `os.execve()` — cross-platform versions of the C functions of the same name.
|
||||||
|
|
||||||
* `os.execute()`, `os.execute_opt()`, `os.execute_or_exit()`, `os.execute_or_panic()` — wrap C calls and wait for a command to completed. Under the hood, they perform a dirty hack by calling `sh` with stream redirection `'exec 2>&1;${cmd}'`. Only stdout and exit_code are available in result.
|
* `os.execute()`, `os.execute_opt()`, `os.execute_or_exit()`, `os.execute_or_panic()` — starts and waits for a command to completed. Under the hood, they perform a dirty hack by calling shell with stream redirection `'exec 2>&1;${cmd}'`. Only stdout and exit_code are available in Result.
|
||||||
|
|
||||||
|
* `util.execute_with_timeout()` (from `os.util`) — just an `os.execute()` wrapper.
|
||||||
|
|
||||||
* `os.system()` — also executes command in the shell, but does not redirect streams. This is fine for running commands that take a long time and write something to the terminal; it's convenient in build scripts.
|
* `os.system()` — also executes command in the shell, but does not redirect streams. This is fine for running commands that take a long time and write something to the terminal; it's convenient in build scripts.
|
||||||
|
|
||||||
* `os.Process` just has an ugly interface with a lot of unnecessary methods. Actually, it's not bad; I copied parts of it.
|
* `os.Process` just has an ugly interface with a lot of unnecessary methods. Actually, it's not bad; I copied parts of it.
|
||||||
|
|
||||||
* `os.Command` runs `popen()` under the hood and is not suitable for anything other than running a command in the shell (again) with stream processing of the mixed stdout and stderr.
|
* `os.Command` calls `C.popen()` under the hood and is not suitable for anything other than running a command in the shell (again) with stream processing of the mixed stdout and stderr.
|
||||||
|
|
||||||
This `runcmd` module is inspired by os/exec from the Go standard library and provides
|
|
||||||
a fairly flexible interface for starting child processes.
|
|
||||||
|
|
||||||
The obvious downside of this module is that it only works on Linux and likely other
|
The obvious downside of this module is that it only works on Linux and likely other
|
||||||
POSIX-compliant operating systems. I'm not interested in working on MS Windows, but
|
POSIX-compliant operating systems. I'm not interested in working on MS Windows, but
|
||||||
@@ -38,20 +36,6 @@ cmd.run()! // Start and wait for process.
|
|||||||
println(cmd.state) // exit status 0
|
println(cmd.state) // exit status 0
|
||||||
```
|
```
|
||||||
|
|
||||||
You can create a `Command` object directly if that's more convenient. The following
|
|
||||||
example is equivalent to the first:
|
|
||||||
|
|
||||||
```v
|
|
||||||
import runcmd
|
|
||||||
|
|
||||||
mut cmd := runcmd.Command{
|
|
||||||
path: 'sh' // automatically resolves to actual path, e.g. /usr/bin/sh
|
|
||||||
args: ['-c', 'echo Hello, World!']
|
|
||||||
}
|
|
||||||
cmd.run()!
|
|
||||||
println(cmd.state)
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't want to wait for the child process to complete, call `start()` instead of `run()`:
|
If you don't want to wait for the child process to complete, call `start()` instead of `run()`:
|
||||||
|
|
||||||
```v
|
```v
|
||||||
@@ -64,12 +48,13 @@ println(pid)
|
|||||||
If you need to capture standard output and standard error, use the `output()` and
|
If you need to capture standard output and standard error, use the `output()` and
|
||||||
`combined_output()`. See examples in its description.
|
`combined_output()`. See examples in its description.
|
||||||
|
|
||||||
See also [examples](examples) dir for more examples.
|
To learn more about the `runcmd`'s capabilities and usage, see the [examples](examples)
|
||||||
|
directory. **Examples are very important**.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] Basic implementation.
|
- [x] Basic implementation.
|
||||||
- [ ] Contexts support for creating cancelable commands, commands with timeouts, etc.
|
- [x] Contexts support for creating cancelable commands, commands with timeouts, etc.
|
||||||
- [ ] Process groups support, pgkill().
|
- [ ] Process groups support, pgkill().
|
||||||
- [ ] Better error handling and more tests...
|
- [ ] Better error handling and more tests...
|
||||||
|
|
||||||
|
|||||||
60
cmd.v
60
cmd.v
@@ -1,11 +1,14 @@
|
|||||||
module runcmd
|
module runcmd
|
||||||
|
|
||||||
|
import context
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import strings
|
import strings
|
||||||
|
|
||||||
type IOCopyFn = fn () !
|
type IOCopyFn = fn () !
|
||||||
|
|
||||||
|
pub type CommandCancelFn = fn () !
|
||||||
|
|
||||||
@[heap]
|
@[heap]
|
||||||
pub struct Command {
|
pub struct Command {
|
||||||
pub mut:
|
pub mut:
|
||||||
@@ -52,15 +55,29 @@ pub mut:
|
|||||||
stdout ?io.Writer
|
stdout ?io.Writer
|
||||||
stderr ?io.Writer
|
stderr ?io.Writer
|
||||||
|
|
||||||
|
// ctx holds a command context. It may be used to make a command
|
||||||
|
// cancelable or set timeout/deadline for it.
|
||||||
|
ctx ?context.Context
|
||||||
|
|
||||||
|
// cancel function is used to terminate the child process. Do
|
||||||
|
// not confuse with the context's cancel function.
|
||||||
|
//
|
||||||
|
// The default command cancel function created in with_context()
|
||||||
|
// terminates the command by sending SIGTERM signal to child. You
|
||||||
|
// can override it by setting your own command cancel function.
|
||||||
|
// If cancel is none the child process won't be terminated even
|
||||||
|
// if context is timed out or canceled.
|
||||||
|
cancel ?CommandCancelFn
|
||||||
|
|
||||||
|
// process holds the underlying Process once started.
|
||||||
|
process ?&Process
|
||||||
|
|
||||||
// state holds an information about underlying process.
|
// state holds an information about underlying process.
|
||||||
// This is set only if process if finished. Call `run()`
|
// This is set only if process if finished. Call `run()`
|
||||||
// or `wait()` to get actual state value.
|
// or `wait()` to get actual state value.
|
||||||
// This value MUST NOT be changed by API user.
|
// This value MUST NOT be changed by API user.
|
||||||
state ProcessState
|
state ProcessState
|
||||||
mut:
|
mut:
|
||||||
// process holds the underlying Process.
|
|
||||||
process &Process = unsafe { nil }
|
|
||||||
|
|
||||||
// stdio holds a file descriptors for I/O processing.
|
// stdio holds a file descriptors for I/O processing.
|
||||||
// There is:
|
// There is:
|
||||||
// * 0 — child process stdin, we must write into it.
|
// * 0 — child process stdin, we must write into it.
|
||||||
@@ -155,7 +172,7 @@ pub fn (mut c Command) combined_output() !string {
|
|||||||
// to complete and release associated resources.
|
// to complete and release associated resources.
|
||||||
// Note: `.state` field is not set after `start()` call.
|
// Note: `.state` field is not set after `start()` call.
|
||||||
pub fn (mut c Command) start() !int {
|
pub fn (mut c Command) start() !int {
|
||||||
if !isnil(c.process) {
|
if c.process != none {
|
||||||
return error('runcmd: process already started')
|
return error('runcmd: process already started')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +257,10 @@ pub fn (mut c Command) start() !int {
|
|||||||
post_fork_child_cb: post_fork_child_cb
|
post_fork_child_cb: post_fork_child_cb
|
||||||
}
|
}
|
||||||
|
|
||||||
pid := c.process.start()!
|
mut pid := -1
|
||||||
|
if c.process != none {
|
||||||
|
pid = c.process.start()!
|
||||||
|
}
|
||||||
|
|
||||||
// Start I/O copy callbacks.
|
// Start I/O copy callbacks.
|
||||||
if c.stdio_copy_fns.len > 0 {
|
if c.stdio_copy_fns.len > 0 {
|
||||||
@@ -252,21 +272,47 @@ pub fn (mut c Command) start() !int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle context here...
|
if c.ctx != none && c.cancel != none {
|
||||||
|
printdbg('${@METHOD}: start watching for context')
|
||||||
|
go c.ctx_watch()
|
||||||
|
}
|
||||||
|
|
||||||
return pid
|
return pid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn (mut c Command) ctx_watch() {
|
||||||
|
mut ch := chan int{}
|
||||||
|
if c.ctx != none {
|
||||||
|
ch = c.ctx.done()
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
_ := <-ch {
|
||||||
|
printdbg('${@METHOD}: context is canceled/done')
|
||||||
|
if c.cancel != none {
|
||||||
|
printdbg('${@METHOD}: cancel command now!')
|
||||||
|
c.cancel() or { eprintln('error canceling command: ${err}') }
|
||||||
|
printdbg('${@METHOD}: command canceled!')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wait waits to previously started command is finished. After call see the `.state`
|
// wait waits to previously started command is finished. After call see the `.state`
|
||||||
// field value to get finished process identifier, exit status and other attributes.
|
// field value to get finished process identifier, exit status and other attributes.
|
||||||
// `wait()` will return an error if the process has not been started or wait has
|
// `wait()` will return an error if the process has not been started or wait has
|
||||||
// already been called.
|
// already been called.
|
||||||
pub fn (mut c Command) wait() ! {
|
pub fn (mut c Command) wait() ! {
|
||||||
if isnil(c.process) {
|
if c.process == none {
|
||||||
return error('runcmd: wait for non-started process')
|
return error('runcmd: wait for non-started process')
|
||||||
} else if c.state != ProcessState{} {
|
} else if c.state != ProcessState{} {
|
||||||
return error('runcmd: wait already called')
|
return error('runcmd: wait already called')
|
||||||
}
|
}
|
||||||
|
if c.process != none {
|
||||||
c.state = c.process.wait()!
|
c.state = c.process.wait()!
|
||||||
|
}
|
||||||
unsafe { c.release()! }
|
unsafe { c.release()! }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
35
examples/command_with_cancel.v
Normal file
35
examples/command_with_cancel.v
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import context
|
||||||
|
import runcmd
|
||||||
|
import time
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Create context with cancel.
|
||||||
|
mut bg := context.background()
|
||||||
|
mut ctx, cancel := context.with_cancel(mut bg)
|
||||||
|
|
||||||
|
// Create new command with context.
|
||||||
|
mut cmd := runcmd.with_context(ctx, 'sleep', '120')
|
||||||
|
|
||||||
|
// Start a command.
|
||||||
|
println('Start command!')
|
||||||
|
cmd.start()!
|
||||||
|
|
||||||
|
// Sleep a bit for demonstration.
|
||||||
|
time.sleep(1 * time.second)
|
||||||
|
|
||||||
|
// Cancel command.
|
||||||
|
//
|
||||||
|
// In a real application, cancel() might be initiated by the user.
|
||||||
|
// For example, a command might take too long to execute and need
|
||||||
|
// to be canceled.
|
||||||
|
//
|
||||||
|
// See also command_with_timeout.v example.
|
||||||
|
println('Cancel command!')
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Wait for command.
|
||||||
|
cmd.wait()!
|
||||||
|
|
||||||
|
// Since command has been terminated, the state would be: `signal: 15 (SIGTERM)`
|
||||||
|
println('Child state: ${cmd.state}')
|
||||||
|
}
|
||||||
46
examples/command_with_cancel_custom.v
Normal file
46
examples/command_with_cancel_custom.v
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import context
|
||||||
|
import runcmd
|
||||||
|
import time
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Create context with cancel.
|
||||||
|
mut bg := context.background()
|
||||||
|
mut ctx, cancel := context.with_cancel(mut bg)
|
||||||
|
|
||||||
|
// Create new command as usual.
|
||||||
|
mut cmd := runcmd.new('sleep', '120')
|
||||||
|
|
||||||
|
// Set the context...
|
||||||
|
cmd.ctx = ctx
|
||||||
|
|
||||||
|
// ...and custom command cancel function.
|
||||||
|
cmd.cancel = fn [mut cmd] () ! {
|
||||||
|
if cmd.process != none {
|
||||||
|
println('Killing ${cmd.process.pid()}!')
|
||||||
|
cmd.process.kill()!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a command.
|
||||||
|
println('Start command!')
|
||||||
|
cmd.start()!
|
||||||
|
|
||||||
|
// Sleep a bit for demonstration.
|
||||||
|
time.sleep(1 * time.second)
|
||||||
|
|
||||||
|
// Cancel command.
|
||||||
|
//
|
||||||
|
// In a real application, cancel() might be initiated by the user.
|
||||||
|
// For example, a command might take too long to execute and need
|
||||||
|
// to be canceled.
|
||||||
|
//
|
||||||
|
// See also command_with_timeout.v example.
|
||||||
|
println('Cancel command!')
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Wait for command.
|
||||||
|
cmd.wait()!
|
||||||
|
|
||||||
|
// Since command has been killed, the state would be: `signal: 9 (SIGKILL)`
|
||||||
|
println('Child state: ${cmd.state}')
|
||||||
|
}
|
||||||
27
examples/command_with_timeout.v
Normal file
27
examples/command_with_timeout.v
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import context
|
||||||
|
import runcmd
|
||||||
|
import time
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Create context with cancel.
|
||||||
|
mut bg := context.background()
|
||||||
|
mut ctx, _ := context.with_timeout(mut bg, 10 * time.second)
|
||||||
|
|
||||||
|
// Create new command with context.
|
||||||
|
mut cmd := runcmd.with_context(ctx, 'sleep', '120')
|
||||||
|
|
||||||
|
// Start a command.
|
||||||
|
started := time.now()
|
||||||
|
println('Start command at ${started}')
|
||||||
|
cmd.start()!
|
||||||
|
|
||||||
|
// Wait for command.
|
||||||
|
cmd.wait()!
|
||||||
|
|
||||||
|
// The `sleep 120` command would run for two minutes without a timeout.
|
||||||
|
// But in this example, it will time out after 10 seconds.
|
||||||
|
println('Command finished after ${time.now() - started}')
|
||||||
|
|
||||||
|
// Since command has been terminated, the state would be: `signal: 15 (SIGTERM)`
|
||||||
|
println('Child state: ${cmd.state}')
|
||||||
|
}
|
||||||
15
runcmd.v
15
runcmd.v
@@ -1,8 +1,9 @@
|
|||||||
module runcmd
|
module runcmd
|
||||||
|
|
||||||
|
import context
|
||||||
import os
|
import os
|
||||||
|
|
||||||
// new creates new Command instance with given command name and arguments.
|
// new creates new command with given command name and arguments.
|
||||||
pub fn new(name string, arg ...string) &Command {
|
pub fn new(name string, arg ...string) &Command {
|
||||||
return &Command{
|
return &Command{
|
||||||
path: name
|
path: name
|
||||||
@@ -10,6 +11,18 @@ pub fn new(name string, arg ...string) &Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// with_context creates new command with context, command name and arguments.
|
||||||
|
pub fn with_context(ctx context.Context, name string, arg ...string) &Command {
|
||||||
|
mut cmd := new(name, ...arg)
|
||||||
|
cmd.ctx = ctx
|
||||||
|
cmd.cancel = fn [mut cmd] () ! {
|
||||||
|
if cmd.process != none {
|
||||||
|
cmd.process.signal(.term)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
// is_present returns true if cmd is present on system. cmd may be a command
|
// is_present returns true if cmd is present on system. cmd may be a command
|
||||||
// name or filepath (relative or absolute).
|
// name or filepath (relative or absolute).
|
||||||
// The result relies on `look_path()` output, see its docs for command search
|
// The result relies on `look_path()` output, see its docs for command search
|
||||||
|
|||||||
3
v.mod
3
v.mod
@@ -1,7 +1,8 @@
|
|||||||
Module {
|
Module {
|
||||||
name: 'runcmd'
|
name: 'runcmd'
|
||||||
description: 'Run external commands'
|
description: 'Run external commands'
|
||||||
version: '0.1.0'
|
version: '0.2.0'
|
||||||
license: 'Unlicense'
|
license: 'Unlicense'
|
||||||
|
repo_url: 'https://github.com/gechandesu/runcmd'
|
||||||
dependencies: []
|
dependencies: []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user