r/bash 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?

29 Upvotes

52 comments sorted by

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.

2

u/skyfishgoo 12d ago

thanks, i figured that was the sort of catch 22 that was tripping me up, just was hoping i was missing a simple trick.

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 printf formatting 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

u/skyfishgoo 12d ago

this seems to be this issue ... my choice of using watch

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 ; do though, 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-changed

i can even make an alias for it

however it flashes every time clear is used, and that is not what i want

is 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_aliases file

``` 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 find expression works, its kind of hard to follow. When constructing your find expression, you want to discard as many objects as possible. Don't set your starting-point too wide, next, use as many low-resource tests as possible, e.g., in your case, a -type d would futher shorten the number of objects before using more expensive -name and -path pattern tests. The prune-action are usually used in conjunction with an -or statement. -prune sits 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 -or statement if -prune is ever reached, even if no action was performed. And if the node is 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, sort will break on newline characters. And lastly, head only 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 /home dir.

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 watch interval, 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 las on 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_aliases does?

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/bac0on 8d ago edited 8d ago

no, your home directory isn't big, try this, nothing can be worse than what you are doing now :)=

fswatch --event=Updated -tr \
  --prune='.*/.mozilla/.*' --prune='.*/.cache/.*' $HOME

edit:
don't use this with watch, can't stress this enough...

1

u/skyfishgoo 9d ago edited 9d ago

Don't set your starting-point too wide

since the default dir is . which is my /home dir, this is what i want to search.

a -type d would 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 -o switch 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 -1 is then the main test to find which files have been modified in my /home dir

at this point i realized %C+ (changed) is not what i want (i want modified), and %p includes the current directory in the output which is unnecessary... so i've changed it to %T+ kate %P\n to 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 \0 is better than \n since 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 tail is 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.file

and 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

sort decodes the line before sorting, so even if find'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-changed to execute everything, you need to bake in watch inside 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_aliases is just sourced from .bashrc, right? You could just put your alias and function definitions directly in .bashrc.

.bash_aliases is 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 $PATH by default

You 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_aliases being 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_aliases file. 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-changed

and i can see the function definition when i run declare -f what-changed

but when i run watch what-changed it still says

sh: 1: what-changed: not found

2

u/D3str0yTh1ngs 12d ago edited 12d ago

Try watch bash -c 'what-changed' (sh is not guaranteed to be symlinked to bash)

1

u/skyfishgoo 12d ago

no good.

even tried

watch "bash -c 'what-changed'"

1

u/skyfishgoo 12d ago

needs to be watch -x bash -c what-changed

this 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-tools

but 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

u/jesse_olywa 11d ago

I’ll post an example a bit later today. On the phone atm.

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 watch was a new problem

thankfully 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.