2013/09/18

main 関数の書き方

こちらの記事に触発されて、 C で同じお題のプログラム (echo (1) のサブセット)を書いてみた。

main には殆ど処理を記述せずに パラメタ解析や割り込みなどの初期設定のみをしてから、 実際のメイン処理(この場合は echo)を実行するというスタンスは同じです。
ただし、今回の例で言えば getopt(3) を利用して オプション解析を済ませた(実際に echo 出力する)パラメタ群のループは main 側で行う方が好きかな。

  1#include    <stdio.h>
  2#include    <unistd.h>
  3
  4void    echo(const char *p, int nonl)
  5{
  6
  7    /*
  8     * ここは以下の様にすると printf(3)、putchar(3) の呼び出しを
  9     * printf(3) の呼び出しのみに変更できソースもスッキリする。
 10     * 1 文字の出力を '%s' で変換する無駄が微妙に気になるのだが、
 11     * どちらかと言えば改行を出力する事の方が多いと思うので
 12     * putchar(3) の呼び出しがない方が良いのか
 13
 14     printf("%s%s", p, nonl ? "" : "\n");
 15
 16     */
 17
 18    printf("%s", p);
 19    if(!nonl)
 20        putchar('\n');
 21
 22}
 23
 24int     main(int argc, char *argv[])
 25{
 26
 27    int     i,
 28            nonl = 0;
 29
 30    while((i = getopt(argc, argv, "n")) != EOF){
 31        switch(i){
 32            case 'n':
 33                nonl = 1;
 34                break;
 35
 36            default:
 37                break;
 38        }
 39    }
 40
 41    for(i=optind; i<argc; i++)
 42        echo(argv[i], nonl);
 43
 44    exit(0);
 45
 46}
    

2013/08/02

シェルスクリプトでループ

シェルスクリプトでループ処理をする場合、 最も多用するのは while だと思う。
良く見かけるのは次の様な書き方だろう。

    :
while true
do
    処理
    if 条件
    then
        break
    fi
done
    :
    
勿論この書き方でも問題なく動作するが、 条件がコマンドの戻り値の場合は以下の方が簡潔に記述できる。
コマンドの戻り値が 0 (正常終了の場合) ループを続けたい場合。
    :
while コマンド
do
    処理
done
    :
    
コマンドの戻り値が 0 以外(正常終了以外の場合) ループを続けるのであれば while の替わりに until が利用できる。
例えば再起動中のリモートサーバに ssh (1) 接続したい場合、 以下の方法で繰り返し接続を試行する事ができる。
until ssh -o 'ConnectTimeout 5' リモートサーバ
do
    sleep 10
done
    
ssh (1) が成功するとリモートサーバから exit した時点で until の条件が真になるのでループを抜ける事ができる。

2013/07/31

MySQL データベースを Amazon S3 にバックアップするスクリプト

自宅サーバで運用している MySQL データベースのデータを Amazon S3 にバックアップするためのスクリプトを作成したので公開。

今回作成したスクリプトでは、 ローカルディレクトリに YYYYMMDD 型式で 7 世代分のデータをフルダンプし、 毎週日曜日の深夜に Amason S3 に当日分のバックアップを転送している。
mysqldump で取得したバックアップデータに VIEW 情報が含まれていると データベースのレストア時にエラーが発生する可能性がある様なので、 mysqldump 時に VIEW 定義は除外してダンプを取得し VIEW 情報のみを別途ダンプする様にしている。

Amason S3 への転送は S3 tools が公開している s3cmd を利用しているので、 s3cmd と python のインストールは必須。
当然 Amazon S3 も利用できる様にしておく必要がある。

