Quantcast
Channel: Bashタグが付けられた新着記事 - Qiita
Viewing all 2787 articles
Browse latest View live

踏み台サーバーを経由してSSH接続する設定を~/.ssh/configに書く

$
0
0

注意

Macで踏み台サーバーを経由してSSH接続する方法です。
以下の記事もあわせてご参照ください。
公開鍵認証を使ってリモートサーバーにSSH接続する
SSH接続の設定を~/.ssh/configに書いておくと接続する際に長いコマンドを打つ必要がなくなる

やり方

ProxyCommandを使うと踏み台サーバーを経由してSSH接続することができます。

例えば、user@hoge.comを踏み台にしてuser@piyo.comにSSH接続する場合、~/.ssh/configには以下のように書きます。

Host hoge # ここは好きな名前でよい
User user
HostName hoge.com
IdentityFile ~/.ssh/hoge/id_rsa # user@hoge.comの秘密鍵までのパス

Host piyo # ここは好きな名前でよい
User user
HostName piyo.com
IdentityFile ~/.ssh/piyo/id_rsa # user@piyo.comの秘密鍵までのパス
ProxyCommand ssh hoge -W %h:%p # hogeは上でつけた名前

上記のように書くと以下のコマンドで踏み台サーバーを経由したSSH接続ができます。

ssh piyo

補足

ProxyCommandの%hは目的のサーバーであるuser@piyo.comに置換され、%pはデフォルトの22に置換されます。


SwiftLintで引っかかったエラーの種類ごとに集計するワンライナー

$
0
0

経緯

SwiftLintを導入するにあたり、autocorrectで修正されない部分を修正するのにルールごとの違反数単位で優先度を付けたかった
(あまりにも違反数が多い場合は一旦disableにして追々有効にして直す等の回避策も考えられるように)

愚直に書いた結果

swiftlint --quiet | awk '{ print $NF }' | sed -E 's/^\((.+)\)$/\1/g' | sort | uniq -c | sort | awk '{ printf "%s : %s\n",$2,$1 }'

出力結果(例)
class_delegate_protocol : 1
empty_string : 1
prohibited_super_call : 1
unused_capture_list : 1
unused_setter_value : 1
contains_over_filter_count : 2
function_parameter_count : 2
large_tuple : 2
valid_ibinspectable : 2
contains_over_filter_is_empty : 3
control_statement : 3
fatal_error_message : 3
multiline_parameters : 3
reduce_boolean : 3
syntactic_sugar : 3
discouraged_direct_init : 4
sorted_first_last : 4
vertical_parameter_alignment_on_call : 4
collection_alignment : 6
nesting : 6
operator_whitespace : 7
reduce_into : 7
toggle_bool : 7
for_where : 8
unavailable_function : 8
yoda_condition : 10
cyclomatic_complexity : 11
weak_delegate : 11
unneeded_break_in_switch : 12
first_where : 15
multiline_arguments : 30
private_action : 39
todo : 55

余談

sort | uniq -c の代わりに awk で集計する方法もあるみたいだけど、どうしても欲しい結果(件数順にソートして欲しいフォーマットで出力)にならなかったので諦めた😔

格闘の様子
swiftlint --quiet | awk '{ print $NF }' | sed -E 's/^\((.+)\)$/\1/g' | awk '{ v[$0]++ } END { for ( k in v ) print k ": " v[k] }'

ソートも何もされてないので論外

brew install gawk
swiftlint --quiet | awk '{ print $NF }' | sed -E 's/^\((.+)\)$/\1/g' | awk '{ v[$0]++; n = asorti(v, s) } END { for (i=1;i<=n;i++) print s[i] ": " v[s[i]] }'

キー(エラー種別)でソートされる(違う、そうじゃない)

lint --quiet | awk '{ print $NF }' | sed -E 's/^\((.+)\)$/\1/g' | awk '{ v[$0]++; n = asorti(v, s); for (i=1;i<=n;i++) a[i] = v[s[i]] ": " s[i]; asort(a) } END { for (x in a) print a[x] }'

文字列ソート扱いになった結果 1->20->2 みたいな出力になってオワタ\(^o^)/

誰か sort | uniq -c 使わずに綺麗に書けた方いたら教えて下さい🙏

参考

https://qiita.com/kazinoue/items/e7b98512186bace00097
http://tounderlinedk.blogspot.com/2010/09/asorti-awk.html
https://qiita.com/eumesy/items/3bb39fc783c8d4863c5f

macOS Catalinaのターミナルでの「デフォルトのシェルがzshになりました」というメッセージを抑止する方法

$
0
0

macOS Catalinaにアップデートしてからターミナルを開くと毎回鬱陶しいメッセージが表示されるようになった。

The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

これを抑止する方法。

~/.bash_profile に以下の1行を追加。

export BASH_SILENCE_DEPRECATION_WARNING=1

[元記事]

https://apple.stackexchange.com/questions/371997/suppressing-zsh-verbose-message-in-macos-catalina

ps.
zshはbashと(ほぼ)コンパチブルというようなことがどこかに書いてあったが、試したところいくつか動かなくなるスクリプトがあったので、すぐにbashに戻した。

プロセス名を部分一致指定で kill する

$
0
0

sbt のプロセス殺したくて、jps で検索した番号をいちいち指定するのがめんどかっただけ。

$ pkill -f sbt

おわり

Remote-SSHがラズパイでも使えるようになったので使ってみた

$
0
0

概要

Raspberry Piでは使えなかったVSCodeのRemote-SSHがバージョンアップでSupportedにラズパイが含まれていたので試してみました。

必要な環境

  • VScode(1.39.1)
  • Raspberry Pi(Raspberry Pi 3 Model B)

インストールする拡張機能

  • ms-vscode-remote.remote-ssh
  • ms-python.python(必要なら。pythonの実行/デバッグで使用)
  • rogalmic.bash-debug(必要なら。bashの実行/デバッグで使用)

セットアップ

拡張機能のインストール

Powershell から下記コマンドを実行。もしくはVSCodeの拡張機能(Ctrl + Shift + X)からRemote-SSHを検索してインストールする。

Powershell-拡張機能のインストール
PS> code --install-extension ms-vscode-remote.remote-ssh

リモートホストに接続

事前準備

SSH用フォルダがなければ作成する

Powersehll-フォルダ作成
PS> mkdir $HOME/.ssh

リモートホストの追加

VSCodeを起動。下記手順でリモートホストを追加する

vscode-remote-ssh-extension-01-1.png

ユーザー名とホスト名を入力
vscode-remote-ssh-extension-04.jpg

SSH設定の保存先を指定。事前準備で作成したフォルダを指定する
vscode-remote-ssh-extension-05.jpg

リモートホストに接続。VSCodeの右下にポップアップが出るのでConnectをクリック。SSH鍵を登録していない場合はSSHアカウントのパスワードを聞かれるので入力する

vscode-remote-ssh-extension-06.png

自分の環境では初回接続時は失敗したのでダイアログでRetryを選択しました。追加したホストはリモートエクスプローラーのSSH TARGETS一覧に表示されるので下記のようにして接続する事もできます。

vscode-remote-ssh-extension-01-2.png

リモートホストのフォルダを開く
vscode-remote-ssh-extension-09.png

以降、開いたフォルダでの操作(新規作成、編集など)はリモートホスト上の対象フォルダ配下に反映されます

リモートでプログラムを実行

Pythonの実行

リモートホストのPythonを実行してみる。
※なお、VSCodeの拡張機能は環境毎にインストールが必要

vscode-remote-ssh-extension-10.png

Pythonの場合、拡張子.py のファイルを開くと拡張機能のインストールをうながされるので指示に従うのが楽。

vscode-remote-ssh-extension-50.png

実行はタブの右にある:arrow_forward:アイコンをクリック
vscode-remote-ssh-extension-51.png

bashのデバッグ

拡張機能をインストール

拡張機能 rogalmic.bash-debug を検索してリモート環境にインストール

設定ファイルの作成

vscode-remote-ssh-extension-60.png

内容は以下↓

launch.json
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "bashdb",
            "request": "launch",
            "name": "Bash-Debug (simplest configuration)",
            "program": "${file}",
            "terminalKind": "debugConsole"
        }
    ]
}

デバッグ

任意のシェルスクリプトを開いてから、下記の手順でデバッグを開始する

vscode-remote-ssh-extension-61.png

まとめ

ローカルホストと同じような操作で実行やデバッグができるので便利!

Macで意識高い系のPS1(プロンプト)を設定する。

$
0
0

プロンプトで意識を高める

勉強会や、技術動画などでターミナルを使っている人のプロンプトを見ると、

