Is it possible to make a list of disk in bash? - linux

I'm a beginner and not a native english speaker please excuse my clumsiness.
I'm trying to make a linux install script for personal use (and to learn more about linux and bash scripting) but I'm struggling on finding a way to create a disk selection menu :
I wish to make a list witch would look like that :
NAME SIZE DEVICES
sda 256gib intel-ssdx
sdb 1000gib TLxxxxxxxx
nvme0n1 128gib WDxxxxxxxx
So far i've tried to echo fdisk -l and lsblk in text file and use cat to prompt it
Code :
lsblk
Set DiskLayout=("Automatic Install" "Manual Install" "Check pending change" "Quit")
select DiskLayoutopt in "${DiskLayout[#]}"
do
case $DiskLayoutopt in
"Automatic Install")
read Sdsk -p "Select drive"
;;
"Manual Install")
parted -a optimal
;;
"Check pending change")
echo ""
"Quit")
exit 1
;;
*) echo "invalid option $REPLY";;
esac
done

The following code will get your menu:
#!/usr/bin/env bash
disk=()
size=()
name=()
while IFS= read -r -d $'\0' device; do
device=${device/\/dev\//}
disk+=($device)
name+=("`cat "/sys/class/block/$device/device/model"`")
size+=("`cat "/sys/class/block/$device/size"`")
done < <(find "/dev/" -regex '/dev/sd[a-z]\|/dev/vd[a-z]\|/dev/hd[a-z]' -print0)
for i in `seq 0 $((${#disk[#]}-1))`; do
echo -e "${disk[$i]}\t${name[$i]}\t${size[$i]}"
done
This is some tough bash script... Hope you'll learn quick.
Here's some help:
First line is a shebang to tell your system which interpreter is needed for that script. Indeed, this script only works with bash.
Try running with bash myscript.sh on systems that don't work (ie BSD).
variable=() is an array.
Adding something to that array is done by variable+=("my value")
The while loop reads variable device from what it gets from find command
while read device; do
something
done < <(find)
The find command uses a regular expression that says anything like /dev/sdX where X goes from a to z, or anything like /dev/vdX or anything like /dev/hdX (where X still goes from a to z).
The or operator is a pipe | which has to be escaped with an antislash, hence giving \|.
The devices read by the while look look like '/dev/sda' so we need so strip '/dev/' out of it using the following:
device=${device/\/dev\//}
This is a bash substitution which works the following way:
variable="my foo function"
echo ${variable/foo/bar}
This outputs my bar function.
Indeed, we still need to escape / since this is the separator character for the substition, so it becomes \/.
Getting the disk name via
"`cat "/sys/class/block/$device/device/model"`"
cat "/sys/class/block/sda/device/model" gives the disk model.
In order to get the result into a variable, we'll need to quote it with ` sign, eg:
myvar=`cat /var/file`
Last but not least, the for loop part:
for i in seq 0 $((${#disk[#]}-1)); do
echo -e "${disk[$i]}\t${name[$i]}\t${size[$i]}"
done
${#disk[#]} is the number of elements in array disk.
Actually ${#var} is the number of elements in var, which when being a string, is the number of characters. ${var[#]} means all elements of an array.
seq 0 X returns a sequence of 0 to X numbers, in order to construct the for loop.
Using echo -e translates escaped characters into litterals. In our case '\t' become tabs.
Last but not least, showing ${disk[$i]} is disk array value of index $i where $i is an integer.
Btw, bash is quite limited to do these tasks, but really fun to learn in the first place.
Harder tasks might be better accomplished in a higher level scripting language like Python. Anyway, have fun learning bash, it's a life saver in sysadmin's career.

Related

How to store command arguments which contain double quotes in an array?

I have a Bash script which generates, stores and modifies values in an array. These values are later used as arguments for a command.
For a MCVE I thought of an arbitrary command bash -c 'echo 0="$0" ; echo 1="$1"' which explains my problem. I will call my command with two arguments -option1=withoutspace and -option2="with space". So it would look like this
> bash -c 'echo 0="$0" ; echo 1="$1"' -option1=withoutspace -option2="with space"
if the call to the command would be typed directly into the shell. It prints
0=-option1=withoutspace
1=-option2=with space
In my Bash script, the arguments are part of an array. However
#!/bin/bash
ARGUMENTS=()
ARGUMENTS+=('-option1=withoutspace')
ARGUMENTS+=('-option2="with space"')
bash -c 'echo 0="$0" ; echo 1="$1"' "${ARGUMENTS[#]}"
prints
0=-option1=withoutspace
1=-option2="with space"
which still shows the double quotes (because they are interpreted literally?). What works is
#!/bin/bash
ARGUMENTS=()
ARGUMENTS+=('-option1=withoutspace')
ARGUMENTS+=('-option2=with space')
bash -c 'echo 0="$0" ; echo 1="$1"' "${ARGUMENTS[#]}"
which prints again
0=-option1=withoutspace
1=-option2=with space
What do I have to change to make ARGUMENTS+=('-option2="with space"') work as well as ARGUMENTS+=('-option2=with space')?
(Maybe it's even entirely wrong to store arguments for a command in an array? I'm open for suggestions.)
Get rid of the single quotes. Write the options exactly as you would on the command line.
ARGUMENTS+=(-option1=withoutspace)
ARGUMENTS+=(-option2="with space")
Note that this is exactly equivalent to your second option:
ARGUMENTS+=('-option1=withoutspace')
ARGUMENTS+=('-option2=with space')
-option2="with space" and '-option2=with space' both evaluate to the same string. They're two ways of writing the same thing.
(Maybe it's even entirely wrong to store arguments for a command in an array? I'm open for suggestions.)
It's the exact right thing to do. Arrays are perfect for this. Using a flat string would be a mistake.

"read" command not executing in "while read line" loop [duplicate]

This question already has answers here:
Read user input inside a loop
(6 answers)
Closed 5 years ago.
First post here! I really need help on this one, I looked the issue on google, but can't manage to find an useful answer for me. So here's the problem.
I'm having fun coding some like of a framework in bash. Everyone can create their own module and add it to the framework. BUT. To know what arguments the script require, I created an "args.conf" file that must be in every module, that kinda looks like this:
LHOST;true;The IP the remote payload will connect to.
LPORT;true;The port the remote payload will connect to.
The first column is the argument name, the second defines if it's required or not, the third is the description. Anyway, long story short, the framework is supposed to read the args.conf file line by line to ask the user a value for every argument. Here's the piece of code:
info "Reading module $name argument list..."
while read line; do
echo $line > line.tmp
arg=`cut -d ";" -f 1 line.tmp`
requ=`cut -d ";" -f 2 line.tmp`
if [ $requ = "true" ]; then
echo "[This argument is required]"
else
echo "[This argument isn't required, leave a blank space if you don't wan't to use it]"
fi
read -p " $arg=" answer
echo $answer >> arglist.tmp
done < modules/$name/args.conf
tr '\n' ' ' < arglist.tmp > argline.tmp
argline=`cat argline.tmp`
info "Launching module $name..."
cd modules/$name
$interpreter $file $argline
cd ../..
rm arglist.tmp
rm argline.tmp
rm line.tmp
succes "Module $name execution completed."
As you can see, it's supposed to ask the user a value for every argument... But:
1) The read command seems to not be executing. It just skips it, and the argument has no value
2) Despite the fact that the args.conf file contains 3 lines, the loops seems to be executing just a single time. All I see on the screen is "[This argument is required]" just one time, and the module justs launch (and crashes because it has not the required arguments...).
Really don't know what to do, here... I hope someone here have an answer ^^'.
Thanks in advance!
(and sorry for eventual mistakes, I'm french)
Alpha.
As #that other guy pointed out in a comment, the problem is that all of the read commands in the loop are reading from the args.conf file, not the user. The way I'd handle this is by redirecting the conf file over a different file descriptor than stdin (fd #0); I like to use fd #3 for this:
while read -u3 line; do
...
done 3< modules/$name/args.conf
(Note: if your shell's read command doesn't understand the -u option, use read line <&3 instead.)
There are a number of other things in this script I'd recommend against:
Variable references without double-quotes around them, e.g. echo $line instead of echo "$line", and < modules/$name/args.conf instead of < "modules/$name/args.conf". Unquoted variable references get split into words (if they contain whitespace) and any wildcards that happen to match filenames will get replaced by a list of matching files. This can cause really weird and intermittent bugs. Unfortunately, your use of $argline depends on word splitting to separate multiple arguments; if you're using bash (not a generic POSIX shell) you can use arrays instead; I'll get to that.
You're using relative file paths everywhere, and cding in the script. This tends to be fragile and confusing, since file paths are different at different places in the script, and any relative paths passed in by the user will become invalid the first time the script cds somewhere else. Worse, you aren't checking for errors when you cd, so if any cd fails for any reason, then entire rest of the script will run in the wrong place and fail bizarrely. You'd be far better off figuring out where your system's root directory is (as an absolute path), then referencing everything from it (e.g. < "$module_root/modules/$name/args.conf").
Actually, you're not checking for errors anywhere. It's generally a good idea, when writing any sort of program, to try to think of what can go wrong and how your program should respond (and also to expect that things you didn't think of will also go wrong). Some people like to use set -e to make their scripts exit if any simple command fails, but this doesn't always do what you'd expect. I prefer to explicitly test the exit status of the commands in my script, with something like:
command1 || {
echo 'command1 failed!' >&2
exit 1
}
if command2; then
echo 'command2 succeeded!' >&2
else
echo 'command2 failed!' >&2
exit 1
fi
You're creating temp files in the current directory, which risks random conflicts (with other runs of the script at the same time, any files that happen to have names you're using, etc). It's better to create a temp directory at the beginning, then store everything in it (again, by absolute path):
module_tmp="$(mktemp -dt module-system)" || {
echo "Error creating temp directory" >&2
exit 1
}
...
echo "$answer" >> "$module_tmp/arglist.tmp"
(BTW, note that I'm using $() instead of backticks. They're easier to read, and don't have some subtle syntactic oddities that backticks have. I recommend switching.)
Speaking of which, you're overusing temp files; a lot of what you're doing with can be done just fine with shell variables and built-in shell features. For example, rather than reading line from the config file, then storing them in a temp file and using cut to split them into fields, you can simply echo to cut:
arg="$(echo "$line" | cut -d ";" -f 1)"
...or better yet, use read's built-in ability to split fields based on whatever IFS is set to:
while IFS=";" read -u3 arg requ description; do
(Note that since the assignment to IFS is a prefix to the read command, it only affects that one command; changing IFS globally can have weird effects, and should be avoided whenever possible.)
Similarly, storing the argument list in a file, converting newlines to spaces into another file, then reading that file... you can skip any or all of these steps. If you're using bash, store the arg list in an array:
arglist=()
while ...
arglist+=("$answer") # or ("#arg=$answer")? Not sure of your syntax.
done ...
"$module_root/modules/$name/$interpreter" "$file" "${arglist[#]}"
(That messy syntax, with the double-quotes, curly braces, square brackets, and at-sign, is the generally correct way to expand an array in bash).
If you can't count on bash extensions like arrays, you can at least do it the old messy way with a plain variable:
arglist=""
while ...
arglist="$arglist $answer" # or "$arglist $arg=$answer"? Not sure of your syntax.
done ...
"$module_root/modules/$name/$interpreter" "$file" $arglist
... but this runs the risk of arguments being word-split and/or expanded to lists of files.

bash: How can I assemble the string: `"filename=output_0.csv"`

I am using a bash script to execute a program. The program must take the following argument. (The program is gnuplot.)
gnuplot -e "filename='output_0.csv'" 'plot.p'
I need to be able to assemble the following string: "filename='output_0.csv'"
My plan is to assemble the string STRING=filename='output_0.csv' and then do the following: gnuplot -r "$STRING" 'plot.p'. Note I left the words STRING without stackoverflow syntax style highlighting to emphasise the string I want to produce.
I'm not particularly proficient at bash, and so I have no idea how to do this.
I think that strings can be concatenated by using STRING="$STRING"stuff to append to string? I think that may be required?
As an extra layer of complication the value 0 is actually an integer which should increment by 1 each time the program is run. (Done by a for loop.) If I have n=1 in my program, how can I replace the 0 in the string by the "string value" or text version of the integer n?
A safest way to append something to an existing string would be to include squiggly brackets and quotes:
STRING="something"
STRING="${STRING}else"
You can create the "dynamic" portion of your command line with something like this:
somevalue=0
STRING="filename='output_${somevalue}.csv'"
There are other tools like printf which can handle more complex formatting.
somevalue=1
fmt="filename='output_%s.csv'"
STRING="$(printf "$fmt" "$somevalue")"
Regarding your "extra layer of complication", I gather that this increment has to happen in such a way as to store the value somewhere outside the program, or you'd be able to use a for loop to handle things. You can use temporary files for this:
#!/usr/bin/env bash
# Specify our counter file
counter=/tmp/my_counter
# If it doesn't exist, "prime" it with zero
if [ ! -f "$counter" ]; then
echo "0" > $counter
fi
# And if it STILL doesn't exist, fail.
if [ ! -f "$counter" ]; then
echo "ERROR: can't create counter." >&2
fi
# Read the last value...
read value < "$counter"
# and set up our string, per your question.
STRING="$(printf "filename='output_%d.csv'" "${value}")"
# Last, run your command, and if it succeeds, update the stored counter.
gnuplot -e "$STRING" 'plot.p' && echo "$((value + 1))" > $counter
As always, there's more than one way to solve this problem. With luck, this will give you a head start on your reading of the bash man page and other StackOverflow questions which will help you learn what you need!
An answer was posted, which I thought I had accepted already, but for some reason it has been deleted, possibly because it didn't quite answer the question.
I posted another similar question, and the answer to that helped me also answer this question. You can find said question and answer here: bash: Execute a string as a command

Read filename with * shell bash

I'am new in Linux and I want to write a bash script that can read in a file name of a directory that starts with LED + some numbers.(Ex.: LED5.5.002)
In that directory there is only one file that will starts with LED. The problem is that this file will every time be updated, so the next time it will be for example LED6.5.012 and counting.
I searched and tried a little bit and came to this solution:
export fspec=/home/led/LED*
LedV=`basename $fspec`
echo $LedV
If I give in those commands one by one in my terminal it works fine, LedV= LED5.5.002 but if i run it in a bash scripts it gives the result: LedV = LED*
I search after another solution:
a=/home/led/LED*
LedV=$(basename $a)
echo $LedV
but here again the same, if i give it in one by one it's ok but in a script: LedV = LED*.
It's probably something small but because of my lack of knowledge over Linux I cannot find it. So can someone tell what is wrong?
Thanks! Jan
Shell expansions don't happen on scalar assignments, so in
varname=foo*
the expansion of "$varname" will literally be "foo*". It's more confusing when you consider that echo $varname (or in your case basename $varname; either way without the double quotes) will cause the expansion itself to be treated as a glob, so you may well think the variable contains all those filenames.
Array expansions are another story. You might just want
fspec=( /path/LED* )
echo "${fspec[0]##*/}" # A parameter expansion to strip off the dirname
That will work fine for bash. Since POSIX sh doesn't have arrays like this, I like to give an alternative approach:
for fspec in /path/LED*; do
break
done
echo "${fspec##*/}"
pwd
/usr/local/src
ls -1 /usr/local/src/mysql*
/usr/local/src/mysql-cluster-gpl-7.3.4-linux-glibc2.5-x86_64.tar.gz
/usr/local/src/mysql-dump_test_all_dbs.sql
if you only have 1 file, you will only get 1 result
MyFile=`ls -1 /home/led/LED*`

Understand when to use spaces in bash scripts

I wanted to run a simple bash timer and found this online (user brent7890)
#!/usr/bin/bash
timer=60
until [ "$timer" = 0 ]
do
clear
echo "$timer"
timer=`expr $timer - 1`
sleep 1
done
echo "-------Time to go home--------"
I couldn't copy and paste this code because the server is on another network. I typed it like this (below) and got an error on the line that starts with "until".
#!/usr/bin/bash
timer=60
#Note I forgot the space between [ and "
until ["$timer" = 0 ]
do
clear
echo "$timer"
timer=`expr $timer - 1`
sleep 1
done
echo "-------Time to go home--------"
Where is spacing like this documented? It seems strange that it matters. Bash scripts can be confusing, I want to understand why the space is important.
There are several rules, two basic of that are these:
You must separate all arguments of a command with spaces.
You must separate a command and the argument, that follows after, with a space.
[ here is a command (test).
If you write ["$timer" that means that you start command [60,
and that is, of course, incorrect. The name of the command is [.
The name of the command is always separated from the rest of the command line with a space. (you can have a command with a space in it, but in this case you must write the name of the command in "" or '').

Resources