以下のスクリプトは MySQL の接続用パスワードや Amazon S3 のアクセスキーなどを平文で保存しているので、 ファイルのアクセス権限には十分に注意が必要である。
バックアップは root 権限で実行する様にし ファイルの権限は 500 に設定しておく事が望ましい。

  1#!/bin/sh
  2
  3# MySQL 用の設定
  4database="データベース名"
  5user="-uユーザ -pパスワード"
  6
  7# Amazon S3 用の設定
  8backet="backet 名"
  9PASSPHRASE="パスフレーズ"
 10AWS_ACCESS_KEY_ID="アクセスキーID"
 11AWS_SECRET_ACCESS_KEY="SECRET アクセスキー"
 12
 13# ローカルにバックアップする世代数
 14max=7
 15# Amazon S3 に転送する曜日指定 (0: 日曜 … 6: 土曜)
 16dayofweek=0
 17
 18# コマンド定義
 19ls="/bin/ls -1"
 20wc="/usr/bin/wc -l"
 21rm="/bin/rm -r"
 22head="/usr/bin/head -1"
 23mkdir="/bin/mkdir -p"
 24
 25# ファイルとディレクトリ定義
 26logfile="/var/log/`basename ${0}`.log"          # ログファイル名
 27backupdir="/var/db/backup"                      # バックアップ先ディレクトリ
 28target="`date '+%Y/%m/%d'`"                     # Amason S3 でバックアップを格納するディレクトリ名
 29targetdir="${backupdir}/`date '+%Y%m%d'`"       # ローカルでバックアップを格納するディレクトリ名
 30backupfile="${targetdir}/all_dump.sql"          # バックアップファイル名
 31createview="${targetdir}/create_view.sql"
 32createtable="${targetdir}/create_table.sql"
 33
 34# MySQL コマンドの定義
 35mysql="/usr/bin/mysql ${socket} ${user} -B -N -s -r "
 36mysqldump="/usr/bin/mysqldump ${user}
 37        -R --hex-blob --single-transaction"
 38sql="select TABLE_NAME from information_schema.TABLES
 39        where TABLE_SCHEMA = '${database}' and TABLE_TYPE = 'VIEW';"
 40
 41# s3cmd の定義
 42s3cmd="/usr/bin/s3cmd put -rr"
 43export PASSPHRASE AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
 44
 45# ロギング開始
 46umask 700
 47exec > ${logfile} 2>&1
 48set -x
 49
 50# ローカルディレクトリの準備
 51# 最大世代数以上の場合は一番古い世代を削除する
 52test `${ls} ${backupdir} | ${wc}` -ge ${max} &&
 53    ${rm} ${backupdir}/`${ls} ${backupdir} | ${head}`
 54${mkdir} ${targetdir}
 55
 56echo "DB backup: start: `date`"
 57
 58# ダンプから除外するために VIEW を取得し create view 文をダンプする
 59for i in `${mysql} ${database} -e "${sql}"`
 60do
 61    view="${view} --ignore-table=${database}.${i}"
 62    ${mysql} ${database} -e "show create view ${i}"
 63done > ${createview}
 64
 65# 全データダンプ
 66${mysqldump} ${view} ${database} > ${backupfile}
 67
 68# create table 文のダンプ
 69${mysqldump} --no-data ${view} ${database} > ${createtable}
 70
 71# 指定された曜日の場合は Amazon S3 に転送する
 72# 以下の 1行をコメントとすると 毎日 Amazon S3 に転送する
 73test `date '+%w'` -eq ${dayofweek} &&
 74    ${s3cmd} ${targetdir}/ s3://${backet}/${target}/
 75
 76echo "DB backup: end: `date`"
    

2013/07/30

awk による正規表現にマッチした部分の抜き出し

入力行に "running"、"stopping"、 "stopped"、"pendding" のいずれかの単語が含まれる場合に その単語を抽出したい場合。
awk (1) の sub()、gsub() 組み込み関数では 後方参照ができないので マッチしたパターンに応じた変換を一度に行う事ができず、 それぞれのパターンにマッチングさせて処理をするしかない。

    awk '
        /^INSTANCE.*running/{
            print running;
        }
        /^INSTANCE.*stopping/{
            print stopping;
        }
        /^INSTANCE.*stopped/{
            print stopped;
        }
        /^INSTANCE.*pending/{
            print pending;
        }
    '
    
しかしこれではあまりにも悲しいのでマニュアルをじっくりと調べた所、 組み込み関数 match() で正規表現にマッチ処理を行うと 組み込み変数 RSTART にマッチした文字の位置、 RLENGTH にマッチした文字列の長さが設定されるので 組み込み関数 substr() と併用する事で マッチした部分を抽出する事ができる事に気がついた。
    awk '
        /^INSTANCE/{
            if(match($0, /running|stopping|stopped|pending/))
                print substr($0, RSTART, RLENGTH);
            if(match($0, /ec2.*\.amazonaws\.com/))
                print substr($0, RSTART, RLENGTH);
        }
    '
    
awk (1) で正規表現によるパターン抽出を行う場合に便利だと思います。