■■■▶

みたいになっている人がよくいますよね。
これをMacのbashで出来るようにしてみます。
(シェルがzshの場合は変更するか諦めてください)

まず、フォントをダウンロードしてください。
「view raw」ををクリックするとダウンロードされます。

フォントがダウンロードされたら、ダブルクリックして「インストール」をクリックするとMacにインストールされます。

その後に、完全に私の備忘録になりますが、bashのプロンプト「PS1」を下記のように設定します。

PS1='\[\e[47;30m\]\!\[\e[42;37m\] \[\e[42;30m\] \u@\h \t \[\e[32;47m\]\[\e[30;47m\] \w \[\e[37;40m\]\[\e[37m\]\n$(__git_ps1 "\[\e[47;103m\]\[\e[30m\] %s \[\e[0;93m\]")\[\e[49m\]\[\e[39m\] \$ \[\e[40;37m\]'

こうすると、
1. ヒストリー番号
2. ユーザー名@ホスト名
3. 時間
4. カレントディレクトリ(フルパス)

になります。

bashスクリプトの最初の方に書いておくと少しだけ安心できる数行のコード

$
0
0

テストを完走したスクリプトが本番環境で動かない!などの不安を和らげる数行のコードで、個人的によく使う 3つを紹介します。

1. 環境変数 $PATH

systemd から実行したら動かないなどの問題が起きないように、環境変数$PATHの値を最初に整えます。スクリプト内で各コマンドをフルパスで書くより、環境変数をテスト環境と同じにした方が安心です。

for _path in $(echo "/usr/local/sbin:/usr/local/bin:/usr/sbin:/opt/myapp" | tr ':' '\n'); do
  echo ${PATH} | tr ':' '\n' | grep -q "^${_path}$" || PATH+=":${_path}"
done

現在の$PATHに含まれないパスが"/usr/local/sbin:/usr/local/bin:/usr/sbin:/opt/myapp"の中にあれば$PATHに追加されます。区切り文字の:を改行に置換して各パスを行に区切ることでgrepでの比較をしやすくしています。
(重複やべき等を気にしなければ、単にPATH+=で追加すればいいだけですが)

2. 多重起動防止

ファイルなどのリソースを操作するスクリプトで安心できるようになります。

(( $$ == $(pgrep -fo "$0") )) || exit 1

「同じコマンドライン文字列で実行された最も古いプロセス」が現在のプロセスなら通過、違っていたら多重起動と判定して異常終了します。実行時のコマンドライン文字列がいつも同じであることが条件になります。

3. 終了処理

trapで指定した関数on_exit()が終了時に実行されます。とりあえず書いておけば、スクリプト作成中に思いついた終了処理をすぐに追加できます。

function on_exit() {
  # $1: exit code
}

trap 'on_exit $?' EXIT

スクリプト内では、エラーで終了したいコマンドや関数の呼び出しをsomecommand || exit $?のように書きます。

関数on_exit()では終了コードの値を見て、正常終了時/異常終了時/共通の処理をそれぞれ記述することができます。

function on_exit() {
  if (($1 == 0)); then
    echo "Completed."
  else
    echo "Failed ($1)." >&2
  fi
  echo "Bye."
}

+1.

sh が bash にリンクされているかどうかに関係なく、使用するシェルは明示的に宣言します。

#!/bin/bash

参考リンク

シェルで標準出力と標準エラー出力を別々のファイルに書き出す

$
0
0

bashだと方法がたくさん出てくるのだけど、cshではなかなか見つからなかったので、メモ。

bash系

標準出力をfile1に、標準エラー出力をfile2に書き出し

command 1> file1 2> file2

標準出力と標準エラー出力を両方ともfileに書き出し

command &> file

csh系

標準出力をfile1に、標準エラー出力をfile2に書き出し

(command > file1) > file2

標準出力と標準エラー出力を両方ともfileに書き出し

command >& file

空出力

fileに/dev/nullを指定すると、空出力する


(Windows) Bashが管理者として実行されているか判定する(ついでにプロンプトに表示する)

$
0
0

Windowsのコマンドライン環境としてCmder とそれに同梱されているBashで作業をしていて,タイトルにあることがやりたくなったのでメモ代わりに初投稿です.

方法

