4

I'm wondering if there is any way to put something in my startup files that will cause other files to be sourced after the first prompt is shown.

I would prefer a method that doesn't rely on hooks like preexec, precmd, or periodic hooks. (I'm using zsh, but I understand that people have written code to add those to bash, through some clever hacking of trap, or something like that.)

My very simple first test (which I did not expect to work, but which I felt the need to try, just to see the precise results) was adding this to my .zshrc,

{ sleep 100 && export VARIABLE_SET=1 } &

and then executing this after the shell loaded, both before and after 100 seconds had passed:

echo $VARIABLE_SET

Of course it (and every permutation I could think of) failed, and yielded no promising clues as to how this might be done.

It was my hope that something from the startup file can trigger the sourcing which affects the shell, even if it isn't immediately available at the first prompt. I want to find a way to delay "expensive" sourcing, and make the prompt show up quickly, since many of the things that bloat one's startup files are things that are not often needed.

I'm hoping to find an approach that is simpler than using hooks (but I've created a proof-of-concept using the precmd hook in case no other approach is possible).

iconoclast
  • 9,198
  • 13
  • 57
  • 97
  • Do you meant "after the first prompt is shown" and before you enter your first command. Then why? What's the use case? – Stéphane Chazelas May 28 '22 at 08:14
  • @StéphaneChazelas: no, I wasn't expecting it to be possible for it to be sourced before entering the first command. In cases where a .zshrc file takes a while to be sourced, it would be nice to delay some of it. Let's say that 5% of the time involved in sourcing .zshrc is for the most essential things that I might use on the first prompt, and the rest can be moved into .zshrc.2, .zshrc.3 etc. files that can be sourced later. .zshrc can be sourced normally, while .zshrc.2 can be sourced after the first command is entered and before the 2nd prompt appears, etc. – iconoclast May 28 '22 at 21:39
  • For profiling .zshrc load times, I found this script to be very helpful: https://github.com/avih/time.sh (some minor tweaks needed to execute it with zsh) – Carl Sep 20 '22 at 11:27

4 Answers4

2

You could always use sched. Add to your ~/.zshrc

sched +30 'source ~/.zsh/more-stuff'

And ~/.zsh/more-stuff would be sourced 30 seconds after starting (or at the next prompt after 30 seconds).

2

As you have found out, running commands in the background with & will put them in a subshell, and a subshell can't change variables/environment variables of the parent process. ("can't" in this sentence should be read as "workarounds are ugly".)

So you'll need the source command to be executed in the current shell. I'll list a few options below.

All of them don't answer exactly the question you gave, but they seem to provide solutions for the actual problem you're trying to solve.

zsh-defer (zsh script)

.zshrc:

# load zsh-defer:
source "$HOME/zsh-defer.plugin.zsh"

... misc. commands ...

defer 'source "$HOME/file"' for later execution:

zsh-defer source "$HOME/file"

It's possible to cue any number of commands (source .zshrc.2, source .zshrc.3 etc.) with repeated calls to zsh-defer.

Benefits: handles a number of edge cases that zinit won't. A more sophisticated solution than sched.

Performance: zsh-defer itself should load within a few ms at most.