文字列はあくまでも例であって特別な意味を持っていません。
万が一類似する文字列があった場合でもそれは偶然の一致です。 ぐ、偶然ですからねっ(汗

2013/07/09

PATH の扱い

unix 系 OS では ${PATH} 環境変数を参照して コマンドの検索パスを指定する事ができる。
通常はログイン時にシステムデフォルトのコマンド検索パスが ${PATH} に自動で設定されているが、 ユーザ毎のシェルのログインファイル (.profile 等)で 自分用のコマンド検索パスを追加設定する事が多い。
ここで気をつけるべきは ${PATH} 中のヌルパス ("::") は カレントディレクトリ (".") と同等に扱われてしまう事だ。

例えば


PATH="${PATH}:${mypath}:${HOME}/bin"

    
と設定した時に ${mypath} が設定されていない場合

PATH="${PATH}::${HOME}/bin"

    
と展開されてしまうので、 結果としてカレントディレクトリが ${PATH} に含まれてしまう。
この挙動は POSIX (IEEE Std 1003.1, 2004 Edition) の 8.3 Other Environment Variables) にも明記されている。
The prefixes shall be separated by a colon ( ':' ). When a non-zero-length prefix is applied to this filename, a slash shall be inserted between the prefix and the filename. A zero-length prefix is a legacy feature that indicates the current working directory.
過去互換性の様だが非常に迷惑な仕様である。

カレントディレクトリをコマンド検索パスに含める事は どんな場合でもセキュリティ上のリスクを伴うので避けるべきでなので、 PATH を設定する場合は注意が必要だ。

例えば .profile で ${PATH} を設定する場合、 以下の様にする事は防衛手段として悪く無い考えだと思う。


PATH="`echo ${PATH}:${mypath}:${HOME}/bin | sed 's/::*/:/g'`"

    

2012/12/27

指定された日の曜日を取得する

先日開催された 第2回チキチキ!シェル芸人養成勉強会 の課題の中で自分の中で話題になった事。

指定された日の曜日を取得したい場合、 gnu 版の date (1) がインストールされている場合は 以下のコマンドで取得可能(0 が日曜日を示す)。

$ date -d YYYYMMDD '+%w'
0
    
gnu 版の date (1) がインストールされていない場合でも、 FreeBSD 版の date (1) は独自拡張されているので 以下のコマンドで取得可能だと言う事を教わった(0 が日曜日を示す)。
$ date -j YYYYMMDDHHMM '+%w'
0
    

但しどちらも posix に準拠していない独自拡張な機能を利用しているため 環境依存となってしまうので、 環境に依存しない方法での解決方法を考えてみた。
日付から曜日を取得するためには ツェラーの公式 という計算式があるので、 posix に準拠したシェルの機能だけでその計算式を実装してみる。

$ y=YYYY; m=MM; d=DD
$ test $m -lt 3 && y=$(($y - 1)) && m=$(($m + 12))
$ echo $(($(($y + $y/4 - $y/100 + $y/400 + $(($((13 * $m + 8))/5)) + $d)) % 7))
    
年と月の値によって調整が必要なのでワンライナーにはならないのが残念だけど、 基本的には単純な四則演算なのでシェルの機能のみで実装が可能だ。
これで gnu 版の date (1) がインストールされていない環境でも 任意の日付の曜日が簡単に取得できる!。

2012/12/26

dog (1)

以前、某雑誌のシェルスクリプト大喜利に投稿したネタ。
こんな感じのお題に対する回答。

cat コマンドの作者が遺言を遺しました。
「私の人生における最大の公開は、 dog コマンドを作り忘れてしまったことだ。 あぁ誰か作ってくれぇー。ガクッ」
さぁ、一体どんなコマンド?

お題がネタっぽいので回答もネタっぽくしてみた。
more (1) を拡張して less (1) が、 yacc (1) を拡張して bison (1) が作られた様に、 dog (1) は cat (1) が拡張されたものという発想で http、https そして ftp といった schema に対応した cat (1) というスクリプトにしてみた。
内容はとても単純なので cat → dog という発想だけが勝負のスクリプトである。

