8

I have been trying to find the answer to this question for a while. I am writing a quick script to run a command based on output from awk.

ID_minimum=1000
for f in /etc/passwd;
do 
    awk -F: -vID=$ID_minimum '$3>=1000 && $1!="nfsnobody" { print "xfs_quota -x -c 'limit bsoft=5g bhard=6g $1' /home "}' $f;       
done

The problems are that the -c argument takes a command in single quotes and I can't figure out how to properly escape that and also that $1 doesn't expand into the username.

Essentially I am just trying to get it to output:

xfs_quota -x -c 'limit bsoft=5g bhard=6g userone' /home
xfs_quota -x -c 'limit bsoft=5g bhard=6g usertwo' /home

etc...

terdon
  • 242,166
ZCT
  • 109

7 Answers7

14

To run the command xfs_quota -x -c 'limit bsoft=5g bhard=6g USER' /home for each USER whose UID is at least $ID_minimum, consider parsing out those users first and then actually run the command, rather than trying to create a string representing the command that you want to run.

If you create the command string, you would have to eval it. This is fiddly and easy to get wrong. It's better to just get a list of usernames and then to run the command.

getent passwd |
awk -F: -v min="${ID_minimum:-1000}" '$3 >= min && $1 != "nfsnobody" { print $1 }' |
while IFS= read -r user; do
    xfs_quota -x -c "limit bsoft=5g bhard=6g $user" /home
done

Note that there is no actual need for single quotes around the argument after -c. Here I use double quotes because I want the shell to expand the $user variable which contains values extracted by awk.

I use ${ID_minimum:-1000} when giving the value to the min variable in the awk command. This will expand to the value of $ID_minimum, or to 1000 if that variable is empty or not set.


If you really wanted to, you could make the above loop print out the commands instead of executing them:

getent passwd |
awk -F: -v min="${ID_minimum:-1000}" '$3 >= min && $1 != "nfsnobody" { print $1 }' |
while IFS= read -r user; do
    printf 'xfs_quota -x -c "limit bsoft=5g bhard=6g %s" /home\n' "$user"
done

Note again that using double quotes in the command string outputted (instead of single quotes) would not confuse a shell in any way if you were to execute the generated commands using eval or though some other means. If it bothers you, just swap the single and double quotes around in the first argument to printf above.

Kusalananda
  • 333,661
  • 1
    the only answer to correctly use getent rather than parse /etc/passwd. – cas Sep 14 '19 at 00:25
  • 1
    @cas macOS is a Unix with no getent (on desktop, anyway), and the question is not tagged "linux." – Kevin E Sep 14 '19 at 20:23
  • 2
    @TheDudeAbides Assuming that the user picked UID 1000 because it is the cutoff between system service accounts and user accounts on their system, we may infer that this user is not running on macOS (first user account is 501). Besides, using XFS utilities on macOS is quite uncommon seeing as XFS is not really supported by Apple. Also, /home isn't really used on macOS (it's there, but usually empty). – Kusalananda Sep 14 '19 at 20:26
  • @Kusalananda Point taken. I was blind to the XFS part. – Kevin E Sep 14 '19 at 21:28
  • 1
    @TheDudeAbides getent isn't linux-only. it's also on netbsd, freebsd, and solaris at least. – cas Sep 14 '19 at 23:14
  • @cas I stand doubly extra corrected. :) – Kevin E Sep 16 '19 at 22:10
7
for f in /etc/passwd;

This is a bit silly as there's really no loop with just one value.

But the issue seems to be printing single quotes from awk. You could escape them in the shell, but you could also use backslash-escapes within awk to print them. \OOO is the character with numerical value OOO (in octal), so \047 is '. So this would be one way to do it:

awk -F: -vID=$ID_minimum '$3>=1000 && $1!="nfsnobody" {
    printf "xfs_quota -x -c \047limit bsoft=5g bhard=6g %s\047 /home\n", $1}' /etc/passwd

You could use the similar escape in hex, \x27, but it can get misinterpreted in some implementations if the following character is a valid hexadecimal digit. (And of course I assumed ASCII or an ASCII-compatible character set, e.g. UTF-8.)

