You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
24 KiB

# -*- mode: shell-script -*-
# Script to make the Mac more Linux-like
# Version 1.592 (c) Silas S. Brown 2012-22.
# Tested in zsh on macOS 11.4 through 12.2,
# and in bash on Mac OS X 10.7 through 10.14.
# This script can go in your ~/.zshrc (10.15+)
# or ~/.bashrc and/or ~/.bash_profile (older Macs)
# for example:
# curl >> ~/.bash_profile
# or if you prefer you can keep it in a separate file
# and source it from your startup files
# (put "source ~/.maclinux" into .zshrc or .bashrc)
# You might wish to also put it in BASH_ENV to ensure all
# commands are copied to subshells, although commands without
# hyphens and dots should be copied over anyway.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
# If you want to compare this code to old versions, the old
# versions are being kept in the E-GuideDog SVN repository at
# and on GitHub at
# and on GitLab at
# and on Bitbucket
# and at
# and in China: git clone
# although some early ones are missing.
if [ -d /Volumes ] && [ "$(uname -s)" = Darwin ]; then
# we're on a Mac
# if in bash, avoid recursion:
export __OldBashEnv="$BASH_ENV" ; unset BASH_ENV
# Check SMART status of primary hard disk:
diskutil info disk0|grep SMART|grep -v 'Status:[^A-z]*Verified$' && echo "Hard disk failing: backup NOW" && echo
# Support installations of HomeBrew/MacTex/MacPorts/Fink:
export PATH=/usr/texbin:/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/local/sbin:$PATH:/usr/local/opt/coreutils/libexec/gnubin
# (gnubin (from coreutils package) comes last so Mac versions take priority, just in case; coreutils has some commands not otherwise available e.g. factor, md5sum)
export MANPATH=$MANPATH:/usr/local/opt/coreutils/libexec/gnuman
[ ! "$ZSH_NAME" ] && [ -e /sw/bin/ ] && . /sw/bin/
for N in "$HOME"/Library/Python/*/bin; do [ -e "$N" ] && export PATH="$PATH:$N"; done # for pip install --user (without --user it's /usr/local/bin)
if [ ! "$ZSH_NAME" ] && /usr/bin/which -s brew; then
# don't need any of our aliases when running HomeBrew scripts & it's quicker to bypass this file when doing so
brew () { BASH_ENV= $(/usr/bin/which brew) "$@" ; } ; export -f brew
if ! /usr/bin/which -s wget; then
# Set wget to curl with appropriate options. Useful
# if you're in the habit of typing "wget". Note that
# it is not real wget however and it won't recognise
# wget options like -c.
wget () { curl -L --remote-name-all --compressed "$@" ; }
# for older curl: alias wget="curl -OL"
export -f wget >/dev/null
if ! /usr/bin/which -s watch; then
# primitive emulation of the "watch" command
watch () (while true; do clear; date; echo "$@"; echo; "$@"; sleep 2; done)
export -f watch >/dev/null
if ! /usr/bin/which -s pidof; then
# primitive emulation of the "pidof" command
pidof () (ps axwww|fgrep "$@"|fgrep -v "fgrep $@"|sed -e 's/^ *//' -e 's/ .*//')
export -f pidof >/dev/null
top () { /usr/bin/top -stats $(if /usr/bin/top -l 1 -s 0 -stats mem >/dev/null 2>/dev/null; then echo pid,cpu,mem,uid,command; else echo pid,vsize,cpu,rsize,uid,command; fi) -o cpu $@ ; }
# 11.x: -r with 'vsize' works but takes more CPU
# 10.7: 'vsize' works w/out -r
# 'state' works but takes more columns
export -f top >/dev/null
alias netstat-tl="lsof -iTCP -sTCP:LISTEN"
alias netstat-t="lsof -iTCP -sTCP:ESTABLISHED"
if ! /usr/bin/which -s free; then
free () { top -l 1 -s 0 | grep -m 1 PhysMem ; }
export -f free >/dev/null
alias do-i-need-more-ram='if test $((vm_stat|grep "^Page[io]"|sed -e "s/.*://" -e "s/\.$//";echo 1;echo +;echo /;echo p)|(dc 2>/dev/null || echo 11)) -lt 10; then echo Maybe; else echo Probably not; fi' # 'maybe' if page outs are over 10% of page ins; +1 to avoid any possible division by zero if there have been no pageouts
alias battery-status='pmset -g batt' # for MacBook laptops
# Note: Anything with a hyphen in it must be an alias, not a
# function (or at least not exported), or /bin/sh won't like
# it and commands like "man" will fail. Hence the above
# comment about BASH_ENV.
# Define the /Applications as "open -a" commands so you
# can type for example "libreoffice", "gimp",
# "audacity" etc with a relative pathname and Mac open
# will take care of translating it into an absolute
# pathname and opening the appropriate Mac app with it.
# Spaces are turned into hyphens, e.g. for google-chrome.
# If a command already exists, we will append -app to it
# (unless command-app already exists as well), for example
# will become emacs-app if there's already emacs.
# The version below unfortunately requires a temp file,
# but it's usually faster than repeated calls to sed as in
# previous versions of this script (1.38 and below).
# (Bash can't source a /dev/fd from <(...), piping to a
# source /dev/stdin invokes a subshell so it loses the
# functions; no /dev/shm on Mac, so we have to use /tmp)
# (see also comments around make-ramdisk function below).
# TODO: if we must write to /tmp, can we keep it cached
# so that new tabs in the Terminal window don't need to do
# a disk write? (e.g. by putting whole script in an 'if'
# clause which also does echo commands to declare the
# functions, or cache the o/p of "declare" and somehow
# remove what we didn't add), but how to check the age of
# the cache? -newer /Applications? with timeout?
# just make an 'update-functions' cmd for use when wanted?
if [ -e /usr/bin/python3 ] ; then export DefineAppsPython=/usr/bin/python3
elif [ -e /usr/bin/python2 ] ; then export DefineAppsPython=/usr/bin/python2
else echo "maclinux cannot find system Python: most of our application shortcuts will NOT be defined" 1>&2; fi # TODO: search path for a user-installed Python?
if ! /usr/bin/which -s python2 && /usr/bin/which -s python2.7; then python2 () { python2.7 "$@" ; } ; export -f python2 >/dev/null; fi # e.g. MacPorts on macOS 12.3 (no system python2)
if [ "$DefineAppsPython" ]; then
if ! /usr/bin/which -s python; then python () { "$DefineAppsPython" "$@" ; } ; export -f python >/dev/null; fi
DefineApps () { export T=$(mktemp /tmp/$PPID''XXXXXX) ; find "$@" '(' -type d -or -type l ')' -name '*.app' -prune -print0 2>/dev/null | xargs -0 "$DefineAppsPython" -c 'import os,sys,re,distutils.spawn;os.chdir("/usr/bin")'$'\n''def orApp(c):'$'\n'' if not distutils.spawn.find_executable(c): return c'$'\n'' elif not distutils.spawn.find_executable(c+"-app"): return c+"-app"'$'\n\n''def getF(Command):'$'\n'' if "." in Command or "-" in Command: return lambda App: "alias \""+Command+"\"=\"open -W -a \\\""+App+"\\\"\""'$'\n'' else: return lambda App:Command+" () { open -W -a \""+App+"\" \"$@\" ; } ; export -f "+Command+" >/dev/null"'$'\n\n''print ("\n".join((lambda Command:getF(orApp(Command))(App))(Command=re.sub("[^a-z0-9._-]","",re.sub(".*/","",App)[:-4].replace(" ","-").lower())) for App in sys.argv[1:]))' >> $T ; . $T ; rm $T ; unset T; } # (the chdir to /usr/bin is because at least some versions of distutils.spawn assume that PATH contains . even when it doesn't)
else DefineApps () { false ; }; fi
DefineApps /Applications /System/Applications /usr/local/Cellar/emacs
if declare -f xcode >/dev/null; then DefineApps "$(declare -f xcode|grep -i '^\s*open.*/'|head -1|sed -e 's/[^"]*"//' -e 's/".*//')/Contents/Applications"; fi #'# (might find iphone-simulator etc there)
unset DefineApps
if test -e /Applications/MacPorts/; then
# override the above "open": might need its command line
# (but not everything listed by -H really works)
vlc () { /Applications/MacPorts/ "$@" ; } ; export -f vlc >/dev/null
# One slight annoyance is if you use some Macs that
# have bbedit installed but others that just have
# TextWrangler. Let's function-ify those edit commands:
if /usr/bin/which -s bbedit; then
edit () { bbedit "$@" ; }
export -f edit >/dev/null
elif /usr/bin/which -s edit; then
bbedit () { edit "$@" ; }
export -f bbedit >/dev/null
edit () { open -e "$@" ; }
export -f edit >/dev/null # and leave bbedit undefined
if [ -e /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/*/jsc ] ; then
# command-line Javascript interpreter not in default PATH: might as well make this available
jsc () { /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/*/jsc "$@" ; }
if ! /usr/bin/which -s node; then
# might as well alias it, in case you're in the
# habit of typing "node" rather than "jsc" (it's
# not the same, but near enough for simple tests)
node () { jsc "$@" ; }
export -f node >/dev/null
# Ditto for mixed Mac/Linux environments where you
# can type "jmacs" on the Linux terminal to get a
# quick emacs-like editor:
if ! /usr/bin/which -s jmacs; then
if /usr/bin/which -s emacs; then
jmacs () { emacs -q "$@" ; } ; export -f jmacs >/dev/null
else # emacs not present in macOS 11, but we might have it as an application:
if declare -f emacs >/dev/null; then
jmacs () { Emacs=$(declare -f emacs|grep -i '^\s*open.*/'|head -1|sed -e 's/[^"]*"//' -e 's/".*//')/Contents/MacOS/Emacs; if [ ! "$1" ]; then $Emacs -q -nw --eval '(switch-to-buffer "*scratch*")'; else $Emacs -q -nw "$@"; fi ; } ; export -f jmacs >/dev/null
elif declare -f aquamacs >/dev/null; then # TODO: not tested
jmacs () { $(declare -f emacs|grep -i '^\s*open.*/'|head -1|sed -e 's/[^"]*"//' -e 's/".*//')/Contents/MacOS/Aquamacs -q -nw "$@" ; } ; export -f jmacs >/dev/null
# and make sure you can type "play" to play sound files
if ! /usr/bin/which -s play; then
play () { afplay "$@" ; } ; export -f play >/dev/null ; fi
# and "umount" to eject auto-mounted USB sticks etc:
umount () { diskutil umount "$@" ; } ; export -f umount >/dev/null
eject () { drutil eject ; } ; export -f eject >/dev/null # for CDs
# and the general "web browser" command used by some
# scripts in Debian etc
alias x-www-browser=open # should recognise a URL
alias gnome-open=open
# If you have installed Macfusion (which requires MacFUSE
# or the MacFUSE compatibility layer of OSXFUSE) then
# make its curlftpfs and sshfs commands available. Note
# however that curlftpfs can be unreliable, e.g. some
# versions do not properly truncate files when you try to
# overwrite with less data, plus large transfers can break,
# so some situations might be better using .netrc or TRAMP.
# If you do use ftpfs and the server is running it via
# inetd then you might need to increase the connections/min
# allowed in inetd.conf, e.g. "ftp stream tcp nowait.32767"
if declare -f macfusion >/dev/null; then
export MF_Path="$(declare -f macfusion|grep -i '^\s*open .*/'|head -1|sed -e 's/[^"]*"//' -e 's/".*//')" #' # (comment for some versions of Emacs syntax highlighting)
export sshfs_Path="$MF_Path/Contents/PlugIns/sshfs.mfplugin/Contents/Resources/sshfs-static"
export curlftpfs_Path="$MF_Path/Contents/PlugIns/ftpfs.mfplugin/Contents/Resources/curlftpfs_static_mg"
unset MF_Path
if ! /usr/bin/which -s curlftpfs &&
test -e "$curlftpfs_Path"; then
curlftpfs () { "$curlftpfs_Path" -o uid="$(id -u)" "$@" ; }
ftpfs () { curlftpfs "$@" ; } # might as well
export -f curlftpfs ftpfs >/dev/null
else unset curlftpfs_Path
if ! /usr/bin/which -s sshfs &&
test -e "$sshfs_Path"; then
sshfs () { "$sshfs_Path" "$@" ; }
export -f sshfs >/dev/null
else unset sshfs_Path
if declare -f chmox >/dev/null && ! /usr/bin/which -s kchmviewer; then
kchmviewer () { chmox "$@" ; } ; export -f kchmviewer >/dev/null
# kchmviewer is probably easier to auto-complete, as
# chmox will suggest chmod also.
# Intel binaries are included in chmox 0.4+
if alias adobe-reader 2>/dev/null >/dev/null && ! /usr/bin/which -s acroread; then
acroread () { open -W -a "$(alias adobe-reader|sed -e 's/[^"]*"//' -e 's/".*//')" "$@" ; } # must define it this way, not just by calling adobe-reader within the function, because our aliases might not be available in a subshell
export -f acroread >/dev/null
elif alias adobe-acrobat-reader-dc 2>/dev/null >/dev/null && ! /usr/bin/which -s acroread; then
acroread () { open -W -a "$(alias adobe-acrobat-reader-dc|sed -e 's/[^"]*"//' -e 's/".*//')" "$@" ; }
export -f acroread >/dev/null
# If you have installed fuse-ext2 then might as well set
# its mke2fs (although usually you'd use fuse-ext2 or
# mount -t fuse-ext2)
if ! /usr/bin/which -s mke2fs && /usr/bin/which -s fuse-ext2.mke2fs; then
mke2fs () { fuse-ext2.mke2fs "$@" ; }
e2fsmount () { fuse-ext2 "$@" ; }
export -f mke2fs e2fsmount >/dev/null
ulimit -n 1024 # allow programs to have more open files
# Setting the volume from the command line (this
# doesn't exactly emulate the Linux commands on various
# distributions but at least it's a start) -
volume () {
if [ ! "$1" ]; then
# if no argument, print current percentage
V=$(osascript -e "output volume of (get volume settings)")
if [ "$V" = "missing value" ]; then
echo "Cannot read current volume"
echo "This can happen if a multi-output device has been set in audio-midi-setup"
echo "(e.g. to copy computer's sound to QuickTime's recorder via BlackHole)" # (or SoundFlower on older Macs?)
echo " - audio-midi-setup must be used to read volume"
echo "(can still set it from command line)" # (at least it could when I tested it)
else echo "$V"; fi
else osascript -e "set volume output volume $1"; fi }
# older Macs just had "set volume" with a number 0 to 7
export -f volume >/dev/null
# might as well have some iTunes command-line access too -
# here's a function that plays a track whose name contains
# whatever string you pass to the function:
play-track () { osascript -e "tell application \"iTunes\" to play (get item 1 of (get every track of current playlist where name contains \"$*\"))"; }
alias list-track-names="osascript -e \"set AppleScript's text item delimiters to \\\"@@@\\\"\" -e \"tell application \\\"iTunes\\\" to get name of (every track of current playlist) as string\"|sed -e $'s/@@@/\\\\\\n/g'|less -S"
alias next-track='osascript -e "tell application \"iTunes\" to next track"'
alias previous-track='osascript -e "tell application \"iTunes\" to previous track"'
pause () { osascript -e "tell application \"iTunes\" to pause" ; } # resume from GUI for now (right-click on the dock)
stop () { osascript -e "tell application \"iTunes\" to stop" ; }
export -f pause stop >/dev/null
# we can also do the following without sudo, as long as a desktop session is running:
halt () { [ ! "$(jobs -s)" ] && ( ( sleep 0.5 && osascript -e "tell application \"Finder\" to shut down") & ) ; exit ; }
# (if the "exit" warns about stopped jobs, we won't do the shutdown)
poweroff () { halt ; }
reboot () { [ ! "$(jobs -s)" ] && ( ( sleep 0.5 && osascript -e "tell application \"Finder\" to restart") & ) ; exit ; }
logout () { [ ! "$(jobs -s)" ] && ( ( sleep 0.5 && osascript -e "tell application \"System Events\" to log out") & ) ; exit ; }
sleepnow () { if ! pmset -g assertions|grep PreventSystemSleep|grep -v '^\s*PreventSystemSleep\s*0$'; then (sleep 0.5; pmset sleepnow) & exit; else false; fi ; }
alias suspend-mac=sleepnow
export -f halt poweroff reboot logout sleepnow >/dev/null
# (the "exit"s at the end of the above are so the terminal doesn't stop it with "are you sure", unless there are other terminal sessions)
# Spotlight is required by app-store, e.g. for XCode updates (otherwise can get misleading error messages about account sign-in). But you might want Spotlight disabled at other times (especially when accessing removable media).
alias spotlight-disable="sudo launchctl unload -w /System/Library/LaunchDaemons/"
alias spotlight-enable="sudo launchctl load -w /System/Library/LaunchDaemons/"
updatedb () { sudo /usr/libexec/locate.updatedb ; }
export -f updatedb >/dev/null
# note that updatedb does not look in any user directories
# that are chmod'd to 700 (user only) - be sure to chmod
# a+rx any dirs you want it to scan
# Here's a function to install arbitrary commands to run at desktop login
# Notes: (1) PATH might be incomplete, (2) this maclinux won't have been executed (unless you source it in the script),
# (3) if the script starts any background processes, it MUST do a "wait" before finishing if you don't want launchctl to terminate its background processes
_install_login_command () {
if ! [ -e "/$1" ]; then echo "Syntax: install-login-command executable-full-path"; echo "(see comments in maclinux for caveats)"; return; fi
mkdir -p "$HOME/Library/LaunchAgents"
L="$(echo "$1"|sed -e 's,.*/,,')"
if [ -e "$HOME/Library/LaunchAgents/$L.plist" ] || [ ! "$L" ] || ! touch "$HOME/Library/LaunchAgents/$L.plist" 2>/dev/null; then
# can't use executable name as plist name (exists or can't create), so make a generic one
C=0; while test -e "$HOME/Library/LaunchAgents/loginscript$C.plist"; do C=$[$C+1]; done; L=loginscript$C
F="$HOME/Library/LaunchAgents/$L.plist"; (echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";echo "<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"\"><plist version=\"1.0\"><dict><key>Label</key><string>$L</string><key>Program</key><string>$1</string><key>RunAtLoad</key><true/></dict></plist>") > "$F"
launchctl load "$F"
echo "Installed to $F" # (use "launchctl unload" and "rm" to uninstall)
echo "To test now, do: launchctl start $L";
echo "(errors to /var/log/system.log)";
# (change $HOME/Library/LaunchAgents to /Library/LaunchAgents to run on boot instead of login, but crontab @reboot can do that)
alias install-login-command=_install_login_command
_make_ramdisk () {
case "$1" in
"") echo "Syntax: make-ramdisk mountpoint size"
# (or size mountpoint : will auto-reverse)
echo "(512-byte blocks, min 1024 for 512k,"
echo "can specify as M or G instead)"
false ;;
[1-9]*) case "$2" in [1-9]*) echo "Mountpoint must not start with a number"; false ;; *) _make_ramdisk "$2" "$1" ;; esac ;;
case "$2" in
[1-9]*[gG]) _make_ramdisk "$1" "$[${2%[gG]}*2097152]" ;;
[1-9]*[mM]) _make_ramdisk "$1" "$[${2%[mM]}*2048]" ;;
*) mkdir "$1" && mount -t hfs "$(echo "$(newfs_hfs $(hdid -nomount "ram://$2"))"|sed -e 's,[^/]*/,/,' -e 's/ .*//' -e 's,/rdisk,/disk,')" "$1" ;;
esac ;;
esac ; }
# (10.5+ can also do e.g. diskutil erasevolume HFS+ "maclinux-RAMdisk" $(hdiutil attach -nomount ram://1024) for /Volumes/maclinux-RAMdisk)
alias make-ramdisk=_make_ramdisk
_drop_ramdisk () { D="$(mount|grep "$(echo "$1"|sed -e 's,/$,,') "|sed -e 's/ .*//')" ; /sbin/umount "$1" ; hdiutil detach "$D" ; rmdir "$1" ; }
alias drop-ramdisk=_drop_ramdisk
export -f _make_ramdisk _drop_ramdisk >/dev/null # so can use from scripts (useful especially if have upgraded HDD to SSD and worried about excessive writes by script temp files)
# NOTE: for making HFS+ disk images that will interoperate
# with Linux, it's really best to do this IN LINUX (use dd
# and then use mkfs.hfs from hfsprogs), rather than on the
# Mac. That way it's less likely you'll end up with a dmg
# that the Linux kernel refuses to mount for some reason.
function xmanpage() { open x-man-page://$@ ; echo "To change appearance: Terminal / Preferences / Settings / Man page / (Text,Window)" ; }
export -f xmanpage >/dev/null
if ! /usr/bin/which -s xman; then xman () { xmanpage "$@" ; } ; export -f xman >/dev/null; fi # (if X11 is present then xman should be installed, but it may fail to read all manpage sections and UTF-8 won't display)
function psmanpage() { man -t "$@" | open -f -a Preview ; } # will need Invert to change the colours of that
export -f psmanpage >/dev/null
alias man-terminal=xmanpage # for auto-complete "man"
alias man-ps=psmanpage
# Mac's multiline editing doesn't always work well with ANSI codes, even when \[..\] delimiters are used
if [ ! "$SSH_CLIENT" ]; then
# simpler prompt for a local Mac (hostname can be a bit long)
[ "$ZSH_NAME" ] && PS1='mac:%~%# ' || PS1='mac:\w\$ '
elif test "$(hostname -s)" = mac || test "$(hostname -s)" = unknown; then
# if hostname has been SET to mac, better make it different
# to distinguish between a local one and one over SSH
[ "$ZSH_NAME" ] && PS1='mac.ssh:%~%# ' || PS1='mac.ssh:\w\$ '
case "$TERM" in xterm*) echo -n $'\033]0;mac ssh\007' ;; esac
[ "$ZSH_NAME" ] && PS1='%m:%~%# ' || PS1='\h:\w\$ '
case "$TERM" in xterm*) echo -n $'\033]0;'"$(hostname -s)"$'\007' ;; esac
case "$TERM" in xterm*) [ "$ZSH_NAME" ] && PS1=$'%{\e[1;37;40m%}'"$PS1"$'%{\e[1;33;40m%}' || PS1='\[\e[1;37;40m\]'"$PS1"'\[\e[1;33;40m\]' ;; esac
export BASH_ENV="$__OldBashEnv"
if [ "$ZSH_NAME" ]; then
unsetopt hup check_running_jobs bad_pattern no_match # as bash
setopt -k # allow comments even in interactive (as bash does)
else # not on a Mac
if [ "$COLUMNS" ] && [ $COLUMNS -le 50 ] && wget --version 2>/dev/null|head -1|grep "Wget 1.17" >/dev/null; then alias wget="echo 'Doing wget -q to work around Debian bug #823891';wget -q" # non-Mac, but that bug affects the version of wget shipped with Ubuntu 16.04 LTS so I put this in for the Cambridge MCS remote GNU/Linux machines at the time
if [ -e /usr/bin/amixer ] ; then
# for consistency, re-define our "volume" command for amixer on non-Mac
volume () {
if [ ! "$1" ]; then amixer sget Master
else amixer sset Master "$1%"; fi }
export -f volume >/dev/null
if [ -n "$SSH_CLIENT" ]; then export DBUS_SESSION_BUS_ADDRESS=$(cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -m 1 DBUS_SESSION_BUS_ADDRESS | cut -d = -f2-); if ! [ -n "$DBUS_SESSION_BUS_ADDRESS" ]; then unset DBUS_SESSION_BUS_ADDRESS ; fi; fi
fi # end of "not on a Mac" block
# (so you can put all this into a .bashrc that will
# get sourced from either Mac or Linux machines)
which unix2dos >/dev/null 2>/dev/null || alias unix2dos="perl -i -p -e 's|[\r\n]+|\r\n|g'" # (outside Mac section because might also be useful on Linux if unix2dos is missing and you need to send a .txt file to someone who has only Windows Notepad)
# "Bonus" command (not just Mac but may be useful on some
# Linux systems) - a simple 'catdocx' (like catdoc if it's
# installed, but for docx files - just extracts text
# between the XML markup). catdocx_utf8 outputs UTF-8;
# catdocx_cp1252 outputs "Windows 1252" as used on old
# Psion PDAs etc.
catdocx_utf8 () { while [ "$1" ]; do unzip -Cp "$1" word/[dfe]*[ts].xml; shift; done|perl -p -e 's/<w:p(\s[^>]*)?>/\n/g;s/<[^>]*>//g;s/ */ /g;s/&lt;/</g;s/&gt;/>/g'|grep -v '^\s*$' ; } # (want to unzip document.xml, endnotes.xml and footnotes.xml but not emit warnings if missing and not match fontTable etc, hence the awkward file specification; perl rather than sed so as to avoid BSD sed / gsed differences re \n in replacement etc)
catdocx_cp1252 () { catdocx_utf8 "$@" | iconv -c -f UTF8 -t CP1252//TRANSLIT ; } # --unicode-subst="[U+%04X]" instead of -c would be nice, but it's not available on all versions of iconv
# Bonus command 2: cat a load of text files into a
# self-extracting shell script (for email or whatever)
# (must o/p to a file; i/p cannot incl dirs)
shellarc () { Out="$1"; if test -e "$Out" ; then echo "Output file $Out exists; I'd rather not overwrite it" ; return; fi; echo "Writing $Out"; (echo '#!/bin/bash' ; while [ "$2" ]; do if echo "$2"|grep / >/dev/null; then echo mkdir -p '"'"$(echo "$2"|sed -e 's,/[^/]*$,,')"'"'; fi; C=0; while grep EOF$C "$2" >/dev/null; do C=$[C+1]; done ; echo "cat > \"$2\" <<\"EOF$C\"" ; cat $2 ; echo EOF$C ; shift; done) > "$Out" ; chmod +x "$Out" ; }
# Bonus command 3: pdfmerge, on any machine with qpdf installed (to help with double-sided printing)
which qpdf >/dev/null 2>/dev/null && pdfmerge () { if test -e merged.pdf; then rm -i merged.pdf; fi && qpdf --empty --pages $(for i in $@; do echo $i 1-z; done) -- merged.pdf && du -h merged.pdf ; }