1

I always write some magic numbers in my interactive shells and shell scripts.

For instance, If I want to list my users's names and shells, I'll write

cut --delimiter=: --fields=1,7 /etc/passwd

There exist two magic-numbers 1,7. And there are more and more magic-numbers in other circumstances.

Question

How to avoid magic-numbers in interactive shells and shell scripts?

Supplementary background

Our teacher told us using cut -d: -f1,7 /etc/passwd. But for new linux-users, they don't konw what's meaning of d,f,1,7.(not just for new linux-users,the whole system has so many configuration files that it is not easy for a person to remember every magic-numbers)

So, in interactive shells, we can use --delimiter, --fields,and the bash repl(or zsh,fish) has good tab completion to it.

How about the 1 and 7? In shell scripts, It's a good method to declare some const variables like LoginField=1 and ShellField=7 after reading the man 5 passwd. But when some one is writing in the interactive shells, it's not a good idea to open a new window and search the constants of LoginField=1,ShellField=7 and define it. how to using some thing like tab completion to simplify operations?

xiang
  • 1,384
  • 1
  • 12
  • 28
  • Why not just declare some variables like `USER_NAME_COLUMN=1` and `BIN_FILE_COLUMN=2` and then finally use it in the script : `cut --delimiter=: --fields=$USER_NAME_COLUMN,$BIN_FILE_COLUMN /etc/passwd` – zeekhuge Aug 05 '17 at 11:41
  • 2
    Other than assigning some meaningful variable name, the question is really whether `1, 7` are actually fixed, or whether you are asking if you can derive them somehow from the data file. You can always find the number of fields, but you need some way to tell the script which to use. You can always pass them as arguments to your script. – David C. Rankin Aug 05 '17 at 11:44
  • We (and also shell documentation) talk about **interactive shells**. No one knows what you are talking about when you write [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop), even though the term is general, but correct. – Michael Jaros Aug 05 '17 at 13:12

6 Answers6

1

Use variables:

LoginField=1 ShellField=7
cut --delimiter=: --fields="$LoginField,$ShellField" /etc/passwd
Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
1

Just like in other languages - by using variables. Example:

$ username_column=1
$ shell_column=7 
$ cut --delimiter=: --fields="$username_column","$shell_column" /etc/passwd

The variables may be defined at the top of the script so that can be easily modified or they can be set in an external config-like file shared by multiple scripts.

Arkadiusz Drabczyk
  • 11,227
  • 2
  • 25
  • 38
  • The above also works in interactive shell if that's what you mean by `REPL`. Run these commands directly in your terminal and it will work the same as in a script. – Arkadiusz Drabczyk Aug 05 '17 at 12:39
1

The classic way to parse /etc/passwd is to read each column into an appropriately named variable:

while IFS=: read name passwd uid gid gecos home shell _; do 
   ...
done < /etc/passwd
William Pursell
  • 204,365
  • 48
  • 270
  • 300
0

Use export: export field_param="1,7" (you can put it .bashrc file to have configured each time shell session is started). This export can be part of .sh script. It's a good practice to put them in the head/top of the file. Then: cut --delimiter=: --fields=$field_param /etc/passwd This way you will need to edit the magic number in the only location.

dimirsen Z
  • 873
  • 13
  • 16
0

Continuing from my comment, it's hard to tell exactly what you are asking. If you just want to give meaningful variable names, then do as shown in the other answers.

If however you want to be able to specify which fields are passed to cut from the command line, then you can use the positional parameters $1 and $2 to pass those values into your script.

You need to validate that two inputs are given and that both are integers. You can do that with a few simple tests, e.g.

#!/bin/bash

[ -n "$1" ] && [ -n "$2" ] || { ## validate 2 parameters given
    printf "error: insufficient input\nusage: %s field1 field2\n" "${0##*/}"
    exit 1
}

## validate both inputs are integer values
[ "$1" -eq "$1" >/dev/null 2>&1 ] || {
    printf "error: field1 not integer value '%s'.\n" "$1"
    exit 1
}

