├── 2-PATH ├── sample.sh └── README.md ├── 16-launchd ├── empty-trash.sh ├── boilerplate.plist ├── local.empty-trash.plist └── README.md ├── 10-exec-redirect-stdio ├── log │ └── hooks-out.log ├── package.json ├── hooks │ └── post-merge └── README.md ├── 15-integrate-with-node ├── log ├── README.md ├── run-test.sh └── filter-ldjson.js ├── 11-case ├── extract.sh └── README.md ├── 8-jq-grep ├── check-unused-deps.sh ├── index.js ├── README.md └── package.json ├── 13-getopts-open-pr ├── README.md └── open-pr.sh ├── 5-shell-parameter-expansions ├── count-files.sh └── README.md ├── 6-shortcuts └── README.md ├── 12-getopts ├── getopts.sh └── README.md ├── 1-alias-bash_profile └── README.md ├── 4-history-expansions └── README.md ├── 14-send-receive-from-nodejs ├── README.md └── qscheck ├── 3-brace-expansions └── README.md ├── 9-stdio └── README.md ├── README.md └── 7-read-json-jq └── README.md /2-PATH/sample.sh: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /16-launchd/empty-trash.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | osascript -e 'tell app "Finder" to empty' 4 | -------------------------------------------------------------------------------- /10-exec-redirect-stdio/log/hooks-out.log: -------------------------------------------------------------------------------- 1 | Sat Nov 3 15:14:09 MDT 2018: Running npm install because package.json changed 2 | -------------------------------------------------------------------------------- /15-integrate-with-node/log: -------------------------------------------------------------------------------- 1 | {"level":"warn", "message":"this is just a warning"} 2 | {"level":"log", "message":"hi there"} 3 | {"level":"error", "message":"error occurred!"} 4 | -------------------------------------------------------------------------------- /11-case/extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | case "$1" in 4 | *.tar|*.tgz) tar -xzvf "$1";; 5 | *.gz) gunzip -k "$1";; 6 | *.zip) unzip -v "$1";; 7 | *) 8 | echo "Cannot extract $1" 9 | exit 1 10 | ;; 11 | esac 12 | -------------------------------------------------------------------------------- /8-jq-grep/check-unused-deps.sh: -------------------------------------------------------------------------------- 1 | 2 | for dep in $(jq -r '.dependencies | keys | .[]' package.json); do 3 | if ! grep "require\(.*$dep.*\)" -Rq --exclude-dir="node_modules" .; then 4 | echo "You can probably remove $dep" 5 | fi 6 | done 7 | -------------------------------------------------------------------------------- /13-getopts-open-pr/README.md: -------------------------------------------------------------------------------- 1 | # Create a bash script to open a pull request on github using getopts 2 | 3 | See `open-pr.sh`. 4 | 5 | Documentation for creating a pull requests on Github: https://developer.github.com/v3/pulls/#create-a-pull-request 6 | -------------------------------------------------------------------------------- /8-jq-grep/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const port = 3000 4 | 5 | app.get('/', (req, res) => res.send('Hello World!')) 6 | 7 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 8 | -------------------------------------------------------------------------------- /5-shell-parameter-expansions/count-files.sh: -------------------------------------------------------------------------------- 1 | # use the first param if available, use current working directory by default 2 | dir=${1:-$PWD} 3 | # count the lines outputted by the find command to get the file count 4 | find "$dir" -type f -maxdepth 1 | wc -l 5 | -------------------------------------------------------------------------------- /16-launchd/boilerplate.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // job config goes here 6 | 7 | 8 | -------------------------------------------------------------------------------- /6-shortcuts/README.md: -------------------------------------------------------------------------------- 1 | # Use bash keyboard shortcuts 2 | 3 | - Ctrl+A - go to the beginning of a line 4 | - Ctrl+E- go to the end of a line 5 | - Ctrl+K - clears line up to the cursor 6 | - Ctrl+W - delete last word 7 | - Ctrl+L - clear the screen (equivalent of the clear command) 8 | -------------------------------------------------------------------------------- /12-getopts/getopts.sh: -------------------------------------------------------------------------------- 1 | 2 | while getopts ':ab:' opt; do 3 | case "$opt" in 4 | a) echo "a found";; 5 | b) echo "b found and the value is $OPTARG";; 6 | \?) echo "unknown option";; 7 | esac 8 | done 9 | 10 | shift $(( OPTIND - 1 )) 11 | 12 | for arg in $@; do 13 | echo "received arg $arg" 14 | done 15 | -------------------------------------------------------------------------------- /2-PATH/README.md: -------------------------------------------------------------------------------- 1 | # Add executable files to your $PATH with bash 2 | 3 | You can append to your system's PATH like so in your bash_profile: 4 | ```bash 5 | export PATH="$PATH:~/my-scripts" 6 | ``` 7 | 8 | You can also symlink an executable into a prexisting folder in your PATH. For example: 9 | ```bash 10 | ln -s my-script /usr/local/bin 11 | ``` 12 | 13 | -------------------------------------------------------------------------------- /8-jq-grep/README.md: -------------------------------------------------------------------------------- 1 | # Use jq and grep to find unused dependencies in a project 2 | 3 | See accompanying files. 4 | 5 | Fun fact: I used something similar to this at work on a project that has 250+ dependencies 😱. Of course, if you really want to do something like this in a robust way, you'd need to do static code analysis. But for something quick and dirty, this works well I think. 6 | -------------------------------------------------------------------------------- /8-jq-grep/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jq-lesson", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": ["jq", "egghead"], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4", 14 | "lodash": "^4.17.11" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /11-case/README.md: -------------------------------------------------------------------------------- 1 | # Use `case` for complicated conditional statements in bash 2 | 3 | The basic syntax: 4 | 5 | ```bash 6 | case $variable in 7 | pattern) command-list;; 8 | esac 9 | ``` 10 | 11 | For example: 12 | ```bash 13 | case "$1" in 14 | a) echo "a matched";; 15 | b) echo "b matched";; 16 | c) 17 | echo "c matched" 18 | ;; 19 | esac 20 | ``` 21 | 22 | See `extract.sh` for the example script. 23 | -------------------------------------------------------------------------------- /1-alias-bash_profile/README.md: -------------------------------------------------------------------------------- 1 | # Create aliases in .bash_profile for common commands 2 | 3 | ```bash 4 | alias git_sync="git pull -r && git push" 5 | 6 | alias ll="ls -laG" 7 | ``` 8 | 9 | ## Additional notes 10 | Aliases don't have much use outside of bash_profile (or .bashrc). To unalias something, there's an `unalias ` command. 11 | 12 | More documentation here: https://www.tldp.org/LDP/abs/html/aliases.html 13 | -------------------------------------------------------------------------------- /4-history-expansions/README.md: -------------------------------------------------------------------------------- 1 | # Rerun Bash commands with history expansions 2 | 3 | `!!` refers to the last command. 4 | 5 | ```bash 6 | date 7 | # running just !! will rerun the last command 8 | !! 9 | ``` 10 | 11 | ```bash 12 | ifconfig en0 down 13 | # this will probably fail, requires super user permissions, so... 14 | sudo !! 15 | ``` 16 | 17 | `!$` is the last argument of the last command. 18 | ```bash 19 | touch script.sh 20 | chmod +x !$ 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /14-send-receive-from-nodejs/README.md: -------------------------------------------------------------------------------- 1 | # Send and receive data from a node.js script in bash using the process object 2 | 3 | Use `node -p` for one-liners, like this: 4 | ```bash 5 | cpus=$(node -p 'os.cpus().length') 6 | echo $cpus 7 | ``` 8 | 9 | See qscheck for the example script that parses and checks querystring values. We can use that script like so: 10 | ```bash 11 | ./qscheck 'abc=123&def=456' abc 123 12 | echo $? # check the exit status, should be 0 13 | ./qscheck 'abc=123&def=456' abc 124 14 | echo $? # should be 1 15 | ``` 16 | -------------------------------------------------------------------------------- /5-shell-parameter-expansions/README.md: -------------------------------------------------------------------------------- 1 | # Set default arguments with bash shell parameter expansions 2 | 3 | They can be used to simply output a variable. 4 | ```bash 5 | echo ${HOME} 6 | ``` 7 | This can be useful in scenarios like this: 8 | ```bash 9 | # this won't work, just outputs the current year because it's looking for a $USER_ variable 10 | echo $USER_$(date '+%Y') 11 | # this works 12 | echo ${USER}_$(date '+%Y') 13 | ``` 14 | 15 | ```bash 16 | # $str doesn't exist, so 'default' is used instead 17 | echo ${str:-'default'} 18 | ``` 19 | -------------------------------------------------------------------------------- /10-exec-redirect-stdio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-sandbox", 3 | "version": "1.0.0", 4 | "description": "Sandbox for testing git automation and hooks and other stuff", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@bitbucket.org/ccnokes/git-sandbox.git" 12 | }, 13 | "keywords": [], 14 | "author": "Cameron Nokes", 15 | "license": "ISC", 16 | "homepage": "https://bitbucket.org/ccnokes/git-sandbox#readme", 17 | "dependencies": { 18 | "lodash": "^4.17.11" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /10-exec-redirect-stdio/hooks/post-merge: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # all stdio in this script after this line will be sent to the log file 4 | # note that we're appending so that the log file is not overwritten every time this script is called 5 | exec >> log/hooks-out.log 2>&1 6 | 7 | if git diff-tree --name-only --no-commit-id ORIG_HEAD HEAD | grep --quiet 'package.json'; then 8 | echo "$(date): Running npm install because package.json changed" 9 | # redirecting stdout to dev/null trashes quiets the stdout, but it's stderr will go to our log file 10 | npm install > /dev/null 11 | else 12 | echo "$(date): No changes in package.json found" 13 | fi 14 | -------------------------------------------------------------------------------- /16-launchd/local.empty-trash.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.cameronnokes.personal.empty-trash 7 | ProgramArguments 8 | 9 | /Users/cameronnokes/cron_empty-trash.sh 10 | 11 | StartCalendarInterval 12 | 13 | Weekday 14 | 1 15 | Hour 16 | 10 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /3-brace-expansions/README.md: -------------------------------------------------------------------------------- 1 | # Create and copy multiple files with brace expansions in Bash 2 | 3 | Brace expansions are commonly used when copying or moving files. 4 | ```bash 5 | cp index.js{,.backup} 6 | # Expands to `cp index.js index.js.backup` 7 | ``` 8 | 9 | Another way it can be used is when making several folders that all follow the same structure. 10 | ```bash 11 | mkdir -p packages/{pkg1,pkg2,pkg3}/src 12 | ``` 13 | 14 | Brace expansions can be used for sequences as well. 15 | ```bash 16 | echo {1..10} 17 | echo {a..z} 18 | ``` 19 | 20 | For example, a sequence could be used to generate N test files quickly. 21 | ```bash 22 | touch test-{1..10} 23 | ``` 24 | -------------------------------------------------------------------------------- /15-integrate-with-node/README.md: -------------------------------------------------------------------------------- 1 | # Transform piped data from bash using a node.js Transform stream 2 | 3 | See `filter-ldjson.js` for the example script. 4 | 5 | ## Additional notes 6 | You can read more about node.js streams (a pretty deep subject) here: https://nodejs.org/api/stream.html 7 | 8 | There's some interesting things happening in the `run-test.sh` script that aren't covered in this course. You can read up on those things here: 9 | - trap: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html 10 | - while loops: http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html 11 | - backgrounding and `jobs`: https://www.digitalocean.com/community/tutorials/how-to-use-bash-s-job-control-to-manage-foreground-and-background-processes 12 | -------------------------------------------------------------------------------- /9-stdio/README.md: -------------------------------------------------------------------------------- 1 | # Understand how to redirect stdin, stdout, and stderr in bash 2 | 3 | ```bash 4 | ls -la /dev | grep 'std.*' 5 | # this should show which file descriptors (fd) are matched to each standard input/output, like this: 6 | # stdin -> fd/0 7 | # stdout -> fd/1 8 | # stderr -> fd/2 9 | ``` 10 | 11 | Redirect stdout 12 | ```bash 13 | ls > ls.txt 14 | cat ls.txt 15 | ``` 16 | 17 | Redirect stderr 18 | ```bash 19 | ls noexist 2> ls-errs.txt 20 | cat ls-errs.txt 21 | ``` 22 | 23 | Redirect stdout and stderr to the same file 24 | Note the ampersand which tells bash that we're redirecting to a file descriptor (rather than an actual file) 25 | ```bash 26 | ls noexist > ls.txt 2>&1 27 | cat ls.txt 28 | ``` 29 | 30 | Redirect stdin 31 | ```bash 32 | cat < ls.txt 33 | ``` 34 | -------------------------------------------------------------------------------- /15-integrate-with-node/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # trap runs the cleanup function when the script completes 4 | # it stops the bash process that's running the push_to_log function 5 | cleanup() { 6 | kill $(jobs -p) 7 | } 8 | trap cleanup EXIT 9 | 10 | push_to_log() { 11 | local level="error" 12 | while true; do 13 | sleep 1 14 | echo "{\"level\":\"$level\", \"message\":\"$(date)\"}" >> log 15 | # flip it from error to log after every iteration 16 | if [[ $level == "error" ]]; then 17 | level="log" 18 | else 19 | level="error" 20 | fi 21 | done 22 | } 23 | 24 | # append to log file once a second in the background 25 | push_to_log & 26 | 27 | # tail it in realtime and pipe it to our JS file to filter it 28 | tail -f log | ./filter-ldjson.js 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advanced Bash Automation for Web Developers 2 | 3 | 4 | This is the GitHub repo to accompany the Egghead Course "Advanced Bash Automation for Web Developers". Watch it on egghead.io here: [https://egghead.io/courses/advanced-bash-automation-for-web-developers](https://egghead.io/courses/advanced-bash-automation-for-web-developers). 5 | 6 | This course was created as a follow-up to my "Automate Daily Development Tasks with Bash" course (repository: https://github.com/ccnokes/automate-daily-development-tasks-with-bash, videos: https://egghead.io/courses/automate-daily-development-tasks-with-bash) 7 | 8 | Each lesson's contents are in their respective folder. 9 | 10 | This course is focused on macOS. Much of it applies to Linux systems as well, but note that sometimes commands or their options are different on Linux. 11 | -------------------------------------------------------------------------------- /14-send-receive-from-nodejs/qscheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // this scripts parses a query string and does a value check, e.g. 4 | // qscheck 'abc=123&def=456' abc 123 5 | // exits with a 0 for true, 1 for false/error 6 | 7 | const { parse } = require('querystring'); 8 | 9 | const [ 10 | queryString, 11 | queryStringPropName, 12 | queryStringExpected 13 | ] = process.argv.slice(2); 14 | 15 | if (!queryString || !queryStringPropName || !queryStringExpected) { 16 | process.stderr.write(`ERROR! Must pass three args \n`); 17 | process.exit(1); 18 | } 19 | 20 | let actual = parse(queryString)[queryStringPropName]; 21 | 22 | if (actual === queryStringExpected) { 23 | process.stdout.write(`${actual} is equal to ${queryStringExpected} \n`); 24 | process.exit(0); 25 | } else { 26 | process.stderr.write(`${actual} is not equal to ${queryStringExpected} \n`); 27 | process.exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /12-getopts/README.md: -------------------------------------------------------------------------------- 1 | # Create a bash script that accepts named options with `getopts` 2 | See `getopts.sh` 3 | 4 | ## Additional notes 5 | 6 | `getopts` is case-sensitive. So `-a` is not equal to `-A`, for example. 7 | 8 | Don't confuse `getopts` with a similar but different command `getopt`. If you're on GNU/Linux, `getopt` is more robust (albeit more difficult to use) but can support different option formats like `--long` options. BSD/macOS `getopt` is different and not as robust as `getopts` is. 9 | 10 | If you want really robust options handling, you should probably handle it in node.js (or some other programming language) and, furthermore, use an existing library to handle all of that logic if you can. 11 | 12 | When it comes to options parsing, there's *lots* of edge cases and different formats. To some extent, you can make `getopts` parse long options, see https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options for possiblities and pitfalls. 13 | 14 | -------------------------------------------------------------------------------- /7-read-json-jq/README.md: -------------------------------------------------------------------------------- 1 | # Read and use JSON in bash using `jq` 2 | 3 | Note: jq has to be installed. On macOS, the easiest way to do this is to run `brew install jq`. To view install instructions for all platforms, see https://stedolan.github.io/jq/download/. 4 | 5 | Example jq queries from this lesson: 6 | 7 | ```bash 8 | # get a property by name 9 | echo '{ "foo": 123 }' | jq '.foo' 10 | 11 | # nested values 12 | echo '{ "a": { "b": { 123 } } }' | jq '.a.b' 13 | 14 | # pipe to jq with no selector to just format the JSON 15 | curl https://api.github.com/repos/facebook/react | jq 16 | 17 | # use with curl or any other JSON producing command 18 | curl -s https://api.github.com/repos/facebook/react | jq '.stargazers_count' 19 | 20 | # output each array element to a new line 21 | echo '[1, 2, 3]' | jq '.[]' 22 | 23 | # combine with property access syntax 24 | echo '[{"id": 1}, {"id": 2}]' | jq '.[].id' 25 | 26 | # realistic example with curl 27 | curl -s https://api.github.com/search/repositories?q=service+worker | jq '.items[].name' 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /10-exec-redirect-stdio/README.md: -------------------------------------------------------------------------------- 1 | # Use exec to redirect stdio in a git hook script 2 | 3 | See `hooks/post-merge` for the main script example. That script will check git's list of changed files and run npm install if package.json was changed after a git pull. 4 | 5 | ## Additional notes 6 | If you ever need to redirect stdio and then restore it back to it's original state, you can do something like this: 7 | ```bash 8 | # store the original file descriptors on an empty one 9 | # file descriptors 3 - 9 are open for scripting use 10 | exec 3>&1 11 | # send stdout to a log file 12 | exec > log 13 | 14 | # script contents here... 15 | 16 | # restore stdout and close fd 3 17 | exec 1>&3 3>&- 18 | ``` 19 | You can run this directly in the shell to see it in action. 20 | 21 | If your package.json isn't at the root level, you'll have to add a `-r` to the `git diff-tree` command. 22 | 23 | ### Resources 24 | `exec` can get a little complicated. Here's some more reading materials: 25 | - https://www.tldp.org/LDP/abs/html/x17974.html 26 | - http://tldp.org/LDP/abs/html/io-redirection.html 27 | -------------------------------------------------------------------------------- /15-integrate-with-node/filter-ldjson.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // this script can be piped to and will filter a log file of ld-json to only lines that have a level of "error" 4 | 5 | const { Transform } = require('stream'); 6 | 7 | class FilterLogs extends Transform { 8 | _transform(chunk, encoding, callback) { 9 | try { 10 | let jsonChunk = chunk.toString() 11 | .split('\n') 12 | .reduce((aggr, line) => { 13 | if (line) { 14 | let json = JSON.parse(line); 15 | if (json.level === 'error') { 16 | aggr.push(JSON.stringify(json)); 17 | } 18 | } 19 | return aggr; 20 | }, []) 21 | .join('\n'); 22 | 23 | if (jsonChunk) { 24 | this.push(jsonChunk + `\n`); 25 | } 26 | 27 | callback(); 28 | } catch (error) { 29 | callback(error); 30 | } 31 | } 32 | } 33 | 34 | // handle errors 35 | process.on('uncaughtException', err => { 36 | process.stderr.write(`Uncaught exception: ${err.message}\n`); 37 | process.exit(1); 38 | }); 39 | 40 | // do the actual work 41 | process.stdin 42 | .pipe(new FilterLogs()) 43 | .pipe(process.stdout); 44 | 45 | -------------------------------------------------------------------------------- /13-getopts-open-pr/open-pr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_branch=$(git rev-parse --abbrev-ref HEAD) 4 | username='' 5 | title='' 6 | password='' 7 | 8 | usage() { 9 | echo "Usage: open-pr [-u ] [-p ] [-t ] <body of the PR>" 10 | } 11 | 12 | while getopts ':u:p:t:h' opt; do 13 | case "$opt" in 14 | u) username="$OPTARG";; 15 | p) password="$OPTARG";; 16 | t) title="$OPTARG";; 17 | h) 18 | usage 19 | exit 20 | ;; 21 | \?) 22 | echo "Invalid option $OPTARG" >&2 23 | usage >&2 24 | exit 1 25 | ;; 26 | esac 27 | done 28 | 29 | shift $((OPTIND - 1)) 30 | 31 | if [[ $current_branch == 'master' ]]; then 32 | echo "You're already on master, create a new branch, push it, and then run this script" >&2 33 | exit 1 34 | fi 35 | 36 | check_is_set() { 37 | if [[ -z $2 ]]; then 38 | echo "ERROR: $1 must be set" >&2 39 | usage >&2 40 | exit 1 41 | fi 42 | } 43 | 44 | check_is_set "username" $username 45 | check_is_set "password" $password 46 | check_is_set "title" $title 47 | 48 | # this is called a heredoc. Read more about them here: http://tldp.org/LDP/abs/html/here-docs.html 49 | data=$(cat <<-END 50 | { 51 | "title": "$title", 52 | "base": "master", 53 | "head": "$current_branch", 54 | "body": "$@" 55 | } 56 | END 57 | ) 58 | 59 | status_code=$(curl -s --user "$username:$password" -X POST "https://api.github.com/repos/ccnokes/git-automation-sandbox/pulls" -d "$data" -w %{http_code} -o /dev/null) 60 | 61 | if [[ $status_code == "201" ]]; then 62 | echo "Complete!" 63 | else 64 | echo "Error occurred, $status_code status received" >&2 65 | exit 1 66 | fi 67 | -------------------------------------------------------------------------------- /16-launchd/README.md: -------------------------------------------------------------------------------- 1 | # Schedule timed jobs on macOS with launchd 2 | 3 | To install the launchd job: 4 | ```bash 5 | mv local.empty-trash.plist ~/Library/LaunchAgents 6 | launchctl load ~/Library/LaunchAgents/local.empty-trash.plist 7 | ``` 8 | 9 | To verify it's active 10 | ```bash 11 | launchctl list | grep 'local' 12 | ``` 13 | 14 | To run it immediately 15 | ```bash 16 | launchctl kickstart gui/$UID/local.empty-trash 17 | ``` 18 | 19 | To unload the job 20 | ```bash 21 | launchctl unload ~/Library/LaunchAgents/local.empty-trash.plist 22 | ``` 23 | Now if you grep for the job (`launchctl list | grep 'local'`), it won't be in that list. 24 | 25 | ## Additional notes 26 | 27 | Launchd is actually a really huge topic and I only went over one of the most basic use cases of it. Here's some other things that I couldn't fit into the video. 28 | 29 | ### Why not `cron`? 30 | Launchd is really robust and has a lot more features than cron does (at least that's the case on macOS, the GNU/Linux crontab is different and more robust than the BSD cron AFAIK). For example, some other possiblities: 31 | 32 | - StartInterval: Run job every N seconds 33 | - StartOnMount: Run when a device has been mounted (for example a backup harddrive) 34 | - WatchPaths: Run when creating, removing files in this directory 35 | - RunAtLoad: Run at startup and login 36 | - StandardOutPath and StandardErrorPath: specify where your stdio goes (otherwise it goes to the syslog I think) 37 | 38 | See "Learn more" below for more. 39 | 40 | Apple also recommends using launchd over cron. For small use cases like this one, I think cron is fine though, I suspect that recommendation is targeted for macOS app devs. 41 | 42 | ### Why use AppleScript to empty the Trash and not `rm -rf ~/.Trash`? 43 | There's a few edge cases with emptying the trash that `rm` doesn't handle. One of them was that if you have files from a different device in the trash then `rm` can run into permission issues deleting those. 44 | 45 | ### Will my scheduled job run if my computer is off or asleep? 46 | From Apple's documentation: "If you schedule a launchd job by setting the StartCalendarInterval key and the computer is asleep when the job should have run, your job will run when the computer wakes up. However, if the machine is off when the job should have run, the job does not execute until the next designated time occurs." 47 | 48 | ### Learn more 49 | http://www.launchd.info/ 50 | https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/ScheduledJobs.html 51 | https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html 52 | --------------------------------------------------------------------------------