web-dev-qa-db-fra.com

Répétez une commande Unix toutes les x secondes pour toujours

Il y a une commande Unix intégrée repeat dont le premier argument est le nombre de répétitions d'une commande, où la commande (avec tous les arguments) est spécifiée par les arguments restants à repeat.

Par exemple,

% repeat 100 echo "I will not automate this punishment."

fera écho à la chaîne donnée 100 fois puis s'arrêtera.

J'aimerais une commande similaire - appelons-la forever - qui fonctionne de la même manière, sauf que le premier argument est le nombre de secondes pour faire une pause entre les répétitions, et il se répète pour toujours. Par exemple,

% forever 5 echo "This will get echoed every 5 seconds forever and ever."

J'ai pensé demander si une telle chose existe avant de l'écrire. Je sais que c'est comme un Perl à 2 lignes ou un script Python, mais il y a peut-être une façon plus standard de le faire. Sinon, n'hésitez pas à publier une solution dans votre langage de script préféré.

PS: Peut-être qu'une meilleure façon de procéder serait de généraliser repeat pour prendre à la fois le nombre de répétitions (avec -1 signifiant l'infini) et le nombre de secondes pour dormir entre les répétitions. Les exemples ci-dessus deviendraient alors:

% repeat 100 0 echo "I will not automate this punishment."
% repeat -1 5 echo "This will get echoed every 5 seconds forever."
497
dreeves

Essayez la commande watch.

Usage: watch [-dhntv] [--differences[=cumulative]] [--help] [--interval=<n>] 
             [--no-title] [--version] <command>`

Pour que:

watch -n1  command

exécutera la commande toutes les secondes (enfin, techniquement, chaque seconde plus le temps nécessaire à command pour s'exécuter en tant que watch (au moins les procps et busybox implémentations) ne dort qu'une seconde entre deux exécutions de command), pour toujours.

Souhaitez-vous passer la commande à exec au lieu de sh -c, utilisation -x option:

watch -n1 -x command

Sur macOS, vous pouvez obtenir watch à partir de Ports Mac :

port install watch

Ou vous pouvez l'obtenir auprès de Homebrew :

brew install watch
637
Gregor Brandt

Bash

while + sleep:

while true
do 
    echo "Hi"
    sleep 1
done

Voici la même chose qu'un raccourci à une ligne (d'après les commentaires ci-dessous):

while sleep 1; do echo "Hi"; done

Les usages ; pour séparer les commandes et utilise sleep 1 pour le test while car il renvoie toujours vrai. Vous pouvez mettre plus de commandes dans la boucle - il suffit de les séparer avec ;

255
pope

Ceci est juste une version plus courte d'autres while+sleep réponses, si vous exécutez ce type de tâches souvent comme routine quotidienne, son utilisation vous évite d'appuyer sur des touches inutiles, et si votre ligne de commande commence à être plus longue, celle-ci est un peu plus facile. Mais celui-ci commence par dormir d'abord.

Ceci est généralement utile si vous devez suivre quelque chose qui a une sortie en ligne comme la charge de la machine:

while sleep 1; do uptime; done
73
Bekir Dogan

Un problème que toutes les réponses publiées jusqu'à présent ont est que le temps d'exécution de la commande peut dériver. Par exemple, si vous effectuez une sleep 10 entre les commandes, et la commande prend 2 secondes pour s'exécuter, puis elle va s'exécuter toutes les 12 secondes; s'il faut un temps variable pour s'exécuter, alors à long terme, le moment où il s'exécute peut être imprévisible.

Cela pourrait être exactement ce que vous voulez; si c'est le cas, utilisez l'une des autres solutions, ou utilisez celle-ci mais simplifiez l'appel sleep.

Pour une résolution d'une minute, les tâches cron s'exécutent à l'heure spécifiée, quelle que soit la durée de chaque commande. (En fait, une nouvelle tâche cron sera lancée même si la précédente est toujours en cours d'exécution.)

Voici un script Perl simple qui dort jusqu'à l'intervalle suivant, donc par exemple avec un intervalle de 10 secondes, la commande peut s'exécuter à 12:34:00, 12:34:10, 12:34:20, etc., même si le la commande elle-même prend plusieurs secondes. Si la commande s'exécute plus de interval secondes, l'intervalle suivant sera ignoré (contrairement à cron). L'intervalle est calculé par rapport à l'époque, donc un intervalle de 86400 secondes (1 jour) s'exécutera à minuit UTC.

#!/usr/bin/Perl

use strict;
use warnings;

if (scalar @ARGV < 2) {
    die "Usage: $0 seconds command [args...]\n";
}

$| = 1;  # Ensure output appears

my($interval, @command) = @ARGV;

# print ">>> interval=$interval command=(@command)\n";

while (1) {
    print "sleep ", $interval - time % $interval, "\n";
    sleep $interval - time % $interval;
    system @command; # TODO: Handle errors (how?)
}
46
Keith Thompson

Je pense que jusqu'à présent, toutes les réponses sont soit trop compliquées, soit répondent à une autre question:

  • "Comment exécuter un programme de façon répétée afin qu'il y ait un délai de X secondes entre la fin du programme et le démarrage suivant".

La vraie question était:

  • "Comment exécuter un programme toutes les X secondes"

Ce sont deux choses très différentes lorsque la commande prend du temps pour se terminer.

Prenez par exemple le script foo.sh (faites comme s'il s'agissait d'un programme qui prend quelques secondes).

#!/bin/bash
# foo.sh
echo `date +"%H:%M:%S"` >> output.txt;
sleep 2.5;
# ---

Vous souhaitez l'exécuter toutes les secondes, et la plupart suggèrent watch -n1 ./foo.sh, ou while sleep 1; do ./foo.sh; done. Cependant, cela donne la sortie:

15:21:20
15:21:23
15:21:27
15:21:30
15:21:34

Ce qui n'est pas exactement exécuté toutes les secondes. Même avec le -p flag, comme man watch la page suggère que cela pourrait résoudre ce problème, le résultat est le même.

Un moyen facile d'accomplir la tâche souhaitée, que certains abordent, consiste à exécuter la commande en arrière-plan. En d'autres termes:

while sleep 1; do (./foo.sh &) ; done

Et c'est tout ce qu'il y a à faire.

Vous pouvez l'exécuter toutes les 500 ms avec sleep 0.5, ou qu'avez-vous.

33
swalog

Dans bash:

bash -c 'while [ 0 ]; do echo "I will not automate this punishment in absurdum."; done'

(echo pourrait être remplacé par n'importe quelle commande ...

Ou dans Perl:

Perl -e 'for (;1;) {print "I will not automate this punishment in absurdum.\n"}'

print "I will not automate this punishment in absurdum.\n" pourrait être remplacé par la commande "any" entourée de guillemets (`).

