17

I have this shell script Test.sh:

#! /bin/bash

FILE_TO_CHECK="/Users/test/start.txt"
EXIT=0

while [ $EXIT -eq 0 ]; do
    if [ -f "$FILE_TO_CHECK" ]
        then
        /usr/bin/java -jar myapp.jar
        EXIT=1
    else
        sleep 30
    fi
done

I need to start this script automatically after login.
So I put it inside a folder Test in /System/Library/StartupItems/

When I reboot the Mac, nothing happens after I log in. Any clue?

I also tried Automator, but with the same result: the java program is not running.

mklement0
  • 382,024
  • 64
  • 607
  • 775
user3472065
  • 1,259
  • 4
  • 16
  • 32
  • 1
    As an aside, you should avoid uppercase variable names, as those are reserved for system use. – tripleee Jan 29 '16 at 06:01
  • One solution is to wrap it up as an application. This is easily done with Platypus: https://apple.stackexchange.com/a/224507/86486 – AnneTheAgile Mar 28 '18 at 23:56

3 Answers3

49

Ivan Kovacevic's pointers, especially the superuser.com link, are helpful; since at least OS X 10.9.2, your options for creating run-at-login scripts are:

Note: The methods are annotated with respect to whether they are:

  • specific to a given user ("[user-SPECIFIC]"); i.e., the installation must be performed for each user, if desired; scripts are typically stored in a user-specific location, and root (administrative) privileges are NOT required for installation.
  • effective for ALL users ("[ALL users]"); i.e., the installation takes effect for ALL users; scripts are typically stored in a shared location and root (administrative) privileges ARE required for installation.

The scripts themselves will run invisibly, but - with the exception of the com.apple.loginwindow login-hook method - you can open applications visibly from them; things to note:

  • There is no guarantee that any such application will be frontmost, so it may be obscured by other windows opened during login.

  • If you want to run another shell script visibly, simply use open /path/to/your-script, which will open it in Terminal.app; however, the Terminal window will automatically close when your script terminates.


Automator [user-SPECIFIC]:

  • File > New, type Application
  • Add a Run Shell Script action, which adds an embedded bash script, and either paste your script code there or add a command that invokes an existing script from there.
  • Save the *.app bundle and add it to the Login Items list in System Preferences > User & Groups > Login Items.

    Note:

    • The embedded script runs with the default "C" locale.
    • $PATH is fixed to /usr/bin:/bin:/usr/sbin:/sbin, which notably does NOT include /usr/local/bin
    • The working dir. is the current user's home directory.

com.apple.loginwindowlogin hook [ALL users - DEPRECATED, but still works]:

If you have admin privileges, this is the easiest method, but it is DEPRECATED, for a variety of reasons (security, limited to a single, shared script, synchronous execution); Apple especially cautions against use of this mechanism as part of a software product.

  • Place your script, e.g., Test.sh, in a shared location - e.g., /Users/Shared - and make sure it is executable (chmod +x /Users/Shared/Test.sh).
  • From Terminal.app, run the following:

    sudo defaults write com.apple.loginwindow LoginHook /Users/Shared/Test.sh

  • Note:

    • The script will run as the root user, so exercise due caution.
      Among the methods listed here, this is the only way to run a script as root.

    • There's only one system-wide login hook.

      • Note that there's also a log-OUT hook, LogoutHook, which provides run-at-logout functionality - unlike the other approaches.
    • The login-hook script runs synchronously before other login actions, and should therefore be kept short.

      • Notably, it runs before the desktop is displayed; you cannot launch applications from the script, but you can create simple interactions via osascript and AppleScript snippets (e.g., osascript -e 'display dialog "Proceed?"'); however, any interactions block the login process.
    • The script runs in the context of the root user and he username of the user logging on is passed as the 1st argument to the script.

    • The script runs with the default "C" locale.
    • $PATH is fixed to /usr/bin:/bin:/usr/sbin:/sbin, which notably does NOT include /usr/local/bin
    • The working dir. is /.

launchd agents:

launchd-agent-executed scripts can be installed for a SPECIFIC user OR for ALL users - the latter requires administrative privileges.