雑誌では割愛されてしまったネタとしてのマニュアルも掲載してみる。
というか、実はマニュアルの方が書きたかったという…(笑

  1#!/bin/sh
  2#
  3# NAME
  4#   dog -- concatenate and print files
  5#
  6# SYNOPSIS
  7#   dog [cat options] [file ...]
  8#
  9# DESCRIPTION
 10#   Dog is a file concatenate and print command in the style of cat(1).
 11#   It should be upwardly compatible with original cat.
 12#
 13#   The dog utility reads files sequentially, writing them to the standard
 14#   output.  The file operands are processed in command-line order.  If file
 15#   has protocol schema such as 'http://', 'https://' or 'ftp://', dog utility
 16#   gets the file from remote system and display it, otherwise dog assumes
 17#   local file. The dog utility uses wget(1), curl(1), or lynx(1) to retrieve
 18#   the file from remote system. If dog utility could not find these commands
 19#   from standard command path, disable remote file display.
 20#   All option of dog is same as cat(1) except '-h'.
 21#
 22# EXIT STATUS
 23#   The dog utility exits 0 on success, and >0 if an error occurs.
 24#
 25# SEE ALSO
 26#   Rob Pike, "UNIX Style, or cat -v Considered Harmful", USENIX Summer
 27#   Conference Proceedings, 1983.
 28#
 29# STANDARDS
 30#   The dog utility is *NOT* complient with the IEEE Std 
 31#   1003.2-1992 (``POSIX.2'') specification, even now and forever.
 32#   
 33
 34curl="-s -o -"
 35wget="-q -O -"
 36lynx="-source"
 37
 38# get command name and path
 39for i in wget curl lynx
 40do
 41    for j in /bin /usr/bin /usr/local/bin /opt/bin /opt/local/bin
 42    do
 43        if [ -x ${j}/${i} ]
 44        then
 45            command="${j}/${i} `eval echo '$'${i}`"
 46            break 2
 47        fi
 48    done
 49done
 50
 51# args
 52while [ ${1} != "" ]
 53do
 54    case ${1} in
 55        -h )
 56            sed -n '2,/^$/s/^#//p' ${0}
 57            exit 1
 58            ;;
 59        -- )
 60            shift
 61            break
 62            ;;
 63        -* )
 64            opt="${opt} $1"
 65            ;;
 66        *  )
 67            break
 68            ;;
 69    esac
 70    shift
 71done
 72
 73for i in $*
 74do
 75    if expr ${i} : "https*://" > /dev/null
 76    then
 77        if [ -n "${command}" ]
 78        then
 79            ${command} ${i} | cat ${opt}
 80        else
 81            echo "${0##*/}: cannot get remote file: ${i}" 1>&2
 82        fi
 83    elif expr ${i} : "ftp*://" > /dev/null
 84    then
 85        case ${command} in
 86            *wget*|*curl* )
 87                ${command} ${i} | cat ${opt}
 88                ;;
 89            * )
 90                echo "${0##*/}: cannot get remote file: ${i}" 1>&2
 91                ;;
 92        esac
 93    else
 94        cat ${opt} ${i}
 95    fi
 96done
    

今回のシェルスクリプト大喜利にも投稿したので、 某雑誌が発売されたら紹介する予定。

2012/11/30

IPv4 アドレスがネットワークに属しているか調べる

ネットマスクとはその名の通り IPv4 アドレスのうち、 どこまでのビットがネットワークアドレスなのかを示しているので、 IPv4 アドレスとネットマスクの論理和はネットワークアドレスとなる。
そのため、取得した論理和とネットワークアドレスが等しい場合は IPv4 アドレスはネットワークに属す事になる。

