diff options
Diffstat (limited to 'bin')
-rw-r--r-- | bin/executable_Internet.m3u.sh | 40 | ||||
-rwxr-xr-x | bin/executable_copyright.awk | 122 | ||||
-rwxr-xr-x | bin/executable_do_blue.sh | 4 | ||||
-rwxr-xr-x | bin/executable_do_dac.sh | 19 | ||||
-rwxr-xr-x | bin/executable_do_speakers.sh | 13 | ||||
-rwxr-xr-x | bin/executable_icd | 112 | ||||
-rwxr-xr-x | bin/executable_lbdb-fetchaddr-wrapper | 7 | ||||
-rwxr-xr-x | bin/executable_mailx-alias | 13 | ||||
-rwxr-xr-x | bin/executable_mice.sh | 25 | ||||
-rwxr-xr-x | bin/executable_mpd_only.sh | 7 | ||||
-rwxr-xr-x | bin/executable_mpd_others.sh | 10 | ||||
-rwxr-xr-x | bin/executable_mutt-fetchbug | 161 | ||||
-rwxr-xr-x | bin/executable_mutt_bgrun | 115 | ||||
-rwxr-xr-x | bin/executable_mutt_oauth2.py | 419 | ||||
-rwxr-xr-x | bin/executable_ptmp | 50 | ||||
-rwxr-xr-x | bin/executable_startaudio | 3 | ||||
-rwxr-xr-x | bin/executable_tmcg | 29 | ||||
-rwxr-xr-x | bin/executable_wd-bak | 14 | ||||
-rwxr-xr-x | bin/executable_wd-mount | 18 | ||||
-rw-r--r-- | bin/executable_wd-umount | 21 |
20 files changed, 1202 insertions, 0 deletions
diff --git a/bin/executable_Internet.m3u.sh b/bin/executable_Internet.m3u.sh new file mode 100644 index 0000000..b417f07 --- /dev/null +++ b/bin/executable_Internet.m3u.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# +# Updates the internet radio playlists for MPD +# Best run from a cronjob + +BASE=/var/lib/mpd/playlists/ + +playlist() { + cat<<EOF > "${BASE}/radio-$1.m3u" +#EXTM3U +#EXTINF:-1,$2 +$3 +EOF +} + +getstream() { + curl -s "$1" | grep '^File1=' | sed -e 's/File1=//g' +} + +playlist "1920s" "1920s Radio Network" "$(getstream 'http://kara.fast-serv.com:8398/listen.pls')" +# playlist "CFSF-FM" "CFSF-FM 99.3 (Sturgeon Falls)" 'http://listenlive.vistaradio.ca/CFSF' +# playlist "CHYK-FM" "CHYK-FM 104.1 (Timmins)" 'http://rubix.wavestreamer.com:8015/stream/1/' +playlist "CINN-FM" "CINN-FM 99.1 (Hearst)" 'http://stream2.statsradio.com:8050/stream' +# playlist "CJFO-FM" "CJFO-FM 94.5 (Ottawa)" 'http://stream03.ustream.ca:8000/cjfofm128.mp3' +playlist "CKGN-FM" "CKGN-FM 89.7 (Kapuskasing)" 'http://stream03.ustream.ca:80/ckgn128.mp3' +playlist "WYEP" "WYEP 91.3 (Pittsburgh)" 'https://ais-sa3.cdnstream1.com/2557_128.mp3' +playlist "WZUM" "WZUM 88.1 (Pittsburgh)" 'http://pubmusic.streamguys1.com/wzum-aac' +playlist "Dismuke" "Radio Dismuke" "$(getstream 'https://early1900s.org/radiodismuke/radiodismuke.pls')" +playlist "russhit" "99.6 Радио Русский Хит" "$(curl -s http://ruhit.fm/player.htm | grep ruhit_64 | sed -e 's/.*="//g;s/".*//g')" + +# http://colombiacrossover.com/ +playlist "salsa.dura" "Colombia Salsa Dura" "$(getstream 'http://64.37.50.226:8054/listen.pls?sid=1')" + +# http://www.tenientiko.com/ +playlist "salsa.catedral" "La Catedral de la Salsa" "$(getstream 'http://176.31.120.166:4450/listen.pls?sid=1')" + +# http://www.rockolapegassera.com +playlist "cumbia.rockola" "Rockola Pegassera 107.9" "$(getstream 'http://54.39.19.215:8004/listen.pls?sid=1')" + +playlist "salsa.metro" "El Metro Salsero" 'http://s5.voscast.com:7516/stream' diff --git a/bin/executable_copyright.awk b/bin/executable_copyright.awk new file mode 100755 index 0000000..371a9c2 --- /dev/null +++ b/bin/executable_copyright.awk @@ -0,0 +1,122 @@ +#!/usr/bin/gawk -f +# Copyright (C) 2013 Ryan Kavanagh <rak@debian.org> +# Given a series of lines in the format +# Copyright (c) NNNN, MMMM-MMMM, ..., NNNN John Smith <jsmith@example.org> +# group years and emails by person. + +{ + match($0, /.*Copyright.*[0-9][,]? +/); + DATE_LENGTH = RLENGTH; + match($0, /<.*>/); + EMAIL_START = RSTART; + if (RLENGTH != -1) { + NAME = substr($0, DATE_LENGTH + 1, EMAIL_START - DATE_LENGTH - 2); + EMAIL = substr($0, EMAIL_START); + } else { + # No email on this line + NAME = substr($0, DATE_LENGTH + 1); + } + match($0, /.*Copyright +\([cC]\) +/); + DATE_START = RLENGTH + 1; + YEARS = substr($0, DATE_START, DATE_LENGTH - DATE_START); + gsub(/, +/, " ", YEARS); + gsub(/,/, " ", YEARS); + people_years[NAME] = people_years[NAME] " " YEARS; + if (EMAIL_LENGTH != -1) { + email_pattern = "/.*" EMAIL ".*/"; + if (!(NAME in people_emails)) { + people_emails[NAME] = EMAIL; + } else if (!match(people_emails[NAME], EMAIL)) { + people_emails[NAME] = people_emails[NAME] "," EMAIL; + } + } +} END { + for (person in people_years) { + delete years_array; + split(people_years[person], years_array); + # Split any hyphenated years; + for (year in years_array) { + if (years_array[year] ~ /[0-9]+-[0-9]+/) { + delete split_year; + split(years_array[year], split_year, /-/); + years_array[year] = split_year[1]; + if (split_year[1] != split_year[2]) { + # Make sure it isn't some crappy input like 2012-2012 + for (j = 1; j <= split_year[2] - split_year[1]; j++) { + years_array[length(years_array) + 1] = \ + years_array[year] + j; + } + } + } + } + # Sort the years + asort(years_array); + # Delete any duplicates: + for (i = 1; i <= length(years_array); i++) { + if (i > 1 && years_array[i-1] == years_array[i]) { + # Delete years_array[i-1] instead of years_array[i] so that we + # can still check the next year with ease + delete years_array[i-1]; + } + } + # Final sort + asort(years_array); + # Remove duplicates and generate year string + year_string = ""; + # Force AWK to access the years in order + added_hyphen = 0; + for (i = 1; i <= length(years_array); i++) { + if (i > 1) { + if (years_array[i - 1] != years_array[i]) { + # added_hyphen tracks if the last character in the string is + # a hyphen + if ((!added_hyphen) && (years_array[i - 1] == years_array[i] - 1)) { + # year_string isn't terminated by a hyphen, and the year + # at i-1 is one less than the current one + year_string = year_string "-"; + added_hyphen = 1; + } else if (added_hyphen && (years_array[i - 1] != years_array[i] - 1)) { + # The string is terminated by a hyphen, but the current + # year does not immediately follow the preceeding + # one + year_string = year_string years_array[i-1] ", " years_array[i]; + added_hyphen = 0; + } else if (!added_hyphen) { + year_string = year_string ", " years_array[i]; + } + } + } else { + year_string = years_array[i]; + } + } + # We've added a hyphen, but run out of years to check, terminate it + if (added_hyphen) { + year_string = year_string years_array[length(years_array)]; + } + final_line[years_array[length(years_array)]][length(years_array)][person] = \ + "Copyright (C) " year_string "\t" person " " people_emails[person]; + } + # We can't sort the years indices with asorti because we want a numerical, + # not lexicographic sort of the indices. + j = 0; + delete years_sorted; + for (i in final_line) years_sorted[j++] = i+0; + n_years_entries = asort(years_sorted); + # And output the lines with the most recent contributor first + for (y = n_years_entries; y >= 1; y--) { + # Sort the contributors with most recent contribution in year + # by_year[y] by number of years contributed: + j = 0; + delete contributions_sorted; + for (i in final_line[years_sorted[y]]) contributions_sorted[j++] = i+0; + n_contrib_entries = asort(contributions_sorted); + for (c = n_contrib_entries; c >= 1; c--) { + # Finally, sort by contributor name + asorti(final_line[years_sorted[y]][contributions_sorted[c]], by_person); + # And output the lines in alphabetical order by person name + for (n = 1; n <= length(by_person); n++) { + print final_line[years_sorted[y]][contributions_sorted[c]][by_person[n]]; + } + } + } +} diff --git a/bin/executable_do_blue.sh b/bin/executable_do_blue.sh new file mode 100755 index 0000000..b908d14 --- /dev/null +++ b/bin/executable_do_blue.sh @@ -0,0 +1,4 @@ +#!/bin/sh +pacmd list-modules module-bluez5-discover | grep -q module-bluez5-discover || \ + pacmd load-module module-bluez5-discover +echo "connect CD:0D:69:69:9A:1B" | bluetoothctl diff --git a/bin/executable_do_dac.sh b/bin/executable_do_dac.sh new file mode 100755 index 0000000..7ae913e --- /dev/null +++ b/bin/executable_do_dac.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +USB_CARD="alsa_card.usb-FiiO_DigiHug_USB_Audio-01" +USB_SINK="alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo" +SPEAKERS="alsa_output.pci-0000_00_1b.0.analog-stereo" + +pacmd set-sink-mute "${SPEAKERS}" 1 +pacmd set-card-profile "${USB_CARD}" output:iec958-stereo +pacmd set-sink-mute "${USB_SINK}" 0 + +if pacmd list-modules | grep module-ladspa-sink; then + pacmd unload-module module-ladspa-sink +fi +pacmd load-module module-ladspa-sink sink_name=binaural sink_master="${USB_SINK}" plugin=bs2b label=bs2b control=700,4.5 + +for s in $(pacmd list-sink-inputs | awk '$1 == "index:" {print $2}') +do + pacmd move-sink-input $s "${USB_SINK}" >/dev/null 2>&1 +done diff --git a/bin/executable_do_speakers.sh b/bin/executable_do_speakers.sh new file mode 100755 index 0000000..84b82e1 --- /dev/null +++ b/bin/executable_do_speakers.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +USB_CARD="alsa_card.usb-FiiO_DigiHug_USB_Audio-01" +USB_SINK="alsa_output.usb-FiiO_DigiHug_USB_Audio-01.iec958-stereo" +SPEAKERS="alsa_output.pci-0000_00_1b.0.analog-stereo" + +pacmd set-sink-mute "${USB_SINK}" 1 +pacmd set-sink-mute "${SPEAKERS}" 0 + +for s in $(pacmd list-sink-inputs | awk '$1 == "index:" {print $2}') +do + pacmd move-sink-input $s "${SPEAKERS}" >/dev/null 2>&1 +done diff --git a/bin/executable_icd b/bin/executable_icd new file mode 100755 index 0000000..367b87f --- /dev/null +++ b/bin/executable_icd @@ -0,0 +1,112 @@ +#!/bin/sh + +XKB_DIR=${HOME}/.xkb +[ -d ${XKB_DIR}/keymap ] || mkdir -p ${XKB_DIR}/keymap + +GLOBAL_OPTIONS="\ + -I${XKB_DIR} \ + -layout icd,ru \ + -variant icd, \ + -option terminate:ctrl_alt_bksp \ + -option nbsp:level3n \ + -option lalt_meta:lalt_meta \ + -option grp:shifts_toggle" + +LAPTOP_OPTIONS="\ + ${GLOBAL_OPTIONS} \ + -option lv3:ralt_switch_multikey" + +KIN_OPTIONS="\ + ${GLOBAL_OPTIONS} \ + -option lv3:switch \ + -option caps:swapescape" + +SUN_OPTIONS="\ + ${GLOBAL_OPTIONS} \ + -geometry sun(type6unix) \ + -option caps:escape \ + -option lv3:menu_switch \ + -option myswap:switch_lalt_lsuper" + +ERG_OPTIONS="\ + ${GLOBAL_OPTIONS} \ + -option lv3:switch \ + -option caps:escape" + + +case `uname` in + OpenBSD) + LAPTOP_ID=$(xinput | grep "/dev/wskbd" | sed -e 's/.*id=\([0-9]\+\).*/\1/g') + ;; + Linux) + LAPTOP_ID=$(xinput | grep "AT Translated Set 2 keyboard" | sed -e 's/.*id=\([0-9]\+\).*/\1/g') + KIN_USB_ID=$(lsusb | grep -i "Kinesis Advantage Pro" | awk '{ print $6 }') + ERG_USB_ID=$(lsusb | grep -i "feed:1307" | awk '{ print $6 }') + SUN_USB_ID=$(lsusb | grep -i "Sun Microsystems, Inc. Type 6 Keyboard" | awk '{ print $6 }') + ;; + *) + ;; +esac + +echo "Setting up laptop" +setxkbmap ${LAPTOP_OPTIONS} -device ${LAPTOP_ID} -print > ${XKB_DIR}/keymap/icd.laptop +# xkbcomp -I${HOME}/.xkb -i ${LAPTOP_ID} -synch \ +xkbcomp -I${HOME}/.xkb -synch \ + ${HOME}/.xkb/keymap/icd.laptop $DISPLAY # 2> /dev/null + +if [ "x${KIN_USB_ID}" != "x" ]; then + echo "Setting up Kinesis" + KIN_XINPUT_ID=$(xinput | grep ${KIN_USB_ID} | sed -e 's/.*id=\([0-9]\+\).*/\1/g') + for XID in $KIN_XINPUT_ID; do + echo $XID + setxkbmap \ + -I${XKB_DIR} \ + -device ${XID} \ + ${KIN_OPTIONS} \ + -print | sed -e 's@+ctrl(nocaps)@@g;s@bksp)@bksp)+lalt_meta(lalt_meta)@g' > ${HOME}/.xkb/keymap/icd.kin + xkbcomp -I${HOME}/.xkb -i ${XID} -synch \ + ${HOME}/.xkb/keymap/icd.kin ${DISPLAY} # 2> /dev/null + done + xkbcomp -I${HOME}/.xkb -synch \ + ${HOME}/.xkb/keymap/icd.kin ${DISPLAY} # 2> /dev/null + xmodmap -e "remove mod1 = Alt_R" + xmodmap -e "add mod4 = Alt_R" +fi + +if [ "x${SUN_USB_ID}" != "x" ]; then + echo "Setting up Sun Type 6" + SUN_XINPUT_ID=$(xinput | grep ${SUN_USB_ID} | sed -e 's/.*id=\([0-9]\+\).*/\1/g') + echo "ID: ${SUN_XINPUT_ID}" + for XID in $SUN_XINPUT_ID; do + echo $XID + setxkbmap \ + -I${XKB_DIR} \ + -device ${XID} \ + ${SUN_OPTIONS} \ + -print | sed -e '/xkb_keycodes/s/"[[:space:]]/+sunt6fix&/' > ${HOME}/.xkb/keymap/icd.sun + xkbcomp -I${HOME}/.xkb -i ${XID} -synch \ + ${HOME}/.xkb/keymap/icd.sun ${DISPLAY} # 2> /dev/null + done +fi + +echo ${ERG_USB_ID} +if [ "x${ERG_USB_ID}" != "x" ]; then + echo "Setting up ergodox" + ERG_XINPUT_ID=$(xinput | grep "ErgoDox EZ" | grep keyboard | sed -e 's/.*id=\([0-9]\+\).*/\1/g') + echo "ID: ${ERG_XINPUT_ID}" + for XID in $ERG_XINPUT_ID; do + echo $XID + setxkbmap \ + -I${XKB_DIR} \ + -device ${XID} \ + ${ERG_OPTIONS} \ + -print > ${HOME}/.xkb/keymap/icd.erg + # -print | sed -e 's@+group(shifts_toggle)@+ctrl(nocaps)&@g' > ${HOME}/.xkb/keymap/icd.erg + # xkbcomp -I${HOME}/.xkb -i ${XID} -synch \ + xkbcomp -I${HOME}/.xkb -synch \ + ${HOME}/.xkb/keymap/icd.erg ${DISPLAY} # 2> /dev/null + done +fi + + +echo icd > ${HOME}/.xmonad/layout diff --git a/bin/executable_lbdb-fetchaddr-wrapper b/bin/executable_lbdb-fetchaddr-wrapper new file mode 100755 index 0000000..bb1270a --- /dev/null +++ b/bin/executable_lbdb-fetchaddr-wrapper @@ -0,0 +1,7 @@ +#!/bin/sh + +if command -v lbdb-fetchaddr > /dev/null; then + { tee /dev/fd/3 | lbdb-fetchaddr >&2; } 3>&1 +else + cat +fi diff --git a/bin/executable_mailx-alias b/bin/executable_mailx-alias new file mode 100755 index 0000000..c1d961a --- /dev/null +++ b/bin/executable_mailx-alias @@ -0,0 +1,13 @@ +#!/bin/sh + +MUTT_ALIAS=${HOME}/.mutt/alias.rc +MAILX_ALIAS=${HOME}/.mailx-aliases.rc + +awk '{ + NAME=""; + for (i = 3; i < NF; i++) { + NAME=NAME " " $i; + }; + NAME = substr(NAME, 2); + print "alias", $2, "\"" $NF, "(" NAME ")\""; +}' $MUTT_ALIAS > $MAILX_ALIAS diff --git a/bin/executable_mice.sh b/bin/executable_mice.sh new file mode 100755 index 0000000..794f86f --- /dev/null +++ b/bin/executable_mice.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Synaptics: +synclient HorizTwoFingerScroll=0 || true +synclient HorizEdgeScroll=1 || true +# Enable circular scrolling with top edge activating +synclient CircularScrolling=1 || true +synclient CircScrollTrigger=1 || true +# One finger is left click +synclient TapButton1=1 || true +# Two is right click +synclient TapButton2=3 || true +# Three is middle click +synclient TapButton3=2 || true +# Enable coasting +synclient CoastingSpeed=5 +synclient CoastingFriction=30 + +trackball=$(xinput | grep "Kensington Expert Wireless TB" | grep pointer | sed -e 's/.*id=//g;s/\s\+.*//g') +if [ "x${trackball}" != "x" ]; then + xinput set-button-map "${trackball}" 1 2 8 4 5 6 7 3 9 10 11 12 13 14 15 16 + xinput set-prop "${trackball}" "libinput Accel Speed" 0.25 +fi + + diff --git a/bin/executable_mpd_only.sh b/bin/executable_mpd_only.sh new file mode 100755 index 0000000..468c6a5 --- /dev/null +++ b/bin/executable_mpd_only.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +systemctl --user stop pulseaudio.socket +mpc enable "DigiHug USB Audio" +mpc disable "My Pulse Output" diff --git a/bin/executable_mpd_others.sh b/bin/executable_mpd_others.sh new file mode 100755 index 0000000..b27e11e --- /dev/null +++ b/bin/executable_mpd_others.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +systemctl --user start pulseaudio.socket +systemctl --user start pulseaudio.service +mpc disable "DigiHug USB Audio" +mpc enable "My Pulse Output" +pacmd load-module module-udev-detect +${HOME}/bin/do_dac.sh diff --git a/bin/executable_mutt-fetchbug b/bin/executable_mutt-fetchbug new file mode 100755 index 0000000..93ffc58 --- /dev/null +++ b/bin/executable_mutt-fetchbug @@ -0,0 +1,161 @@ +#!/usr/bin/perl -w +# +# mutt-fetchbug, extensively based off of +# mutt-notmuch - notmuch (of a) helper for Mutt +# +# Copyright: © 2011 Stefano Zacchiroli <zack@upsilon.cc> +# License: GNU General Public License (GPL), version 3 or above +# +# Differences between mutt-notmuch and mutt-fetchbug are +# Copyright: © 2012 Ryan Kavanagh <rak@debian.org> +# License: GNU General Public License (GPL), version 3 or above +# +# See the bottom of this file for more documentation. +# A manpage can be obtained by running "pod2man mutt-fetchbug > mutt-fetchbug.1" + +use strict; +use warnings; + +use File::Path; +use Getopt::Long; +use Pod::Usage; + +# search($btsmbox, $query) +# Fetch bugs matching $query with bts; store results in $btsmbox +sub search($$) { + my ($btsmbox, $query) = @_; + + system("bts --cache-mode=mbox cache $query" + . " && ln -fs ~/.cache/devscripts/bts/$query.mbox $btsmbox"); +} + +sub search_action($$@) { + my ($interactive, $btsmbox, @params) = @_; + + if (! $interactive) { + fetch($btsmbox, join(' ', @params)); + } else { + my $query = ""; + my $done = 0; + while (! $done) { + print "bug number ('?' for man): "; + chomp($query = <STDIN>); + if ($query eq "?") { + system("man bts"); + } elsif ($query eq "") { + $done = 1; # quit doing nothing + } else { + search($btsmbox, $query); + $done = 1; + } + } + } +} + +sub die_usage() { + my %podflags = ( "verbose" => 1, + "exitval" => 2 ); + pod2usage(%podflags); +} + +sub main() { + my $btsmbox = "$ENV{HOME}/.cache/mutt_btsresults"; + my $interactive = 0; + my $help_needed = 0; + + my $getopt = GetOptions( + "h|help" => \$help_needed, + "o|output-mbox=s" => \$btsmbox, + "p|prompt" => \$interactive); + if (! $getopt || $#ARGV < 0) { die_usage() }; + my ($action, @params) = ($ARGV[0], @ARGV[1..$#ARGV]); + + if ($help_needed) { + die_usage(); + } elsif ($action eq "search" && $#ARGV == 0 && ! $interactive) { + print STDERR "Error: no search term provided\n\n"; + die_usage(); + } elsif ($action eq "search") { + search_action($interactive, $btsmbox, @params); + } else { + die_usage(); + } +} + +main(); + +__END__ + +=head1 NAME + +mutt-fetchbug - 'bts show' frontend for Mutt + +=head1 SYNOPSIS + +=over + +=item B<mutt-fetchbug> [I<OPTION>]... search [I<SEARCH-TERM>]... + +=back + +=head1 DESCRIPTION + +mutt-fetchbug is a frontend to the 'bts show' command (Debian package: +devscripts) designed to fetch bugs and place them in a predefined mbox. The +search term should typically be a bug number. + +=head1 OPTIONS + +=over 4 + +=item -o DIR + +=item --output-mbox DIR + +Store search results as (symlink) mbox MBOX. Beware: MBOX will be overwritten. +(Default: F<~/.cache/mutt_btsresults/>) + +=item -p + +=item --prompt + +Instead of using command line search terms, prompt the user for them (only for +"search"). + +=item -h + +=item --help + +Show usage information and exit. + +=back + +=head1 INTEGRATION WITH MUTT + +mutt-fetchbug can be used to integrate 'bts show' with the Mutt mail user agent +(unsurprisingly, given the name). To that end, you should define the following +macros in your F<~/.muttrc> (replacing F<~/bin/mutt-fetchbug> for the actual +location of mutt-fetchbug on your system): + + macro index <F7> \ + "<enter-command>unset wait_key<enter><shell-escape>~/bin/mutt-fetchbug --prompt search<enter><change-folder-readonly>~/.cache/mutt_btsresults<enter><enter-command>set wait_key<enter>" \ + "fetch bug(s) (using bts show)" + +The macro (activated by <F7>) will prompt the user for a bug number and then +jump to a temporary mbox showing the fetched bug. + +=head1 SEE ALSO + +mutt(1), bts(1) + +=head1 AUTHOR + +mutt-fetchbug is extensively based off of 'mutt-notmuch', which is +Copyright: (C) 2011 Stefano Zacchiroli <zack@upsilon.cc>. + +All differences between mutt-fetchbug and mutt-notmuch are +Copyright (C) 2012 Ryan Kavanagh <rak@debian.org> + +License: GNU General Public License (GPL), version 3 or higher + +=cut diff --git a/bin/executable_mutt_bgrun b/bin/executable_mutt_bgrun new file mode 100755 index 0000000..f833bab --- /dev/null +++ b/bin/executable_mutt_bgrun @@ -0,0 +1,115 @@ +#!/bin/sh +# @(#) mutt_bgrun $Revision: 1.4 $ + +# mutt_bgrun - run an attachment viewer from mutt in the background +# Copyright (C) 1999-2002 Gary A. Johnson +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# SYNOPSIS +# mutt_bgrun viewer [viewer options] file +# +# DESCRIPTION +# Mutt invokes external attachment viewers by writing the +# attachment to a temporary file, executing the pipeline specified +# for that attachment type in the mailcap file, waiting for the +# pipeline to terminate, writing nulls over the temporary file, +# then deleting it. This causes problems when using graphical +# viewers such as qvpview and acroread to view attachments. +# +# If qvpview, for example, is executed in the foreground, the mutt +# user interface is hung until qvpview exits, so the user can't do +# anything else with mutt until he or she finishes reading the +# attachment and exits qvpview. This is especially annoying when +# a message contains several MS Office attachments--one would like +# to have them all open at once. +# +# If qvpview is executed in the background, it must be given +# enough time to completely read the file before returning control +# to mutt, since mutt will then obliterate the file. Qvpview is +# so slow that this time can exceed 20 seconds, and the bound is +# unknown. So this is again annoying. +# +# The solution provided here is to invoke the specified viewer +# from this script after first copying mutt's temporary file to +# another temporary file. This script can then quickly return +# control to mutt while the viewer can take as much time as it +# needs to read and render the attachment. +# +# EXAMPLE +# To use qvpview to view MS Office attachments from mutt, add the +# following lines to mutt's mailcap file. +# +# application/msword; mutt_bgrun qvpview %s +# application/vnd.ms-excel; mutt_bgrun qvpview %s +# application/vnd.ms-powerpoint; mutt_bgrun qvpview %s +# +# AUTHOR +# Gary A. Johnson +# <garyjohn@spk.agilent.com> +# +# ACKNOWLEDGEMENTS +# My thanks to the people who have commented on this script and +# offered solutions to shortcomings and bugs, especially Edmund +# GRIMLEY EVANS <edmundo@rano.org> and Andreas Somogyi +# <aso@somogyi.nu>. + +prog=${0##*/} + +# Check the arguments first. + +if [ "$#" -lt "2" ] +then + echo "usage: $prog viewer [viewer options] file" >&2 + exit 1 +fi + +# Separate the arguments. Assume the first is the viewer, the last is +# the file, and all in between are options to the viewer. + +viewer="$1" +shift + +while [ "$#" -gt "1" ] +do + options="$options $1" + shift +done + +file=$1 + +# Create a temporary directory for our copy of the temporary file. +# +# This is more secure than creating a temporary file in an existing +# directory. + +tmpdir=/tmp/$LOGNAME$$ +umask 077 +mkdir "$tmpdir" || exit 1 +tmpfile="$tmpdir/${file##*/}" + +# Copy mutt's temporary file to our temporary directory so that we can +# let mutt overwrite and delete it when we exit. + +cp "$file" "$tmpfile" + +# Run the viewer in the background and delete the temporary files when done. + +( + "$viewer" $options "$tmpfile" + sleep 2 + rm -f "$tmpfile" + rmdir "$tmpdir" +) & diff --git a/bin/executable_mutt_oauth2.py b/bin/executable_mutt_oauth2.py new file mode 100755 index 0000000..f67364e --- /dev/null +++ b/bin/executable_mutt_oauth2.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# +# Mutt OAuth2 token management script, version 2020-08-07 +# Written against python 3.7.3, not tried with earlier python versions. +# +# Copyright (C) 2020 Alexander Perlis +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. +'''Mutt OAuth2 token management''' + +import sys +import json +import argparse +import urllib.parse +import urllib.request +import imaplib +import poplib +import smtplib +import base64 +import secrets +import hashlib +import time +from datetime import timedelta, datetime +from pathlib import Path +import socket +import http.server +import subprocess + +# The token file must be encrypted because it contains multi-use bearer tokens +# whose usage does not require additional verification. Specify whichever +# encryption and decryption pipes you prefer. They should read from standard +# input and write to standard output. The example values here invoke GPG, +# although won't work until an appropriate identity appears in the first line. +ENCRYPTION_PIPE = ['cat'] +DECRYPTION_PIPE = ['cat'] + +registrations = { + 'google': { + 'authorize_endpoint': 'https://accounts.google.com/o/oauth2/auth', + 'devicecode_endpoint': 'https://oauth2.googleapis.com/device/code', + 'token_endpoint': 'https://accounts.google.com/o/oauth2/token', + 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'imap_endpoint': 'imap.gmail.com', + 'pop_endpoint': 'pop.gmail.com', + 'smtp_endpoint': 'smtp.gmail.com', + 'sasl_method': 'OAUTHBEARER', + 'scope': 'https://mail.google.com/', + 'client_id': '', + 'client_secret': '', + }, + 'microsoft': { + 'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + 'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode', + 'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + 'redirect_uri': 'https://login.microsoftonline.com/common/oauth2/nativeclient', + 'tenant': 'common', + 'imap_endpoint': 'outlook.office365.com', + 'pop_endpoint': 'outlook.office365.com', + 'smtp_endpoint': 'smtp.office365.com', + 'sasl_method': 'XOAUTH2', + 'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All ' + 'https://outlook.office.com/POP.AccessAsUser.All ' + 'https://outlook.office.com/SMTP.Send'), + 'client_id': '08162f7c-0fd2-4200-a84a-f25a4db0b584', + 'client_secret': 'TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82', + }, +} + +ap = argparse.ArgumentParser(epilog=''' +This script obtains and prints a valid OAuth2 access token. State is maintained in an +encrypted TOKENFILE. Run with "--verbose --authorize" to get started or whenever all +tokens have expired, optionally with "--authflow" to override the default authorization +flow. To truly start over from scratch, first delete TOKENFILE. Use "--verbose --test" +to test the IMAP/POP/SMTP endpoints. +''') +ap.add_argument('-v', '--verbose', action='store_true', help='increase verbosity') +ap.add_argument('-d', '--debug', action='store_true', help='enable debug output') +ap.add_argument('tokenfile', help='persistent token storage') +ap.add_argument('-a', '--authorize', action='store_true', help='manually authorize new tokens') +ap.add_argument('--authflow', help='authcode | localhostauthcode | devicecode') +ap.add_argument('-t', '--test', action='store_true', help='test IMAP/POP/SMTP endpoints') +args = ap.parse_args() + +token = {} +path = Path(args.tokenfile) +if path.exists(): + if 0o777 & path.stat().st_mode != 0o600: + sys.exit('Token file has unsafe mode. Suggest deleting and starting over.') + try: + sub = subprocess.run(DECRYPTION_PIPE, check=True, input=path.read_bytes(), + capture_output=True) + token = json.loads(sub.stdout) + except subprocess.CalledProcessError: + sys.exit('Difficulty decrypting token file. Is your decryption agent primed for ' + 'non-interactive usage, or an appropriate environment variable such as ' + 'GPG_TTY set to allow interactive agent usage from inside a pipe?') + + +def writetokenfile(): + '''Writes global token dictionary into token file.''' + if not path.exists(): + path.touch(mode=0o600) + if 0o777 & path.stat().st_mode != 0o600: + sys.exit('Token file has unsafe mode. Suggest deleting and starting over.') + sub2 = subprocess.run(ENCRYPTION_PIPE, check=True, input=json.dumps(token).encode(), + capture_output=True) + path.write_bytes(sub2.stdout) + + +if args.debug: + print('Obtained from token file:', json.dumps(token)) +if not token: + if not args.authorize: + sys.exit('You must run script with "--authorize" at least once.') + print('Available app and endpoint registrations:', *registrations) + token['registration'] = input('OAuth2 registration: ') + token['authflow'] = input('Preferred OAuth2 flow ("authcode" or "localhostauthcode" ' + 'or "devicecode"): ') + token['email'] = input('Account e-mail address: ') + token['access_token'] = '' + token['access_token_expiration'] = '' + token['refresh_token'] = '' + writetokenfile() + +if token['registration'] not in registrations: + sys.exit(f'ERROR: Unknown registration "{token["registration"]}". Delete token file ' + f'and start over.') +registration = registrations[token['registration']] + +authflow = token['authflow'] +if args.authflow: + authflow = args.authflow + +baseparams = {'client_id': registration['client_id']} +# Microsoft uses 'tenant' but Google does not +if 'tenant' in registration: + baseparams['tenant'] = registration['tenant'] + + +def access_token_valid(): + '''Returns True when stored access token exists and is still valid at this time.''' + token_exp = token['access_token_expiration'] + return token_exp and datetime.now() < datetime.fromisoformat(token_exp) + + +def update_tokens(r): + '''Takes a response dictionary, extracts tokens out of it, and updates token file.''' + token['access_token'] = r['access_token'] + token['access_token_expiration'] = (datetime.now() + + timedelta(seconds=int(r['expires_in']))).isoformat() + if 'refresh_token' in r: + token['refresh_token'] = r['refresh_token'] + writetokenfile() + if args.verbose: + print(f'NOTICE: Obtained new access token, expires {token["access_token_expiration"]}.') + + +if args.authorize: + p = baseparams.copy() + p['scope'] = registration['scope'] + + if authflow in ('authcode', 'localhostauthcode'): + verifier = secrets.token_urlsafe(90) + challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest())[:-1] + redirect_uri = registration['redirect_uri'] + listen_port = 0 + if authflow == 'localhostauthcode': + # Find an available port to listen on + s = socket.socket() + s.bind(('127.0.0.1', 0)) + listen_port = s.getsockname()[1] + s.close() + redirect_uri = 'http://localhost:'+str(listen_port)+'/' + # Probably should edit the port number into the actual redirect URL. + + p.update({'login_hint': token['email'], + 'response_type': 'code', + 'redirect_uri': redirect_uri, + 'code_challenge': challenge, + 'code_challenge_method': 'S256'}) + print(registration["authorize_endpoint"] + '?' + + urllib.parse.urlencode(p, quote_via=urllib.parse.quote)) + + authcode = '' + if authflow == 'authcode': + authcode = input('Visit displayed URL to retrieve authorization code. Enter ' + 'code from server (might be in browser address bar): ') + else: + print('Visit displayed URL to authorize this application. Waiting...', + end='', flush=True) + + class MyHandler(http.server.BaseHTTPRequestHandler): + '''Handles the browser query resulting from redirect to redirect_uri.''' + + # pylint: disable=C0103 + def do_HEAD(self): + '''Response to a HEAD requests.''' + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + + def do_GET(self): + '''For GET request, extract code parameter from URL.''' + # pylint: disable=W0603 + global authcode + querystring = urllib.parse.urlparse(self.path).query + querydict = urllib.parse.parse_qs(querystring) + if 'code' in querydict: + authcode = querydict['code'][0] + self.do_HEAD() + self.wfile.write(b'<html><head><title>Authorizaton result</title></head>') + self.wfile.write(b'<body><p>Authorization redirect completed. You may ' + b'close this window.</p></body></html>') + with http.server.HTTPServer(('127.0.0.1', listen_port), MyHandler) as httpd: + try: + httpd.handle_request() + except KeyboardInterrupt: + pass + + if not authcode: + sys.exit('Did not obtain an authcode.') + + for k in 'response_type', 'login_hint', 'code_challenge', 'code_challenge_method': + del p[k] + p.update({'grant_type': 'authorization_code', + 'code': authcode, + 'client_secret': registration['client_secret'], + 'code_verifier': verifier}) + try: + response = urllib.request.urlopen(registration['token_endpoint'], + urllib.parse.urlencode(p).encode()) + except urllib.error.HTTPError as err: + print(err.code, err.reason) + response = err + response = response.read() + if args.debug: + print(response) + response = json.loads(response) + if 'error' in response: + print(response['error']) + if 'error_description' in response: + print(response['error_description']) + sys.exit(1) + + elif authflow == 'devicecode': + try: + response = urllib.request.urlopen(registration['devicecode_endpoint'], + urllib.parse.urlencode(p).encode()) + except urllib.error.HTTPError as err: + print(err.code, err.reason) + response = err + response = response.read() + if args.debug: + print(response) + response = json.loads(response) + if 'error' in response: + print(response['error']) + if 'error_description' in response: + print(response['error_description']) + sys.exit(1) + print(response['message']) + del p['scope'] + p.update({'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', + 'client_secret': registration['client_secret'], + 'device_code': response['device_code']}) + interval = int(response['interval']) + print('Polling...', end='', flush=True) + while True: + time.sleep(interval) + print('.', end='', flush=True) + try: + response = urllib.request.urlopen(registration['token_endpoint'], + urllib.parse.urlencode(p).encode()) + except urllib.error.HTTPError as err: + # Not actually always an error, might just mean "keep trying..." + response = err + response = response.read() + if args.debug: + print(response) + response = json.loads(response) + if 'error' not in response: + break + if response['error'] == 'authorization_declined': + print(' user declined authorization.') + sys.exit(1) + if response['error'] == 'expired_token': + print(' too much time has elapsed.') + sys.exit(1) + if response['error'] != 'authorization_pending': + print(response['error']) + if 'error_description' in response: + print(response['error_description']) + sys.exit(1) + print() + + else: + sys.exit(f'ERROR: Unknown OAuth2 flow "{token["authflow"]}. Delete token file and ' + f'start over.') + + update_tokens(response) + + +if not access_token_valid(): + if args.verbose: + print('NOTICE: Invalid or expired access token; using refresh token ' + 'to obtain new access token.') + if not token['refresh_token']: + sys.exit('ERROR: No refresh token. Run script with "--authorize".') + p = baseparams.copy() + p.update({'client_secret': registration['client_secret'], + 'refresh_token': token['refresh_token'], + 'grant_type': 'refresh_token'}) + try: + response = urllib.request.urlopen(registration['token_endpoint'], + urllib.parse.urlencode(p).encode()) + except urllib.error.HTTPError as err: + print(err.code, err.reason) + response = err + response = response.read() + if args.debug: + print(response) + response = json.loads(response) + if 'error' in response: + print(response['error']) + if 'error_description' in response: + print(response['error_description']) + print('Perhaps refresh token invalid. Try running once with "--authorize"') + sys.exit(1) + update_tokens(response) + + +if not access_token_valid(): + sys.exit('ERROR: No valid access token. This should not be able to happen.') + + +if args.verbose: + print('Access Token: ', end='') +print(token['access_token']) + + +def build_sasl_string(user, host, port, bearer_token): + '''Build appropriate SASL string, which depends on cloud server's supported SASL method.''' + if registration['sasl_method'] == 'OAUTHBEARER': + return f'n,a={user},\1host={host}\1port={port}\1auth=Bearer {bearer_token}\1\1' + if registration['sasl_method'] == 'XOAUTH2': + return f'user={user}\1auth=Bearer {bearer_token}\1\1' + sys.exit(f'Unknown SASL method {registration["sasl_method"]}.') + + +if args.test: + errors = False + + imap_conn = imaplib.IMAP4_SSL(registration['imap_endpoint']) + sasl_string = build_sasl_string(token['email'], registration['imap_endpoint'], 993, + token['access_token']) + if args.debug: + imap_conn.debug = 4 + try: + imap_conn.authenticate(registration['sasl_method'], lambda _: sasl_string.encode()) + # Microsoft has a bug wherein a mismatch between username and token can still report a + # successful login... (Try a consumer login with the token from a work/school account.) + # Fortunately subsequent commands fail with an error. Thus we follow AUTH with another + # IMAP command before reporting success. + imap_conn.list() + if args.verbose: + print('IMAP authentication succeeded') + except imaplib.IMAP4.error as e: + print('IMAP authentication FAILED (does your account allow IMAP?):', e) + errors = True + + pop_conn = poplib.POP3_SSL(registration['pop_endpoint']) + sasl_string = build_sasl_string(token['email'], registration['pop_endpoint'], 995, + token['access_token']) + if args.debug: + pop_conn.set_debuglevel(2) + try: + # poplib doesn't have an auth command taking an authenticator object + # Microsoft requires a two-line SASL for POP + # pylint: disable=W0212 + pop_conn._shortcmd('AUTH ' + registration['sasl_method']) + pop_conn._shortcmd(base64.standard_b64encode(sasl_string.encode()).decode()) + if args.verbose: + print('POP authentication succeeded') + except poplib.error_proto as e: + print('POP authentication FAILED (does your account allow POP?):', e.args[0].decode()) + errors = True + + # SMTP_SSL would be simpler but Microsoft does not answer on port 465. + smtp_conn = smtplib.SMTP(registration['smtp_endpoint'], 587) + sasl_string = build_sasl_string(token['email'], registration['smtp_endpoint'], 587, + token['access_token']) + smtp_conn.ehlo('test') + smtp_conn.starttls() + smtp_conn.ehlo('test') + if args.debug: + smtp_conn.set_debuglevel(2) + try: + smtp_conn.auth(registration['sasl_method'], lambda _=None: sasl_string) + if args.verbose: + print('SMTP authentication succeeded') + except smtplib.SMTPAuthenticationError as e: + print('SMTP authentication FAILED:', e) + errors = True + + if errors: + sys.exit(1) diff --git a/bin/executable_ptmp b/bin/executable_ptmp new file mode 100755 index 0000000..e42c695 --- /dev/null +++ b/bin/executable_ptmp @@ -0,0 +1,50 @@ +#!/bin/sh +# Copyright 2020 Ryan Kavanagh <rak@rak.ac> +# Upload as temporary file +# +# Permission to use, copy, modify, and/or distribute this software for +# any purpose with or without fee is hereby granted, provided that the +# above copyright notice and this permission notice appear in all +# copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +DEST_DIR=hades.rak.ac:public_tmp +OPT_SUFFIX="" + +while getopts s: f +do + case $f in + s) + OPT_SUFFIX=".${OPTARG}";; + esac +done +shift `expr $OPTIND - 1` + +FILE="$@" + +if command -v md5sum >/dev/null; then + MD5=md5sum; +elif command -v md5 >/dev/null; then + MD5="md5 -r"; +else + echo "No md5 found"; +fi + +PAD=$(${MD5} "${FILE}" | cut -f1 -d' ') +BASE=$(basename "${FILE}") + +DEST_NAME="${PAD}.${BASE}${OPT_SUFFIX}" + +echo "${DEST_NAME}" + +scp "${FILE}" "${DEST_DIR}/${DEST_NAME}" + +echo "https://rak.ac/~tmp/${DEST_NAME}" diff --git a/bin/executable_startaudio b/bin/executable_startaudio new file mode 100755 index 0000000..3fbfa7f --- /dev/null +++ b/bin/executable_startaudio @@ -0,0 +1,3 @@ +#!/bin/sh +start-pulseaudio-x11 +${HOME}/bin/mpd_others.sh diff --git a/bin/executable_tmcg b/bin/executable_tmcg new file mode 100755 index 0000000..5ebb3cf --- /dev/null +++ b/bin/executable_tmcg @@ -0,0 +1,29 @@ +#!/bin/sh + +current_session () { + tty=$(tty) + for s in $(tmux list-sessions -F '#{session_name}'); do + tmux list-panes -F '#{pane_tty} #{session_name}' -t "$s" + done | grep "${tty}" | awk '{print $2}' +} + +SESSION=$(current_session) +NEW=1 + +if [ "x${SESSION}" = "x" ]; then + SESSION="irc" + tmux new-session -s "${SESSION}" -d + NEW=$? +fi + +if ! tmux switch -t ${SESSION}:catgirl >/dev/null 2>&1; then + tmux new-window -c '~' -n catgirl catgirl libera + tmux split-window -t ${SESSION}:catgirl -c '~' catgirl oftc + tmux split-window -t ${SESSION}:catgirl -c '~' catgirl sdf + tmux split-window -t ${SESSION}:catgirl -c '~' catgirl tilde + tmux set-option -t ${SESSION}:catgirl remain-on-exit on + tmux selectl -t ${SESSION}:catgirl tiled + if test ${NEW} -eq 0; then + tmux kill-window -t ${SESSION}:0 + fi +fi diff --git a/bin/executable_wd-bak b/bin/executable_wd-bak new file mode 100755 index 0000000..f824fba --- /dev/null +++ b/bin/executable_wd-bak @@ -0,0 +1,14 @@ +#!/bin/sh + +wd-mount + +if ! mount | grep /media/wd-bak > /dev/null ; then + echo "/media/wd-bak not mounted" + exit 1 +fi + +rsync -avhHP --delete-after /media/t/ /media/wd-bak + +sudo umount /media/wd-bak +sudo cryptdisks_stop wd-bak +sudo cryptdisks_stop wd-bak-work diff --git a/bin/executable_wd-mount b/bin/executable_wd-mount new file mode 100755 index 0000000..49466c2 --- /dev/null +++ b/bin/executable_wd-mount @@ -0,0 +1,18 @@ +#!/bin/sh + +case `uname` in + OpenBSD) + doas bioctl -c C -l fe3fd8d53b06049b.a softraid0 + doas mount /media/wd + ;; + Linux) + if ! zpool list wd-pass > /dev/null 2>&1 ; then + sudo zpool import -l wd-pass + fi + ;; + *) + echo "Unknown host" + exit 1 +esac + +# vim: set sw=4: diff --git a/bin/executable_wd-umount b/bin/executable_wd-umount new file mode 100644 index 0000000..51a11e1 --- /dev/null +++ b/bin/executable_wd-umount @@ -0,0 +1,21 @@ +#!/bin/sh + +case `uname` in + OpenBSD) + doas umount /mnt/wd + doas bioctl -d 79665ba14a5187ba + ;; + Linux) + if zpool list wd-pass > /dev/null 2>&1; then + if mount -l | grep -E '^/var/lib/mpd/music '; then + sudo umount --lazy -f /var/lib/mpd/music + fi + mount -l -t zfs | IFS=' on ' awk '{ print $1 }' | sudo xargs -I '{}' zfs umount -u '{}' + sudo zpool export wd-pass + fi + ;; + *) + echo "Unknown host" + exit 1 + ;; +esac |