Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/completions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ _u7_complete_entities() {
COMPREPLY=($(compgen -W "text slashes tabs perms owner --help" -- "$cur"))
;;
run|rn)
COMPREPLY=($(compgen -W "job script check terminal --help" -- "$cur"))
COMPREPLY=($(compgen -W "job script check terminal watch --help" -- "$cur"))
;;
esac
}
Expand Down Expand Up @@ -78,6 +78,7 @@ _u7_complete_args() {
case "$entity" in
check) COMPREPLY=($(compgen -W "syntax" -- "$cur")) ; _filedir ;;
script) _filedir ;;
watch) _filedir ;;
esac
;;
esac
Expand Down
44 changes: 44 additions & 0 deletions lib/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,49 @@ _u7_run() {
fi
;;

watch)
local file="$1"
if [[ -z "$file" || "$2" != "run" ]]; then
echo "Usage: u7 rn watch <file> run <command>"
return 1
fi
if [[ ! -e "$file" ]]; then
echo "File not found: $file"
return 1
fi
shift 2
local cmd="$*"
if [[ -z "$cmd" ]]; then
echo "Usage: u7 rn watch <file> run <command>"
return 1
fi
if [[ "$_U7_DRY_RUN" == "1" ]]; then
echo "[dry-run] watch $file and run $cmd on change"
return 0
fi
echo "Watching $file for changes... (Ctrl+C to stop)"
if command -v inotifywait &>/dev/null; then
while inotifywait -qq -e modify "$file"; do
eval "$cmd"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using eval on command strings can be risky and lead to unexpected behavior due to double evaluation of shell metacharacters. It's generally safer to execute commands in a more controlled environment.

While eval allows executing shell functions from the parent environment, consider if this is a required feature. If not, a safer alternative like bash -c "$cmd" could be used, which runs the command in an isolated subshell. This advice also applies to line 118.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using eval can introduce security vulnerabilities and unexpected behavior due to its complex parsing rules. A safer and more robust approach is to execute the command in a subshell using bash -c. This contains the execution and avoids altering the current shell's state. This pattern is already used for the priority command in this script.

Suggested change
eval "$cmd"
bash -c "$cmd"

done
else
local last_mod cur_mod
last_mod=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null)
while true; do
sleep 1
cur_mod=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null)
if [[ -z "$cur_mod" ]]; then
echo "File no longer exists, stopping watch." >&2
break
fi
if [[ "$cur_mod" != "$last_mod" ]]; then
last_mod="$cur_mod"
eval "$cmd"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

For the same security and robustness reasons mentioned for the inotifywait branch, please replace eval with bash -c here as well.

Suggested change
eval "$cmd"
bash -c "$cmd"

fi
done
Comment on lines +111 to +122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The polling loop does not handle file deletion gracefully. If the file is deleted, stat will fail, cur_mod will be empty, and the command will be executed one last time before the loop goes idle. The loop will then continue polling a non-existent file indefinitely.

The loop should detect when stat fails and exit cleanly, informing the user that the file is no longer accessible.

Suggested change
while true; do
sleep 1
local cur_mod
cur_mod=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null)
if [[ "$cur_mod" != "$last_mod" ]]; then
last_mod="$cur_mod"
eval "$cmd"
fi
done
while true; do
sleep 1
local cur_mod
cur_mod=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null)
if [[ -z "$cur_mod" ]]; then
echo "File '$file' no longer accessible. Stopping watch." >&2
break
fi
if [[ "$cur_mod" != "$last_mod" ]]; then
last_mod="$cur_mod"
eval "$cmd"
fi
done

fi
;;

--help|-h)
cat << 'EOF'
u7 rn (run) - Execute/Control
Expand All @@ -89,6 +132,7 @@ Usage: u7 rn <entity> [arguments]
Entities:
job <cmd> in <time> Schedule command (5s, 10m, 1h)
script <path> Execute shell script
watch <file> run <command> Watch file and run command on change
<command> in background Run command in background
<command> with priority <nice> Run with CPU priority
check syntax in file <path> Check single file syntax
Expand Down
24 changes: 24 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,30 @@ else
((PASSED++))
fi

# ===== rn watch tests =====

# Test: rn watch usage (missing args)
result=$(u7 rn watch 2>&1)
assert_contains "rn watch shows usage on missing args" "Usage" "$result"

# Test: rn watch usage (missing run keyword)
touch watch_target.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This touch command is unnecessary for this test case. The u7 rn watch command checks for the run keyword before it checks for the file's existence. The test will pass correctly without this line, making it more focused on its purpose: validating the presence of the run keyword. This line can be removed.

result=$(u7 rn watch watch_target.txt 2>&1)
assert_contains "rn watch shows usage when run keyword missing" "Usage" "$result"

# Test: rn watch file not found
result=$(u7 rn watch nonexistent_file.txt run echo hi 2>&1)
assert_contains "rn watch reports missing file" "not found" "$result"

# Test: rn watch dry-run
touch watch_target.txt
result=$(u7 -n rn watch watch_target.txt run echo hello 2>&1)
assert_equals "rn watch dry-run output" "[dry-run] watch watch_target.txt and run echo hello on change" "$result"

# Test: rn watch help text includes watch
result=$(u7 rn --help 2>&1)
assert_contains "rn help mentions watch" "watch" "$result"

# ===== sh log tests =====

# Setup: create a temp log file
Expand Down
Loading