C で実装する場合はこんな感じかな。

  1int     checkAddr(const char *network, const char *netmask, const char *ipaddr)
  2{
  3
  4    struct  in_addr net,
  5                    mask,
  6                    addr;
  7
  8    if(inet_aton(network, &net) &&
  9            inet_aton(netmask, &mask) &&
 10            inet_aton(ipaddr", &addr))
 11        return((addr.s_addr & mask.s_addr) == (net.s_addr & mask.s_addr));
 12    else
 13        return(0);
 14
 15}
    

ちなみに仕事で実装した java のコード。
  1boolean checkAddr(String network, String netmask, String ipaddr)
  2{
  3
  4    try {
  5        int     n = 0,
  6                m = 0,
  7                a = 0;
  8        byte[]  net = InetAddress.getByName(network).getAddress(),
  9                mask = InetAddress.getByName(netmask).getAddress(),
 10                addr = InetAddress.getByName(ipaddr).getAddress();
 11
 12        for(int i=0; i<4; i++)
 13            n |= ((int)net[i] & 0xff) << (8 * (3 - i));
 14        for(int i=0; i<4; i++)
 15            m |= ((int)mask[i] & 0xff) << (8 * (3 - i));
 16        for(int i=0; i<4; i++)
 17            a |= ((int)addr[i] & 0xff) << (8 * (3 - i));
 18
 19        return((a & m) == (n & m));
 20
 21    } catch (Throwable e){
 22        return false;
 23    }
 24
 25}
    
java はそれほど詳しくないのでコーディングが冗長だけど、 取りあえず動作している(気がする)。

2012/11/02

CentOS 6.3 インストール直後にする作業

仕事では Linux サーバ、特に CentOS の導入~初期設定の機会が多いので、 全ての CentOS 6.3 サーバの導入時に必ず行っている作業をメモする。

インストール作業は CentOS 6.3 の DVD から起動して行う。
CentOS 6 からはテキストベースのインストーラの場合、 パーティションの設定やファイルシステムの指定、 初期インストールされるパッケージの追加・削除など指定できないので、 GUI を利用した通常のインストールを実行する。

インストールが終了したら初期環境設定を行う。
以下の作業は CentOS 6.3 を導入したマシンでほぼ必ず実施する作業。

自分のユーザを作製する
# adduser アカウント
# password アカウント
            
自分のアカウントを wheel グループに追加する
sudo (1) 可能な様に自分のアカウントを wheel グループに追加する。
# (rm /etc/group; sed 's/^wheel.*/&,アカウント/g' > /etc/group) < /etc/group
            
sudo(1) の設定を変更する
sudo(1) しても元の環境変数を保持する様設定
Defaults env_reset を Defaults !env_reset に変更
# Defaults env_keep += "HOME" のコメント記号 `#' を削除
            
wheel グループのアカウントのみ sudo (1) 可能にする
# %wheel  ALL=(ALL)   ALL のコメント記号 `#' を削除
            
# visudo
            
termcap を使用するアプリケーション対応
# ln -s /usr/lib/libtinfo.so /usr/lib/libtermcap.so.2
            
selinux を無効にする
# (rm /etc/sysconfig/selinux; sed 's/^SELINUX=.*/SELINUX=disabled/' > /etc/sysconfig/selinux) < /etc/sysconfig/selinux
# ex -s /etc/syconfig/selinux << EOF
> /^SELINUX=/s/.*/SELINUX=disabled
> wq
> EOF
            

2012/11/09 追記

CentOS の /etc/sysconfig/selinux は /etc/selinux/config へのシンボリックリンクで 実際は /etc/selinux/config を参照しているのだが、 rm (1) と sed (1) を使った方法だと /etc/sysconfig/selinux が通常ファイルになって 書き換わってしまい、元の /etc/selinux/config が 変更されていないために selinux が無効にならない という問題を指摘して頂いたので編集方法を修正しました。

システム更新
# yum update
            
最低限必要なソフトウェアのインストール
システムを運用する上でこれだけは必要だとうソフトウェアを導入する。
# yum install openssh-clients libtermcap-devel tcpdump
# yum install ntpdate telnet wget tcpdump unzip mlocate rsync bind-utils
            
開発環境のインストール
ソフトウェアを開発する場合は導入する。
通常はサーバでは開発しないので導入は不要な筈…
# yum install make gcc bison flex
            
ファイアウォールの設定
グローバル環境に晒されるサーバであれば 最低限のファイアウォールは設定する必要がある。
基本的には以下のポリシーで必要に応じて追加・削除する。
#!/bin/sh
iptables=/sbin/iptables

# 全て初期化
${iptables} -F

# デフォルトルール
${iptables} -P INPUT DROP
${iptables} -P OUTPUT ACCEPT
${iptables} -P FORWARD DROP

# ループバックを有効にする 
${iptables} -A INPUT -i lo -p all -j ACCEPT

# 接続済みのパケットを受け付ける
${iptables} -A INPUT -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT

# http, httpsはどこからでもアクセス可
${iptables} -A INPUT -p tcp --dport 80 -j ACCEPT
${iptables} -A INPUT -p tcp --dport 443 -j ACCEPT
${iptables} -A INPUT -p tcp --dport 8080 -j ACCEPT
${iptables} -A INPUT -p tcp --dport 9080 -j ACCEPT

# dns パケットを許可
${iptables} -A INPUT -p tcp --sport 53 -j ACCEPT
${iptables} -A INPUT -p udp --sport 53 -j ACCEPT
${iptables} -A INPUT -p tcp --dport 53 -j ACCEPT
${iptables} -A INPUT -p udp --dport 53 -j ACCEPT

# ntp パケットを許可
${iptables} -A INPUT -p tcp --dport 123 -j ACCEPT
${iptables} -A INPUT -p udp --dport 123 -j ACCEPT

# そのほか必要なルールを記載
    :
    :
            
上記設定を /tmp/firewall.sh に格納した場合、 ファイアウォールを有効にして設定をセーブする。
# sh /tmp/firewall.sh
# /etc/init.d/iptables save
iptables: ファイアウォールのルールを /etc/sysconfig/iptableに保存中: [  OK  ]
            
起動サービスの設定
起動サービスを設定する。
基本的には atd crond iptables network ntpd rsyslog sshd のみを有効にする
# for i in `chkconfig --list | awk '/:on/{ print $1 }'`
> do
>  case $i in
>      atd|crond|iptables|network|ntpd|rsyslog|sshd )
>          ;;
>      * )
>          chkconfig $i off
>          chkconfig --del $i
>          ;;
>  esac
> done
                

