2

I want to read a number of strings from command line and read all strings in a loo. How to process (like Print) them in a loop, further. Sorry I am a newbie, please excuse my novice question.

#!/usr/bin/bash

echo "Enter the number of names of students" read classcount

for ((i=1;i<=classcount;i++)); do read names[${i}] done

for ((i=1;i<=classcount;i++)); do echo $names[${i}] done

3 Answers3

6

In general, user interaction in scripts or any utility used from the command line is best avoided as it hinders automation.

For instance, you call rm file1 file2 'file 3', not rm alone expecting it to ask you the number of files to remove, and then prompting separately for each.

Similarly, here, it would be much better to take the list of students from command line arguments:

#! /usr/bin/bash -
classcount=$#
names=("$@")

if (( classcount > 0 )); then echo "Student${name[1]+s}:" printf ' - %s\n' "${name[@]}" fi

And invoke as:

that-script 'John Doe' 'Alice Smith' ...

Or, to take the list of students one per line from a text file for instance (assuming GNU xargs):

xargs -d '\n' -a student.list that-script

You could also read them one per line from standard input, but again better without interaction and reading the list until end-of-input instead of prompting the user for a count first:

#! /usr/bin/bash -

readarray -t names classcount="${#names[@]}"

if (( classcount > 0 )); then echo "Student${name[1]+s}:" printf ' - %s\n' "${name[@]}" fi

If stdin is a terminal, the user would signify end-of-input with Ctrl+D.

To get the list from a file:

that-script < student.list

read can be used to read a line from stdin, but the syntax is IFS= read -r lvalue, not read lvalue.

Also note that [...] is a globbing operator so should be quoted as well as all parameter expansions that appear in list context.

Also, array indices (other than for the "$@" Bourne array) in bash like in ksh (but unlike most other shells), start at 0, not 1.

So for your approach, the syntax should rather be:

#! /usr/bin/bash -
PROGNAME=$0
die() {
  (( $# == 0 )) || printf >&2 '%s: ERROR: %s\n' "$PROGNAME" "$1"
  exit 1
}

IFS= read -rp "Enter the number of names of students: " classcount || die

validate to avoid command injection vulnerability

shopt -s extglob # only needed in older versions of bash [[ $classcount = @(0|[123456789]*([0123456789])) ]] || die "Not a valid decimal integer number: $classcount"

for (( i = 1; i <= classcount; i++ )); do IFS= read -rp "Student $i: " 'names[i-1]' || die "$(( classcount - i + 1 )) students missing" done

for (( i=1; i <= classcount; i++)); do printf 'Student %s: %s\n' "$i" "${names[i-1]}" done

$name[$i] would be valid in (t)csh (the shell where arrays were first introduced in the late 70s), though you'd need $name[$i]:q to make sure whitespace or wildcards are not treated specially in them, or in zsh, but not in bash which copied ksh's array design.

ksh tried hard to be backward compatible with the Bourne shell in that regard, so echo $name[$i] would output the list of files matching the <contents-of-$name>[<contents-of-$i>] pattern ($name and $i also subject to splitting) like in the Bourne shell.

In ksh/bash, you need the ${name[i]} syntax (${name[$i]} or ${name[${i}]} also work), and like for every parameter expansion, make sure it's quoted ("${name[i]}") to prevent split+glob.

In any case, echo can't be used to output arbitrary data.

  • Thanks @Stéphane Chazelas, for a detailed explanation about non-interactive way of handling variable number of variables in bash. and your time in replying me. But my requirement was a small beginner type of script which takes this information from user. I wish there could be a choice to accept many answers for accepting yours. – Rama Krishna Majety Jun 08 '21 at 14:15
3

Apart from various ways to improve the data input as shown in the other answer, a small change will make your script work as intended.

In the line

echo $names[${i}]

the shell will expand $names and ${i}, probably resulting in

[1]
[2]

etc, provided that names is not defined. Otherwise it will prepend the value of names before the opening brackets.
(As mentioned in Stéphane Chazelas' comment, the resulting [1] etc could be replaced with matching file names 1 etc because of missing quotes, if such a files exist.)

Instead, use

echo "${names[${i}]}"

Edit:

As stated in ilkkachu's comment, in the script from the question it is sufficient to use

echo "${names[i]}"

since the index is an arithmetic expansion, and you don't need the $ there when referencing variables.

Bodo
  • 6,068
  • 17
  • 27
2

Even if you want to ask for the list interactively, it may be more user-friendly to have the script detect the end of the list through some terminator, instead of requiring the user to count them beforehand and enter the correct count.

(After all, the computer should be better at counting the lines exactly than a regular human is. Asking for the count first is only useful if you're using a language with manual memory management, and don't want to resize the array to size after getting more input. Might make sense in a memory-limited embedded environment, but not that much on a "full" computer, and definitely not in the shell.)

Instead, you could just ask the user to terminate the list by entering an empty line (could be something else, too), and append the names received to an the array as long as you get non-empty lines. Also if don't actually need the indexes when handling the data, you can just get the list of the array entries with "${names[@]}", and loop over that:

#!/bin/bash
names=()
echo "Please enter the names of students (end with an empty line)"
while IFS= read -r name && [ "$name" ]; do
    names+=("$name")
done

echo "You entered ${#names[@]} name(s): " for n in "${names[@]}"; do echo "$n" done

[ "$name" ] tests if the entered name is empty. The loop will also end if end-of-file is seen.

ilkkachu
  • 138,973