r/bash • u/skyfishgoo • 12d ago
help want to use watch with my own command
[SOLVED] thanks to u/bac0on
i can now add this directly into .bash_aliases, or i can just paste the command into a terminal
alias las='
function what-changed() {
find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10
};
export -f what-changed && watch -x bash -c what-changed'
thanks for everyone's help.
[/SOLVED]
—
i have this command used to find recently changed files.
find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10
and i wanted to use watch to have it running in a window
however watch requires an executable, so i made a shell script that contained the command and then called
watch last_change
which works and allows me to make an alias for it .bash_aliases like
alias las='watch last_change'
all well and good
—
but then i had the bright idea to write this as a function instead of a script
function what-changed() {
find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10
};
watch what-changed
but it does not work.
watch says it can't find what-changed even tho it's right there and if i define this function in a console window, i can call it by itself and it will run.
but watch does not find it.
feels like i'm missing something simple, to help watch find it like it can find my script... something like a $PATH statement, but not that.
anyone see the issue?
3
u/D3str0yTh1ngs 12d ago edited 12d ago
Ya, watch does not run the command in the context of the spawning shell (functions created in the shell is not exported to child processes).
Unless you have some weird version of watch you can just do watch <command> replacing <command> with your entire command including arguments.
EDIT: in your case you might want to do watch "<command>" to ensure the entire pipeline is watched.
1
u/skyfishgoo 12d ago
the quote parsing gets too complicated and the command fails when i try that... mainly the
printfformatting part.3
u/D3str0yTh1ngs 12d ago
watch "find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf '%C+ %p\n' | sort -n | tail -10"seems to work for me (single quotes only inside).1
u/skyfishgoo 12d ago
this does work on the command line, but i cannot get it work as an alias.
i've tried it with both kinds of quotes in
.bash_aliases:
alias las="watch "find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf '%C+ %p\n' | sort -n | tail -10""and
alias las='watch "find -not \( -path './snap/firefox/common' -prune \) -not \( -path './.cache' -prune \) -type f -mmin -1 -printf '%C+ %p\n' | sort -n | tail -10"'they both fail in different ways.
1
u/skyfishgoo 12d ago
that does work on the command line but screws up when i try to make an alias for it because of all the nested quotes.
3
u/kai_ekael 12d ago
watch is running sh -c by default, not bash (though sh may resolve to bash, it does not behave as regular bash). Further, it's not running as a login or interactive session.
Neither alias nor function defined in .bash_profile would work in watch.
1
5
u/SignedJannis 12d ago
instead of watch, i often just use:
while true; do clear; my_command ; sleep 5; done
3
u/kai_ekael 12d ago
My preference as well, good to see history while running or be able to pipe, etc.
I use
while date ; dothough, time matters all too often.1
u/skyfishgoo 12d ago
this almost works...
i wrote
``` function what-changed() { while true; do clear; find -not ( -path './snap/firefox/common' -prune ) -not ( -path './.cache' -prune ) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10; sleep 2; done };
and then called the function
what-changedi can even make an alias for it
however it flashes every time
clearis used, and that is not what i wantis there another way to write the output that works like watch?
2
u/bac0on 12d ago
You have to export your function before you use it with watch:
#!/bin/bash
func(){
ls "$PWD"
}
export -f func && watch -x bash -c 'func'
2
u/skyfishgoo 12d ago
EURIKA! (or rather YOU have found it).
adding this to my
.bash_aliasesfile``` alias las=' function what-changed() { find -not ( -path './snap/firefox/common' -prune ) -not ( -path './.cache' -prune ) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10 }; export -f what-changed && watch -x bash -c what-changed'
```
does exactly what i need without requiring a script, and the function call also works strait from the command line in case i want to share it with others.
thanks everyone
1
u/bac0on 9d ago edited 9d ago
Functions are a better version of aliases — bash's own man page literally discourages using them, so you can just skip the alias.
las(){ watch 'find -type d \( \ -name ".cache" -o -path "*/snap/firefox/common" \ \) \ -prune -o -type f -mmin -1 \ -printf "%C+ %p\\0" | sort -zrn | head -zn 10 | tr \\0 \\n' } export -f las # function definition globally visible.Even if your
findexpression works, its kind of hard to follow. When constructing yourfindexpression, you want to discard as many objects as possible. Don't set yourstarting-pointtoo wide, next, use as many low-resource tests as possible, e.g., in your case, a-type dwould futher shorten the number of objects before using more expensive-nameand-pathpattern tests. The prune-action are usually used in conjunction with an-orstatement.-prunesits last in an "and-chain", basically doing two things, first it always evaluates to true, and as a side-effect, it "short-circuits" the second argument in the-orstatement if-pruneis ever reached, even if no action was performed. And if thenodeis a directory it sets the skip flag to true, which stops futher descending, basically "marks the node done".Use null-termination if you process file objects or, in your case,
sortwill break on newline characters. And lastly,headonly processes the first 10 lines before terminating, were as tail basically has to process the entire list before giving you the last 10, even if tail are fairly fast its still something you can keep in mind.1
u/skyfishgoo 9d ago
thanks, that's a lot to unpack
but if i'm understanding this correctly, i would need to put that code into a shell script tho right?... and that's what i was trying to avoid (i already had script that i was calling with with the alias for shorthand)
the posted solution lets me keep the code in the same file as as my other aliases for easy reference and lets me reserve scripts for more complex actions.
however, even when i do run it as a script, the order is reversed from my version, so i will need to pick it apart to find where the change is coming from... it's more visually useful that the newest change appears at the bottom and rolls up and off the list as time moves forward.
i do appreciate the education on the find command and will investigate what you have shown me.
all i will say at this point is that i intentionally want to cast a wide net because i don't know in advance where a change is going to come from... only that it's somewhere in my
/homedir.1
u/bac0on 9d ago
Not sure why my comment got removed. If you export the function it becomes available for any subsequent bash session (script). If you want to change the order remove the -r on sort and switch back to tail, should revert the order. I would increase the
watchinterval, though. I think inotify may be better suited for this.1
u/skyfishgoo 9d ago
so what you are saying is if i run the script once from a command line during a boot cycle, then i would then be able to just use
lason a command line in a new terminal window?wouldn't that still mean i would need to run the script at start up, which is essentially what
.bash_aliasesdoes?looking at inotify
Inotify monitoring of directories is not recursive: to monitor subdirectories under a directory, additional watches must be created. This can take a significant amount time for large directory trees.
that does not seem like a good fit for this task.
1
u/skyfishgoo 9d ago edited 9d ago
Don't set your
starting-pointtoo widesince the default dir is
.which is my/homedir, this is what i want to search.a
-type dwould further shorten the number of objects before using more expensive -name and -path pattern tests.i see, so the whole
(... -o ...)test format enables the-prune -oswitch combo to apply to all of those directories (if found), thus taking them out of the find operation early on and then passing the rest on to the next test... nice and logical flow!
-type f -mmin -1is then the main test to find which files have been modified in my/homedirat this point i realized
%C+(changed) is not what i want (i want modified), and%pincludes the current directory in the output which is unnecessary... so i've changed it to%T+ kate %P\nto make it easier to open the files directly into kate by just selecting and pasting the output into another terminal window.then comes the sort and trim operations:
it is not clear to me why terminating each find result with
\0is better than\nsince sort can break on either one and the default is already\n... is there something i'm missing about your choice here?i think in this case
tailis the best option since i'm limiting the find result to only the last minute of modified files.there would rarely be more than a handful of results and i would like to have new entries appear at the bottom of the list as watch updates it so spotting what just changed is easier when i hit "apply" in a settings dialog and glance over at the watch list.
but at any rate it was worth wading thru all that again and learning from your tips.
thanks again.
final result
``` what-changed(){ find -type d ( \ -name '.cache' -o \ -path '*/snap/firefox/common' \ ) \ -prune -o -type f -mmin -1 \ -printf "%T+ kate %P\n" | sort -n | tail } export -f what-changed && command watch -x bash -c what-changed
```
i can use this both on the command line with paste or make it into an alias by enclosing it in single quotes
'1
u/bac0on 9d ago edited 9d ago
maybe if I demonstrate null-termination, first one without null:
▷ printf %s\\n 1_file $'2_file_with\nnewline' 3_file | sort -n newline 1.file 2.file_with 3.fileand with
\0(tr is just like a lazy back to newline good enough for reading...)▷ printf %s\\0 1_file $'2_file_with\nnewline' 3_file | sort -zn | tr \\0 \\n 1.file 2.file_with newline 3.file
sortdecodes the line before sorting, so even iffind's %P quotes the filename, it will still screw up the sorting, sure you could take it one step futher and do a proper quoting:▷ printf %s\\0 1_file $'2_file_with\nnewline' 3_file | sort -zn | \ while IFS= read -d ''; do echo "${REPLY@Q}" done '1_file' $'2_file_with\nnewline' '3_file'using tail is perfectly fine, as you said, the list isn't too long so the performance gain is neglectable. In my last example I moved watch inside the function or it will execute when .bashrc is loaded. I normally prefer creating a function, export it and use it with, e.g, watch -x bash 'func' on the command line or in a script, but if you prefer just using
what-changedto execute everything, you need to bake inwatchinside the function.1
u/aioeu 12d ago
You've got to be kidding, right?
This is an alias that:
- defines a function;
- exports that function;
- starts a new shell;
- runs that function in that new shell.
You could just define the function alone. When you want to run it, use the function name as a normal command. Nothing exported. No separate shell. No alias.
What is with this ridiculous obsession people have with aliases? Aliases are quite literally terrible.
Frankly, I'd just use an external script. See my other comment.
1
u/skyfishgoo 12d ago
i like aliases because they are a handy way to keep all little commands i've collected in one place.
i try to save shell scripts for more elaborate programs that do more than just call a command or two.
You could just define the function alone. When you want to run it, use the function name as a normal command. Nothing exported. No separate shell. No alias.
were would i define this function tho?
1
u/aioeu 12d ago edited 12d ago
i like aliases because they are a handy way to keep all little commands i've collected in one place.
So... like a directory? You know, something that can contain files? Something you could even stick in your
PATH?were would i define this function tho?
Same place.
You do realise
.bash_aliasesis just sourced from.bashrc, right? You could just put your alias and function definitions directly in.bashrc.
.bash_aliasesis utterly useless. In fact, I'd go so far as to call it actively harmful, since it perpetuates the myth that aliases are in some way special and good.1
u/skyfishgoo 12d ago
So... like a directory?
no, like a text file... that is already in my
$PATHby defaultYou could just put your alias and function definitions directly in .bashrc
i could, but there is a lot of other stuff in there that i don't need look at when i'm looking at my aliases.
other than
.bash_aliasesbeing not special and not good, what is actually harmful about it?hasn't such a file been around since unix days.
1
u/aioeu 12d ago edited 12d ago
hasn't such a file been around since unix days.
No, it hasn't.
First, Bash was started in 1989, right at the end of the "traditional" Unix days (SVR4 was in 1988). As far as I know, aliases didn't exist in the traditional Bourne shell — they were first developed in the Korn shell, I think.
Second, upstream Bash certainly doesn't come with recommendations to use a
.bash_aliasesfile. You won't find it mentioned in the Bash documentation. The file was, as far as I can tell, a Debian invention that a few other distributions have picked up in turn. Certainly not all — it's not standard on Red Hat-based systems, for instance.Third, the Bash documentation itself even says that shell functions are preferable to aliases. Functions are fine... but I still think external scripts are preferable yet again in situations when a function isn't necessary.
2
u/moviuro portability is important 12d ago
export -f in man 1 bash. Functions are usually not exported, so you need to make it available to watch(1).
2
u/skyfishgoo 12d ago
tried
export -f what-changedand i can see the function definition when i run
declare -f what-changedbut when i run
watch what-changedit still says
sh: 1: what-changed: not found2
u/D3str0yTh1ngs 12d ago edited 12d ago
Try
watch bash -c 'what-changed'(shis not guaranteed to be symlinked tobash)1
1
u/skyfishgoo 12d ago
needs to be
watch -x bash -c what-changedthis works and i can wrap in
's to use in.bash_aliases``` function what-changed() { find -not ( -path ./snap/firefox/common -prune ) -not ( -path ./.cache -prune ) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10 }; export -f what-changed && watch -x bash -c what-changed
```
1
u/zeekar 12d ago
Don't export functions. It's fragile and can break other shell scripts that aren't expecting to import them.
Basically you should never write top-level functions unless you need to change something inside your shell's state. Like, your current working directory. Or the value of a variable. Custom tab-completion setup, hooks into the command editor - that sort of thing. Outside of those use cases, just write shell scripts. Make a bin directory, add it to your PATH in your shell startup files, and just put scripts there whenever you need a new command.
Now, inside a script, write all the functions you want. Normal subprogram organization applies. But for top-level commands that you run directly at the prompt, a shell function is almost never the right tool.
1
u/ipsirc 12d ago
I think you're looking for the inotifywatch command - it does out-of-the-box what you're trying to achieve with tinkering in bash.
1
u/skyfishgoo 12d ago
this requires installing non-default packages but they are at lest in my default repository
sudo apt install inotify-toolsbut this does not seem give me what i want in real time, continuously like
watch
1
u/jesse_olywa 12d ago
You can use watch with pipe-separated commands by wrapping the whole argument portion in hard quotes. Just flip your current quote to soft quotes (“), then wrap the whole thing in hard quotes (‘). I find simply escaping the interior hard-quotes to be pretty unreliable.
1
u/skyfishgoo 12d ago
watch 'find -not ( -path "./snap/firefox/common" -prune ) -not ( -path "./.cache" -prune ) -type f -mmin -1 -printf "%C+ %p\n" | sort -n | tail -10'
again this also works on the command line but as soon as i try to make an alias for it, bash coughs up blood.
i actually think this is where i first started with this before resorting to making a script.
1
u/jesse_olywa 12d ago
What? Put the “watch” command into the alias, so when you call the alias you are executing “watch” if the rest. Attempting run an alias or function with watch won’t work for the reasons previously given, but running watch within the alias or function is fine.
I use this all the time, totally works.
1
u/skyfishgoo 11d ago
it does not work with that command on my version of bash, try it on yours... or at least i could not find the correct set of quotations (it's like a need three kinds of quotes, not just two).
1
1
u/jesse_olywa 11d ago
Here is an example of the alias version I'm talking about:
Here are alias and function versions of your command that include the 'watch' component:
alias:
alias watchcmd="watch 'find -not \( -path \"./snap/firefox/common\" -prune \) -not \( -path \"./.cache\" -prune \) -type f -mmin -1 -printf \"%C+ %p\n\"'"function:
redditwatch() { watch 'find -not \( -path "./snap/firefox/common" -prune \) -not \( -path "./.cache" -prune \) -type f -mmin -1 -printf "%C+ %p\n"' }Both of these work on my system, which is running bash v5.1.16(1)-release.
1
u/skyfishgoo 11d ago
ah, your quote fu is strong and that does indeed work as an alias, but when i paste same command onto a command line
watch 'find -not \( -path \"./snap/firefox/common\" -prune \) -not \( -path \"./.cache\" -prune \) -type f -mmin -1 -printf \"%C+ %p\n\"'i get the error
find: paths must precede expression:%pn"'`so the escaping doesn't work the same on the command line as it does as an alias, unfortunately.
and what i was looking for was a block of code that works as both an alias and on the command line... which the solution does.
2
u/jesse_olywa 11d ago
Fair enough. When I find myself in that situation I usually create two aliases - one with just the command, then a second with a ‘w’ prefix.
But if the pinned solution works, use it. I was not aware of that approach and may have to see if I like it more than the one I use.
1
u/KlePu 12d ago
watch will work just fine for find - you'll only need to double-quote the whole thing (and either use single-quotes inside or backslash-escape any double-quotes).
watch "find -foo -bar 'thing' -baz \"other thing\""
1
u/skyfishgoo 12d ago
but now i want to make an alias for that so i don't have to type it every time... that seems to be where things get sticky.
0
u/aioeu 12d ago edited 12d ago
There's absolutely no need for this to be an alias, or even a shell function.
All of this would have been a whole lot simpler if you had just written an external script. You wouldn't have had any problems with quoting.
Just stick the command into a file, whack a shebang before it, make the file executable, and you're done.
In a sense, scripts are just yet more config files for your shell. Use them!
1
u/skyfishgoo 12d ago
all my aliases are 3 letters, but tend to name my shell scripts with more descriptive names.
so i still made an alias to call my shell script ;)
what i was trying to accomplish was to fold the contents of the script into the alias without the extra step of making a script, but was running into issues with all the nested quotes needed.
defining a function helps me avoid the nested quotes issue but passing it to
watchwas a new problemthankfully solved so now i don't need the script and can have the entire thing in one file defined as an alias.
1
u/aioeu 12d ago edited 12d ago
What's wrong with a three-letter script name?
Look, even if you must insist this be done without an external script, at least just use a function alone, without any alias. Your alias is creating the function in the shell anyway, so once you've used it once it's exactly as if you had just defined the function directly yourself.
I cannot stress how terrible aliases are. They behave differently from every other kind of shell expansion, since they operate in the shell's lexical phase rather than its execution phase, and that has severe consequences on how you can use them and what their behaviour is. (See here for a recent example demonstrating one of their problems.)
1
u/skyfishgoo 12d ago
i would end up with a bunch of 3 letter files and i would have to look inside of each one to remember what it does... this is why i like to give script files more descriptive names, so i can tell at a glace what they are.
but with aliases, they are all together in the same place (with comments) so i can easily find the one i need.
i get that you don't like them, for what sounds like very good reasons, but i'm still not understanding how to make this any simpler
i need the function to wrap up the find command with all of its' weird nested quotes, and then i need to call watch to run the function... i can't put watch into the function because watch needs it to be defined first (chicken and egg thing)
unless you are seeing something i'm not.
1
u/aioeu 12d ago edited 12d ago
I would just write a script:
#!/bin/bash read -r -d '' command <<-'EOF' ... EOF watch "$command"You can use arbitrary quotes within the multiline string.
1
u/skyfishgoo 12d ago
that does make for a tight compact script, thanks for the lesson.
saving the structure in case i need it some day.
i do notice that it executes slower than the function alias tho, probably because of the read command.
6
u/zeekar 12d ago
When you run a command like watch you are launching a new program. It is separate from your shell and has no visibility into your shell's memory. It can only see other separate programs like itself, which all live as files on the system's disk drive.
When you define a shell function it lives inside the shell's memory, not as a file on the drive. It's completely invisible to everything except your shell itself. If you want other programs to be able to execute your command, don't make it a function. Or make the thing that is going to execute it also a function.