]> git.ipfire.org Git - thirdparty/dracut.git/blob - dracut-functions.sh
Merge pull request #314 from danimo/simplify-amd-ucode
[thirdparty/dracut.git] / dracut-functions.sh
1 #!/bin/bash
2 #
3 # functions used by dracut and other tools.
4 #
5 # Copyright 2005-2009 Red Hat, Inc. All rights reserved.
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #
20 export LC_MESSAGES=C
21
22 # is_func <command>
23 # Check whether $1 is a function.
24 is_func() {
25 [[ "$(type -t "$1")" = "function" ]]
26 }
27
28
29 # Generic substring function. If $2 is in $1, return 0.
30 strstr() { [[ $1 = *"$2"* ]]; }
31 # Generic glob matching function. If glob pattern $2 matches anywhere in $1, OK
32 strglobin() { [[ $1 = *$2* ]]; }
33 # Generic glob matching function. If glob pattern $2 matches all of $1, OK
34 strglob() { [[ $1 = $2 ]]; }
35 # returns OK if $1 contains literal string $2 at the beginning, and isn't empty
36 str_starts() { [ "${1#"$2"*}" != "$1" ]; }
37 # returns OK if $1 contains literal string $2 at the end, and isn't empty
38 str_ends() { [ "${1%*"$2"}" != "$1" ]; }
39
40 # find a binary. If we were not passed the full path directly,
41 # search in the usual places to find the binary.
42 find_binary() {
43 if [[ -z ${1##/*} ]]; then
44 if [[ -x $1 ]] || { [[ "$1" == *.so* ]] && ldd "$1" &>/dev/null; }; then
45 printf "%s\n" "$1"
46 return 0
47 fi
48 fi
49
50 type -P "${1##*/}"
51 }
52
53 ldconfig_paths()
54 {
55 ldconfig -pN 2>/dev/null | grep -E -v '/(lib|lib64|usr/lib|usr/lib64)/[^/]*$' | sed -n 's,.* => \(.*\)/.*,\1,p' | sort | uniq
56 }
57
58 # Version comparision function. Assumes Linux style version scheme.
59 # $1 = version a
60 # $2 = comparision op (gt, ge, eq, le, lt, ne)
61 # $3 = version b
62 vercmp() {
63 local _n1=(${1//./ }) _op=$2 _n2=(${3//./ }) _i _res
64
65 for ((_i=0; ; _i++))
66 do
67 if [[ ! ${_n1[_i]}${_n2[_i]} ]]; then _res=0
68 elif ((${_n1[_i]:-0} > ${_n2[_i]:-0})); then _res=1
69 elif ((${_n1[_i]:-0} < ${_n2[_i]:-0})); then _res=2
70 else continue
71 fi
72 break
73 done
74
75 case $_op in
76 gt) ((_res == 1));;
77 ge) ((_res != 2));;
78 eq) ((_res == 0));;
79 le) ((_res != 1));;
80 lt) ((_res == 2));;
81 ne) ((_res != 0));;
82 esac
83 }
84
85 # Create all subdirectories for given path without creating the last element.
86 # $1 = path
87 mksubdirs() {
88 [[ -e ${1%/*} ]] || mkdir -m 0755 -p -- "${1%/*}"
89 }
90
91 # Function prints global variables in format name=value line by line.
92 # $@ = list of global variables' name
93 print_vars() {
94 local _var _value
95
96 for _var in "$@"
97 do
98 eval printf -v _value "%s" \""\$$_var"\"
99 [[ ${_value} ]] && printf '%s="%s"\n' "$_var" "$_value"
100 done
101 }
102
103 # normalize_path <path>
104 # Prints the normalized path, where it removes any duplicated
105 # and trailing slashes.
106 # Example:
107 # $ normalize_path ///test/test//
108 # /test/test
109 normalize_path() {
110 shopt -q -s extglob
111 set -- "${1//+(\/)//}"
112 shopt -q -u extglob
113 printf "%s\n" "${1%/}"
114 }
115
116 # convert_abs_rel <from> <to>
117 # Prints the relative path, when creating a symlink to <to> from <from>.
118 # Example:
119 # $ convert_abs_rel /usr/bin/test /bin/test-2
120 # ../../bin/test-2
121 # $ ln -s $(convert_abs_rel /usr/bin/test /bin/test-2) /usr/bin/test
122 convert_abs_rel() {
123 local __current __absolute __abssize __cursize __newpath
124 local -i __i __level
125
126 set -- "$(normalize_path "$1")" "$(normalize_path "$2")"
127
128 # corner case #1 - self looping link
129 [[ "$1" == "$2" ]] && { printf "%s\n" "${1##*/}"; return; }
130
131 # corner case #2 - own dir link
132 [[ "${1%/*}" == "$2" ]] && { printf ".\n"; return; }
133
134 IFS="/" __current=($1)
135 IFS="/" __absolute=($2)
136
137 __abssize=${#__absolute[@]}
138 __cursize=${#__current[@]}
139
140 while [[ "${__absolute[__level]}" == "${__current[__level]}" ]]
141 do
142 (( __level++ ))
143 if (( __level > __abssize || __level > __cursize ))
144 then
145 break
146 fi
147 done
148
149 for ((__i = __level; __i < __cursize-1; __i++))
150 do
151 if ((__i > __level))
152 then
153 __newpath=$__newpath"/"
154 fi
155 __newpath=$__newpath".."
156 done
157
158 for ((__i = __level; __i < __abssize; __i++))
159 do
160 if [[ -n $__newpath ]]
161 then
162 __newpath=$__newpath"/"
163 fi
164 __newpath=$__newpath${__absolute[__i]}
165 done
166
167 printf "%s\n" "$__newpath"
168 }
169
170
171 # get_fs_env <device>
172 # Get and the ID_FS_TYPE variable from udev for a device.
173 # Example:
174 # $ get_fs_env /dev/sda2
175 # ext4
176 get_fs_env() {
177 local evalstr
178 local found
179
180 [[ $1 ]] || return
181 unset ID_FS_TYPE
182 ID_FS_TYPE=$(blkid -u filesystem -o export -- "$1" \
183 | while read line || [ -n "$line" ]; do
184 if [[ "$line" == TYPE\=* ]]; then
185 printf "%s" "${line#TYPE=}";
186 exit 0;
187 fi
188 done)
189 if [[ $ID_FS_TYPE ]]; then
190 printf "%s" "$ID_FS_TYPE"
191 return 0
192 fi
193 return 1
194 }
195
196 # get_maj_min <device>
197 # Prints the major and minor of a device node.
198 # Example:
199 # $ get_maj_min /dev/sda2
200 # 8:2
201 get_maj_min() {
202 local _maj _min _majmin
203 _majmin="$(stat -L -c '%t:%T' "$1" 2>/dev/null)"
204 printf "%s" "$((0x${_majmin%:*})):$((0x${_majmin#*:}))"
205 }
206
207
208 # get_devpath_block <device>
209 # get the DEVPATH in /sys of a block device
210 get_devpath_block() {
211 local _majmin _i
212 _majmin=$(get_maj_min "$1")
213
214 for _i in /sys/block/*/dev /sys/block/*/*/dev; do
215 [[ -e "$_i" ]] || continue
216 if [[ "$_majmin" == "$(<"$_i")" ]]; then
217 printf "%s" "${_i%/dev}"
218 return 0
219 fi
220 done
221 return 1
222 }
223
224 # get a persistent path from a device
225 get_persistent_dev() {
226 local i _tmp _dev
227
228 _dev=$(get_maj_min "$1")
229 [ -z "$_dev" ] && return
230
231 for i in \
232 /dev/mapper/* \
233 /dev/disk/${persistent_policy:-by-uuid}/* \
234 /dev/disk/by-uuid/* \
235 /dev/disk/by-label/* \
236 /dev/disk/by-partuuid/* \
237 /dev/disk/by-partlabel/* \
238 /dev/disk/by-id/* \
239 /dev/disk/by-path/* \
240 ; do
241 [[ -e "$i" ]] || continue
242 [[ $i == /dev/mapper/control ]] && continue
243 [[ $i == /dev/mapper/mpath* ]] && continue
244 _tmp=$(get_maj_min "$i")
245 if [ "$_tmp" = "$_dev" ]; then
246 printf -- "%s" "$i"
247 return
248 fi
249 done
250 printf -- "%s" "$1"
251 }
252
253 expand_persistent_dev() {
254 local _dev=$1
255
256 case "$_dev" in
257 LABEL=*)
258 _dev="/dev/disk/by-label/${_dev#LABEL=}"
259 ;;
260 UUID=*)
261 _dev="${_dev#UUID=}"
262 _dev="${_dev,,}"
263 _dev="/dev/disk/by-uuid/${_dev}"
264 ;;
265 PARTUUID=*)
266 _dev="${_dev#PARTUUID=}"
267 _dev="${_dev,,}"
268 _dev="/dev/disk/by-partuuid/${_dev}"
269 ;;
270 PARTLABEL=*)
271 _dev="/dev/disk/by-partlabel/${_dev#PARTLABEL=}"
272 ;;
273 esac
274 printf "%s" "$_dev"
275 }
276
277 shorten_persistent_dev() {
278 local _dev="$1"
279 case "$_dev" in
280 /dev/disk/by-uuid/*)
281 printf "%s" "UUID=${_dev##*/}";;
282 /dev/disk/by-label/*)
283 printf "%s" "LABEL=${_dev##*/}";;
284 /dev/disk/by-partuuid/*)
285 printf "%s" "PARTUUID=${_dev##*/}";;
286 /dev/disk/by-partlabel/*)
287 printf "%s" "PARTLABEL=${_dev##*/}";;
288 *)
289 printf "%s" "$_dev";;
290 esac
291 }
292
293 # find_block_device <mountpoint>
294 # Prints the major and minor number of the block device
295 # for a given mountpoint.
296 # Unless $use_fstab is set to "yes" the functions
297 # uses /proc/self/mountinfo as the primary source of the
298 # information and only falls back to /etc/fstab, if the mountpoint
299 # is not found there.
300 # Example:
301 # $ find_block_device /usr
302 # 8:4
303 find_block_device() {
304 local _dev _majmin _find_mpt
305 _find_mpt="$1"
306 if [[ $use_fstab != yes ]]; then
307 [[ -d $_find_mpt/. ]]
308 findmnt -e -v -n -o 'MAJ:MIN,SOURCE' --target "$_find_mpt" | { \
309 while read _majmin _dev || [ -n "$_dev" ]; do
310 if [[ -b $_dev ]]; then
311 if ! [[ $_majmin ]] || [[ $_majmin == 0:* ]]; then
312 _majmin=$(get_maj_min $_dev)
313 fi
314 if [[ $_majmin ]]; then
315 printf "%s\n" "$_majmin"
316 else
317 printf "%s\n" "$_dev"
318 fi
319 return 0
320 fi
321 if [[ $_dev = *:* ]]; then
322 printf "%s\n" "$_dev"
323 return 0
324 fi
325 done; return 1; } && return 0
326 fi
327 # fall back to /etc/fstab
328
329 findmnt -e --fstab -v -n -o 'MAJ:MIN,SOURCE' --target "$_find_mpt" | { \
330 while read _majmin _dev || [ -n "$_dev" ]; do
331 if ! [[ $_dev ]]; then
332 _dev="$_majmin"
333 unset _majmin
334 fi
335 if [[ -b $_dev ]]; then
336 [[ $_majmin ]] || _majmin=$(get_maj_min $_dev)
337 if [[ $_majmin ]]; then
338 printf "%s\n" "$_majmin"
339 else
340 printf "%s\n" "$_dev"
341 fi
342 return 0
343 fi
344 if [[ $_dev = *:* ]]; then
345 printf "%s\n" "$_dev"
346 return 0
347 fi
348 done; return 1; } && return 0
349
350 return 1
351 }
352
353 # find_mp_fstype <mountpoint>
354 # Echo the filesystem type for a given mountpoint.
355 # /proc/self/mountinfo is taken as the primary source of information
356 # and /etc/fstab is used as a fallback.
357 # No newline is appended!
358 # Example:
359 # $ find_mp_fstype /;echo
360 # ext4
361 find_mp_fstype() {
362 local _fs
363
364 if [[ $use_fstab != yes ]]; then
365 findmnt -e -v -n -o 'FSTYPE' --target "$1" | { \
366 while read _fs || [ -n "$_fs" ]; do
367 [[ $_fs ]] || continue
368 [[ $_fs = "autofs" ]] && continue
369 printf "%s" "$_fs"
370 return 0
371 done; return 1; } && return 0
372 fi
373
374 findmnt --fstab -e -v -n -o 'FSTYPE' --target "$1" | { \
375 while read _fs || [ -n "$_fs" ]; do
376 [[ $_fs ]] || continue
377 [[ $_fs = "autofs" ]] && continue
378 printf "%s" "$_fs"
379 return 0
380 done; return 1; } && return 0
381
382 return 1
383 }
384
385 # find_dev_fstype <device>
386 # Echo the filesystem type for a given device.
387 # /proc/self/mountinfo is taken as the primary source of information
388 # and /etc/fstab is used as a fallback.
389 # No newline is appended!
390 # Example:
391 # $ find_dev_fstype /dev/sda2;echo
392 # ext4
393 find_dev_fstype() {
394 local _find_dev _fs
395 _find_dev="$1"
396 if ! [[ "$_find_dev" = /dev* ]]; then
397 [[ -b "/dev/block/$_find_dev" ]] && _find_dev="/dev/block/$_find_dev"
398 fi
399
400 if [[ $use_fstab != yes ]]; then
401 findmnt -e -v -n -o 'FSTYPE' --source "$_find_dev" | { \
402 while read _fs || [ -n "$_fs" ]; do
403 [[ $_fs ]] || continue
404 [[ $_fs = "autofs" ]] && continue
405 printf "%s" "$_fs"
406 return 0
407 done; return 1; } && return 0
408 fi
409
410 findmnt --fstab -e -v -n -o 'FSTYPE' --source "$_find_dev" | { \
411 while read _fs || [ -n "$_fs" ]; do
412 [[ $_fs ]] || continue
413 [[ $_fs = "autofs" ]] && continue
414 printf "%s" "$_fs"
415 return 0
416 done; return 1; } && return 0
417
418 return 1
419 }
420
421 # find_mp_fsopts <mountpoint>
422 # Echo the filesystem options for a given mountpoint.
423 # /proc/self/mountinfo is taken as the primary source of information
424 # and /etc/fstab is used as a fallback.
425 # No newline is appended!
426 # Example:
427 # $ find_mp_fsopts /;echo
428 # rw,relatime,discard,data=ordered
429 find_mp_fsopts() {
430 if [[ $use_fstab != yes ]]; then
431 findmnt -e -v -n -o 'OPTIONS' --target "$1" 2>/dev/null && return 0
432 fi
433
434 findmnt --fstab -e -v -n -o 'OPTIONS' --target "$1"
435 }
436
437 # find_dev_fsopts <device>
438 # Echo the filesystem options for a given device.
439 # /proc/self/mountinfo is taken as the primary source of information
440 # and /etc/fstab is used as a fallback.
441 # Example:
442 # $ find_dev_fsopts /dev/sda2
443 # rw,relatime,discard,data=ordered
444 find_dev_fsopts() {
445 local _find_dev _opts
446 _find_dev="$1"
447 if ! [[ "$_find_dev" = /dev* ]]; then
448 [[ -b "/dev/block/$_find_dev" ]] && _find_dev="/dev/block/$_find_dev"
449 fi
450
451 if [[ $use_fstab != yes ]]; then
452 findmnt -e -v -n -o 'OPTIONS' --source "$_find_dev" 2>/dev/null && return 0
453 fi
454
455 findmnt --fstab -e -v -n -o 'OPTIONS' --source "$_find_dev"
456 }
457
458
459 # finds the major:minor of the block device backing the root filesystem.
460 find_root_block_device() { find_block_device /; }
461
462 # for_each_host_dev_fs <func>
463 # Execute "<func> <dev> <filesystem>" for every "<dev> <fs>" pair found
464 # in ${host_fs_types[@]}
465 for_each_host_dev_fs()
466 {
467 local _func="$1"
468 local _dev
469 local _ret=1
470
471 [[ "${#host_fs_types[@]}" ]] || return 2
472
473
474 for _dev in "${!host_fs_types[@]}"; do
475 $_func "$_dev" "${host_fs_types[$_dev]}" && _ret=0
476 done
477 return $_ret
478 }
479
480 host_fs_all()
481 {
482 printf "%s\n" "${host_fs_types[@]}"
483 }
484
485 # Walk all the slave relationships for a given block device.
486 # Stop when our helper function returns success
487 # $1 = function to call on every found block device
488 # $2 = block device in major:minor format
489 check_block_and_slaves() {
490 local _x
491 [[ -b /dev/block/$2 ]] || return 1 # Not a block device? So sorry.
492 if ! lvm_internal_dev $2; then "$1" $2 && return; fi
493 check_vol_slaves "$@" && return 0
494 if [[ -f /sys/dev/block/$2/../dev ]]; then
495 check_block_and_slaves $1 $(<"/sys/dev/block/$2/../dev") && return 0
496 fi
497 [[ -d /sys/dev/block/$2/slaves ]] || return 1
498 for _x in /sys/dev/block/$2/slaves/*/dev; do
499 [[ -f $_x ]] || continue
500 check_block_and_slaves $1 $(<"$_x") && return 0
501 done
502 return 1
503 }
504
505 check_block_and_slaves_all() {
506 local _x _ret=1
507 [[ -b /dev/block/$2 ]] || return 1 # Not a block device? So sorry.
508 if ! lvm_internal_dev $2 && "$1" $2; then
509 _ret=0
510 fi
511 check_vol_slaves_all "$@" && return 0
512 if [[ -f /sys/dev/block/$2/../dev ]]; then
513 check_block_and_slaves_all $1 $(<"/sys/dev/block/$2/../dev") && _ret=0
514 fi
515 [[ -d /sys/dev/block/$2/slaves ]] || return 1
516 for _x in /sys/dev/block/$2/slaves/*/dev; do
517 [[ -f $_x ]] || continue
518 check_block_and_slaves_all $1 $(<"$_x") && _ret=0
519 done
520 return $_ret
521 }
522 # for_each_host_dev_and_slaves <func>
523 # Execute "<func> <dev>" for every "<dev>" found
524 # in ${host_devs[@]} and their slaves
525 for_each_host_dev_and_slaves_all()
526 {
527 local _func="$1"
528 local _dev
529 local _ret=1
530
531 [[ "${host_devs[@]}" ]] || return 2
532
533 for _dev in "${host_devs[@]}"; do
534 [[ -b "$_dev" ]] || continue
535 if check_block_and_slaves_all $_func $(get_maj_min $_dev); then
536 _ret=0
537 fi
538 done
539 return $_ret
540 }
541
542 for_each_host_dev_and_slaves()
543 {
544 local _func="$1"
545 local _dev
546
547 [[ "${host_devs[@]}" ]] || return 2
548
549 for _dev in "${host_devs[@]}"; do
550 [[ -b "$_dev" ]] || continue
551 check_block_and_slaves $_func $(get_maj_min $_dev) && return 0
552 done
553 return 1
554 }
555
556 # ugly workaround for the lvm design
557 # There is no volume group device,
558 # so, there are no slave devices for volume groups.
559 # Logical volumes only have the slave devices they really live on,
560 # but you cannot create the logical volume without the volume group.
561 # And the volume group might be bigger than the devices the LV needs.
562 check_vol_slaves() {
563 local _lv _vg _pv _dm
564 for i in /dev/mapper/*; do
565 [[ $i == /dev/mapper/control ]] && continue
566 _lv=$(get_maj_min $i)
567 _dm=/sys/dev/block/$_lv/dm
568 [[ -f $_dm/uuid && $(<$_dm/uuid) =~ LVM-* ]] || continue
569 if [[ $_lv = $2 ]]; then
570 _vg=$(lvm lvs --noheadings -o vg_name $i 2>/dev/null)
571 # strip space
572 _vg="${_vg//[[:space:]]/}"
573 if [[ $_vg ]]; then
574 for _pv in $(lvm vgs --noheadings -o pv_name "$_vg" 2>/dev/null)
575 do
576 check_block_and_slaves $1 $(get_maj_min $_pv) && return 0
577 done
578 fi
579 fi
580 done
581 return 1
582 }
583
584 check_vol_slaves_all() {
585 local _lv _vg _pv
586 for i in /dev/mapper/*; do
587 [[ $i == /dev/mapper/control ]] && continue
588 _lv=$(get_maj_min $i)
589 if [[ $_lv = $2 ]]; then
590 _vg=$(lvm lvs --noheadings -o vg_name $i 2>/dev/null)
591 # strip space
592 _vg="${_vg//[[:space:]]/}"
593 if [[ $_vg ]]; then
594 for _pv in $(lvm vgs --noheadings -o pv_name "$_vg" 2>/dev/null)
595 do
596 check_block_and_slaves_all $1 $(get_maj_min $_pv)
597 done
598 return 0
599 fi
600 fi
601 done
602 return 1
603 }
604
605
606
607 # fs_get_option <filesystem options> <search for option>
608 # search for a specific option in a bunch of filesystem options
609 # and return the value
610 fs_get_option() {
611 local _fsopts=$1
612 local _option=$2
613 local OLDIFS="$IFS"
614 IFS=,
615 set -- $_fsopts
616 IFS="$OLDIFS"
617 while [ $# -gt 0 ]; do
618 case $1 in
619 $_option=*)
620 echo ${1#${_option}=}
621 break
622 esac
623 shift
624 done
625 }
626
627 check_kernel_config()
628 {
629 local _config_opt="$1"
630 local _config_file
631 [[ -f /boot/config-$kernel ]] \
632 && _config_file="/boot/config-$kernel"
633 [[ -f /lib/modules/$kernel/config ]] \
634 && _config_file="/lib/modules/$kernel/config"
635
636 # no kernel config file, so return true
637 [[ $_config_file ]] || return 0
638
639 grep -q -F "${_config_opt}=" "$_config_file" && return 0
640 return 1
641 }
642
643
644 # get_cpu_vendor
645 # Only two values are returned: AMD or Intel
646 get_cpu_vendor ()
647 {
648 if grep -qE AMD /proc/cpuinfo; then
649 printf "AMD"
650 fi
651 if grep -qE Intel /proc/cpuinfo; then
652 printf "Intel"
653 fi
654 }
655
656 # get_host_ucode
657 # Get the hosts' ucode file based on the /proc/cpuinfo
658 get_ucode_file ()
659 {
660 local family=`grep -E "cpu family" /proc/cpuinfo | head -1 | sed s/.*:\ //`
661 local model=`grep -E "model" /proc/cpuinfo |grep -v name | head -1 | sed s/.*:\ //`
662 local stepping=`grep -E "stepping" /proc/cpuinfo | head -1 | sed s/.*:\ //`
663
664 if [[ "$(get_cpu_vendor)" == "AMD" ]]; then
665 if [[ $family -ge 21 ]]; then
666 printf "microcode_amd_fam%xh.bin" $family
667 else
668 printf "microcode_amd.bin"
669 fi
670 fi
671 if [[ "$(get_cpu_vendor)" == "Intel" ]]; then
672 # The /proc/cpuinfo are in decimal.
673 printf "%02x-%02x-%02x" ${family} ${model} ${stepping}
674 fi
675 }
676
677 # Not every device in /dev/mapper should be examined.
678 # If it is an LVM device, touch only devices which have /dev/VG/LV symlink.
679 lvm_internal_dev() {
680 local dev_dm_dir=/sys/dev/block/$1/dm
681 [[ ! -f $dev_dm_dir/uuid || $(<$dev_dm_dir/uuid) != LVM-* ]] && return 1 # Not an LVM device
682 local DM_VG_NAME DM_LV_NAME DM_LV_LAYER
683 eval $(dmsetup splitname --nameprefixes --noheadings --rows "$(<$dev_dm_dir/name)" 2>/dev/null)
684 [[ ${DM_VG_NAME} ]] && [[ ${DM_LV_NAME} ]] || return 0 # Better skip this!
685 [[ ${DM_LV_LAYER} ]] || [[ ! -L /dev/${DM_VG_NAME}/${DM_LV_NAME} ]]
686 }
687
688 btrfs_devs() {
689 local _mp="$1"
690 btrfs device usage "$_mp" \
691 | while read _dev _rest; do
692 str_starts "$_dev" "/" || continue
693 _dev=${_dev%,}
694 printf -- "%s\n" "$_dev"
695 done
696 }