バッチファイルでの方法(参考: https://commandprompt.noyokan.com/command/ck_admin.html, https://amksystem.com/cmd/cmd7/) と同様にWindowsの管理者権限を必要とするコマンドの返り値や出力で判定できる.(正規の方法はないんだろうか?)

今回はopenfilesを使ってみる.外部コマンドなのでbashからでも呼べる.

openfiles > /dev/null 2>&1
IS_ADMIN=$?

これでIS_ADMINに管理者権限の有無が入る.(若干紛らわしいが管理者として実行されている場合が0
$?がバッチファイルでいう%ERRORLEVEL%相当の特殊変数.

プロンプトに表示する

操作している端末が管理者として実行されているのかすぐ分かるように表示したかったというのが今回の動機.
管理者として実行されている場合にはプロンプトにその旨を表示するようにする.
.bashrcに次を書く.

openfiles > /dev/null 2>&1
IS_ADMIN=$?

_PS_ADMIN=''
if [ $IS_ADMIN -eq '0' ]; then
        _PS_ADMIN='(as Admin)'
fi

PS1='\[\033]0;$MSYSTEM:${PWD//[^[:ascii:]]/?}\007\]\[\033[32m\]\u@\h \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\] \033[31m$_PS_ADMIN\033[0m\n'

これで管理者として実行されている場合のみデフォルトのプロンプトに加えて赤字で(as Admin)と表示されるようになる.

おわりに

もっとましな方法やまともな書き方があったらご教授ください.

シェルスクリプト オプション解析 徹底解説 (getopt / getopts)

$
0
0

シェルスクリプトでオプションを解析といったらまず挙がるのが getoptgetopts です。さてどちらを使うべきでしょうか?

始めに断っておくと実は私はどちらも積極的には使っていません。なぜなら独自実装でもほとんどコードは変わらず、より柔軟な処理ができるからです。とはいえ getoptgetopts はシェルスクリプトの基本なのでこれらの使い方について解説したいと思います。(解説が不要な人はそれぞれの「使用方法」を読んでください。)

getoptgetopts の違いですが、まず getopt は外部コマンドです。getopt は 関数としては POSIX で規定されています (getopt)が、コマンドとしては規定されておらず、それに対して getopts はシェルビルトインコマンドで POSIX で規定されています (getopts)。ほぼすべてのPOSIX準拠シェルで実装されている getopts の方が移植性が高くなります。

オリジナル版(BSD, macOS 版も同等)の getopt は機能が最も少なく、GNU 版は高機能です。(なおこのドキュメントでは macOS 版と GNU 版で検証しています。)

また英語版 Wikipedia の getopts にはこう書かれています。

getopts was first introduced in 1986 in the Bourne shell shipped with Unix SVR3.

getopts was developed as an improvement to the original getopt Unix program.
The original getopt program had fewer features than getopts.

An alternative to getopts is the GNU enhanced version of getopt.

つまり歴史的にみると以下のようになると思われます。(想像です)

  1. より少ない機能を持った getopt (オリジナル版) が作られた。
  2. getopt の機能を強化した getopts が作られた。
  3. GNU 版では getopt を拡張することで getopts よりも高機能になった。
    (一方で macOS を 含む BSD 版は、オリジナル版と同等なので機能が少ない。)

それぞれの機能の違いの概略を以下に示します。

getopt getopts getopt (GNU版) 補足
ショートオプション Yes Yes (POSIX) Yes -x
結合されたショートオプション Yes Yes (POSIX) Yes -xy
オプション引数 Yes Yes (POSIX) Yes -x value, -xvalue
省略可能なオプション引数 - - Yes -x or -x value
引数に空白を含めることができる - Yes (POSIX) Yes "next day" のような引数
エラーメッセージの抑制 Yes Yes (POSIX) Yes
エラーメッセージのカスタマイズ - Yes (POSIX) -
ロングオプション - - Yes
オプションとオプション以外の混在 - - Yes
- で始まるロングオプション - - - -exec
+ で始まるオプション - zsh, ksh, mksh - +x

GNU 版 getopt が最も高機能ですが、BSD や macOS を考慮する場合は、getopts を使うしかありません。ただしロングオプションは使えません。

  • homebrew でよければ GNU版 getopt である gnu-getopt がインストール可能です。
  • 頑張れば getopts を使いつつロングオプションに対応することも出来ますが独自実装の方が簡単です。

さてこれよりそれぞれのコマンドの詳細な使い方について解説したいと思いますが、歴史的な流れに従って「getopt(オリジナル版)」「getopts」「getopt(GNU版)」の順で解説します。

getopt (オリジナル版)

まず GNU 拡張を含まない(おそらく)オリジナル版の getopt の解説です。macOS を含む BSD 版の動作も同じです。オリジナル版の getopt は引数に空白文字を含めることが出来ないという大きな欠点があるので推奨しません。移植性があり引数に空白文字を含めることができる getopts の方が優れているので、オリジナル版を使う理由はないのですが getopts と GNU 版 getopt の前提知識として必要なため解説します。

getopt は一般的にオプション解析をするためのコマンドと言われていますが、オプション解析をしやすくするために引数を整形するためのコマンドと捉えたほうが正確です。例えば -ab のようなオプションを -a -b に分離したり、-xvalue のようなオプションを -x value に分離したりします。

getopt の使用方法は以下のとおりです。

getopt optstring parameters

第一引数の optstring にオプションとして解析したい文字(このドキュメントでは「オプション文字列」と呼ぶことにします。)を指定します。parametersgetopt で整形させる引数です。

getopt による出力(整形)の例を以下に示します。

呼び出し 出力
getopt abc -a -b -c -a -b -c --
getopt abc -abc -a -b -c --
getopt abc -a -b -c foo bar baz -a -b -c -- foo bar baz
getopt a:bc -afoo -bc -a foo -b -c --
getopt abc -a foo -b bar -c -a -- foo -b bar -c
getopt a:bc -a foo -b bar -c -a foo -b -- bar -c

※ GNU 版では環境変数 POSIXLY_CORRECT を定義すると同等の出力になります。

getopt の機能

ショートオプション

オプション文字列に a と指定すると オプション -a が使えるようになります。オプションとオプション以外は -- を挟んで前後に分割されます。

呼び出し 出力
getopt a -a -a --
getopt a --
getopt a foo -- foo
getopt a -a foo -a -- foo

結合されたショートオプション

(オプション引数を取らない)オプションは -abc のようにまとめて書くことが出来ます。まとめて書いても getopt が分解してくれるのでオプション解析が楽になります。

呼び出し 出力
getopt abc -abc -a -b -c --

オプション引数

オプション文字列の各文字の後に : をつけると「オプション引数」を持つオプションという意味になります。オプション引数はオプションにつなげて指定する方法と、スペースで区切って次の引数で指定する方法があります。

呼び出し 出力 補足
getopt a: -a bc -a bc --
getopt a: -abc -a bc --
getopt a: -a -x -a -x -- この場合の -x-a のオプション引数であってオプションではありません

エラーメッセージ

不明なオプション、もしくはオプション引数がない場合は、エラーメッセージを出力し終了ステータスが非0で終了します。

呼び出し エラー出力(例)
getopt a: -x getopt: 無効なオプション -- 'x'
getopt a: -a getopt: オプションには引数が必要です -- 'a'

オプション文字列の先頭に : をつけるとエラーメッセージの出力を抑制することが出来ます。抑制するのはエラーメッセージの出力だけで、エラーが発生した時の終了ステータスは非0です。終了ステータスでエラーになったことはわかりますが、エラーとなったオプションや理由はわかりません。そのため getopt を使用する場合はデフォルトのエラーメッセージを使用したほうが良いでしょう。

使用方法

位置パラメータへの設定

まずオプションを解析を行うために getopt で整形した引数(出力)を位置パラメータ($1, $2 ...)に設定します。

args=$(getopt abc $*) || exit 1
# args=`getopt abc $*` 同等の意味の古い書き方

set -- $args
# eval "set -- $args" # zsh の shwordsplit に対応するにはこちらを使う

時折、以下のように変数に入れずにいきなり set している例を見かけますが、これだと getopt でエラーが発生しても set 呼び出しで正常終了になってしまうため間違った使い方です。(同様に for ... in に直接渡すのもよくありません。)

set -- $(getopt abc $*)

ところで macOS (BSD) の man getopt には以下のような記述があります。

you should not use `getopt abo: "$@"` since that would parse
the arguments differently from what the set command below does.

どうやら、getopt abc "$@" としてはいけないようです。おそらく IFS の値によって、set コマンドと異なる解釈になるのを避けるためだと思うのですが納得できていません。getopt コマンドは引数をスペース区切りでしか出力しないので、結局の所 IFS はスペース区切り前提にするしか無いと思います。釈然としませんがオリジナル版の getopt を使うことはないのでこれ以上追求しないことにします。理由をご存じの方はコメントで教えて下さい。

オプション解析

位置パラメータへ設定したら次はループを使ってオプションを解析していきます。ループには forwhile がありますが、どちらを使っても殆ど変わりません。

まずは for を使った例です。

# 位置パラメータへの設定
args=$(getopt a:b $*) || exit 1
set -- $args

# オプション解析のためのループ
for opt in "$@"; do # in "$@" を省略して for opt と書くことも出来ます。
  case $opt in
    -a) A_VALUE=$2; shift 2 ;;
    -b) B_FLAG=1; shift ;;
    --) shift; break ;;
  esac
done

次に while を使った例です。

# 位置パラメータへの設定 は 同じなので省略

# オプション解析のためのループ
while [ $# -gt 0 ]; do
  case $1 in
    -a) A_VALUE=$2; shift 2 ;;
    -b) B_FLAG=1; shift ;;
    --) shift; break ;;
  esac
done

ループ変数 opt を使用するか位置パラメータ $1 を使用するかの違いだけです。while の場合に shift が必要なのは当然として、なぜ for でも shift が必要なのでしょうか? それはオプション引数を参照するためです。for を使って現在のオプションが opt 変数に代入されたとしても、オプションの次にあるオプション引数はわかりません。処理済みのオプションを shift していくことで、オプション引数は常に位置パラメータ $2 から取得できるようになります。オプションは opt 変数、オプション引数は位置パラメータ $2 と違う方法で取得することになるので個人的には while の方が好みです。もしオプション引数が必要ないなら shift も必要なくなるので for の方がわずかにシンプルになります。

getopts

getopt が引数を整形するコマンドに対して、getopts は呼び出すたびに一つずつオプションを読み取る関数です。オリジナル版の getopt とは違い引数に空白文字が含まれていても問題ありません。解析したオプションは指定したシェル変数に代入され、オプション引数が存在する場合は OPTARG 変数に代入されます。全てのオプションの解析が終わると、getopts は 終了ステータスとして非0を返し、OPTIND 変数にはオプション以外の残りの引数(オペランド)を処理できるよう、最初のオペランドのインデックス番号が代入されます。シェルビルトインコマンドとして実装されたことによりシェル変数を使うようになったのが大きな特徴で getopt に比べてシンプルな使い方になっています。

getopts による出力例を以下に示します。

呼び出し OPT (実際は指定した名前) OPTARG OPTIND
getopts abc OPT -a -b -c 1回目 a, 2回目 b, 3回目 c 4
getopts abc OPT -abc 1回目 a, 2回目 b, 3回目 c 2
getopts abc OPT -a -b -c foo bar baz 1回目 a, 2回目 b, 3回目 c 4
getopts a:bc OPT -afoo -bc 1回目 a, 2回目 b, 3回目 c 1回目 foo 3
getopts abc OPT -a foo -b bar -c 1回目 a 2
getopts a:bc OPT -a foo -b bar -c 1回目 a, 2回目 b 1回目 foo 4

※ 全てのオプション解析が終わった時(= getopts が非0を返した時)に OPT 変数に代入される値は POSIX 仕様では未定義のようです。殆どのシェルでは ? になりますが zsh では最後に読み取ったオプションでした。

getopts の機能

getopts の使用法方は以下のとおりです。

getopts optstring name [args]

オプション文字列の意味は、オリジナル版の getopt と同じなので省略します。第二引数は変数名で読み取ったオプションが代入されます。argsgetopts に解析させたい引数で省略された場合は現在の位置パラメータ "$@" を指定したのと同じ意味になります。

getopts の仕様 によるとオプション文字列に使用できる文字は英数字のみで、その他の文字を使用した場合の結果は不定です。(The use of other option characters that are not alphanumeric produces unspecified results.) getopts でロングオプションに対応させる方法としてオプション文字列に - を含めるやり方がありますが、POSIX 的には好ましくありません。

エラーメッセージ

getopts では独自のエラーメッセージに変更することが可能です。エラーメッセージ抑制する方法は getopt と同じくオプション文字列の先頭を : にします。エラーが発生すると、エラーメッセージは抑制され (getopt の場合とは違い)終了ステータスは 0 となります。発生したエラーが「不正なオプション」の場合は第二引数で指定した変数に ? が代入され、OPTARG 変数に不正なオプション文字が代入されます。発生したエラーが「オプション引数がない」場合は第二引数で指定した変数に : が代入されます。

