Compare commits
No commits in common. "master" and "v0.3.0" have entirely different histories.
10 changed files with 118 additions and 233 deletions
65
README.md
65
README.md
|
@ -11,7 +11,7 @@ A simple watcher program written in Go that runs shell command(s) when the speci
|
|||
- Great for students who work on a small set of source files for assignments.
|
||||
- Also great for validating configuration files.
|
||||
|
||||
### Other Features
|
||||
## Other Features
|
||||
|
||||
- Choose between listening to kernel events, or wait between checks on an interval.
|
||||
- Run the command when any or all files specified are modified.
|
||||
|
@ -22,65 +22,6 @@ A simple watcher program written in Go that runs shell command(s) when the speci
|
|||
- Helpful if linters are involved, such as `clang-format`, to avoid multiple runs of a command.
|
||||
- Free and Open Source software!
|
||||
|
||||
## Installation
|
||||
|
||||
You can install this package easily using the Go CLI:
|
||||
|
||||
```bash
|
||||
# main repo
|
||||
go install forge.steck.dev/bryson/listen@latest
|
||||
# codeberg mirror
|
||||
go install codeberg.org/brysonsteck/listen@latest
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Using Go
|
||||
|
||||
When possible, you should [install Go](https://go.dev/doc/install) on your system and build from source:
|
||||
|
||||
```bash
|
||||
# you can alternatively clone the codeberg mirror: codeberg.org/brysonsteck/listen
|
||||
git clone forge.steck.dev/bryson/listen && cd listen
|
||||
go build . -o out/listen
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
|
||||
On **Windows and Linux**, you can alternatively build Listen in a Docker container and copy it out:
|
||||
|
||||
```bash
|
||||
# you can alternatively clone the codeberg mirror: codeberg.org/brysonsteck/listen
|
||||
git clone forge.steck.dev/bryson/listen && cd listen
|
||||
|
||||
# replace "linux" with "windows" appropriately
|
||||
docker build -t listen-build -f docker/build.linux.Dockerfile .
|
||||
mkdir -p out
|
||||
docker create --name listen-build-tmp listen-build
|
||||
# the exe is located at:
|
||||
# /usr/src/listen/listen - on Linux
|
||||
# C:\build\listen\listen.exe - on Windows
|
||||
docker cp listen-build-tmp:/usr/src/listen/listen out/
|
||||
docker rm listen-build-tmp
|
||||
```
|
||||
|
||||
Listen is not intended for use in a standalone Docker environment (currently) due to it's function. However, you could build Listen in a stage and copy the executable to another stage to run a program inside a container:
|
||||
|
||||
```dockerfile
|
||||
# Example Dockerfile
|
||||
FROM golang:1-alpine AS build
|
||||
|
||||
WORKDIR /usr/src/listen
|
||||
COPY . .
|
||||
RUN go build .
|
||||
|
||||
FROM python:3 AS main
|
||||
|
||||
COPY --from=build /usr/src/listen /usr/local/bin
|
||||
# expect a file mounted to /usr/src/main.py to listen to
|
||||
CMD ["listen", "-f", "/usr/src/main.py", "--", "python", "main.py"]
|
||||
```
|
||||
|
||||
## History
|
||||
|
||||
Listen was originally a Perl script that came about when I wanted something for my college assignments and also needed to learn Perl for an internship. I wrote it once and haven't touched it or improved upon it since I created it.
|
||||
|
@ -101,9 +42,9 @@ the Free Software Foundation, either version 3 of the License, or
|
|||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
```
|
||||
|
|
56
Taskfile.yml
56
Taskfile.yml
|
@ -1,56 +0,0 @@
|
|||
version: '3'
|
||||
|
||||
vars:
|
||||
UNAME:
|
||||
sh: uname
|
||||
DOCKER_EXE: /usr/src/listen/out/{{OS}}/listen
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps:
|
||||
- build:go
|
||||
|
||||
build:go:
|
||||
aliases:
|
||||
- build
|
||||
cmds:
|
||||
- "go build -o out/{{OS}}/ ."
|
||||
preconditions:
|
||||
- sh: "which go"
|
||||
msg: Go is not installed. Install Go or build with Docker using the "docker" task
|
||||
|
||||
build:docker:
|
||||
aliases:
|
||||
- docker
|
||||
cmds:
|
||||
- task: build:docker:dockercli
|
||||
- "docker build -t listen-build -f docker/build.Dockerfile --build-arg GOOS='{{OS}}' --build-arg GOARCH='{{ARCH}}' ."
|
||||
- "mkdir -p out/{{OS}}"
|
||||
- "docker create --name listen-build-tmp listen-build"
|
||||
- cmd: "docker cp listen-build-tmp:{{.DOCKER_EXE}}.exe out/{{OS}}/"
|
||||
platforms:
|
||||
- windows
|
||||
- cmd: "docker cp listen-build-tmp:{{.DOCKER_EXE}} out/{{OS}}/"
|
||||
platforms:
|
||||
- darwin
|
||||
- linux
|
||||
- "docker rm listen-build-tmp"
|
||||
preconditions:
|
||||
- sh: "uname | grep -qe Linux -e MINGW -e Darwin"
|
||||
msg: "This task cannot be ran on OS: {{OS}}"
|
||||
- sh: "which docker"
|
||||
msg: Docker is not installed. Install Docker or build with Go using the "build" task
|
||||
|
||||
build:docker:dockercli:
|
||||
cmds:
|
||||
- cmd: '"C:\Program Files\Docker\Docker\DockerCli.exe" -SwitchLinuxEngine'
|
||||
platforms:
|
||||
- windows
|
||||
|
||||
clean:
|
||||
cmds:
|
||||
- rm -rf out/
|
||||
- cmd: docker rm listen-build-tmp
|
||||
ignore_error: true
|
||||
- cmd: docker image rm listen-build
|
||||
ignore_error: true
|
49
build
Executable file
49
build
Executable file
|
@ -0,0 +1,49 @@
|
|||
#!/bin/sh
|
||||
# Create all the different builds for listen
|
||||
|
||||
# verify we are at root of repository
|
||||
if ! [ -d .git ]; then
|
||||
echo build: this script must be run at the root of the repo
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if uname | grep -qe Linux; then
|
||||
os=linux
|
||||
elif uname | grep -qe MINGW; then
|
||||
os=windows
|
||||
else
|
||||
os=other
|
||||
fi
|
||||
|
||||
build_go() {
|
||||
go build -o out/$(go env GOOS)/listen .
|
||||
}
|
||||
|
||||
build_docker() {
|
||||
if [ $os = "windows" ]; then
|
||||
/c/Program\ Files/Docker/Docker/DockerCli.exe -SwitchWindowsEngine
|
||||
exe=C:/build/listen/listen.exe
|
||||
else
|
||||
exe=/usr/src/listen/listen
|
||||
fi
|
||||
|
||||
if docker build -t listen-build -f docker/build.$os.Dockerfile .; then
|
||||
mkdir -p out/$os
|
||||
docker create --name listen-build-tmp listen-build
|
||||
docker cp listen-build-tmp:$exe out/$os
|
||||
docker rm listen-build-tmp
|
||||
fi
|
||||
}
|
||||
|
||||
# if an arg is specified, force building with the specified method
|
||||
[ $1 ] && (build_$1; exit)
|
||||
|
||||
# prefer building with local go install if it exists on path
|
||||
if which go &> /dev/null; then
|
||||
build_go; exit
|
||||
# if windows or linux, try building with docker
|
||||
elif echo $os | grep -qe linux -e windows; then
|
||||
which docker &> /dev/null && (build_docker; exit)
|
||||
fi
|
||||
|
||||
echo could not find valid build method for OS && exit 2
|
|
@ -1,16 +0,0 @@
|
|||
# This Dockerfile is meant for building listen ONLY
|
||||
# listen is currently not intended to run in a Docker container
|
||||
|
||||
FROM golang:1-alpine AS build
|
||||
|
||||
WORKDIR /usr/src/listen
|
||||
COPY . .
|
||||
|
||||
RUN apk upgrade --no-cache
|
||||
ARG GOOS="linux"
|
||||
RUN go env -w GOOS=${GOOS}
|
||||
ARG GOARCH="amd64"
|
||||
RUN go env -w GOARCH=${GOARCH}
|
||||
RUN go build -o out/$(go env GOOS)/ .
|
||||
|
||||
CMD ["tail", "-f", "/dev/null"]
|
14
docker/build.linux.Dockerfile
Normal file
14
docker/build.linux.Dockerfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This Dockerfile is meant for building listen for Linux ONLY
|
||||
# listen is currently not intended to run in a Docker container
|
||||
|
||||
ARG IMAGE="1-alpine"
|
||||
|
||||
FROM golang:${IMAGE} AS build
|
||||
|
||||
WORKDIR /usr/src/listen
|
||||
COPY . .
|
||||
|
||||
RUN apk upgrade --no-cache
|
||||
RUN go build .
|
||||
|
||||
CMD ["tail", "-f", "/dev/null"]
|
13
docker/build.windows.Dockerfile
Normal file
13
docker/build.windows.Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
# This Dockerfile is meant for building listen for Windows ONLY
|
||||
# listen is currently not intended to run in a Docker container
|
||||
|
||||
ARG IMAGE="1-nanoserver"
|
||||
|
||||
FROM golang:${IMAGE} AS build
|
||||
|
||||
WORKDIR C:/build/listen
|
||||
COPY . .
|
||||
|
||||
RUN go build .
|
||||
|
||||
CMD ["ping.exe", "-t", "localhost"]
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
|||
module forge.steck.dev/bryson/listen
|
||||
|
||||
go 1.23
|
||||
go 1.24
|
||||
|
||||
require github.com/jessevdk/go-flags v1.6.1
|
||||
|
||||
|
|
85
listen.go
85
listen.go
|
@ -17,14 +17,12 @@ import (
|
|||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
var triggered bool = false
|
||||
|
||||
type Listen struct {
|
||||
FileMap map[string]bool
|
||||
CksumMap map[string][]byte
|
||||
IntervalMap map[string]time.Time
|
||||
Condition string
|
||||
NoCksum bool
|
||||
Cksum bool
|
||||
Interval time.Duration
|
||||
Command []string
|
||||
Quiet bool
|
||||
|
@ -76,7 +74,6 @@ func allCheck(l Listen) bool {
|
|||
}
|
||||
|
||||
func runTrigger(l Listen, f string, r bool, quit chan bool) bool {
|
||||
triggered = true
|
||||
if !l.Quiet && !r {
|
||||
if l.Condition == "any" {
|
||||
printStatus(fmt.Sprintf("& File %s modified. ", f))
|
||||
|
@ -97,32 +94,12 @@ func runTrigger(l Listen, f string, r bool, quit chan bool) bool {
|
|||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
exit := make(chan bool)
|
||||
s := make(chan os.Signal, 1)
|
||||
signal.Notify(s, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if rc, ok := err.(*exec.ExitError); ok {
|
||||
if !l.Quiet {
|
||||
switch rc.ExitCode() {
|
||||
case -1:
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "\n& WARNING: Command interrupted with ^C.\n")
|
||||
default:
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "& WARNING: Command exited with code %d.\n", rc.ExitCode())
|
||||
}
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
if rc, ok := err.(*exec.ExitError); ok {
|
||||
if !l.Quiet {
|
||||
color.New(color.FgYellow).Fprintf(os.Stderr, "& WARNING: Command exited with code %d.\n", rc.ExitCode())
|
||||
}
|
||||
}
|
||||
|
||||
exit <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-s:
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
<-exit
|
||||
case <-exit:
|
||||
}
|
||||
|
||||
if !l.Quiet {
|
||||
|
@ -134,11 +111,9 @@ func runTrigger(l Listen, f string, r bool, quit chan bool) bool {
|
|||
}
|
||||
|
||||
quit <- true
|
||||
triggered = false
|
||||
return true
|
||||
}
|
||||
|
||||
triggered = false
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -165,7 +140,13 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
|
||||
for key := range l.FileMap {
|
||||
if event.Name == key {
|
||||
if l.NoCksum {
|
||||
if l.Cksum {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if event.Has(fsnotify.Write) {
|
||||
if l.Condition == "any" {
|
||||
fileMod = key
|
||||
|
@ -188,12 +169,6 @@ func loopFsnotify(l Listen, quit chan bool) {
|
|||
|
||||
l.FileMap[key] = true
|
||||
}
|
||||
} else {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end for
|
||||
|
@ -231,7 +206,13 @@ func loopInterval(l Listen, quit chan bool) {
|
|||
trigger := false
|
||||
|
||||
for key := range l.FileMap {
|
||||
if l.NoCksum {
|
||||
if l.Cksum {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
} else {
|
||||
s, err := os.Stat(key)
|
||||
if err != nil {
|
||||
log.Fatal()
|
||||
|
@ -247,12 +228,7 @@ func loopInterval(l Listen, quit chan bool) {
|
|||
|
||||
l.FileMap[key] = true
|
||||
}
|
||||
} else {
|
||||
// return value indicates a break is needed
|
||||
if cksumCheck(l, key, &trigger) {
|
||||
fileMod = key
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
} // end for key
|
||||
|
||||
|
@ -286,7 +262,7 @@ func startMessage(l Listen) {
|
|||
printStatus("& listen will exit when ")
|
||||
}
|
||||
|
||||
if !l.NoCksum {
|
||||
if l.Cksum {
|
||||
printStatus("the checksum of ")
|
||||
}
|
||||
|
||||
|
@ -302,10 +278,10 @@ func startMessage(l Listen) {
|
|||
printStatus("this file has ")
|
||||
}
|
||||
|
||||
if l.NoCksum {
|
||||
printStatus("been modified:\n")
|
||||
} else {
|
||||
if l.Cksum {
|
||||
printStatus("changed:\n")
|
||||
} else {
|
||||
printStatus("been modified:\n")
|
||||
}
|
||||
|
||||
for key := range l.FileMap {
|
||||
|
@ -313,9 +289,8 @@ func startMessage(l Listen) {
|
|||
}
|
||||
}
|
||||
|
||||
func (l Listen) Run() int {
|
||||
func (l Listen) Run() {
|
||||
// catch ^C
|
||||
ret := 0
|
||||
quit := make(chan bool)
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
|
@ -342,14 +317,7 @@ func (l Listen) Run() int {
|
|||
|
||||
select {
|
||||
case <-c:
|
||||
for {
|
||||
if !triggered {
|
||||
fmt.Println()
|
||||
ret = 130
|
||||
break
|
||||
}
|
||||
<-c
|
||||
}
|
||||
fmt.Println()
|
||||
case <-quit:
|
||||
}
|
||||
|
||||
|
@ -357,5 +325,4 @@ func (l Listen) Run() int {
|
|||
printStatus("& Exiting...\n")
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
42
main.go
42
main.go
|
@ -13,16 +13,16 @@ import (
|
|||
flags "github.com/jessevdk/go-flags"
|
||||
)
|
||||
|
||||
const VERSION string = "v0.4.1"
|
||||
const VERSION string = "v0.3.0"
|
||||
|
||||
type Options struct {
|
||||
Version bool `short:"v" long:"version" description:"Displays version info and exits"`
|
||||
Quiet bool `short:"q" long:"quiet" description:"Suppresses status messages (stderr lines beginning with '&')"`
|
||||
Files []string `short:"f" long:"file" description:"File(s) to listen to (watch)" value-name:"FILE"`
|
||||
Condition string `short:"w" long:"when" description:"If multiple files are specified, choose if any file or all files specified are needed to trigger COMMAND" default:"any" choice:"any" choice:"all"`
|
||||
NoChecksum bool `short:"x" long:"no-checksum" description:"Do not calculate checksum as an additional check for file changes (see MODES section in man page for more info)"`
|
||||
Interval string `short:"i" long:"interval" description:"Use this time interval (ex. 5m30s, 1s) between filesystem checks instead of watching kernel events. If the interval is effectively 0 (the default), kernel events are used" default:"0s"`
|
||||
RunFirst bool `short:"r" long:"run" description:"Runs COMMAND (if specified) before starting listen"`
|
||||
Version bool `short:"v" long:"version" description:"Displays version info and exits"`
|
||||
Quiet bool `short:"q" long:"quiet" description:"Suppresses status messages (stderr lines beginning with '&')"`
|
||||
Files []string `short:"f" long:"file" description:"File(s) to listen to (watch)" value-name:"FILE"`
|
||||
Condition string `short:"w" long:"when" description:"If multiple files are specified, choose if any file or all files specified are needed to trigger COMMAND" default:"any" choice:"any" choice:"all"`
|
||||
Checksum bool `short:"c" long:"checksum" description:"Use checksum to determine when file(s) are changed instead of writes/modification time"`
|
||||
Interval string `short:"i" long:"interval" description:"Use this time interval (ex. 5m30s, 1s) between filesystem checks instead of watching kernel events. If the interval is effectively 0 (the default), kernel events are used" default:"0s"`
|
||||
RunFirst bool `short:"r" long:"run" description:"Runs COMMAND (if specified) before starting listen"`
|
||||
}
|
||||
|
||||
func validateArgs(opts Options, commandLen int) error {
|
||||
|
@ -66,7 +66,7 @@ func validateArgs(opts Options, commandLen int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func setup(opts Options, command []string) int {
|
||||
func setup(opts Options, command []string) {
|
||||
var filesMap map[string]bool = make(map[string]bool)
|
||||
var cksumMap map[string][]byte = make(map[string][]byte)
|
||||
var intervalMap map[string]time.Time = make(map[string]time.Time)
|
||||
|
@ -103,40 +103,22 @@ func setup(opts Options, command []string) int {
|
|||
}
|
||||
|
||||
if success {
|
||||
l := Listen{filesMap, cksumMap, intervalMap, opts.Condition, opts.NoChecksum, intervalDuration, command, opts.Quiet, opts.RunFirst}
|
||||
return l.Run()
|
||||
l := Listen{filesMap, cksumMap, intervalMap, opts.Condition, opts.Checksum, intervalDuration, command, opts.Quiet, opts.RunFirst}
|
||||
l.Run()
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func main() {
|
||||
ret := 0
|
||||
opts := Options{}
|
||||
parser := flags.NewParser(&opts, flags.Default)
|
||||
parser.Usage = "[OPTIONS] -- [COMMAND]"
|
||||
remaining, err := parser.Parse()
|
||||
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
switch ret {
|
||||
case -1:
|
||||
os.Exit(5)
|
||||
default:
|
||||
os.Exit(ret)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err == nil {
|
||||
if opts.Version {
|
||||
fmt.Printf("listen %s\n", VERSION)
|
||||
} else if err := validateArgs(opts, len(remaining)); err == nil {
|
||||
ret = setup(opts, remaining)
|
||||
if ret != 0 {
|
||||
panic(ret)
|
||||
}
|
||||
setup(opts, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
% listen | General Commands Manual
|
||||
|
||||
# NAME
|
||||
|
||||
listen - A simple watcher program that runs commands when specified files are modified
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
listen [OPTIONS] -- [COMMAND]
|
Loading…
Add table
Reference in a new issue