ilkkachu
  • 138,973
  • Note that it's fine here as l is not a hex digit, but "\x27df\n" would be treated as "\x27" "df" in busybox awk or mawk but as "\xdf" (0x27df cast to 8 bit char) in the original awk and gawk (that's the reason why POSIX doesn't specify \xHH). Octals (\047) don't that the same problem and are POSIX. In any case, that assumes an ASCII system (a reasonable assumption these days). – Stéphane Chazelas Sep 13 '19 at 19:57
6

Use the -f - option to awk to take the script from stdin and a here-document:

awk -F: -v "ID=$ID_minimum" -f - <<'EOT' /etc/passwd
$3>=1000 && $1!="nfsnobody" {
    print "xfs_quota -x -c 'limit bsoft=5g bhard=6g "$1"' /home "
}
EOT
2

This did it.

awk -F: -vID=$ID_minimum '$3>=1000 && $1!="nfsnobody" { print "xfs_quota -x -c '"'"'limit bsoft=5g bhard=6g ''"$1"'''"'"' /home "}' /etc/passwd
ZCT
  • 109
  • 1
    @Jesse_b, yes, you can't put single quotes inside a single-quoted string, so you have to end it first, and then quote the single quote you want to insert. It doesn't matter if you do '\'' or '"'"'. Both work, both look annoying. – ilkkachu Sep 13 '19 at 19:26
  • @OP, hopefully you saw jordanm's comment about your loop being unnecessary. Your loop is only ever run once so it's not different than simply running the same awk command outside of a loop. – jesse_b Sep 13 '19 at 19:29
1

This looks to me like an ideal opportunity to employ you some xargs (or GNU Parallel):

getent passwd \
  | awk -F: '$3>=1000 && $1!="nfsnobody" {print $1}' \
  | xargs -I{} \
      echo xfs_quota -x -c \"limit bsoft=5g bhard=6g {}\" /home

# output:
# xfs_quota -x -c "limit bsoft=5g bhard=6g userone" /home
# xfs_quota -x -c "limit bsoft=5g bhard=6g usertwo" /home

The advantage to using xargs or parallel is that you can simply remove the echo when you're ready to run the command for real (possibly replacing it with sudo, if necessary).

You can also use these utilities' -p / --interactive (the latter is GNU-only) or --dry-run (parallel only) options, to get confirmation before running each one, or just to see what would run, before you run it.

The general method used above should work on most Unixes, and requires no GNU-specific xargs options. The double quotes do need to be "escaped" so that they appear literally in the output. Note that the "replacement string," {}, in xargs -I{} can be anything you prefer, and -I implies -L1 (run one command per input line rather than batching them up).

GNU Parallel does not require the -I option ({} is the default replacement string), and gives you the instant bonus of running many jobs in parallel, even if you don't want to bother learning about any of its other features.

As a side note, I'm not even sure if xfs_quota's -c option is supposed to be used like this, though I have no XFS filesystems handy to test. You may not have even have needed to deal with a quoted string in the first place (unless you expect usernames with whitespace in them, which I guess is possible), since it looks like you can give multiple -c options on the same command line, according to the man page included with xfsprogs 4.5.something.

Kevin E
  • 468
1

It's an awful bodge, but it's quick and easy...

awk -F: -vQ="'" -vID=$ID_minimum '$3>=1000 && $1!="nfsnobody" { print "xfs_quota -x -c " Q "limit bsoft=5g bhard=6g $1" Q " /home "}' $f;       
Grump
  • 225
0

With GNU Parallel you can do:

getent passwd |
  parallel --colsep : -q xfs_quota -x -c \
    'limit bsoft=5g bhard=6g {=1 $_ eq "nfsnobody" and skip(); $arg[3] <= 1000 and skip();  =}' /home

Explanation:

--colsep : Split on :
-q do not split the command on spaces (keep the '...' as a single string)
{=1 ... =} Evaluate this perl expression on the first argument of the line
$_ eq "nfsnobody" and skip(); If the value==nfsnobody: skip
$arg[3] <= 1000 and skip(); If argument3 <= 1000: skip

Ole Tange
  • 35,514