呼び出し OPT OPTARG
getopts :a OPT -x ? x
getopts :a: OPT -a :

※ bash では OPTERR 変数に 0 を設定してもエラーメッセージを抑制することができます。OPTERR 変数は POSIX の初期の提案でなされたものですが : を指定する方法に置き換わり bash 以外は対応していません。

+ で始まるオプション

zsh, mksh, ksh では + で始まるオプションを受け付けることが出来ますが、これは POSIX の仕様ではありません。

zsh と mksh では + で始まるオプションを受け付けるために特にすることはありません。そのため、+ で始まる(オプションではない)引数が意図せずオプションとして扱われてしまう可能性があるので注意してください。ksh ではオプション文字列の先頭(エラーメッセージを抑制する : がある場合はその次)を + にします。

呼び出し OPT (zsh, mksh) OPT (ksh)
getopts ab OPT +b +b ?
getopts +ab OPT +b +b +b
getopts +ab OPT ++ ++ ?

OPTIND について

getopts は実行するたびにオプションを一つずつ処理していきます。つまりどこまで処理したかという状態を持っているわけです。その状態を持っている変数が OPTIND 変数です。OPTIND 変数に 1 を代入するとこの状態はリセットされ、新たなオプションを解析することができます。

さて以下のようなコードで、実行するたびに OPTIND 変数がどのように変わるか確認してみましょう。

echo "OPTIND:$OPTIND (start)"

set -- -a -bc -d data -e

while getopts abcd:e OPT; do
  echo "OPT:$OPT OPTIND:$OPTIND"
done

echo "OPT:$OPT OPTIND:$OPTIND (end)"
行番号 出力 (dash, mksh) 出力 (bash, ksh, posh) 出力 (zsh) 出力 (yash)
1 OPTIND:1 (start) OPTIND:1 (start) OPTIND:1 (start) OPTIND:1 (start)
2 OPT:a OPTIND:2 OPT:a OPTIND:2 OPT:a OPTIND:1 OPT:a OPTIND:1:2
3 OPT:b OPTIND:3 OPT:b OPTIND:2 OPT:b OPTIND:2 OPT:b OPTIND:2:2
4 OPT:c OPTIND:3 OPT:c OPTIND:3 OPT:c OPTIND:2 OPT:c OPTIND:2:3
5 OPT:d OPTIND:5 OPT:d OPTIND:5 OPT:d OPTIND:5 OPT:d OPTIND:5
6 OPT:e OPTIND:6 OPT:e OPTIND:6 OPT:e OPTIND:5 OPT:e OPTIND:5:2
7 OPT:? OPTIND:6 (end) OPT:? OPTIND:6 (end) OPT:e OPTIND:6 (end) OPT:? OPTIND:6 (end)

なんとバラバラです。特に yash の出力はどういうことでしょうか?

OPTIND 変数の内容を次に処理すべきインデックス番号と仮定すると、-a -bc -d data -e という引数の場合に、1番目の引数 -a を処理するときは 1 ですが、さて2番目の引数のうち -c を処理するときはどうなるべきでしょうか?

つまりショートオプションが結合されてる場合ではインデックス番号の数値一つだけでは足りないのです。そのため yash では : 区切りで二つの数値を OPTIND 変数に持っているわけです。他のシェルでは内部に見えない状態変数を持っているようです。

getopts によると、OPTIND 変数は、getopts が非0で終了した時に、オプションを除くの最初の引数のインデックス番号になるとしか規定されてないようです。つまり途中の値がそれぞれ違っていても POSIX 仕様上は何の問題もなさそうです。(話がそれますが、zshの7行目の出力 OPT の値は e ですが POSIX ではオプション解析処理が終了したときに ? にするとなってるので POSIX 準拠してないようです。)

さて、上の方で「OPTIND 変数に 1 を代入するとリセットされる。」と書きました。その検証をしてみます。

set -- -o -x # パターン1
# set -- -ox # パターン2
getopts ox OPT1
OPTIND=1
getopts ox OPT2
echo "$OPT1 $OPT2" # すべて o o と表示されるはず
パターン 出力 (dash, bash, ksh, mksh, yash) 出力 (zsh) 出力 (posh)
パターン1 o o o x o o
パターン2 o o o x o x

zsh と posh ではリセットされませんでした。POSIX に準拠していないということでしょうか? zsh や posh では OPTIND 変数を unset することで状態をリセットできましたが、unset すると、dash でエラーになりました。0 を代入すると yash でおかしくなります。どのシェルでも動く汎用的なリセット方法は見つからなかったのでシェル毎にリセット方法を変えるしかなさそうです。

使用方法

もっともシンプルな例はこんな感じでしょうか?

while getopts a:b OPT; do
  case $OPT in
    a) A_VALUE=$OPTARG ;;
    b) B_FLAG=1 ;;
    *) exit 1 ;; # 不正なオプション または オプション引数がない場合(OPT = ?)
  esac
done
shift $((OPTIND - 1))

getopt と比べて簡単になっている点は、事前の引数の整形が必要ないので位置パラメータへの設定が不要であるのと、オプション引数を飛ばす処理を getopts が行うのでオプション毎の shift が不要になっている点です。代わりにオプション解析でエラーが発生したときの中断処理と残りの引数を処理するための shift $((OPTIND - 1)) が増えています。

上記のコードは、オプション解析でエラーが発生した時のメッセージを getopts にまかせていますが、メッセージをカスタマイズしたい場合は以下のようになります。エラーメッセージを抑制すると OPT 変数に入っている文字でエラーの内容を区別することができます。

while getopts :a:b OPT; do
  case $OPT in
    a) A_VALUE=$OPTARG ;;
    b) B_FLAG=1 ;;
    :) echo "Mission arg: -$OPTARG" >&2; exit 1 ;; # オプション引数がない (OPT = :)
    *) echo "Unknown option: -$OPTARG" >&2; exit 1 ;; # 不正なオプション (OPT = ?)
  esac
done
shift $((OPTIND - 1))

getopt (GNU版)

GNU 版の getopts の使用法方は以下のとおりです。

  1. getopt optstring parameters
  2. getopt [options] [--] optstring parameters
  3. getopt [options] -o|--options optstring [options] [--] parameters

1 はオリジナル版の getopt に近い動きとなり、引数に空白や特殊文字が使えません。 2, 3 が GNU で拡張された動きになります。個人的には 2 は直感的でないと感じるので、3 をおすすめします。

GNU 版の getopt による出力例を以下に示します。

呼び出し 出力
getopt -o abc -- -a -b -c -a -b -c --
getopt -o abc -- -abc -a -b -c --
getopt -o abc -- -a -b -c foo bar baz -a -b -c -- 'foo' 'bar' 'baz'
getopt -o a:bc -- -afoo -bc -a 'foo' -b -c --
getopt -o abc -- -a foo -b bar -c -a -b -c -- 'foo' 'bar'
getopt -o a:bc -- -a foo -b bar -c -a 'foo' -b -c -- 'bar'

GNU版 getopt の機能

ロングオプション

-l または --longoptions でロングオプションを指定できます。ロングオプションはカンマ区切りで複数指定できます。ロングオプションだけを指定する場合でも、ショートオプションを指定する -o または --options は省略できないことに注意してください。

呼び出し 出力 補足
getopt -o '' -l long-a,long-b -- --long-a --long-b --long-a --long-b --
getopt -o '' -l reverse,recursive -- --rev --reverse -- オプション名は短縮指定できます

ロングオプションのオプション引数

ショートオプションと同様に、オプション名の最後に : をつけることで指定します。オプション引数はスペースで区切る他、オプションに = でつないで渡します。

呼び出し 出力
getopt -o '' -l long-a: -- --long-a foo --long-a 'foo' --
getopt -o '' -l long-a: -- --long-a --long-a --long-a '--long-a' --
getopt -o '' -l long-a: -- --long-a=foo --long-a 'foo' --

省略可能なオプション引数

省略可能なオプション引数は、オプション名の最後に :: をつけることで指定します。オプション引数はショートオプションにつなげて渡すか、ロングオプションに = でつないで渡します。(スペースで区切って渡すことはできません。)

呼び出し 出力
getopt -o a::b -- -a -b -a '' -b --
getopt -o a::b -- -a foo -b -a '' -b -- 'foo'
getopt -o a::b -- -afoo -b -a 'foo' -b --
getopt -o '' -l long-a::,long-b -- --long-a --long-b --long-a '' --long-b
getopt -o '' -l long-a::,long-b -- --long-a foo --long-b --long-a '' --long-b -- 'foo'
getopt -o '' -l long-a::,long-b -- --long-a=foo --long-b --long-a 'foo' --long-b --