[ "$2" -eq "$2" >/dev/null 2>&1 ] || {
    printf "error: field2 not integer value '%s'.\n" "$2"
    exit 1
}

cut --delimiter=: --fields=$1,$2 /etc/passwd

Example Use/Output

$ bash fields.sh
error: insufficient input
usage: fields.sh field1 field2

$  bash fields.sh 1 d
error: field2 not integer value 'd'.

$  bash fields.sh 1 7
root:/bin/bash
bin:/usr/bin/nologin
daemon:/usr/bin/nologin
mail:/usr/bin/nologin
ftp:/usr/bin/nologin
http:/usr/bin/nologin
uuidd:/usr/bin/nologin
dbus:/usr/bin/nologin
nobody:/usr/bin/nologin
systemd-journal-gateway:/usr/bin/nologin
systemd-timesync:/usr/bin/nologin
systemd-network:/usr/bin/nologin
systemd-bus-proxy:/usr/bin/nologin
<snip>

Or if you choose to look at fields 1 and 3, then all you need do is pass those as the parameters, e.g.

$  bash fields.sh 1 3
root:0
bin:1
daemon:2
mail:8
ftp:14
http:33
uuidd:68
dbus:81
nobody:99
systemd-journal-gateway:191
systemd-timesync:192
systemd-network:193
systemd-bus-proxy:194
<snip>

Look things over and let me know if you have further questions.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • Sorry for my poor ability to express. Our teacher told us using `cut -d: -f1,7 /etc/passwd`. But for new linux-users, they don't konw what's meaning of `d`,`f`,`1`,`7`.So, in shell REPL, we can use `--delimiter`, `--fields`,instead. How about the `1` and `7`? – xiang Aug 05 '17 at 12:11
  • The fields `1` and `7` just correspond to the Linux `username` and default `shell` values stored in `/etc/password`. In my example above when I passed `1` and `3` for the fields the `username` and `UID` were extracted from the file. `1` and `7` just tell `cut` to get what lies before the `1st` and `7th` colon (`':'`) in the file. In my example `$1` holds the first command-line parameter and `$2` holds the second. You can reassign them to more meaningful names if you like, but for small scripts you can just use them directly as well. – David C. Rankin Aug 05 '17 at 12:16
  • No problem, If you are looking to find/replace the values `1,7`, then that is the entire purpose behind passing them as parameters. You do not have tab-completion within shell scripts (unless you write it), but you can provide normal responses to `-h` or `--help` or provide `usage:` information if the user enters nothing, or enters an incorrect value as I have done above. If you simply run the script I named `fields.sh` with no parameters, it tells you what is needed, e.g. `"usage: fields.sh field1 field2"`. You can add any additional descriptive text you like and check for `-h` or `--help`. – David C. Rankin Aug 05 '17 at 13:21
0

Scraping the output of man 5 passwd for human-readable header names:

declare $(man 5 passwd | 
          sed -n '/^\s*·\s*/{s/^\s*·\s*//;y/ /_/;p}' | 
          sed -n 'p;=' | paste -d= - - )

See "how it works" below for what that does, then run:

cut --delimiter=: \
    --fields=${login_name},${optional_user_command_interpreter} /etc/passwd

Which outputs the specified /etc/passwd fields.


How it works.

The man page describing /etc/passwd contains a bullet list of header names. Use GNU sed to find the bullets (·) and leading whitespace, then remove the bullets and whitespace, replace the remaining spaces with underlines; a 2nd instance of sed provides fresh line numbers, then paste the header names to the line numbers, with a = between:

man 5 passwd | 
sed -n '/^\s*·\s*/{s/^\s*·\s*//;y/ /_/;p}' | 
sed -n 'p;=' | paste -d= - -

Outputs:

login_name=1
optional_encrypted_password=2
numerical_user_ID=3
numerical_group_ID=4
user_name_or_comment_field=5
user_home_directory=6
optional_user_command_interpreter=7

And declare makes those active in the current shell.

agc
  • 7,973
  • 2
  • 29
  • 50