Et pour une pause, ajoutez une instruction sleep à l'intérieur de la boucle for:

bash -c 'while [ 0 ]; do echo "I will not automate this punishment in absurdum."; sleep 1; done'

et

Perl -e 'for (;1;) {print "I will not automate this punishment in absurdum.\n"; sleep 1}'
17
Tooony

Bash récent> = 4.2 sous noyau Linux récent, réponse basée.

Afin de limiter le temps d'exécution, il n'y a pas de fourches ! Seuls les intégrés sont utilisés.

Pour cela, j'utilise la fonction intégrée read au lieu de sleep. Malheureusement, cela ne fonctionnera pas avec les sessions notty.

Quick bash fonction "repeat" comme demandé:

repeat () {
   local repeat_times=$1 repeat_delay=$2 repeat_foo repeat_sleep
   read -t .0001 repeat_foo
   if [ $? = 1 ] ;then
       repeat_sleep() { sleep $1 ;}
   else
       repeat_sleep() { read -t $1 repeat_foo; }
   fi
   shift 2
   while ((repeat_times)); do
        ((repeat_times=repeat_times>0?repeat_times-1:repeat_times))
        "${@}"
        ((repeat_times))&& ((10#${repeat_delay//.})) &&
            repeat_sleep $repeat_delay
   done
}

Petit test avec des chaînes entre guillemets:

repeat 3 0 printf "Now: %(%T)T, Hello %s.\n" -1 Guy
Now: 15:13:43, Hello Guy.
Now: 15:13:43, Hello Guy.
Now: 15:13:43, Hello Guy.

repeat -1 .5 printf "Now: %(%T)T, Hello %s.\n" -1 Guy
Now: 15:14:14, Hello Guy.
Now: 15:14:14, Hello Guy.
Now: 15:14:15, Hello Guy.
Now: 15:14:15, Hello Guy.
Now: 15:14:16, Hello Guy.
Now: 15:14:16, Hello Guy.
Now: 15:14:17, Hello Guy.
Now: 15:14:17, Hello Guy.
Now: 15:14:18, Hello Guy.
Now: 15:14:18, Hello Guy.
^C

Selon la granularité et la durée de la commande soumise ...

Sous les noyaux Linux récents, il y a un procfile /proc/timer_list contenant des informations temporelles en nanosecondes.

Si vous souhaitez exécuter une commande exactement une fois par seconde, votre commande doit se terminer en moins d'une seconde! Et à partir de là, vous devez sleep uniquement le reste de seconde actuelle.

Si le délai est plus important et que votre commande ne nécessite pas beaucoup de temps, vous pouvez:

command=(echo 'Hello world.')
delay=10
while :;do
    printf -v now "%(%s)T" -1
    read -t $(( delay-(now%delay) )) foo
    ${command[@]}
  done.

Mais si votre objectif est d'obtenir une granularité plus fine, vous devez:

Utilisez les informations en nanosecondes pour attendre le début d'une seconde ...

Pour cela, j'ai écrit une petite fonction bash:

# bash source file for nano wait-until-next-second

mapfile  </proc/timer_list _timer_list
for ((_i=0;_i<${#_timer_list[@]};_i++));do
    ((_c+=${#_timer_list[_i]}))
    [[ ${_timer_list[_i]} =~ ^now ]] && TIMER_LIST_READ=$_c
    [[ ${_timer_list[_i]} =~ offset:.*[1-9] ]] && \
        TIMER_LIST_OFFSET=${_timer_list[_i]//[a-z.: ]} && \
        break
done
unset _i _timer_list _c
readonly TIMER_LIST_OFFSET TIMER_LIST_READ
waitNextSecondHires() {
    local nsnow nsslp
    read -N$TIMER_LIST_READ nsnow </proc/timer_list
    nsnow=${nsnow%% nsecs*}
    nsnow=$((${nsnow##* }+TIMER_LIST_OFFSET))
    nsslp=$((2000000000-10#${nsnow:${#nsnow}-9}))
    read -t .${nsslp:1} foo
}

Après les avoir achetés, vous pouvez:

command=(echo 'Hello world.')
while :;do
    waitNextSecondHires
    ${command[@]}
  done.

courir ${command[@]} directement sur la ligne de commande, que comparer à

command=(eval "echo 'Hello world.';sleep .3")
while :;do
    waitNextSecondHires
    ${command[@]}
  done.

cela doit donner exactement le même résultat.

Embauche bash fonction "repeat" comme demandé:

Vous pouvez vous procurer ceci:

mapfile  </proc/timer_list _timer_list
for ((_i=0;_i<${#_timer_list[@]};_i++));do
    ((_c+=${#_timer_list[_i]}))
    [[ ${_timer_list[_i]} =~ ^now ]] && TIMER_LIST_READ=$_c
    [[ ${_timer_list[_i]} =~ offset:.*[1-9] ]] && \
        TIMER_LIST_OFFSET=${_timer_list[_i]//[a-z.: ]} && \
        break
done
unset _i _timer_list _c
readonly TIMER_LIST_OFFSET TIMER_LIST_READ

repeat_hires () {
    local repeat_times=$1 repeat_delay=$2 repeat_foo repeat_sleep repeat_count
    read -t .0001 repeat_foo
    if [ $? = 1 ] ;then
        repeat_sleep() { sleep $1 ;}
    else
        repeat_sleep() { read -t $1 repeat_foo; }
    fi
    shift 2
    printf -v repeat_delay "%.9f" $repeat_delay
    repeat_delay=${repeat_delay//.}
    read -N$TIMER_LIST_READ nsnow </proc/timer_list
    nsnow=${nsnow%% nsec*}
    started=${nsnow##* }
    while ((repeat_times)); do
        ((repeat_times=repeat_times>0?repeat_times-1:repeat_times))
        "${@}"
        ((repeat_times)) && ((10#$repeat_delay)) && {
            read -N$TIMER_LIST_READ nsnow </proc/timer_list
            nsnow=${nsnow%% nsec*}
            nsnow=${nsnow##* }
            (( (nsnow - started) / 10#$repeat_delay - repeat_count++ )) &&
                printf >&2 "WARNING: Command '%s' too long for %f delay.\n" \
                           "${*}" ${repeat_delay:0:${#repeat_delay}-9
                           }.${repeat_delay:${#repeat_delay}-9}
            printf -v sleep "%010d" $((
                10#$repeat_delay - ( ( nsnow - started ) % 10#$repeat_delay ) ))
            repeat_sleep ${sleep:0:${#sleep}-9}.${sleep:${#sleep}-9}
        }
    done
}

Alors essayez-le:

time repeat_hires 21 .05 sh -c 'date +%s.%N;sleep .01'
1480867565.152022457
1480867565.201249108
1480867565.251333284
1480867565.301224905
1480867565.351236725
1480867565.400930482
1480867565.451207075
1480867565.501212329
1480867565.550927738
1480867565.601199721
1480867565.651500618
1480867565.700889792
1480867565.750963074
1480867565.800987954
1480867565.853671458
1480867565.901232296
1480867565.951171898
1480867566.000917199
1480867566.050942638
1480867566.101171249
1480867566.150913407

real    0m1.013s
user    0m0.000s
sys     0m0.016s

time repeat_hires 3 .05 sh -c 'date +%s.%N;sleep .05'
1480867635.380561067
WARNING: Command 'sh -c date +%s.%N;sleep .05' too long for 0.050000 delay.
1480867635.486503367
WARNING: Command 'sh -c date +%s.%N;sleep .05' too long for 0.050000 delay.
1480867635.582332617

real    0m0.257s
user    0m0.000s
sys     0m0.004s
10
F. Hauri

Bash et certains de ses obus apparentés ont la commodité (( ... )) notation dans laquelle les expressions arithmétiques peuvent être évaluées.

Donc, pour répondre à votre troisième défi, où le nombre de répétitions et le délai entre chaque répétition doivent être configurables, voici une façon de le faire:

repeat=10
delay=1

i=0
while (( i++ < repeat )); do
  echo Repetition $i
  sleep $delay
done

Cette réponse souffre également de la dérive temporelle couverte par la réponse de Keith .

4
Thor

Si votre intention n'est pas d'afficher un message sur votre écran et si vous pouviez vous permettre de répéter le travail en minutes, crontab , peut-être, serait votre meilleur outil. Par exemple, si vous souhaitez exécuter votre commande toutes les minutes, vous écririez quelque chose comme ceci dans votre fichier crontab:

* * * * * my_precious_command

Veuillez consulter le tutoriel pour plus d'exemples. En outre, vous pouvez définir les horaires facilement en utilisant Générateur de code Crontab .

4
Barun

Perl

#!/usr/bin/env Perl
# First argument is number of seconds to sleep between repeats, remaining
# arguments give the command to repeat forever.

$sleep = shift;
$cmd = join(' ', @ARGV);

while(1) {
  system($cmd);
  sleep($sleep); 
}
4
dreeves

Vous voulez essayer ça (Bash)?

forever ()   {
    TIMES=shift;
    SLEEP=shift;
    if [ "$TIMES" = "-1" ]; then  
        while true;
        do 
            $@
            sleep $SLEEP
        done
    else
        repeat "$TIMES" $@ 
    fi; }
4
Gregg Lind

Comme mentionné par gbrandt, si la commande watch est disponible, utilisez-la définitivement. Certains systèmes Unix, cependant, ne l'ont pas installé par défaut (du moins, ils ne le font pas où je travaille).

Voici une autre solution avec une syntaxe et une sortie légèrement différentes (fonctionne en BASH et SH):

while [ 1 ] ; do
    <cmd>
    sleep <x>
    echo ">>>>>>>>>>>>>" `date` ">>>>>>>>>>>>>>"
done

Edit: j'ai supprimé certains "." dans la dernière déclaration d'écho ... reliquat de mes jours en Perl;)

3
bedwyr

Et si nous avions les deux?

Voici l'idée: avec seulement "intervalle", il se répète pour toujours. Avec "intervalle" et "fois", il répète ce nombre de fois, séparés par "intervalle".

L'usage :

$ loop [interval [times]] command

Ainsi, l'algorithme sera:

  • Élément de liste
  • si $ 1 ne contient que des chiffres, c'est l'intervalle (par défaut 2)
  • si $ 2 ne contient que des chiffres, c'est le nombre de fois (infini par défaut)
  • en boucle avec ces paramètres
    • "intervalle" de sommeil
    • si un certain nombre de fois a été donné, décrémenter un var jusqu'à ce qu'il soit atteint

Par conséquent :

loop() {
    local i=2 t=1 cond

    [ -z ${1//[0-9]/} ] && i=$1 && shift
    [ -z ${1//[0-9]/} ] && t=$1 && shift && cond=1
    while [ $t -gt 0 ]; do 
        sleep $i
        [ $cond ] && : $[--t]
        $@
    done
}
3
Baronsed

J'ai fini par créer une variante de la réponse de swalog. Avec le sien, il a fallu attendre X secondes pour la première itération, je passe aussi mon premier plan donc ..

./foo.sh;while sleep 1; do (./foo.sh) ; done
2
Duane Lortie

Vous pouvez obtenir la puissance de l'interpréteur python de Shell.

python3 -c "import time, os
while True:
    os.system('your command')
    time.sleep(5)

remplacer cinq en tout temps (sec) que vous aimez.

Attention: le retour utilise SHIFT + ENTER pour retourner une entrée dans Shell. Et n'oubliez pas de taper 4 ESPACE indentation dans chaque ligne de la boucle while.

1
pah8J

Un moyen facile de répéter un travail depuis crontab avec un intervalle inférieur à une minute (exemple de 20 secondes):

crontab: * * * * * script.sh

script.sh:

#!/bin/bash
>>type your commands here.

sleep 20
>>retype your commands here.

sleep 20
>>retype your commands here.
1
Charles nakhel

Vous pouvez exécuter un script depuis init (en ajoutant une ligne à/etc/inittab). Ce script doit exécuter votre commande, s'endormir pendant le temps que vous souhaitez attendre jusqu'à ce que vous exécutiez à nouveau le script et le quitter. Init relancera votre script après sa sortie.

1
Roberto Paz

Rapide, sale et probablement dangereux pour démarrer, mais si vous êtes aventureux et savez ce que vous faites, mettez ceci dans repeat.sh et chmod 755 il,

while true
do 
    eval $1 
    sleep $2 
done

Invoquez-le avec ./repeat.sh <command> <interval>

Mon sens spidey dit que c'est probablement une mauvaise façon de faire cela, mon sens spidey est-il vrai?

1
mallyone
#! /bin/sh

# Run all programs in a directory in parallel
# Usage: run-parallel directory delay
# Copyright 2013 by Marc Perkel
# docs at http://wiki.junkemailfilter.com/index.php/How_to_run_a_Linux_script_every_few_seconds_under_cron"
# Free to use with attribution

if [ $# -eq 0 ]
then
   echo
   echo "run-parallel by Marc Perkel"
   echo
   echo "This program is used to run all programs in a directory in parallel" 
   echo "or to rerun them every X seconds for one minute."
   echo "Think of this program as cron with seconds resolution."
   echo
   echo "Usage: run-parallel [directory] [delay]"
   echo
   echo "Examples:"
   echo "   run-parallel /etc/cron.20sec 20"
   echo "   run-parallel 20"
   echo "   # Runs all executable files in /etc/cron.20sec every 20 seconds or 3 times a minute."
   echo 
   echo "If delay parameter is missing it runs everything once and exits."
   echo "If only delay is passed then the directory /etc/cron.[delay]sec is assumed."
   echo
   echo 'if "cronsec" is passed then it runs all of these delays 2 3 4 5 6 10 12 15 20 30'
   echo "resulting in 30 20 15 12 10 6 5 4 3 2 executions per minute." 
   echo
   exit
fi

# If "cronsec" is passed as a parameter then run all the delays in parallel

if [ $1 = cronsec ]
then
   $0 2 &
   $0 3 &
   $0 4 &
   $0 5 &
   $0 6 &
   $0 10 &
   $0 12 &
   $0 15 &
   $0 20 &
   $0 30 &
   exit
fi

# Set the directory to first prameter and delay to second parameter

dir=$1
delay=$2

# If only parameter is 2,3,4,5,6,10,12,15,20,30 then automatically calculate 
# the standard directory name /etc/cron.[delay]sec

if [[ "$1" =~ ^(2|3|4|5|6|10|12|15|20|30)$ ]]
then
   dir="/etc/cron.$1sec"
   delay=$1
fi

# Exit if directory doesn't exist or has no files

if [ ! "$(ls -A $dir/)" ]
then
   exit
fi

# Sleep if both $delay and $counter are set

if [ ! -z $delay ] && [ ! -z $counter ]
then
   sleep $delay
fi

# Set counter to 0 if not set

if [ -z $counter ]
then
   counter=0
fi

# Run all the programs in the directory in parallel
# Use of timeout ensures that the processes are killed if they run too long

for program in $dir/* ; do
   if [ -x $program ] 
   then
      if [ "0$delay" -gt 1 ] 
      then
         timeout $delay $program &> /dev/null &
      else
         $program &> /dev/null &
      fi
   fi
done

# If delay not set then we're done

if [ -z $delay ]
then
   exit
fi

# Add delay to counter

counter=$(( $counter + $delay ))

# If minute is not up - call self recursively

if [ $counter -lt 60 ]
then
   . $0 $dir $delay &
fi

# Otherwise we're done
0
user56318

... Je me demande combien de solutions compliquées peuvent être créées pour résoudre ce problème.

Cela peut être si facile ...

open /etc/crontab 

y mettre 1 ligne suivante à la fin du fichier comme:

*/NumberOfSeconds * * * * user /path/to/file.sh

Si vous voulez exécuter quelque chose toutes les 1 seconde, mettez-le simplement ici:

*/60 * * * * root /path/to/file.sh 

where that file.sh could be chmod 750 /path/to/file.sh

et à l'intérieur de ce fichier.sh devrait être:

#!/bin/bash 
#What does it do
#What is it runned by

your code or commands

et c'est tout!

PRENDRE PLAISIR!

0
MIrra

Avec zsh et une implémentation sleep qui accepte les arguments à virgule flottante:

typeset -F SECONDS=0 n=0
repeat 100 {cmd; sleep $(((n+=3) - SECONDS))}

Ou pour forever:

for ((;;)) {cmd; sleep $(((n+=3) - SECONDS))}

Si votre sleep ne prend pas en charge les flottants, vous pouvez toujours le redéfinir comme un wrapper autour du zsh de zselect:

zmodload zsh/zselect
sleep() zselect -t $((($1 * 100) | 0))
0
Stéphane Chazelas

Vous pouvez trouver cette fonction récursive:

#!/bin/bash
ininterval () {
    delay=$1
    shift
    $*
    sleep $delay
    ininterval $delay $*
}

ou ajoutez un:

ininterval $*

et appelez le script.

0
user unknown

Pour exécuter plusieurs fois une commande dans une fenêtre de console, j'exécute généralement quelque chose comme ceci:

while true; do (run command here); done

Cela fonctionne également pour plusieurs commandes, par exemple, pour afficher une horloge constamment mise à jour dans une fenêtre de console:

while true; do clear; date; sleep 1; done

0
Thomas Bratt

Cette solution fonctionne dans MacOSX 10.7. Cela fonctionne à merveille.

bash -c 'while [ 0 ]; do \
      echo "I will not automate this punishment in absurdum."; done'

Dans mon cas

bash -c 'while [ 0 ]; do ls; done'

ou

bash -c 'while [ 0 ]; do mv "Desktop/* Documents/Cleanup"; done'

pour nettoyer mon bureau en permanence.

0
Dan Ruiz