How to get unique `uid`? - linux

I'm making a bash script which should create an ftp user.
ftpasswd --passwd --file=/usr/local/etc/ftpd/passwd --name=$USER --uid=[xxx]
--home=/media/part1/ftp/users/$USER --shell=/bin/false
The only supplied argument to script is user name. But ftpasswd also requires uid. How do I get this number? Is there an easy way to scan passwd file and get the max number, increment it and use it? Maybe it's possible to obtain that number from the system?

Instead of reading /etc/passwd, you can also do it in a more nsswitch-friendly way:
getent passwd
Also don't forget that there isn't any guarantee that this sequence of UIDs will be already sorted.

To get UID given an user name "myuser":
cat /etc/passwd | grep myuser | cut -d":" -f3
To get the greatest UID in passwd file:
cat /etc/passwd | cut -d":" -f3 | sort -n | tail -1

To get a user's UID:
cat /etc/passwd | grep "^$usernamevariable:" | cut -d":" -f3
To add a new user to the system the best option is to use useradd, or adduser if you need a fine-grained control.
If you really need just to find the smallest free UID, here's a script that finds the smallest free UID value greater than 999 (UIDs 1-999 are usually reserved to system users):
#!/bin/bash
# return 1 if the Uid is already used, else 0
function usedUid()
{
if [ -z "$1" ]
then
return
fi
for i in ${lines[#]} ; do
if [ $i == $1 ]
then
return 1
fi
done
return 0
}
i=0
# load all the UIDs from /etc/passwd
lines=( $( cat /etc/passwd | cut -d: -f3 | sort -n ) )
testuid=999
x=1
# search for a free uid greater than 999 (default behaviour of adduser)
while [ $x -eq 1 ] ; do
testuid=$(( $testuid + 1))
usedUid $testuid
x=$?
done
# print the just found free uid
echo $testuid

I changed cat /etc/passwd to getent passwd for Giuseppe's answer.
#!/bin/bash
# From Stack Over Flow
# http://stackoverflow.com/questions/3649760/how-to-get-unique-uid
# return 1 if the Uid is already used, else 0
function usedUid()
{
if [ -z "$1" ]
then
return
fi
for i in ${lines[#]} ; do
if [ $i == $1 ]
then
return 1
fi
done
return 0
}
i=0
# load all the UIDs from /etc/passwd
lines=( $( getent passwd | cut -d: -f3 | sort -n ) )
testuid=999
x=1
# search for a free uid greater than 999 (default behaviour of adduser)
while [ $x -eq 1 ] ; do
testuid=$(( $testuid + 1))
usedUid $testuid
x=$?
done
# print the just found free uid
echo $testuid

This is a much shorter approach:
#!/bin/bash
uids=$( cat /etc/passwd | cut -d: -f3 | sort -n )
uid=999
while true; do
if ! echo $uids | grep -F -q -w "$uid"; then
break;
fi
uid=$(( $uid + 1))
done
echo $uid

since this is bash things could get simpler
#!/bin/bash
get_available_uid_basic(){
local uid_free=1000
local uids_in_use=( $(cut -d: -f3 < /etc/passwd) )
while [[ " ${uids_in_use[#]} " == *" $uid_free "* ]]; do
(( uid_free++ ))
done
echo $uid_free
}
uid=$(get_available_uid_basic)
echo $uid
Explanation:
uids_in_use is an array to get rid of new-line characters
there is no "| sort" and it's useless in other answers too
${uids_in_use[#]} is uids_in_use array exploded with spaces as separators
there are spaces before and after first and last array entry, so each entry is separated by spaces on each side
bash's [[ ]] accepts glob character after '=='
System/User uid and first/last available
useradd/adduser has --system argument, this creates user with uid between 100-999
also, useradd does look for available uid starting at 999 going downwards.
In my case I needed both of these behaviors, this is a function which accepts "system" and "reverse" arguments
#!/bin/bash
get_available_uid(){
local system_range
[[ $* == *system* ]] && system_range=TRUE
local reverse
[[ $* == *reverse* ]] && reverse=TRUE
local step
local uid_free
if [ -n "$system_range" ]; then
if [ -n "$reverse" ]; then
uid_free=999
step=-1
else
uid_free=100
step=1
fi
else
if [ -n "$reverse" ]; then
uid_free=9999
step=-1
else
uid_free=1000
step=1
fi
fi
local uids_in_use=( $(cut -d: -f3 < /etc/passwd) )
while [[ " ${uids_in_use[#]} " == *" $uid_free "* ]]; do
(( uid_free+=step ))
done
if [ -n "$system_range" ]; then
if (( uid_free < 100 )) || (( uid_free > 999 )); then
echo "No more available uids in range" >&2
return 1
fi
else
if (( uid_free < 1000 )); then
echo "No more available uids in range" >&2
return 1
fi
fi
echo $uid_free
return 0
}
uid=$(get_available_uid)
echo "first available user uid: $uid"
uid=$(get_available_uid system)
echo "first available system uid: $uid"
uid=$(get_available_uid reverse)
echo "last available user uid: $uid"
uid=$(get_available_uid system reverse)
echo "last available system uid: $uid"

Related

Bash Scripting checking for home directories

I'm trying to create a script to check if user accounts have valid home directories.
This is what i got at the moment:
#!/bin/bash
cat /etc/passwd | awk -F: '{print $1 " " $3 " " $6 }' | while read user userid directory; do
if [ $userid -ge 1000 ] && [ ! -d "$directory ]; then
echo ${user}
fi
done
This works. I get the expected output which is the username of the account with an invalid home directory.
eg. output
student1
student2
However, I am unable to make it so that ONLY if there is no issues with the valid home directories and all of them are valid, echo "All home directories are valid".
Didn't run it, but it should be something like:
#!/bin/bash
users=()
cat /etc/passwd | awk -F: '{print $1 " " $3 " " $6 }' | while read user userid directory; do
if [ $userid -ge 1000 ] && [ ! -d "$directory" ]; then
users=+("${user}")
fi
done
if test -n ${#users[#]} == 0; then
echo "All home directories are valid"
else
for (( i=0; i<${#users[#]}; i++ )); do echo "${users[$i]}" ; done
fi
You could set a flag, and unset it if you see an invalid directory. Or you could simply check whether your loop printed anything.
You have a number of common antipatterns which you'll want to avoid, too.
# Avoid useless use of cat
# If you are using Awk anyway,
# use it for user id comparison, too
awk -F: '$3 >= 1000 {print $1, $6 }' /etc/passwd |
# Basically always use read -r
while read -r user directory; do
# Fix missing close quote
if [ ! -d "$directory" ]; then
# Quote user
echo "$user"
fi
done |
# If no output, print default message
grep '^' >&2 || echo "No invalid directories" >&2
A proper tool prints its diagnostic output to standard error, not standard output, so I added >&2 to the end.

Verify account creation from text file in bash script

I am trying to output which accounts have been successfully created from a text file and which haven't. I would also like to output the number of successfully created accounts. I currently the get the following error: grep: 3: No such file or directory. The script and text file and saved in the same folder. I have use the following commands in my script.
file=users.txt
verify =grep "verify" $file |cut -f2 -d:`
cat /etc/passwd | grep $verify
echo -e "\nYou have Currently"
cat /etc/passwd | grep $verify |wc -l;
echo "users added from your Text File"
Edit:
#!/bin/bash
ROOT_UID=0 #The root user has a UID of 0
if [ "$UID" -ne "$ROOT_UID" ]; then
echo "**** You must be the root user to run this script!****"
exit
fi
clear
echo
echo "######################################################"
echo "##### Batch script to automate creation of users #####"
echo -e "######################################################\n"
while true;
do
file=notvalid
while [ $file == "notvalid" ]
do
#echo "repeat $repeat"
#echo -e "\n"
echo -n "Please enter import filename:"
read filename
echo -e "\r"
exists=0
if [ -e $filename ]; then
file=valid
while IFS=":" read firstname lastname userid password group
do
egrep -i "^$userid:" /etc/passwd &>/dev/null
if [ $? -eq 0 ]; then
exists=$((exists+1))
#echo -e "${firstname} ${lastname} already exists on the system"
#grep ${userid} /etc/passwd
aname=$( getent passwd "$userid" | cut -d: -f3)
echo "Account Exists: $aname"
euserid=$( getent passwd "$userid" | cut -d: -f1)
echo "User ID: $userid"
homedir=$( getent passwd "$userid" | cut -d: -f6)
echo "Home Directory: $homedir"
usershell=$( getent passwd "$userid" | cut -d: -f7)
echo "User Shell: $usershell"
g=$( id -Gn "$userid")
echo "Groups: $g"
echo -e "\r"
else
egrep -i "^$group:" /etc/group &>/dev/null
if [ $? -eq 1 ]; then
/usr/sbin/addgroup ${group} &>/dev/null
fi
useradd -d /home/"${userid}" -m -s /bin/bash -c \
"${firstname}${lastname}" -g "${group}" "${userid}"
echo "Creating Account: ${firstname} ${lastname}"
nuserid=$( getent passwd "$userid" | cut -d: -f1)
echo "Creating User ID: ${nuserid}"
{ echo ${password}; echo ${password}; } | sudo passwd ${userid} > /dev/null 2>&1
echo "Creating Password: ${password}"
echo "Creating Home Directory: /home/${userid}"
echo "Creating User Shell: /bin/bash"
echo -e "Assigning Group: ${group}\n"
fi
done < $filename
else
echo -e "##### CANNOT FIND OR LOCATE FILE #####"
fi
verify=`grep "verify" /home/pi/$filename | cut -f3 -d:`
echo "$verify"
count=0
for id in $verify
do grep -wo ^$id /etc/passwd && count=$((count+1))
done
echo $count users added from your text file
echo these are not added:
for id in $verify
do grep -wq ^$id /etc/passwd || echo $id
done
while true
do
echo -n "Create additional accounts [y/n]: "
read opt
if [[ $opt == "n" || $opt == "y" ]];then
break
else
echo "Invalid Input"
fi
done
if [ $opt = "n" ]; then
clear
break
else
clear
fi
done
You were almost there.
The main issue with your approach is that you try to search for multiple accounts at once with grep. The variable verify has multiple userids so you need to process it one by one.
file=users.txt
verify=`grep "verify" $file | cut -f2 -d:`
count=0
for id in $verify
do grep -wo ^$id /etc/passwd && count=$((count+1))
done
echo $count users added from your text file
echo these are not added:
for id in $verify
do grep -wq ^$id /etc/passwd || echo $id
done
The for loop will take each element in your verify variable into id and search with grep (-w matches only whole words, not fragments, ^ matches the beginning of line and -o outputs only the matching word not the whole line).
We count the number of matches in the count variable. Alternative approach to run the for loop twice and pipe the second one to wc -l as you did.
&& operator means it will increase count if the previous command found a match (the return code of grep was 0).
The next loop will not print matching ids (-q), and will echo id if grep did not found a match (the return code was not 0). This is achieved with the || operator.
One last note on iteration of a list: if the members can contain spaces (unlike userids), you should use ${verify[#]} (this is a bash-ism) instead of $verify .
And forget this: cat /etc/passwd | grep pattern, use grep pattern /etc/passwd instead.

Linux single instance shell script fails to open in correct workspace at times

I have written the following Linux shell script through snippits gleaned from the web (I'm very new to shell scripts), its purpose is to ensure only a single instance of a programme is run with the added option of specifying which workspace a programme will open to.
I'm sure much of the code could be better constructed, however it works with one bug, when some, like Thunderbird, are opened they ignore the workspace switch unless the workaround I've added is used, but why? and is there a better way?
The script uses wmctrl: sudo apt-get install wmctrl
Usage: single-switch programme_name [-ws(int)] where (int) is number of workspace (must exist), the -ws param must be the last listed
#!/bin/bash
if ! [ $1 ] ; then exit ; fi
if [ "?" = "$1" ] ; then
FILE=$(echo "$0" | rev | cut -d "/" -f1 | rev) # extract filename from path
echo "usage $FILE <program name> [-ws(int)]"
exit 1;
fi
TITLE=$1
NAME=""
for var in "$#"; do [ "$(echo ${var} | head -c3)" != '-ws' ] && NAME="$NAME $var" ; done # remove our param from command
ntharg() { shift $1 ; echo $1 ; }
PARAM=`ntharg $# "$#"` # get the last param
if [ "-ws" != "$(echo ${PARAM} | head -c3)" ]; then PARAM=-1 ; # check its ours
else
PARAM=$( echo "$PARAM" | egrep -o '[0-9]+' ) # get the number
PARAM=$((PARAM-1)) # decrement
fi
if [ $PARAM -ge 0 ] ; then
wmctrl -x -a "$TITLE" || ( wmctrl -s $PARAM && zenity --title="Launch $TITLE" --warning --text="$TITLE is starting" --timeout="1" ; $NAME )
# dummy message otherwise some (ie thunderbird) ignore switch
else
wmctrl -x -a "$TITLE" || $NAME & # no switch, just raise or run programme
fi
# Done.
#

Why do this sample script, keep outputting error near token?

enter image description hereI was trying to see how a shell scripts work and how to run them, so I toke some sample code from a book I picked up from the library called "Wicked Cool Shell Scripts"
I re wrote the code verbatim, but I'm getting an error from Linux, which I compiled the code on saying:
'd.sh: line 3: syntax error near unexpected token `{
'd.sh: line 3:`gmk() {
Before this I had the curly bracket on the newline but I was still getting :
'd.sh: line 3: syntax error near unexpected token
'd.sh: line 3:`gmk()
#!/bin/sh
#format directory- outputs a formatted directory listing
gmk()
{
#Give input in Kb, output converted to Kb, Mb, or Gb for best output format
if [$1 -ge 1000000]; then
echo "$(scriptbc -p 2 $1/1000000)Gb"
elif [$1 - ge 1000]; then
echo "$$(scriptbc -p 2 $1/1000)Mb"
else
echo "${1}Kb"
fi
}
if [$# -gt 1] ; then
echo "Usage: $0 [dirname]" >&2; exit 1
elif [$# -eq 1] ; then
cd "$#"
fi
for file in *
do
if [-d "$file"] ; then
size = $(ls "$file"|wc -l|sed 's/[^[:digit:]]//g')
elif [$size -eq 1] ; then
echo "$file ($size entry)|"
else
echo "$file ($size entries)|"
fi
else
size ="$(ls -sk "$file" | awk '{print $1}')"
echo "$file ($(gmk $size))|"
fi
done | \
sed 's/ /^^^/g' |\
xargs -n 2 |\
sed 's/\^\^\^/ /g' | \
awk -F\| '{ printf "%39s %-39s\n", $1, $2}'
exit 0
if [$#-gt 1]; then
echo "Usage :$0 [dirname]" >&2; exit 1
elif [$# -eq 1]; then
cd "$#"
fi
for file in *
do
if [ -d "$file" ] ; then
size =$(ls "$file" | wc -l | sed 's/[^[:digit:]]//g')
if [ $size -eq 1 ] ; then
echo "$file ($size entry)|"
else
echo "$file ($size entries)|"
fi
else
size ="$(ls -sk "$file" | awk '{print $1}')"
echo "$file ($(convert $size))|"
fi
done | \
sed 's/ /^^^/g' | \
xargs -n 2 | \
sed 's/\^\^\^/ /g' | \
awk -F\| '{ printf "%-39s %-39s\n", $1, $2 }'
exit 0
sh is very sensitive to spaces. In particular assignment (no spaces around =) and testing (must have spaces inside the [ ]).
This version runs, although fails on my machine due to the lack of scriptbc.
You put an elsif in a spot where it was supposed to be if.
Be careful of column alignment between starts and ends. If you mismatch them it will easily lead you astray in thinking about how this works.
Also, adding a set -x near the top of a script is a very good way of debugging what it is doing - it will cause the interpreter to output each line it is about to run before it does.
#!/bin/sh
#format directory- outputs a formatted directory listing
gmk()
{
#Give input in Kb, output converted to Kb, Mb, or Gb for best output format
if [ $1 -ge 1000000 ]; then
echo "$(scriptbc -p 2 $1/1000000)Gb"
elif [ $1 -ge 1000 ]; then
echo "$(scriptbc -p 2 $1/1000)Mb"
else
echo "${1}Kb"
fi
}
if [ $# -gt 1 ] ; then
echo "Usage: $0 [dirname]" >&2; exit 1
elif [ $# -eq 1 ] ; then
cd "$#"
fi
for file in *
do
if [ -d "$file" ] ; then
size=$(ls "$file"|wc -l|sed 's/[^[:digit:]]//g')
if [ $size -eq 1 ] ; then
echo "$file ($size entry)|"
else
echo "$file ($size entries)|"
fi
else
size="$(ls -sk "$file" | awk '{print $1}')"
echo "$file ($(gmk $size))|"
fi
done | \
sed 's/ /^^^/g' |\
xargs -n 2 |\
sed 's/\^\^\^/ /g' | \
awk -F\| '{ printf "%39s %-39s\n", $1, $2}'
exit 0
By the way, with respect to the book telling you to modify your PATH variable, that's really a bad idea, depending on what exactly it advised you to do. Just to be clear, never add your current directory to the PATH variable unless you intend on making that directory a permanent location for all of your scripts etc. If you are making this a permanent location for your scripts, make sure you add the location to the END of your PATH variable, not the beginning, otherwise you are creating a major security problem.
Linux and Unix do not add your current location, commonly called your PWD, or present working directory, to the path because someone could create a script called 'ls', for example, which could run something malicious instead of the actual 'ls' command. The proper way to execute something in your PWD, is to prepend it with './' (e.g. ./my_new_script.sh). This basically indicates that you really do want to run something from your PWD. Think of it as telling the shell "right here". The '.' actually represents your current directory, in other words "here".

Create files based on user input

I have a bash script that asks the user for 3 numbers (example, 123).
I'm stuck on how to separate these numbers in order to create file1, file2, file3, I also have to determine if they are unique.
Any help would be appreciated.
I can post my bash script if needed.
! /bin/bash
clear
echo -n "Enter three digits number: "
read number
echo $number | grep "^[0-9][0-9][0-9]$"
if [ "$?" -eq 1 ]
then
echo "Error!! Please enter only 3 numbers."
exit 1
fi
if [ -d ~/a2/numbers ]
then
rm -r ~/a2/numbers
fi
mkdir ~/a2/numbers
if [ ! -e ~/a2/products ]
then
echo "Error the file \'products\'! does not exist"
exit 1
fi
echo ' '
cat ~/a2/products
echo ' '
cut -f2 -d',' ~/a2/products > ~/a2/names
cat ~/a2/names
echo "I have $(cat ~/a2/names | wc -l) products in my product file"
echo ' '
You could use the command fold which will split your string by character. Example:
echo ${number} | fold -w1
To check if they are unique just use the if statement, because in your case you allow only three one digit numbers.
#!/bin/bash
read -p "enter 3 numbers: " nums
if [[ $nums != [0-9][0-9][0-9] ]]; then
echo "digits only please"
exit
fi
read n1 n2 n3 < <(sed 's/./& /g' <<< $nums)
if ((n1 == n2)) || ((n1 == n3)) || ((n2 == n3)); then
echo "no duplicate numbers"
exit
fi

Resources