オプションとオプション以外の混在

GNU 版の getopt ではオプションとオプション以外を混在しても処理しやすい形に並び替えてくれます。途中でオプションとして -- 見つかった場合はそこで並び替えを中断します。この機能を無効にしてオリジナル版の getopt と同じ動きにするためには、オプション文字列の最初に + を置くか、POSIXLY_CORRECT 変数を定義します。

オプション文字列の先頭を - にすると、オプションでない引数は(-- の後ろのまとめられるのではなく)そのままの位置になります。(結合されたショートオプションの分解だけを行いたい時に使う?)

呼び出し 出力
getopt -o abc -- -a foo -b bar -c -a -b -c -- 'foo' 'bar'
getopt -o +abc -- -a foo -b bar -c -a -- 'foo' '-b' 'bar' '-c'
getopt -o -abc -- -a foo -b bar -c -a 'foo' -b 'bar' -c --
getopt -o abc -- -a foo -b -- bar -c -a -b -- 'foo' 'bar' '-c'
getopt -o abc -- -abc foo -b bar -c -a -b -c -b -c -- 'foo' 'bar'
getopt -o '' -l long -- --long=foo --long 'foo' --

単一の - で始まるロングオプション

-a または --alternative オプションを指定すると有効になります。find コマンドで使われてる形式のオプションですね。(例 find . -name "*.txt"- で始まるロングオプションは -- で始まるロングオプションに整形されます。

呼び出し 出力
getopt -o cetux -l exec -- -exec echo -e -x -e -c -- 'echo'
getopt -o cetux -l exec -a -- -exec echo --exec -- 'echo'
getopt -o cetux -l exec -a -- --exec echo --exec -- 'echo'
getopt -o cetux -l exec -a -- -execute echo -e -x -e -c -u -t -e -- 'echo'
getopt -o 'c:e:t:u:x:' -l exec -a -- -exec echo --exec -- 'echo'
getopt -o 'c:e:t:u:x:' -l exec -a -- -execute echo -e 'xecute' -- 'echo'

使用方法

ロングオプションなどの機能が追加されているだけで、使用方法はオリジナル版の getopt と殆ど変わりません。getopts に比べるとロングオプションが使えるなど機能は増えましたが、getopts のシンプルさはなくなりました。

# 位置パラメータへの設定
args=$(getopt -o a:b -l long-a:,long-b -- "$@") || exit 1 # $@ を使うこと。$* では空白を正しく扱えない
eval "set -- $args" # オリジナル版と違いクォートでくくられるので eval する必要がある

# オプション解析のためのループ
while [ $# -gt 0 ]; do
  case $1 in
    -a | --long-a) A_VALUE=$2; shift 2 ;;
    -b | --long-b) B_FLAG=1; shift ;;
    --) shift; break ;;
  esac
done

実際に書いてみるとすぐに気づくと思うのですが、ロングオプションの数が多くなると getopt の引数は長くなり case の部分とニ箇所に同じオプションを書かなければなりません。これは getopt が引数を整形する機能しかないからです。実際の引数の解釈は別の case で行わなければなりません。ショートオプションだけならまだマシですが、ロングオプションが増えてくると大変になってきます。

なお Ubuntu 18.04 の man getopt より使用例が書かれたファイルがあることがわかるのですが、正しいパスは /usr/share/doc/util-linux/examples/getopt-parse.bashgetopt-parse.tcsh)でした。


(ba)sh と (t)csh での使用例のスクリプトは、 getopt(1) ディストリビューションで提供されている。
これらはオプションとして /usr/local/lib/getopt または /usr/lib/getopt にインストールされている。

まとめ

以上、getoptgetopts について徹底的に解説してみましたが、結局私は使わないんですよね・・・。以下が私の方針です。

  • オリジナル版(BSD版)の getopt は使用しません。
  • ショートオプションだけの簡単なものであれば getopts を使用します。
  • ロングオプションやその他の機能が欲しくなったら独自実装します。
  • GNU版の getopt は特定の環境だけで動けばよく簡易なものを手っ取り早く作りたいときには使うかもしれません。

独自実装のオプション解析コードについて

getopt, getopts を使用しない独自実装のオプション解析コードについては 「高機能で短いシェルスクリプト用のオプション解析コード(POSIXシェル準拠・独自実装) 」を参照してください。

関連リンク

パイプ処理の終了ステータスの取得方法

$
0
0

パイプしたときの終了ステータスの取得方法

パイプ処理の終了ステータスを取得する
パイプ処理を行った場合、特殊変数 $? に設定される値は
一番最後に実行されたコマンドの終了ステータスとなる。

例)

sample.sh
#!/bin/bash

$ exit 0 | exit 1 | exit 2
$ echo $?
2

パイプ処理の途中のコマンドの終了ステータスの取得方法

パイプ処理の最後のコマンドの終了ステータスではなく、
パイプ処理の途中で実行しているコマンドの終了ステータスを取得したい場合の方法

特殊変数 $PIPESTATUS (配列)を使用することで、
パイプ処理で実行された各コマンドの終了ステータスを取得することができる (※bash 限定で ksh は不可)

sample.sh
#!/bin/bash

$ exit 0 | exit 1 | exit 2
$ echo ${PIPESTATUS[@]}
0 1 2

# $PIPESTATUS [@]で配列の全要素を出力している
# @を数字に変えれば指定した位置のコマンドの終了ステータスを取得可能

コマンドの実行結果と各コマンドの終了ステータスの両方取得したい

コマンドを実行した後にパイプ処理でPIPESTATUSをexitで終了ステータスを指定する

sample.sh
#!/bin/bash

result=`exit 1 | echo "test"`
echo $?
echo $result

result=`exit 1 | echo "test2" ; exit ${PIPESTATUS[0]}`
echo $?
echo $result

#出力結果
0
test
1
test2

※備忘録のためわかりにくい部分ありましたらコメントください

Windows10でSSHサーバーを立ててデフォルトシェルをBashにする

$
0
0

LinuxっぽくWindowsにSSH接続する方法です。
OpenSSHだとデフォルトシェルがコマンドプロンプトになってしまうので、Bashに変更する方法です。

手順

  • OpenSSHをインストール
  • デフォルトシェルを変更
  • sshdサービスを自動起動

OpenSSHをインストール

  • Windows10 1803からOpenSSHサーバーが正式につかえるようになった
  • スタートメニューの[アプリと機能]-[オプション機能の管理]-[機能の追加]でOpenSSH サーバーを選択して[インストール]をクリック

デフォルトシェルをBashに変更

  • デフォルトではデフォルトシェルがコマンドプロンプトになってる涙
  • powershellを起動して以下を実行。ここでは、Git for Windowsのbashを指定。
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Program Files\Git\bin\bash.exe" -PropertyType String -Force 

sshdサービスを自動起動

  • Start-Service sshdでsshdが起動
  • サービス(services.msc)を起動して、"OpenSSH SSH Server"のスタートアップの種類を「手動」から「自動」へ変更

Q&A

  • Q: わざわざSSHで入らなくてもリモートデスクトップでアクセスすればいいじゃん。
    • A: Homeエディションなのでリモデが使えません。。(非公式リモデだといつ使えなくなるか怖いし。)あと、Chromeのリモデは解像度が調整できないので×。
  • Q: WSL使わないの?
    • A: WSLだとGPUが使えないので×。
  • Q: Ubuntu入れればいいじゃん。
    • A: 事務用にも使ってるPCなので、Officeが使いたいんですよ。

リファレンス

高機能で短いシェルスクリプト用のオプション解析コード(POSIXシェル準拠・独自実装)

$
0
0

はじめに