ここまで設定して再起動する。
MTA や httpd など固有のサービスは個々に設定する。

2012/10/31

hbstudy まとめ

先日参加した hbstudy #38 ハンズオンの 課題 の幾つかを シェルのみ 縛りで回答してみる。
流石にシェルのみで解く場合はワンライナーは無理なので、 普通のシェルスクリプトの型式で記述している。

問題1: ユーザの抽出
/etc/passwd から、ユーザ名を抽出したリストを作って下さい
  1IFS=:
  2while read id other
  3do
  4    echo ${id}
  5done  </etc/passwd
            
IFS を : に変更して /etc/passwd を 直接フィールド分割して変数 u に read している。
問題2: ユーザの抽出
/etc/passwd から、次を調べて下さい。
ログインシェルが bash のユーザと sh のユーザどちらが多い?
  1IFS=:
  2while read id p u g e d shell
  3do
  4    case ${shell} in
  5        */sh )
  6            sh=$((${sh} + 1));;
  7        */bash )
  8            bash=$((${bash} + 1));;
  9    esac
 10done < /etc/passwd
 11echo "bash: " ${bash}
 12echo "sh:   " ${sh}
            
問題:1 同様に IFS を : に変更して /etc/passwd を直接フィールド分割して 変数 u、shell に read し、 変数 sh と bash に出現回数をカウントしている。
問題5: FizzBuzz
1,2,3,4,5…と数えていって…
  • 3 の倍数だったら数字の代わりに「Fizz」
  • 5 の倍数だったら数字の代わりに「Buzz」
  • 15 の倍数だったら数字の代わりに「FizzBuzz」
と出力する
  1i=1
  2while [ ${i} -lt ${1:-100} ]
  3do
  4    if [ $((${i} % 15)) -eq 0 ]
  5    then
  6        echo -n "FizzBuzz,"
  7    elif [ $((${i} % 5)) -eq 0 ]
  8    then
  9        echo -n "Buzz,"
 10    elif [ $((${i} % 3)) -eq 0 ]
 11    then
 12        echo -n "Fizz,"
 13    else
 14        echo -n "${i},"
 15    fi
 16    i=$((${i} + 1))
 17done
 18echo
            
素直に 1 から $1 (指定されていない場合は 100) までを $((…)) にて % 演算子で剰余計算して出力している
問題8: CPU 使用率
top の出力からどのユーザが 何% CPU を使用しているか集計して下さい。
  1exec 3<&0 0<< EOF
  2`ps aux`
  3EOF
  4
  5while read u p c other
  6do
  7    if [ "${u}" != "USER" ]
  8    then
  9        case ${users} in
 10            *${u}* )
 11                ;;
 12            *    )
 13                users="${users} ${u}"
 14                ;;
 15        esac
 16        eval "$u=\$((\$$u + ${c%%.*}))"
 17    fi
 18done
 19
 20for i in ${users}
 21do
 22    echo "${i} : `eval echo '$'$i`"
 23done
            
CPU 使用率は小数点以下切り捨てで計算している。

出現したユーザID を users 変数に格納しておく。
eval と $((…)) を利用して、 ユーザID を変数名とした変数に CPU 利用率を集計している。
最後に for ループで users に格納された ユーザID とユーザIDを変数名とした変数に格納されている 集計値を表示している。
FreeBSD では top -b -n 1 が利用不可なので ps aux の出力を集計する

全てのスクリプトは FreeBSD 上の /bin/sh で動作確認済み。
手元に Linux 環境が存在しないので Linux での動作は未確認だけど、 posix 機能だけしか使用していない(筈)なので Linux でも動作すると思う。


Copyright © 2008-2020 Mitzyuki IMAIZUMI. All rights reserved.