Implementation: executes the commands from zle (zsh's line editor).

The zsh/sched zsh module

(as suggested in previous answer by @StéphaneChazelas)

Simplistic and straight-forward.

.zshrc:

sched +10 "source $HOME/file" &>/dev/null || source "$HOME/file"

The ... &>/dev/null || ... part: if sched is not available, or exits with an error, the file is source'd immediately instead, under the assumption that that's better than not at all.

Details on sched: see zsh's documentation.

Drawbacks:

  • if the command takes noticeable time to execute, it will block the shell while executing
  • risk of accidental input if a deferred command unexpectedly asks for user input ("Yearly check: do <evil thing>? [y/N]") while you're typing a command

Performance: the sched command takes a few ms to execute at most.

Use the powerlevel10k prompt with instant prompt

No extra work needed - do whatever you want in .zshrc; the prompt will be loaded and ready for interaction (with some caveats) while .zshrc is being processed.

Performance: the prompt should be visible within some 10 ms from when the shell starts processing .zshrc.

Implementation: various zsh features and tricks. Takes some extra care, like redirecting (and buffering) stdin while executing commands, to avoid accidental input.

Plugin manager zinit

With zinit you can use zinit ice wait for deferred execution of any command, e.g. source-ing a file.

zinit can be used just for that functionality, no need to use it as a plugin manager and it won't interefere with other plugin managers, if any.

The original zdharma/zinit is no longer maintained, but these forks are:

I don't think a long-running command blocks the shell, but I haven't tested this myself.

Performance: on my system, zinit itself loads in about 10 ms, or 5 ms without completions.

Implementation: zinit adds a shell function to zsh/sched, which when invoked immediately adds itself back to the sched table and in addition to that executes the commands the user has set up, using zsh hooks and other zsh features.

zsh-async library

Also possibly useful - mentioned for completeness.

Is a small general library for running asynchronous tasks in zsh. It will however run them in a separate process, which won't be able to set environment variables in the parent shell. I believe there are simple workarounds for this though, by assigning a callback function that should allow executing results in the parent shell.

Benefits: Quite widely used, and actively maintained for eight years.

Implementation: Uses zsh/zpty to launch a pseudo-terminal executing the deferred commands.


Recommended reading

romkatv's (author of zsh-defer and powerlevel10k) general thoughts and advice on staged zsh startup/lazy-loading/deferred execution.

Bonus FAQ on shell loading performance

Q: Can I compile .zshrc to wordcode to make it execute faster?
A: It's possible in theory, but easily introduces problems with aliases, missed re-compilations and more. Not recommended.
Q: I now understand the possible problems. I still want to try this.
A: No, really, you don't.

iconoclast
  • 9,198
  • 13
  • 57
  • 97
Carl
  • 201
1

You can't use the {...} & idea because the & causes the stuff to be put in a subshell; variables set there don't impact the parent.

So instead we could write out the settings to a temporary file and have that read-in to the parent.

One way might be to use the DEBUG trap.

Not fully tested but maybe something like

( sleep 100 ; echo MYVAR=1 ; echo trap - DEBUG ) > ~/.slowstartfile.$$
trap '. ~/.slowstartfile.$$' DEBUG

The DEBUG trap is typically called after a command is executed.

So

% ( sleep 10 ; echo MYVAR=1 ; echo trap - DEBUG ) > ~/.slowstartfile.$$ &
[1]     9411
% trap 'source ~/.slowstartfile.$$' DEBUG                           
% sleep 10
[1] +  Done                    ( sleep 10 ; echo MYVAR=1 ; echo trap - DEBUG ) > ~/.slowstartfile.$$ &
% echo $MYVAR                                                       
1
1

If you are looking to source a .env file to load environment variables then dotenv can help you.

  • Thanks, I already built my own tool to source files per-project (therefore based on directory) before I knew about dotenv (or maybe before it existed?) but that's useful info for someone else reading this. – iconoclast Mar 17 '22 at 17:43
  • @iconoclast Well thats awesome! Also do mark the answer as accepted so other have a easier time. – Vaibhav Dhiman Mar 18 '22 at 09:03
  • It's useful information that someone reading this question might like, but it doesn't really answer the question I'm asking. (At least not yet.) dotenv is aimed at specific directories, but I'm looking for something generalized. (Not every shell starts in ~.) It might source the file again and again—I'm not sure—but I am looking for something that will only source a file once, specifically to break up a large .zshrc into multiple pieces because it takes a noticeable time when sourced all at once. And I want to be able to source multiple different chunks (only once each). – iconoclast Mar 18 '22 at 15:50
  • @iconoclast It only sources the file once tho, it prompts you if you want to source it and if you select always then it sources the file automatically when you cd into that directory again. I am not sure what you mean. – Vaibhav Dhiman Mar 18 '22 at 16:57
  • you're not sure what I mean about what specifically? Additionally, dotenv's description says it "loads environment variables". I'm looking for something not limited to environment variables. Is it limited in that way? Or is it simply doing a bad job of marketing its capabilities? – iconoclast Mar 18 '22 at 19:06
  • @iconoclast I am not sure what else you want to source and if dotenv can load other source files. I am guessing that you mean like a venv activation script, am I right? You can just append it like source ~/foo to your `.zshrc. – Vaibhav Dhiman Mar 24 '22 at 02:20