While using launchd is Apple's preferred method, it's also the most cumbersome, as it requires creating a separate *.plist configuration file.
On the upside, you can install multiple scripts independently.

  • Note:
    • No specific timing or sequencing of launchd scripts is guaranteed; loosely speaking, they "run at the same time at login"; there is even no guaranteed timing between the user-specific and the all-user tasks.
    • The script runs with the default "C" locale.
    • $PATH is fixed to /usr/bin:/bin:/usr/sbin:/sbin, which notably does NOT include /usr/local/bin
    • The working dir. is / by default, but you can configure it via the .plist file - see below.
    • The script-file path must be specified as a full, literal path (e.g., /Users/jdoe/script.sh; notably , ~-prefixed paths do not work.
    • For a description of all keys that can be used in *.plist configuration files, see man launchd.plist.
    • Both user-specific and all-users tasks run as the current user (the user logging on).

launchd [user-SPECIFIC]:

  • Note: Lingon 3 ($5 as of early 2014) is a GUI application that facilitates the process below, but only for user-specific scripts.
  • Place your script, e.g., Test.sh, in your home folder, e.g., /Users/jdoe
  • Create a file with extension .plist in ~/Library/LaunchAgents, e.g., ~/Library/LaunchAgents/LoginScripts.Test.plist, by running the following in Terminal.app:

    touch ~/Library/LaunchAgents/LoginScripts.Test.plist
    
  • Open the file and save it with the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>Label</key>
          <!-- YOUR SELF-CHOSEN *UNIQUE* LABEL (TASK ID) HERE -->
        <string>LoginScripts.Test.sh</string>
        <key>ProgramArguments</key>
        <array>
              <!-- YOUR *FULL, LITERAL* SCRIPT PATH HERE -->
            <string>/Users/jdoe/Test.sh</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
    </dict>
    </plist>
    
  • The <!-- ... --> comments indicate the places to customize; you're free to choose a label, but it should be unique - ditto for the .plist filename; for simplicity, keep the label and the filename root the same.

  • From Terminal.app, run the following:

    launchctl load ~/Library/LaunchAgents/LoginScripts.Test.plist
    
  • Note that, as a side effect, the script will execute right away. From that point on, the script will execute whenever the CURRENT user logs on.

  • It is not strictly necessary to run launchctl load -- since, by virtue of the file's location, it will be picked up automatically on next login -- but it's helpful for verifying that the file loads correctly.

launchd [ALL users]

  • Place your script, e.g., Test.sh, in a SHARED location, e.g., /Users/Shared
  • Create a file with extension .plist in /Library/LaunchAgents (requires admin privileges), e.g., /Library/LaunchAgents/LoginScripts.Test.plist, by running the following in Terminal.app:

    sudo touch /Library/LaunchAgents/LoginScripts.Test.plist
    
  • Open the file and save it with the following content (make sure your text editor prompts for admin privileges on demand; alternatively, use sudo nano /Library/LaunchAgents/LoginScripts.Test.plist):

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>Label</key>
          <!-- YOUR SELF-CHOSEN *UNIQUE* LABEL (TASK ID) HERE -->
        <string>LoginScripts.Test.sh</string>
        <key>ProgramArguments</key>
        <array>
              <!-- YOUR *FULL, LITERAL* SCRIPT PATH HERE -->
            <string>/Users/Shared/Test.sh</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
    </dict>
    </plist>
    
  • The <!-- ... --> comments indicate the places to customize; you're free to choose a label, but it should be unique - ditto for the .plist filename; for simplicity, keep the label and the filename root the same.

  • From Terminal.app, run the following:

    sudo chown root /Library/LaunchAgents/LoginScripts.Test.plist
    sudo launchctl load /Library/LaunchAgents/LoginScripts.Test.plist
    
  • Note that, as a side effect, the script will execute right away. From that point on, the script will execute whenever ANY user logs on.

  • It is not strictly necessary to run launchctl load -- since, by virtue of the file's location, it will be picked up automatically on next login -- but it's helpful for verifying that the file loads correctly.
Frungi
  • 506
  • 5
  • 16
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • While this is a great answer, the PATH info for Launch Agents may not be correct. I had to add /usr/local/bin/:/bin/:/usr/bin/. Those were not already included in the PATH available to the script - what @mklement0 terms the "fixed" path – stonedauwg Mar 21 '19 at 18:00
  • Thanks, @stonedauwg, but from what I can tell the answer still applies as of macOS 10.14 - the `PATH` environment variable that launch agent-launched scripts see is fixed at `/usr/bin:/bin:/usr/sbin:/sbin`, which - as stated - doesn't include the commonly used `/usr/local/bin` dir. So, what is not correct? – mklement0 Mar 21 '19 at 19:59
  • As i said, i had to add 3 paths total, 2 of which are paths you said are fixed, namely /usr/bin and /bin. My script complained abt not finding tools at those paths. Once I added those (plus /usr/local/bin as well, for other tools) it could see things at all 3 of those paths. My point is in my macOS 10.14 env, /usr/bin and /bin are NOT fixed - I had to add them explicitly. /usr/local/bin Im in agreement, that one is not fixed either – stonedauwg Mar 21 '19 at 23:56
  • @stonedauwg: So, to be clear: you're sayin that the only `PATH` entries you saw when running via launchd were `/usr/sbin:/sbin`? From my personal experience, I'd say that's very unusual, and looking around the web, I see references to the same list of dirs. as in my answer (e.g., https://apple.stackexchange.com/a/284758/28668, https://serverfault.com/q/694439/176094). So, if you could explain _why and when_ someone would see different paths - such as in your case - that would be helpful. – mklement0 Mar 22 '19 at 01:22
  • 1
    You are correct. I was trying to use $PATH as part of the plist variable new path, which I found, from ServerFault articles, does NOT work. Variable expansion does not work in the launchd plists, so you need to specific all of the path yourself, basically forcing you to repeat what the default is plus more :( – stonedauwg Mar 22 '19 at 17:27
  • macos monterey on macbook air m1 here, I found I have to relaunch my script every time I resume from suspend (close the lid for a while)... any idea to make this persistent? it works with launchd at login but I rarely if ever reboot the machine – filippo Aug 18 '22 at 06:32
  • @filippo, I wouldn't expect a script to terminate just because the machine goes to sleep (but I've never tested). As far as I know, there is no "RunAtWake" event type or similar that would allow you to run something when the machine wakes up. Perhaps it's worth asking a separate question focused on your problem. – mklement0 Aug 18 '22 at 13:31
  • @mklement0 thanks I will open ask another question! it's not that it terminates, the keyboard mapping is being reset and the remapping script should be somehow launched again to fix that. I could probably solve it with some script that runs every N seconds instead than once at login – filippo Aug 19 '22 at 09:20
7

You can't just place plain scripts in that folder. You need a "specialized bundle" how Apple calls it, basically a folder with your executable, and a .plist configuration. And you should put it in /Library/StartupItems since /System/Library/StartupItems/ is reserved for the operating system. Read all about it here:

https://developer.apple.com/library/mac/documentation/macosx/conceptual/bpsystemstartup/chapters/StartupItems.html

Also note that the whole stuff is marked as deprecated technology. And that Apple is suggesting the use of launchd. There is an example how to set it up here:

https://superuser.com/questions/229773/run-command-on-startup-login-mac-os-x

Community
  • 1
  • 1
Ivan Kovacevic
  • 1,322
  • 12
  • 30
3

launchd-oneshot is used to install script as a launchd job to run on login, with

brew install cybertk/formulae/launchd-oneshot
sudo launchd-oneshot Test.sh --on-login

Disclosure: I am the author of this package.

tripleee
  • 175,061
  • 34
  • 275
  • 318
Quanlong
  • 24,028
  • 16
  • 69
  • 79
  • 5
    Kudos for figuring out a way to run a script at login time _as root_. It's implied by the CLI's name, but just to state it explicitly: It is only for *one-time* runs of scripts. I suggest you state in your answer that this is a project _of yours_. – mklement0 Mar 11 '17 at 18:13
  • Indeed, the [Stack Overflow promotion policy](/help/promotion) ***requires*** you to disclose any affiliation of yours to resources you recommend. I'll edit your answer to conform. – tripleee Jul 18 '21 at 07:23