~sotirisp/bootsh

ref: bd10b5ee69321dd4c936ef0c8ad39a6e820d7c7d bootsh/bootsh -rwxr-xr-x 5.6 KiB
bd10b5eeSotiris Papatheodorou Follow manpage guidelines better 8 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/bin/sh
# SPDX-FileCopyrightText: 2021-2022 Sotiris Papatheodorou
# SPDX-License-Identifier: GPL-3.0-or-later
set -eu

usage() {
	printf 'Usage: %s [OPTION] FILE\n\n' "${0##*/}"
	printf 'Create a bootable device using the disk image FILE. bootsh\n'
       	printf 'will show a menu to select the USB device or memory card\n'
	printf 'where FILE will be written. It will then prompt to confirm\n'
	printf "the selected device's storage capacity to avoid writing to\n"
	printf 'the wrong device.\n\n'
	printf 'bootsh will only show removable devices to prevent writing\n'
	printf 'to internal disks. It will prompt for a superuser password\n'
	printf 'using sudo or doas if required.\n'
	printf '\n  -h  Show the program help and exit.'
	printf '\n  -v  Show the program version and exit.\n'
}

version() {
	printf '%s 1.1.2\n' "${0##*/}"
}

# Usage: check_dependencies
# Check if the dependencies are present and exit with an appropriate error
# message otherwise.
check_dependencies() {
	printf 'lsblk\nsync\n' | while IFS= read -r dep
	do
		if ! command -v "$dep" > /dev/null 2>&1
		then
			printf 'Error: dependency %s not found\n' "$dep"
			exit 1
		fi
	done
}

# Usage: trim_string STRING
# SPDX-FileCopyrightText: 2019 Dylan Araps
# SPDX-License-Identifier: MIT
# https://github.com/dylanaraps/pure-sh-bible#trim-leading-and-trailing-white-space-from-string
trim_string() {
    trim=${1#${1%%[![:space:]]*}}
    trim=${trim%${trim##*[![:space:]]}}
    printf '%s\n' "$trim"
}

# Usage: print_block_devices
# Pretty-print all block devices.
print_block_devices() {
	if command -v udevadm > /dev/null 2>&1
	then
		udevadm settle > /dev/null 2>&1
	fi
	# The following command was copied from bootiso
	# SPDX-FileCopyrightText: 2018-2020 Jules Randolph
	# SPDX-License-Identifier: GPL-3.0-or-later
	lsblk --nodeps --list --output NAME,VENDOR,MODEL,SIZE,TRAN,HOTPLUG
}

# Usage: print_removable_devices
# Pretty-print all removable block devices.
print_removable_devices() {
	devices=$(print_block_devices)
	# Keep the header and all devices that are hotpluggable.
	printf '%s\n' "$devices" | head -n 1 | sed 's/ *HOTPLUG$//'
	printf '%s\n' "$devices" | grep ' *1$' | grep -v 'sata *1$' |  sed 's/ *1$//'
}

# Usage: removable_device_names
# Print the names of all removable devices, one per line.
removable_device_names() {
	print_removable_devices | tail -n +2 | awk '{ print $1 }'
}

# Usage: device_capacity NAME
# Print the capacity of device NAME in human-readable form.
device_capacity() {
	lsblk --nodeps --list --output NAME,SIZE | grep -F "$1" | awk '{ print $2 }'
}

# Usage: device_mount_points NAME
# Print the mounted partitions of device NAME and their respective mount
# points, separated by tabs, one per line.
device_mount_points() {
	mount | grep -F "$1" | sed 's|^\('"$1"'[0-9][0-9]*\) on \(/.*\) type .*$|\1\t\2|'
}

# Usage: valid_input INPUT VALID_INPUTS
# Return 0 when INPUT exactly matches one of the lines in VALID_INPUTS.
valid_input() {
	# Matching INPUT as a fixed pattern can match substrings, e.g. sd
	# matches sda. Matching INPUT as a regular expression can produce weird
	# matches if INPUT is a regular expression. Just do both matches to be
	# more robust.
	printf '%s\n' "$2" | grep -qF "$1" && printf '%s\n' "$2" | grep -q "^$1"'$'
}

# Usage: superuser_command
# Print the command needed to become a superuser. Prints nothing if already
# superuser.
superuser_command() {
	if [ "$(id -u)" != '0' ]
	then
		printf 'sudo\ndoas\n' | while IFS= read -r c
		do
			if command -v "$c" > /dev/null 2>&1
			then
				printf '%s' "$c"
				break
			fi
		done
	fi
}



# Parse command line options.
while getopts 'hv' option
do
	case "$option" in
		h)
			usage
			exit 0
			;;
		v)
			version
			exit 0
			;;
		*)
			usage
			exit 1
			;;
	esac
done
shift "$((OPTIND - 1))"

check_dependencies

# Check for valid input argument.
if [ "$#" -ne 1 ]
then
	usage
	exit 2
fi
if [ ! -s "$1" ] || [ -d "$1" ]
then
	printf 'Error: %s must be a non-empty file\n' "$1"
	exit 1
fi

# Find removable devices.
names=$(removable_device_names)
if [ -z "$names" ]
then
	printf 'Error: no removable devices found\n'
	exit 1
fi
names_pretty=$(print_removable_devices)

# Prompt the user to select the device to write the image to.
target=''
while [ -z "$target" ]
do
	printf '%s\n' "$names_pretty"
	printf 'Enter the NAME of one of the above removable devices or press Ctrl+C to exit:\n'
	read -r device_input
	device_input=$(trim_string "$device_input")
	if valid_input "$device_input" "$names"
	then
		# Ask the user to confirm the device's capacity to avoid
		# writing to the wrong device.
		while true
		do
			capacity=$(device_capacity "$device_input")
			printf '\nSelected removable device %s with capacity %s\n' \
				"$device_input" "$capacity"
			printf 'Enter the device capacity (%s) to continue or press Ctrl+C to exit:\n' \
				"$capacity"
			read -r capacity_input
			capacity_input=$(trim_string "$capacity_input")
			if [ "$capacity_input" = "$capacity" ]
			then
				target=/dev/"$device_input"
				break
			fi
		done
	fi
done

# Unmount any mounted partitions of the target device.
device_mount_points "$target" | while IFS= read -r line
do
	partition=$(printf '%s\n' "$line" | awk 'BEGIN { FS="\t" }; { print $1 }')
	mount_point=$(printf '%s\n' "$line" | awk 'BEGIN { FS="\t" }; { print $2 }')
	sync
	if ! umount "$mount_point" > /dev/null 2>&1
	then
		printf 'Error: could not unmount partition %s mounted on %s\n' \
			"$partition" "$mount_point"
		exit 1
	fi
done

# Write the image to the device, becoming superuser if necessary.
printf '\nWriting image %s to device %s\n' "$1" "$target"
"$(superuser_command)" dd bs=1024k if="$1" of="$target" > /dev/null
sync
printf 'Done! You may remove device %s\n' "$target"