シェルスクリプト用のオプション解析コードのサンプルは検索するといくつも見つかるのですが、機能が不足していたり雛形とするには長くメンテナンスしづらいものばかりだったので作成しました。以下のような特徴があります。

  • POSIXシェル準拠なので bash 以外でも動作可能(dash, zsh, ksh, mksh, yash 等で動作確認)
  • ロングオプションにも対応(getopt, getopts 未使用)
  • オプションとオプション以外の引数の順番を混在させることが可能
  • 単一の - をオプションではなく引数として扱う
  • オプション解析を打ち切る -- に対応
  • 結合されたショートオプションに対応(例 -abc
  • 省略可能なオプション引数に対応(例 -ovalue, --option=value
  • フラグを反転させるオプションに対応(例 +o, --no-option
  • 必要な機能のみ使えるように段階的に実装
  • コードが短くメンテナンスしやすい
  • shellcheck によるチェックと shellspec を使ったテストコードあり

ちなみに独自実装しているのは getopt がロングオプション対応してるのが GNU 版だけで macOS では動かないからです。getopt(と getopts)を使用せずに、十分に置き換え可能な機能を実装しています。また本質的なコードよりもオプション解析コードの方が長いなんて事にならないよう短く仕上げています。そのため少し変わったコーディングスタイルになってますので気に入らない人は好きに書き換えてください。雛形(サンプルコード)として作成してるので自由に変更して使ってもらって構いません。

実際に動作するサンプルコードは こちら です。参考ですが getopt, getopts の詳細な解説は「シェルスクリプト オプション解析 徹底解説 (getopt / getopts)」で記事にしています。

※ これまで自分が書いていたコードをベースに機能追加して新たに作成したものなのでバグがあるかもしれません。使用する場合は十分確認をお願いします。

サンプルコード一覧

  1. オプション引数が必要ない場合
  2. オプション引数に対応する
  3. オプションとオプション以外の引数の順番の混在に対応する
  4. -- でオプション解析処理を打ち切る
  5. 結合されたショートオプションに対応する
  6. 結合されたオプション引数に対応する
  7. 省略可能なオプション引数に対応する
  8. +, --no- でフラグを反転させる

上から解説を加えつつ機能を実装しています。使用する際は全てを実装する必要はなく必要だと思う所までで十分です。殆どの場合 4. まで実装すれば必要十分だと思います。5. 以降を実装すると GNU 版の getopt と比べても遜色がない機能になりますがコードの量が増えます。解説の都合上、一度定義した関数は省略しています。例えば unknown 関数は 1. で定義しています。

1. オプション引数が必要ない場合

全ての引数がオプションまたはオプション以外の引数で、オプション引数が必要なければ for を使って簡潔に書くことができます。

abort() { echo "$*" >&2; exit 1; }
unknown() { abort "unrecognized option '$1'"; }

FLAG_A='' FLAG_B=''

for arg; do
  case $arg in
    -a | --flag-a) FLAG_A=1 ;;
    -b | --flag-b) FLAG_B=1 ;;
    -?*) unknown "$@" ;;
    *) set -- "$@" "$arg"
  esac
  shift
done

このコードはオプションを環境変数に設定し、オプション以外の引数を位置パラメータ $@ に残します。多少分かりづらいと思うので説明します。

  1. まず for arg in "$@" により $@ がループの要素リストとしてメモリに保持されます。
  2. この状態で for ループの中で shift を行っても、メモリに保持された方の $@ には影響がありません。
    言い換えると、引数が5個ある場合、for ループの中で何度 shift を行っても5回ループします。
  3. ループの中にある shift により、ループする前に $@ にあった引数はすべてなくなります。
  4. しかし、ループの中でオプション以外の引数を set -- "$@" "$arg"$@ に追加しています。
  5. つまり、最初あった引数はなくなりますが、新たに追加した引数が $@ に残ります。

for を使うのはこのパターンだけです。なぜなら例えば --flag1 --file data.txt --flag2 という引数の場合、data.txt の部分はオプション引数なので位置パラメータに残したくありません。ですが for ループは引数を一つずつ繰り返すため data.txt を飛ばすことができません。また -abc--option=value のような一つの引数が複数の意味を持つ場合の対応も難しくなります。while を使用すればそのような処理をシンプルに書くことができます。

2. オプション引数に対応する

オプション引数に対応させます。

required() { [ $# -gt 1 ] || abort "option '$1' requires an argument"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J=''

while [ $# -gt 0 ]; do
  case $1 in
    -a | --flag-a) FLAG_A=1 ;;
    -b | --flag-b) FLAG_B=1 ;;
    -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
    -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
    -?*) unknown "$@" ;;
    *) break
  esac
  shift
done

1. との差分(関数と変数定義以外)
-for arg; do
-  case $arg in
+while [ $# -gt 0 ]; do
+  case $1 in
     -a | --flag-a) FLAG_A=1 ;;
     -b | --flag-b) FLAG_B=1 ;;
+    -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
+    -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
     -?*) unknown "$@" ;;
-    *) set -- "$@" "$arg"
+    *) break
   esac
   shift
 done

3. オプションとオプション以外の引数の順番の混在に対応する

オプションとオプション以外の引数の順番を混在できるようにします。

param() { eval "$1=\$$1\ \\\"\"\\\${$2}\"\\\""; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

2. との差分(関数と変数定義以外)
+parse_options() {
+  OPTIND=$(($# + 1))
 while [ $# -gt 0 ]; do
   case $1 in
     -a | --flag-a) FLAG_A=1 ;;
@@ -12,10 +14,14 @@
     -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
     -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
     -?*) unknown "$@" ;;
-    *) break
+      *) param PARAMS $((OPTIND - $#))
   esac
   shift
 done
+}
+
+parse_options "$@"
+eval "set -- $PARAMS"

新しく増えたのは param 関数です。オプション以外の引数を取り出すための PARAMS 変数を組み立てています。変数名を変更できるようにするため eval を使った読みづらいコードとなっていますが、最終的に PARAMS="$PARAMS \"\${$((OPTIND - $#))}\"" というコードを実行します。例えばコマンドライン引数が -a foo -b bar の場合 PARAMS 変数には "${2}" "${4}" という文字列が入ります。そしてこの文字列を eval "set -- $PARAMS" で位置パラメータ $@ に取り出しています。bash 等であれば配列に引数を入れていくだけで良いのですが、POSIX 準拠の範囲では配列が使えないのでこのような方法を使っています。

また OPTIND 変数を本来とは違う用途で使用しているので注意してください。本来は getopts コマンドによって使用されるシェル変数で、これを違う目的に使い回すのは一般に良くないことですが、シェルスクリプトには(POSIX準拠の範囲では)グローバル変数しかなく衝突を避けるためにこのようにしています。オプション解析を自前で行うので OPTIND 変数が本来の用途で使われることはまずないので無害でしょう。気になる方は別の名前に変更してください。

4. -- でオプション解析処理を打ち切る

引数の途中で -- が出てきたときに、そこでオプション解析処理を打ち切るための対応です。

params() { [ "$2" -ge "$3" ] || params_ "$@"; }
params_() { param "$1" "$2"; params "$1" $(($2 + 1)) "$3"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

3. との差分(関数と変数定義以外)
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ]; do
     case $1 in
       -a | --flag-a) FLAG_A=1 ;;
       -b | --flag-b) FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"

新しく増えたのは params 関数です。-- が現れたらそれ以降の引数をすべて PARAMS 変数に追加しているだけです。

5. 結合されたショートオプションに対応する

ls -al のようにショートオプションをつなげたものへの対応です。

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ]; do
    case $1 in
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) FLAG_A=1 ;;
      -b | --flag-b) FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

4. との差分(関数と変数定義以外)
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ]; do
+    case $1 in
+      -[!-]?*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+    esac
+
     case $1 in
       -a | --flag-a) FLAG_A=1 ;;
       -b | --flag-b) FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"

例えば -abcd のようになってる場合-a -bcd と最初の一文字と残りに分解しています。この処理をループのたびに行うことで、その他のコードを修正すること無く結合されたショートオプションに対応しています。

OPTIND 変数と同様に OPTARG 変数も本来の目的とは違った用途で使用しているので注意してください。OPTARG は結合されたショートオプションを分解するためのワーク変数として使用しています。

6. 結合されたオプション引数に対応する

-i value-ivalue--arg-i value--arg-i=value という指定の仕方もできるようにするための対応です。

noarg() { [ ! "$OPTARG" ] || abort "option '$1' doesn't allow an argument"; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      -[ij]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

5. との差分(関数と変数定義以外)
 parse_options() {
   OPTIND=$(($# + 1))
-  while [ $# -gt 0 ]; do
+  while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
+      --?*=*) OPTARG=$1; shift
+        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+      -[ij]?*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
     esac

     case $1 in
-      -a | --flag-a) FLAG_A=1 ;;
-      -b | --flag-b) FLAG_B=1 ;;
+      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
+      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"

「結合されたオプション引数」は 5. の「結合されたショートオプション」と区別する必要があるので、どれが「結合されたオプション引数」であるかを判定するコードが必要になります。上記コードの -[ij]?*) が該当のコードです。(case の部分と二重管理に定義する必要があるので少し嫌ですね。)

また OPTARG は「結合されたオプション引数だったか?」を判定するフラグ変数としても再利用しています。

7. 省略可能なオプション引数に対応する

省略可能なオプション引数とは、「-i または -ivalue」「--arg-i または --arg-i=value」という指定ができるオプションです。-i value--arg-i value という指定の仕方はできないので注意してください。(GNU 版の getopt も同じ仕様です。)

optional() { [ "$OPTARG" ] && OPTARG=$2; }

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      -[ijop]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
    esac

    case $1 in
      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
      -p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      -?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

6. との差分(関数と変数定義以外)
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
       --?*=*) OPTARG=$1; shift
         set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
-      -[ij]?*) OPTARG=$1; shift
+      -[ijop]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
     esac

     case $1 in
       -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
       -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
+      -o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
+      -p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
       -?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"

OPTARG はさらに省略可能なオプション引数の値を入れた変数としても使用しています。

8. +, --no- でフラグを反転させる

GNU 版の getopt でも対応してない機能ですが、比較的よく使われている機能なので対応しています。

FLAG_A='' FLAG_B='' ARG_I='' ARG_J='' OPT_O='' OPT_P='' PARAMS=''

parse_options() {
  OPTIND=$(($# + 1))
  while [ $# -gt 0 ] && OPTARG=; do
    case $1 in
      --?*=*) OPTARG=$1; shift
        set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
      --no-*) unset OPTARG ;;
      -[ijop]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
      -[!-]?*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
      +??*) OPTARG=$1; shift
        set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
      +*) unset OPTARG ;;
    esac

    case $1 in
      [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
      [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
      -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
      -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
      -o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
      -p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
      --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
      [-+]?*) unknown "$@" ;;
      *) param PARAMS $((OPTIND - $#))
    esac
    shift
  done
}

parse_options "$@"
eval "set -- $PARAMS"

7. との差分(関数と変数定義以外)
 parse_options() {
   OPTIND=$(($# + 1))
   while [ $# -gt 0 ] && OPTARG=; do
     case $1 in
       --?*=*) OPTARG=$1; shift
         set -- "${OPTARG%%\=*}" "${OPTARG#*\=}" "$@" ;;
+      --no-*) unset OPTARG ;;
       -[ijop]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "${OPTARG#??}" "$@" ;;
       -[!-]?*) OPTARG=$1; shift
         set -- "${OPTARG%"${OPTARG#??}"}" "-${OPTARG#??}" "$@"; OPTARG= ;;
+      +??*) OPTARG=$1; shift
+        set -- "${OPTARG%"${OPTARG#??}"}" "+${OPTARG#??}" "$@"; unset OPTARG ;;
+      +*) unset OPTARG ;;
     esac

     case $1 in
-      -a | --flag-a) noarg "$@"; FLAG_A=1 ;;
-      -b | --flag-b) noarg "$@"; FLAG_B=1 ;;
+      [-+]a | --flag-a | --no-flag-a) noarg "$@"; FLAG_A=${OPTARG+1} ;;
+      [-+]b | --flag-b | --no-flag-b) noarg "$@"; FLAG_B=${OPTARG+1} ;;
       -i | --arg-i ) required "$@" && shift; ARG_I=$1 ;;
       -j | --arg-j ) required "$@" && shift; ARG_J=$1 ;;
       -o | --opt-o ) optional "$@" && shift; OPT_O=$OPTARG ;;
       -p | --opt-p ) optional "$@" && shift; OPT_P=$OPTARG ;;
       --) shift; params PARAMS $((OPTIND - $#)) $OPTIND; break ;;
-      -?*) unknown "$@" ;;
+      [-+]?*) unknown "$@" ;;
       *) param PARAMS $((OPTIND - $#))
     esac
     shift
   done
 }

 parse_options "$@"
 eval "set -- $PARAMS"


FLAG_A 変数は、フラグが立っているときは 1 立っていないときは空文字になります。0 でないのは [ "$FLAG_A" ] という短い書き方でフラグを判定することができるようにするためです。

OPTARG をさらに反転させたフラグであるかを判断するために使用しています。

対応してない機能について

  • 単一の - で始まるロングオプション (例 -exec)
    • GNU 版の getopt で使用可能だが、find コマンドと Java や PowerShell 関連でしか見たことがなく、シェルスクリプトで実装するものとしては需要は少ないと思われるため
  • + で始まる結合されたオプション引数 (例 +ovalue)
    • zsh, ksh, mksh で使用可能だが、使われてる例を知らないため
  • オプション名を短縮名で指定できるようにする
    • コードが長くなるし、個人的にあまり好きな機能ではないので

さいごに

このコードは必要な部分をコピーして手直して組み込むことを想定したものですが、ライブラリの形にすると更に使いやすくなりそうです。気が向いたら作るかもしれません。

bash > fileIO > 並列化 > ls + awk > 入力ファイルをINTVLとINDX指定で分割する > コマンド | bashスクリプト v0.1, v0.2 | brace expansionによる失敗

$
0
0
動作環境
CentOS Linux release 7.7.1908 (Core)

状況

  • OpenMPで処理が遅いので、自分が考える最適な並列化に変更する
  • そのためにはlsで読込むファイルリストを分割する方法が必要となる

分割対象ファイル

$ ls DATA/
inp.001  inp.002  inp.003  inp.004  inp.005  inp.006  inp.007  inp.008  inp.009  inp.010  inp.011  inp.012

環境変数とawkを使った分割例

$ INTVL=3 && INDX=1 && ls DATA/inp.0* | awk '(NR % '$INTVL' == '$INDX'){print $1}'
DATA/inp.001
DATA/inp.004
DATA/inp.007
DATA/inp.010

環境変数にするとパイプで渡す必要がなくなる。

bashスクリプト v0.1

loop_interval_191023_exec

loop_interval_191023_exec
#!/usr/bin/env bash

function myls ()
{
    INTVL=$1 && INDX=$2 && ls DATA/inp.0* | awk '(NR % '$INTVL' == '$INDX'){print $1}'
}

if [ $# -lt 2 ];then
    echo "[cmd] [interval] [start]"
    echo "e.g."
    echo "[cmd] 4 1"
    exit
fi

INTERVAL=$1
START=$2

for elem in $(myls $INTERVAL $START);
do
    echo $elem
done

実行例 > パラメータ不足

$ bash loop_interval_191023_exec 
[cmd] [interval] [start]
e.g.
[cmd] 4 1

実行例 > パラメータ指定

$ bash loop_interval_191023_exec  4 1
DATA/inp.001
DATA/inp.005
DATA/inp.009

bashスクリプト v0.2

対象ファイルを指定できるように変更

loop_interval_191023_exec
#!/usr/bin/env bash

function myls_skipped ()
{
    INTVL=$1 && INDX=$2 && ls $3* | awk '(NR % '$INTVL' == '$INDX'){print $1}'
}

if [ $# -lt 3 ];then
    echo "[cmd] [interval] [start] [Dir/prefix]"
    echo "e.g."
    echo "[cmd] 4 1 DATA/inp.0"
    exit
fi

INTERVAL=$1
START=$2
TARGET_DIR_WITH_PREFIX=$3

for elem in $(myls_skipped $INTERVAL $START $TARGET_DIR_WITH_PREFIX);
do
    echo $elem
done

実行例 > パラメータ不足

$ bash loop_interval_191023_exec 
[cmd] [interval] [start] [Dir/prefix]
e.g.
[cmd] 4 1 DATA/inp.0

実行例 > パラメータ指定

$ bash loop_interval_191023_exec  4 1 DATA/inp.0
DATA/inp.001
DATA/inp.005
DATA/inp.009

備考

$ bash loop_interval_191023_exec  4 1 DATA/inp.0*

のように使うとBrace Expansionにて展開された引数を関数mylsが受けることになり失敗する。

そのために上記のようにした。

関連

下記は上記の実装の先で使った情報。

ファイル名の最初の6文字だけ保持してrenameする

$
0
0

bmpファイルの最初の6文字を残す処理.

  • forでファイル名のループ
    • for; do; doneでくくる(基本)
  • $iがファイル名
  • ${i:0:6}$iの0から6文字目を表す
    • 拡張子が固定だから.bmpを足す
for i in *.bmp ; do mv $i ${i:0:6}.bmp ; done

bash > fileIO > 並列化 > ls + awk > 入力ファイルをINTVLとINDX指定で分割する > bashスクリプト v0.3

$
0
0
動作環境
CentOS Linux release 7.7.1908 (Core)

状況

  • bashスクリプト(ImageMagickのconvertコマンドを実行する)を並列化したい
  • 自作のlsで並列化用のファイルリスト分割をする

v0.1, v0.2

v0.3

v0.2の場合、DIR/PREFIX*のようなアスタリスクで終わる場合には対応していたが、DIR/*.psのように後ろに文字指定したい場合には使用できなかった。

今回はBrace Expansionの結果を配慮して対応することにした。

  • 1つ目と2つ目の引数は分割の指定用
    • 1つ目: インターバル
    • 2つ目: 開始インデックス
  • 3つ目以降にファイルリストがある (Brace Expansionの結果)

code

my_ls_skipped_191024_exec
#!/usr/bin/env bash

function myls_skipped()
{
    INTVL=$1 && INDX=$2 && echo $* | awk '{$1="";$2="";print}' | \
    xargs --max-args=1 | awk '(NR % '$INTVL' == '$INDX'){print $1}'
}

if [ $# -lt 3 ];then
    echo "[cmd] [interval] [start] [list of files]"
    echo "e.g."
    echo "[cmd] 4 1 *.ps"
    exit
fi

for elem in $(myls_skipped $*);
do
    echo $elem
done

実行分が長くなったので行連結とした。

実行例

$ bash my_ls_skipped_191024_exec 3 0 DATA/*.ps                               
DATA/tmp3.ps
$ bash my_ls_skipped_191024_exec 3 1 DATA/*.ps                               
DATA/tmp1.ps
DATA/tmp4.ps
$ bash my_ls_skipped_191024_exec 3 2 DATA/*.ps                               
DATA/tmp2.ps
DATA/tmp5.ps

シェルで変数とその内容を全部出力する方法

$
0
0

デバッグ時、エラー出力時に便利です。

引数なしのsetコマンドで全て出力されます。
set

Bashの場合は以下です。
(set -o posix; set)
POSIXと異なり、setだけだと変数に加え関数まで出力されてしまいます。
そこでPOSIXモードで出力すれば変数だけになる仕組みです。

bashrcをOS等の環境ごとに異なる設定にする書き方

$
0
0

bashrcを環境ごとに異なる設定にする書き方

OS、ディストリビューション、ホスト、ターミナルごとに異なる設定を行う場合の書き方まとめ
(それとbashrcが長くなりすぎる場合の他ファイルへの分割の仕方)

OSごとに固有の設定

OSによる場合分けはunameで判定する

case `uname -a` in
  Darwin* )
    # Darwin(MacOS)に固有の設定はここに書く
    # 例)brew
    [ -f "/usr/local/bin/brew" ] && export HOMEBREW_CASK_OPTS="--appdir=/Applications"
    ;;
  Linux* )
    # Linuxに固有の設定はここに書く
    ;;
  MINGW* )
    # WindowsのMINGWに固有の設定はここに書く
    ;;
esac

Linuxのディストリビューションごとに固有の設定

ディストリビューションは/etc/os-releaseのNAMEの値で判定する

if [ -f "/etc/os-release" ]; then
  read line < /etc/os-release && eval ${line}
  case ${NAME} in
    "CentOS Linux" )
      # CentOSに固有の設定はここに書く
      ;;
    "Ubuntu" )
      # Ubuntuに固有の設定はここに書く
      export LANG="en_US.utf8"
      export LC_ALL="en_US.utf8"
      ;;
    * )
      # その他の設定はここに書く
      ;;
  esac
fi

ホスト(端末)ごとに固有の設定

ホストごとに固有の設定はHOSTNAMEで判定する

case ${HOSTNAME} in
  homePC )
    # 自宅PCに固有の設定はここに書く
    ;;
  officePC )
    # 会社PCに固有の設定はここに書く
    # 例)プロキシ設定
    export http_proxy="~~"
    export https_proxy="~~"
    export ftp_proxy="~~"
    ;;
  192-168* )
    # 例)192-168で始まるホスト名に固有の設定はここに書く
    ;;
esac

ターミナルの種類ごとに固有の設定

ターミナルに固有の設定はTERMで判定する
下記は$TERMがxterm-colorか-256colorを含む場合に色をつける設定の例

case "$TERM" in
  # カラー対応の端末ならフラグを立てる
  xterm-color|*-256color) color_prompt=yes;;
esac
if [ "$color_prompt" = yes ]; then
  # カラー端末用のプロンプト(レポジトリ名も表示する)
  PS1='\[\e[1;36m\]$(__git_ps1 "(%s)")\[\e[1;32m\]\u\[\e[0;32m\]@\h:\[\e[1;33m\]\W \[\e[00m\]\$ '
else
  # モノクロ端末用のプロンプト
  PS1='\u@\h:\W \$ '
fi
unset color_prompt force_color_prompt

bashrcが長くなる場合は他ファイルに分割する

例としてホストごとに固有の設定を他のファイルに書く場合、
bashrc内で自動的にファイル名を特定できるよう、ファイル名を「.bash_ホスト名」としておく。
実際に読み込む部分は下記の通り。
同様にOSごとやディストリビューションごとの設定も他ファイルに分割できる。

# .bash_${HOSTNAME}のファイルが存在すればそれを読み込む
[[ -f "${HOME}/.bash_${HOSTNAME}" ]] && source "${HOME}/.bash_${HOSTNAME}"

bashで複数選択のメニューを作る

$
0
0

検索してもあまり情報が出てこなかったので、自分用にメモ。

関数

multiple_select() {
  local PS3="$1(複数選択する場合は、半角スペースを開けてください)> "
  local items="$2 exit"
  local itemarray=($items)
  local itemlen=${#itemarray[@]}
  local regex='^[1-9][0-9]*$'
  local res=''

  select selection in $items; do
    for i in $REPLY; do
      if ! [[ $i =~ $regex && $i -le $itemlen ]]; then
        echo "入力値が不正です。" 1>&2
        return 1
      fi
      if [ $i -eq $itemlen ]; then
        echo ""
        return 0
      fi
      # 設定済みの場合は無視する
      if [[ $res == *"${itemarray[i-1]}"* ]]; then
        continue
      fi
      res+=" ${itemarray[i-1]}"
    done
    echo $res
    return 0
  done
}

使い方

my_favorite_fruits=`multiple_select \
  "好きな果物は何ですか?" \
  "りんご みかん いちご バナナ"` \
|| exit 1

出力

1) りんご
2) みかん
3) いちご
4) バナナ
5) exit
好きな果物は何ですか?(複数選択する場合は、半角スペースを開けてください)> 1 2 2 4 1 4 4
りんご みかん バナナ

1) りんご
2) みかん
3) いちご
4) バナナ
5) exit
好きな果物は何ですか?(複数選択する場合は、半角スペースを開けてください)> a
入力値が不正です。

【Bash】Shellの関数内だけsetコマンドオプションを有効化

$
0
0

普段Bashを使うことが多いので、よく使うコマンドとかワンコマンドで自動化したいものを関数化しています。関数のログとか未定義の変数を使用時にエラーメッセージを出力するようにしたかったのでsetコマンドを使用しています。ただそれを関数内だけ有効にしないと関数以外の普通のコマンドを実行した時も出力されてしまうので、setコマンドのオプションを無効化する必要がありました。
備忘録としてその方法を説明したいと思います。

setコマンドオプションの無効化

公式マニュアル

setコマンドオプションの無効化ですが、公式マニュアルには以下のように書かれていました。

Using + rather than - causes these options to be turned off.

-でオプションを指定して有効化しますが、+を使えばオプションを無効にすることができます。

方法

まずは-で有効化したいオプションを指定します。指定できるオプションはマニュアルを参照してください。

set -eux

3つのオプションを無効にするには+を使います。

set +eux

それぞれを関数の先頭行と最終行に記述すれば、関数内だけ実行コマンドのログやエラーメッセージを出力できるようになります。こんな感じに↓

function test {
    set -eux
    # Shellに設定されているオプションの確認
    # つまり、差分をとればsetで設定されたものが確認できる
    echo $SHELLOPTS
    echo "test"
    set +eux
    echo $SHELLOPTS
}

実行ログ

先頭にが付いているのがxオプションによって出力されたログになります。

+ echo braceexpand:emacs:errexit:hashall:histexpand:history:interactive-comments:monitor:nounset:xtrace
braceexpand:emacs:errexit:hashall:histexpand:history:interactive-comments:monitor:nounset:xtrace
+ echo test
test
+ set +eux
braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor

set -euxset +euxの差分は以下の3つのオプションです。

option description
errexit -eによって指定する
何かしらのコマンドがexit code0以外を出力した際に即座にerror exit
nounset -uによって指定する
未定義の変数の利用があればエラーメッセージを出力してerror exit
xtrace -xによって指定する
実行ログを出力する

:wave::wave:

Viewing all 2787 articles
Browse latest View live