echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Xamarin.Forms】コントロールのスタイルを設定する

Xamarin.Formsでラベルやボタンなどのスタイルを設定する方法。
(Windows Formなら「プロパティ」で簡単に設定できるんだけど...)

XAMLの場合

「ResourceDictionary」を使用する。

    <ContentPage.Resources>
        <ResourceDictionary>
            <Style x:Key="ButtonRedStyle" TargetType="Button">
                <Setter Property="TextColor" Value="Black" />
                <Setter Property="BackgroundColor" Value="Red" />
                <Setter Property="BorderColor" Value="Black" />
                <Setter Property="BorderWidth" Value="2" />
                <Setter Property="BorderRadius" Value="0" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    <ContentPage.Content>
        <StackLayout>
            <Button x:Name="button_red" Text="Red" Style="{StaticResource ButtonRedStyle}" />
        </StackLayout>
    </ContentPage.Content>

結果:
f:id:Makky12:20180412102843j:plain

ResourceDictionaryでスタイル及び「x:Key」を設定して、それをコントロール側で「style="{StaticResource xxx}"」で参照する。

C#で直書きする場合

            var buttonBlueStyle = new Style(typeof(Button))
            {
                Setters = {
                    new Setter { Property = Button.TextColorProperty, Value = Color.White },
                    new Setter { Property = Button.BackgroundColorProperty, Value = Color.Blue },
                    new Setter { Property = Button.BorderColorProperty, Value = Color.Black },
                    new Setter { Property = Button.BorderWidthProperty, Value = 2 },
                    new Setter { Property = Button.BorderRadiusProperty, Value = 0 },
                }
            };

            var blueButton = new Button
            {
                Text = "Blue",
                Style = buttonBlueStyle,
            };

            var stackLayout = new StackLayout
            {
                Children =
                {
                    blueButton
                },
            };
            
            this.Content = stackLayout;

結果:
f:id:Makky12:20180412102836j:plain

C#の場合は「Style」クラスで普通に参照する。

ちなみに、これはページ毎に設定してますが、全ページ共通のスタイルにする場合、AppクラスにStyleを定義すれば、共通スタイルとして定義できる。
参照:Xamarin.Forms で Style を利用する - Xamarin 日本語情報

※公式サイト(Xamarin Documentation - Xamarin | Microsoft Docs)ではC#(コードビハインド)で書かれているので、コードビハインドで記述するのが良いのかも...


昔に比べて、だいぶ良くなってきたとはいえ、まだまだXamarinは改良してほしい点が多いですね。

【python】pythonでExcelを操作する その2【brainf*ck】

前回Pythonで簡単なExcel操作について書きました。
今回はもうちょっと踏み込んだものを、と考えた結果「brainf*ck」のインタプリタPythonで作る事にしました。

ちなみに「brainf*ck」とは...

Brainfuck(ブレインファック)は難解プログラミング言語のひとつ。なお名称に卑語が含まれるため、Brainf*ckなどと表記されることがある。
(Wikipediaより)

だそうです。

ちなみに、下記命令文字列は「Hello,Python」と出力するものなのですが...なんのことやら、さっぱりですね。

 +++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.<<++++++++.>>>+++++++++++[>+++++++++++<-]>.-----.<++++[>---<-]>.<<<.-.

なお参考としては、先述のWikipedia(https://ja.wikipedia.org/wiki/Brainfuck)、

また実際の動きを見たい場合、以前ネットニュースでも話題になった、t-kihiraさんの動画が大変分かりやすくてよいと思います。
(こちらはjavascriptで作成してます。てかHTMLやDOM操作など全込みで1時間は凄すぎ!)
nicotter.net

どこをExcelと連携するか

ですが、brainf*ckには

  • 命令文字列(><+-.,[])の読み込み
  • メモリへの書き込み

があるので、前者をExcelシートから読み込み、後者をExcelシートに書き込もうと思います。

共通変数は?

共通変数(≒クラス変数)ですが、先述のWikipediaに、brainf*ckで管理が必要そうなものとして

  • 現在のポインタ
  • メモリの値(アドレスごと)

がありますので、これを共通変数にすればよいかと。

また、「メモリのアドレスの個数」あたりも共通変数にしとくと便利そうです。

あ、肝心のExcel関連(ブック・シート)も共通変数にしたほうが便利そうですね。

で、まあこんな感じかと。

import sys
sys.path.append([openpyxlの格納パス])  ##デフォルトで読み込み可能なら不要

import openpyxl as pyxl
from openpyxl import load_workbook

MEMORY = []  ##メモリの値(アドレス毎)  
POINTER = 0  ##現在のポインタ
MEMORY_LENGTH = 30  ##メモリのアドレスの個数
RESULT_STRING=""

##対象のブックとシート
WORK_BOOK = load_workbook('brainf_ck.xlsx')
WORK_SHEET = WORK_BOOK.get_sheet_by_name('Sheet1')


実際の読み書き処理

ですが、これは例えば

  • B1セルは命令文字列
  • B4~K6セルはメモリの値

というように決め打ちすれば、比較的楽ではないかと。

またメモリ書き込みは何回も呼び出すので、関数にしておきましょう。

で、コードとしてはこんな感じかな?
(てかpythonって、文字列の切り出しが'変数名[添字]'でいいんですね。
substring()とかいらないので、便利です。)

## 命令文字列の読み込み
command_string = str(WORK_SHEET['B1'].value)
## 命令文字列の書き込み関数
## valueは書き込む値(数値)
def writeMemoryToSheet(value):

    #global POINTER
    memory_string_length = len("000" + str(value))
    memory_string = ("000" + str(value))[memory_string_length -3:]

    row = 0
    col = (POINTER % 10) + 2

    if(POINTER <= 9):
        row = 4
    elif(POINTER >= 10 and POINTER <= 19):
        row = 5
    else:
        row = 6

    WORK_SHEET.cell(row=row, column=col).value = memory_string

命令文本体

で、肝心の命令文ですが、Wikipediaによると、

  • >: ポインタをインクリメントする。ポインタをptrとすると、C言語の「ptr++;」に相当する。
  • <: ポインタをデクリメントする。C言語の「ptr--;」に相当。
  • +: ポインタが指す値をインクリメントする。C言語の「(*ptr)++;」に相当。
  • -: ポインタが指す値をデクリメントする。C言語の「(*ptr)--;」に相当。
  • .: ポインタが指す値を出力に書き出す。C言語の「putchar(*ptr);」に相当。
  • ,: 入力から1バイト読み込んで、ポインタが指す先に代入する。C言語の「*ptr=getchar();」に相当。
  • [: ポインタが指す値が0なら、対応する ] の直後にジャンプする。C言語の「while(*ptr){」に相当。
  • ]: ポインタが指す値が0でないなら、対応する [ (の直後[1])にジャンプする。C言語の「}」に相当[2]。

ということですが、下2つ以外は、特に問題ないかと思います。
他の「brainf*ck作ったよ!」というサイトなどでも説明されていますが、

  • POINTERの値を加減する
  • MEMORY[POINTER]の値を加減する...

といった感じですね。

MEMORY[POINTER]の値を加減した場合、先程の関数でExcelにも値を書きます。
また、コンソールからの入力にも対応してあげなくてはなりません(=input()関数)
※chr(value)関数は、value文字コードを文字に変換する関数。
 逆にord(value)関数は、valueの文字を文字コードに変換する関数。

てかPythonって、switch case構文ないんですね...

   i = 0

    while i < len(command_string):

        char = command_string[i]

        if(char == ">"):
            POINTER += 1
            if(POINTER >= MEMORY_LENGTH):
                print("エラー:不正な\'>\'があります。")
                return
        elif (char == "<"):
            POINTER -= 1
            if(POINTER < 0):
                print("エラー:不正な\'<\'があります。")
                return
        elif(char == "+"):
            MEMORY[POINTER] += 1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "-"):
            MEMORY[POINTER] -=1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "."):
            RESULT_STRING += chr(MEMORY[POINTER])
        elif(char == ","):
            while True:
                input_char = input("半角英数1文字を入力してください。")
                if(len(input_char) != 1):
                    print("エラー:1文字ではありません。")
                else:
                    char_code = ord(input_char)

                    if(char_code < 0 or 127 < char_code):
                        print("エラー:半角文字ではありません。")
                    else:
                        MEMORY[POINTER] = char_code
                        writeMemoryToSheet(MEMORY[POINTER])
                        break
        i += 1


問題の下二つ

ですが、これは「'['や']'があったら、対応する']'や'['を見つける」処理が必要です。
この処理は関数にしてしまい、「対応する'['や']'の位置」は、リスト(他言語でいう配列)で管理すると楽です。

てな訳で、ソースはこちら。

## idxはcommand_string内での'['の位置
## target_stringはcommand_string。
## KAKKOは対応する位置を管理するlist(共通関数にする)

def checkKakkoRange(idx, target_string):

    for i in range(idx + 1, len(target_string)):

        kakko_count = 0
        check_char = target_string[i]

        if(check_char == "["):
            ## 2重以上の'['があったら、kakko_countを+1する
            kakko_count += 1
        elif(check_char == "]"):

            if(kakko_count == 0):
                ##kakko_countが0の場合の']'は、対応する']'である。
                KAKKO[idx] = i
                KAKKO[i] = idx
                break
            else:
                ##そうじゃない場合の']'は、対応する']'ではないので、kakko_countを-1する。
                kakko_count -= 1

で、先ほどの「命令文本体」のところに、こんな分岐処理を追加すればよいかと。

        if(char == ">")
            ## 中略
        elif(char == "["):
            ## 初回のみ、checkKakkoRange()を呼び出す。
            if(KAKKO[i] == -1):
                checkKakkoRange(i, command_string)
            
            if(MEMORY[POINTER] == 0):
                ## ちゃんと対応する']'があるか?
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == "]"):
            ## ']'の場合、checkKakkoRange()は呼び出し不要。(対応する'['の時点で呼び出されてるはずなので)
            if(MEMORY[POINTER] != 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return


ソース全体

import sys
sys.path.append("C:\\Program Files\\Anaconda3\\Lib\\site-packages\\")

import openpyxl as pyxl
from openpyxl import load_workbook

#print(pyxl.__file__)

MEMORY = []
KAKKO = []
POINTER = 0
MEMORY_LENGTH = 30
RESULT_STRING=""

WORK_BOOK = load_workbook('brainf_ck.xlsx')
WORK_SHEET = WORK_BOOK.get_sheet_by_name('Sheet1')

def init():
    for i in range(MEMORY_LENGTH):

        global POINTER

        POINTER = i
        MEMORY.append(0)
        ##ワークシートに書き出し
        writeMemoryToSheet(MEMORY[i])

    POINTER = 0

def interpreter():

    global RESULT_STRING
    global POINTER
    command_string = str(WORK_SHEET['B1'].value)

    if(command_string.count('[') != command_string.count(']')):
        print("エラー:\'[\'\']\' の数が一致しません。")
        return

    for j in range(len(command_string)):
        KAKKO.append(-1)

    i = 0
    if(command_string[0] == "'"):
        i = 1

    while i < len(command_string):

        char = command_string[i]

        if(char == ">"):
            POINTER += 1
            if(POINTER >= MEMORY_LENGTH):
                print("エラー:不正な\'>\'があります。")
                return
        elif (char == "<"):
            POINTER -= 1
            if(POINTER < 0):
                print("エラー:不正な\'<\'があります。")
                return
        elif(char == "+"):
            MEMORY[POINTER] += 1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "-"):
            MEMORY[POINTER] -=1
            writeMemoryToSheet(MEMORY[POINTER])
        elif(char == "."):
            RESULT_STRING += chr(MEMORY[POINTER])
        elif(char == "["):
            if(KAKKO[i] == -1):
                checkKakkoRange(i, command_string)

            if(MEMORY[POINTER] == 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == "]"):
            if(MEMORY[POINTER] != 0):
                if(KAKKO[KAKKO[i]] == i):
                    i = KAKKO[i] + 1
                    continue
                else:
                    print("エラー:\'[\'\']\' の数が一致しません。")
                    return
        elif(char == ","):
            while True:
                input_char = input("半角英数1文字を入力してください。")
                if(len(input_char) != 1):
                    print("エラー:1文字ではありません。")
                else:
                    char_code = ord(input_char)

                    if(char_code < 0 or 127 < char_code):
                        print("エラー:半角文字ではありません。")
                    else:
                        MEMORY[POINTER] = char_code
                        writeMemoryToSheet(MEMORY[POINTER])
                        break
        i += 1

def checkKakkoRange(idx, target_string):

    for i in range(idx + 1, len(target_string)):

        kakko_count = 0
        check_char = target_string[i]

        if(check_char == "["):
            kakko_count += 1
        elif(check_char == "]"):

            if(kakko_count == 0):
                KAKKO[idx] = i
                KAKKO[i] = idx
                break
            else:
                kakko_count -= 1


def writeMemoryToSheet(value):

    #global POINTER
    memory_string_length = len("000" + str(value))
    memory_string = ("000" + str(value))[memory_string_length -3:]

    row = 0
    col = (POINTER % 10) + 2

    if(POINTER <= 9):
        row = 4
    elif(POINTER >= 10 and POINTER <= 19):
        row = 5
    else:
        row = 6

    WORK_SHEET.cell(row=row, column=col).value = memory_string

##ここからメイン処理
init()
interpreter()

WORK_BOOK.save('brainf_ck.xlsx')

print("文字列:" + RESULT_STRING)
print("\n" + "終了")


ちなみに、実際に実行させてみた動画がこちら。

いや、やはりどんなものであれ、まずは1個何か作ってみると、(Pythonに限らず)その言語の理解がより深まりますね。

てかbrainf*ck、「難解言語」とか「ク*言語」とか言われるけど、文字列、メモリ、ポインタの考え方とか、むしろ初学者とかの教育にはかなり向いてるんじゃないの?とか思ったり...

ただ、もっとポインタやメモリの動きがリアルタイムに分かるといいなあ...
(本当はExcelを表示させながらやる予定だったが、起動してたら書き込み時にエラーになるので、断念...

てか、当然といえば当然ですよね...)

【python】pythonでExcelを操作する

去年、python関連の書籍を探してたら、こんな本があった。

退屈なことはPythonにやらせよう

pythonといえば、AIや機械学習...と思っていたけど、それ以外にもルーティンワーク系処理の自動化にもなかなか便利。

で、今回はこの本にもある「Excel操作」について、ちょこっと触れてみた。
(ExcelPythonにも対応するらしいし。)

ブックを開く&シートの値取得

import openpyxl as pyxl
from openpyxl import load_workbook

## 既存のブックを開く場合
work_book = load_workbook('test.xlsx')
work_sheet = work_book.get_sheet_by_name('Sheet1')

## 新規WorkBookを作成する場合
new_workbook = pyxl.Workbook()

## 新規シートを作成して取得。
## もちろんシート名指定以外にも、やり方はある。(あくまで一例)
new_workbook.create_sheet('New_Sheet', 0)
new_sheet = new_workbook.get_sheet_by_name('New_Sheet')

## 値をCell形式で取得する方法
target_cell = work_sheet.cell(row=1, column=1)
    
## Range形式で取得する方法
target_range = work_sheet['A1']

## 複数範囲で取得する方法。
## coordinameはアドレスを取得できる。
for rows in work_sheet['A1':'C3']:
    for col in rows:
        print(col.coordinate, col.value)


シートの値設定&ファイルの保存

## 値&数式の設定(Cells形式&Range形式)
new_sheet.cell(row=1, column=3).value = 'abc'
new_sheet["B2"] = "SUM(A3:C3)"

## シートを保存(物理ファイルとして)
new_workbook.save('test2.xlsx')


まだまだ本当に基本的なところしか網羅できてないけど、もっといろいろ知識を増やしていきたいなあ。

てか、Excelpythonが使用できれば、ソースファイルの共有とか、いろいろ可能性が出てくるので、今後に期待したいところです。

【Linux】lsコマンドで「引数が多すぎます」となった場合の対処方法

'ls'コマンドは、Linuxでも非常によく使うコマンドの一つだと思いますが、例えば

> ls -l /mnt/c/users/makky12/*.*

みたいにワイルドカード指定で実行した時に、一致するファイルが多すぎると*1「引数が長すぎます」というエラーが発生してしまいます。

こんな時、どういう回避策があるか、という話です。

ワイルドカードを指定しない

> ls -l /mnt/c/users/makky12/

上記のように、ワイルドカードを指定しなければ「引数が長すぎます」エラーは発生しません。(ここで記載した通り、ワイルドカード指定すると、一致したファイルすべてを引数にしてしまうため)

ただし、当然ワイルドカードでの指定ができないため、その後に

といったことが必要になります。(てか、ただでさえ一致するファイルが膨大なのに、もし条件に一致しないファイルのほうがはるかに多い場合、かなり処理に時間がかかってしまう気がします。)

■findコマンドを使う

> find /mnt/c/users/makky12/ -name "*.*" -type f

というように、findコマンドの-nameオプションを指定すれば、やはり「引数が長すぎます」エラーを発生させず、ファイルの一覧を表示させることができます。

※ちなみに、例えば「'ls'コマンドの'-l'オプションを付けた場合と同じ結果を得たい」とかなら、

> find /mnt/c/users/makky12/ -name "*.*" -type f -exec ls -l {} \;

としたり、あるいは'xargs'を使用して、

> find /mnt/c/users/makky12/ -name "*.*" -type f | xargs ls -l

とするやりかたもあります。

ただ、長い(=たくさんの)処理を実行させるには、ちょっと不向きかも。

そして、このやり方だと、別の問題が…


サブディレクトリは対象にしたくない!

'find'コマンドは、条件に一致すれば、サブディレクトリのファイルも取得します。
それで問題ないならいいんですが、場合によっては「サブディレクトリは対象外にしたい」ということもあると思います。(てか、僕がこの問題に直面した時はそうでした)

じゃあどうやるのか…といえば、こんなやり方があります。

■「-maxdepth 1」オプションを付ける

「-maxdepth」オプションを使用すれば「検索する最大の深さ」が指定できます。
これを使用して

> find /mnt/c/users/makky12/ -maxdepth 1 -name "*.*" -type f

とすれば、サブディレクトリは検索対象にしません。
※なお、検索ベースのディレクトリが「/mnt/c/users/makky12/」なので、maxdepthの引数は'1'(=1つ下の階層、つまり「/mnt/c/users/makky12/」ディレクトリ直下まで)とする必要があります。
これが例えば「/mnt/c/users/makky12/*.* 」を指定していた場合、すでに「/mnt/c/users/makky12/」の内部を指定しているため、maxdepthは'0'とする必要があります。

ただ、僕の使用していた環境では、maxdepthが使用できなかったわけでして…

■「grep」コマンドで頑張る

とまあ、結局'grep' コマンド頼みになってしまったわけですが、サブディレクトリ以下のファイルの場合、かならずパスの「/mnt/c/users/makky12/」の後のどこかに'/'があるのを利用して、

> find /mnt/c/users/makky12/ -name "*.*" -type f | grep '^/mnt/c/users/makky12/[^/]*$'

てな感じで、「'/mnt/c/users/makky12/'で始まり、そのあとに'/'を含まない文字列」のみ表示対象とすれば、結果的にサブディレクトリのファイルを除外することができる、というわけです。
(てか、ネットでこのやり方を知ったときは、目からうろこでした。「何かよいコマンドやスクリプトが…」と思っていたのですが、考え方が固いというか、もっと柔軟な考え方ができなければ...と思った次第です。)

以上、僕が業務で直面した「lsコマンドで「引数が多すぎます」となった場合の対処方法」でした。

…まあ、'maxdepth'が使えれば問題なかったんですけどね。

てか、本番稼働のサーバー(しかもAPサーバー(ファイルサーバじゃない))の1つのディレクトリに15万以上もファイルを置くって、運用上どうなのよ、それ?

*1:正確には「一致したファイル名の総バイト数が「ARG_MAX」より大きい」場合に発生します。

【node.js】Google Home Notifierを使ってGoogle Homeにしゃべらせる

実は昨年末に、Google Homeを衝動買いしました。

始めは「なんか色々使えないかなあ...」と思っていましたが、購入してしばらくは、そんなに使用していませんでした。

が、先日Twitterで、マイクロソフトエバンジェリスト、千代田まどか(ちょまど)さんがこんなツイートをしているのを見まして。

「やっぱりこういうことができるんだ!面白そう!」って思い、さっそく(二番煎じもいいとこ)実行してみました。(僕は女の子が大好きなので、男の声にはしないですが…)

※これ以降の作業は、Windowsにインストールする前提で記載しています。

1.インストール

■node.jsのインストール
Google Homeにしゃべらせるには、Google Home Notifierというjavascriptのプログラムが必要です。
そしてGoogle Home Notifierを動かすには、node.jsが必要です。
というわけで、node.jsのインストールから始めます。

といっても、node.jsの公式サイト(https://nodejs.org/ja/)からインストーラーをダウンロードして、実行するだけですので、難しいことはないです。
※「推奨版」と「最新版」がありますが、「推奨版」が無難。

インストール後、下記コマンドをコマンドプロンプトで入力して、バージョンが表示されればOK。(ここを含めた全手順で、コマンドプロンプトは「管理者で実行」してください。)

> node --version

■node-gypのインストール
node-gypは「C++を用いた拡張機能を作るためのビルドツール的なもの」らしく、Google Home Notifierインストールに必要になります。
ここをちゃんとやらないと、Google Home Notifierのインストールでエラーが出まくるので、要注意です。(てか、僕はそれで1週間近くハマった…)

※ここはnode-gypのgithubのドキュメント(https://github.com/nodejs/node-gyp)に沿って説明をします。

まずインストールは、コマンドプロンプトで下記コマンドを入力すればOK。(ここは問題ないと思います)

> npm install -g node-gyp

そして公式サイトの「Option 1」に、
Install all the required tools and configurations using Microsoft's windows-build-tools using npm install --global --production windows-build-tools from an elevated PowerShell or CMD.exe (run as Administrator).
(訳:「PowerShellコマンドプロンプトで'npm install --global --production windows-build-tools'というコマンドを使用して、Microsoft's windows-build-toolsを介して必要なツールや設定情報をインストールしてください。(管理者権限で実行してね))

とある通りに、コマンドプロンプトから

> npm install --global --production windows-build-tools

と入力して、この作業を完了させます。

※なお「Option 2」の手順は「必要なツールや設定情報」を自分でインストールする方法です。「Option 1」の手順はそれをすべて上記コマンド一発で行えるので、「Option 1」の手順のほうが楽です。(実施するのはいずれか一方でよい)

最後に、
If node-gyp is called by way of npm and you have multiple versions of Python installed, then you can set npm's 'python' config key to the appropriate value:
(訳:node-gypをnpm経由で実行する場合で、pythonの複数バージョンがインストールされている場合、(下記コマンドのように)npmの'python'コンフィグの値を適切な値にしてください。)

> npm config set python /path/to/executable/python2.7

とあるので、この通りに設定します。(なお、node-gypはpython3.xでは動作しませんが、Option1の手順を実施すれば、別途2.xをインストールしてくれますので、問題ないはずです。)

※なお、公式ドキュメントに
If you have multiple Python versions installed, you can identify which Python version node-gyp uses by setting the '--python' variable:
(訳:もし複数バージョンのpythonがインストールされている場合、node-gypに下記コマンドでnode-gypで使用するバージョンを設定してください)

> node-gyp --python /path/to/python2.7

とありますが、僕が実施した際は、「--pythonオプションなんてないよ」と言われ、これは実行できませんでした。


次に使用方法ですが、
To compile your native addon, first go to its root directory:
(訳:あなたの環境のアドオンをコンパイルするために、まず初めに(プロジェクトの)ルートフォルダに移動してください。)

> cd my_node_addon

とあるので、まずはコマンドプロンプトの'cd'コマンドで、*.jsファイルを作成するフォルダに移動します。

次に、
The next step is to generate the appropriate project build files for the current platform. Use configure for that:
(訳:次に、プラットフォーム毎に適切なプロジェクトのビルドファイルを作成します。次の'configure'コマンドを使用します。)
> node-gyp configure

Auto-detection fails for Visual C++ Build Tools 2015, so --msvs_version=2015 needs to be added (not needed when run by npm as configured above):
(訳:VC++ビルドツールを自動検出ができなかった場合、'--msvs_version=2015'というオプションを付けてください。(上のコマンドでうまくいけば不要です))

> node-gyp configure --msvs_version=2015

とある通り、'node-gyp configure'コマンドを実行します。それで「VC++ビルドツールがない」という旨のエラーが表示されたら、下の'node-gyp configure --msvs_version=2015'コマンドを実施すればOKです。

なお、
Note: The configure step looks for the binding.gyp file in the current directory to process. See below for instructions on creating the binding.gyp file.
Now you will have either a Makefile (on Unix platforms) or a vcxproj file (on Windows) in the build/ directory. Next invoke the build command:
(訳:'configure'コマンドは'binding.gyp'というファイルをカレントフォルダ内から検索します。'binding.gyp'ファイルを作成するには、「build」フォルダ内に'vcxproj'ファイルがあると思うので(Unixでは'Makefile'ファイル)、下記の’node-gyp build’コマンドを実行してください。)

> node-gyp build

という記載もあるので、もし「binding.gypがない」とかいう旨のエラーが表示されたら、'node-gyp build'コマンドを一度実行してから、再度'configure'コマンドを実行すればよいと思います。

Google-Home-Notifierのインストール
で、ようやくメインのGoogle-Home-Notifierのインストールです。
といってもこれ自体は単純で、下記コマンドを実行するだけです。

> npm install google-home-notifier

※ここで「dns_sd.h」ファイルがない、というエラーが表示された場合、先に下記「Bonjour SDK for Windowsのインストール」を実行してから、再度上記コマンドを実行します。(エラーが出る場合、大量に出るはずです)

Bonjour SDK for Windowsのインストール
dns_sd.h」ファイルがないというエラーは、「Bonjour SDK for Windows」をインストールすることで解決できるので、これをインストールします。
これはApple Developerにあるので、ここからダウンロードしてインストーラを実行すればOKです。

【参考サイト】
http://kghr.blog.fc2.com/blog-entry-118.html
http://blog.livedoor.jp/sce_info3-craft/archives/9706909.html


2.プログラミング(ようやく)
で、実際のプログラムですが、公式サイト(https://github.com/noelportugal/google-home-notifier)のソースをそのままコピペで大丈夫かと思います。
でも英語なので、試しに上記【参考サイト】内のサンプルソースを「sample.js」とかいう名前でファイルに保存して、下記コマンドで実施させてみます。

※コード自体は非常に短いですが、特に問題なく動作すると思います。(同じネットワークにGoogle Homeがある事が前提。なお文字コードを「UTF-8」にする必要があるので、それだけは注意。)

> node sample.js

var googlehome = require('google-home-notifier');
var language = 'ja'; // ここに日本語を表す ja を設定

// ネットワーク内からGoogle Homeを見つけてくれる
googlehome.device('Google Home', language); 
// もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
// googlehome.ip('xxx.xxx.xxx.xxx', language);

googlehome.notify('こんにちは', function(res) {
  console.log(res);
});

OK、問題ないです。

■続けてしゃべらせる
最後に「続けてしゃべらせる」ですが、たぶん最初に下記のようなプログラムを思いつくかと思います。

// ネットワーク内からGoogle Homeを見つけてくれる
googlehome.device('Google Home', language); 
// もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
// googlehome.ip('xxx.xxx.xxx.xxx', language);

googlehome.notify('こんにちは', function(res) {
  console.log(res);
});

googlehome.notify('こんばんは', function(res) {
  console.log(res);
});
makeSpeak('こんにちは');
makeSpeak('こんばんは');

function makeSpeak(sentence) {

    // ネットワーク内からGoogle Homeを見つけてくれる
    googlehome.device('Google Home', language); 
    // もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換える
    // googlehome.ip('xxx.xxx.xxx.xxx', language);

    googlehome.notify(sentence, function(res) {
        console.log(res);
    });
}

が、残念ながら、これはどちらもエラーになります。(下記のようなエラーが出ると思います。)
Error: mdns service already started
at Browser.start (c:\dev\js\google_home\node_modules\mdns\lib\mdns_service.js:30:11)
at Object.notify (c:\dev\js\google_home\node_modules\google-home-notifier\google-home-notifier.js:28:13)
at Timeout.makeSpeak [as _onTimeout] (c:\dev\js\google_home\google_home.js:37:16)
at ontimeout (timers.js:475:11)
at tryOnTimeout (timers.js:310:5)
at Timer.listOnTimeout (timers.js:270:5)

といっても1行目に思いっきり「mdns service already started」とある通り、要は最初の命令がすでに実施中なので、次の命令を実行できない、というだけです。

なので、例えば下記のように、前の命令が終了してから次の命令を実行するようにすれば問題ないです。(タスクキューに追加するとか、もっといい方法はあると思いますが、ここではそれは置いときます)

// 前のmakeSpeak()の終了を待つためにsetInterval()をする。
var count = 0;
var val = setInterval(makeSpeak, 7500);

function makeSpeak() {

    // Google Home Notifierの設定
    var googlehome = require('google-home-notifier');
    var language = 'ja'; // ここに日本語を表す ja を設定

    var sentence = '';

    switch(count)
    {
        // しゃべらせる内容
        case 0:
            sentence = 'あくしろよ';
            break;
        case 1:
            sentence = 'おう考えてやるよ';
            break;
        case 2:
            sentence = 'すいません許してください。何でもしますから';
            break;
        default:
    }

    count++;

    // ネットワーク内からGoogle Homeを見つけてくれる
    googlehome.device('Google Home', language);

    // もし Google Home のIPアドレスを指定するなら、以下のスクリプトに置き換え
    // googlehome.ip('xxx.xxx.xxx.xxx', language);

    // 実際にしゃべらせる
    googlehome.notify(sentence, function(res) {
        console.log(res);
    });

    // 全部しゃべったら終了。
    if(count > 2) {
        clearInterval(val);
    }
}

※なお、上記プログラムを実際に実行した動画が、下記になります(手ブレはご勘弁を。)

いやー、苦労しましたね。
でもこれで、Google Homeの使用幅が広がりました。
これからは、Google Homeどんどん使いたいと思います。

…てか、我ながら長い文章だなあ。

【Linux】ワイルドカードを変数に代入する方法

おととい(2018/01/22)は全国的に大雪で、東京や関東で、ものすごい数の人が駅で立ち往生を食らっている、という映像をニュースやSNSで見ました。

幸い、愛知県平野部は雪の影響はなかったですが、あの人だかりを見ると、やはり東京(というか関東)は僕にとって「(リモート等で)仕事する場所」ではあっても、「住む場所」ではないな、と改めて感じました。


で、本題

例えば、下記のような内容のテキストファイルから「ファイルパス&ファイル名を1行ずつ読み込んで、何か処理する」とします。

/mnt/c/users/makky12/,sample.txt
/mnt/c/users/makky12/,*
...

で、これを

while read line; do
    ##${line}は「ファイル一行の内容」。
    declare -a array
    array=(`echo ${line} | tr ',' ' '`)
    dir=$array[0]
    file=$array[1]
    echo "dir=${dir}"
    echo "file=${file}"
done < ${sample} 

とすると、例えばテキストの2行目の内容なら普通、${file}には'*'が入ると思いますよね。
ところが、実際は、

dir='/mnt/c/users/makky12/'
file='abc.txt'  ##←なにこれ?

ということが起こります。

僕も最初、意味が全く分からなかったんですが、色々調べてみて、ようやく分かりました。


ワイルドカードはまず展開される

と、いきなり結論を書きましたが、要はワイルドカード文字(「*」「?」など)があると、それを「文字列」ではなく「ワイルドカード」として認識してしまうので、この場合「(カレントディレクトリの)条件に一致するファイル」を取得してしまいます。(要は下記と同じ意味)

file=`ls *`

なので先述の例では「カレントディレクトリ内にあるファイル名」が変数fileに代入されてしまうのです。

※これは各種コマンドの引数に「ワイルドカードでファイル名を指定した場合」も同じです。例えば'cp'コマンドの場合

cp /mnt/c/users/makky12/* /mnt/c/users/makky12/sample/
## 例えば'a.txt,b,txt,c.txt'の3つがあった場合、下記と同じような意味になる。
cp [/mnt/c/users/makky12/a.txt /mnt/c/users/makky12/b.txt /mnt/c/users/makky12/c.txt] /mnt/c/users/makky12/sample/

なので、条件に一致した全ファイルを処理対象としてくれるわけですが、上記のように該当するファイルをすべて引数に設定してしまうので、

if [ ${#} -gt 2 ]; then
    echo "引数は2つまでです。"
    exit 1
fi

declare file=${1}
declare mode=${2}

上記のような処理があるシェルスクリプトで、

bash 〇〇.sh /mnt/c/users/makky12/* -r

と指定すると、一致したファイル数によっては(「〇〇.sh」以外の)引数が2個以上、となり「引数は2つまでです」と表示されてしまいます。


対策方法

■1:""で囲む

先程の例では$array[1]を「""」で囲みませんでしたが、これを

file="$array[1]"

と、「""」で囲ってあげると、ワイルドカード文字であっても、正しく変数fileに代入されます。

■2:'set -f'を使用する

'set -f'コマンドは「ワイルドカードの自動展開を無効にする」オプションです。
これを利用して

set -f
file=$array[1]
set +f

とすれば、やはりワイルドカード文字であっても、正しく変数fileに代入されます。
ただし、当然'set -f'コマンド実行後はずっと「ワイルドカードの自動展開が無効」なので、使用後はすぐに'set +f'コマンドで元に戻さないと、予期せぬバグの原因になったりするので、要注意です。

■ちなみに...
上記の処理をしても、僕はまだ不安なところがあったので、実際のソースでは

set -f
file=`echo "$array[1]"`
set +f

としました。(意味があるかは不明ですが、念のため)

・先述の例は「変数に代入する場合」の話ですが、直接コマンド(の引数など)で実行する場合、先述の方法の他に

## 「''」で囲む方法
$ echo '*'

## 「\」を使う方法
$ echo \*

なども使用できます。

参考URL:
qiita.com
・sh での変数とワイルドカードの落とし穴(https://www.ecoop.net/memo/archives/2006-02-02-1.html


シェルスクリプトは便利な反面、こういう一見分かりにくい動作をする事もあるので、気を付けなくてはなりませんね。


終わり!閉廷!以上!解散!

【Linux】パーミッションによるコマンドやスクリプトの動作の違い

昨日(2018/01/20)、豊橋市で行われた「ブイアールサンダー」というVR(XR)関連のイベントに参加しました。
XR関連の知識が得られたのも大きいですが、何より大学時代の思い出の地、豊橋でIT関連のイベントが開かれたというのが嬉しかったです。

僕は豊橋市民ではないですが、豊橋市のこういうイベントには積極的に参加&協力したいな、と心から思いました。(てか、懇親会に参加できなかったのが、今でも悔やまれる...)
uzura.doorkeeper.jp

さて、10月から参画しているプロジェクトでガッツリUnixLinuxに触れることになり、その関係でシェルスクリプト(shやbash)もバリバリ書いています。
最初は戸惑いましたが、決して難易度は高くないし、慣れると快適ですね。
これからWindows Subsystem for Linuxもあることだし、Linuxに触れる機会もさらに多くなるでしょう。

で、先日仕事でシェルスクリプトを書いてて、下記の処理を書きまして(てかはてなブログって、'bash'はないのか…)

if [ -d ${dir} ]; then
    ## ディレクトリがある場合は書き込み権限のチェック
    if [ -w ${dir} ]; then
        echo "書き込み権限がある"
    else
        echo "書き込み権限がない"        
    fi
else
    ## ディレクトリがない場合
    echo "ディレクトリがない"
fi

これをあるディレクトリ(もちろん実在)で動かしたら、まあ上二つのどちらかだと思ったんですが、結果は

"ディレクトリがない"

だったんです。

で、「???」と思って調査したところ、どうやら'-d'や'-f'などはパーミッション次第では、実際のディレクトリ有無と違う動作になるらしく。
もっと調べると、ファイル&ディレクトリ関連のコマンドも、パーミッション次第で動作結果が異なる事がわかりました。

じゃあ...ってわけで、仕事が暇だったので実際に調べてみました。

その1.コマンド
※調べたコマンドは下記の通り

  • ls(フォルダなどの一覧表示)
  • cp(ファイルのコピペ)
  • mv((今回は)ファイルのカット&ペースト)
  • rm(ファイル削除)
  • touch(ファイルの新規作成)


【結果】

f:id:Makky12:20180121181816p:plain
lsコマンド
f:id:Makky12:20180121183141p:plain
cpコマンド
f:id:Makky12:20180121183151p:plain
mv&rm&touchコマンド

【結果】

  • 新規作成&削除は、対象ファイル自体の権限は全く関係なし。(ディレクトリの権限は「中のファイル自身」ファイルの権限は「そのファイルの中身」を扱う権限、ということなのかも)
  • 'cp'コマンドは、ちゃんとファイルの'r'権限も必要(「中身をコピーするから」という意味合い?)
  • 'mv'コマンドは、最初は'cp'と全く同じと思ったけど、「切り取り」は'rm'、「貼り付け」は'cp'と同様と考えると、確かに納得。


その2(本題).シェルスクリプト
※調べたスクリプトは「-d(ディレクトリ有無)」と「-e(ファイル有無)」で、下記スクリプトで確認。
※${dir}や${file}は確実に存在する、という前提。

if [ -d ${dir} ][ -e ${file} ]); then
    echo "true"
else
    ## ない場合
    echo "false"
fi


【結果】
※どちらも「対象ディレクトリ/ファイルの親フォルダ」の権限

f:id:Makky12:20180121181805p:plain
ファイル&ディレクト
【結論】

  • こちらも、そのディレクトリ&ファイル自体の権限は全く関係なし。(親ディレクトリの権限に依存)
  • '-e'は、0(≒true) or 1(≒false)だからまだいいけど、'-d'は権限がないとエラーになるから、判定が大変かも。(初めに'-d'('-e')の${?}(=ステータス)でエラー判断→エラーがなければ、次に実際の有無判断?でも'-d'('-e')を2回実行するのは、あまりスマートじゃないような...)
  • いずれにしろ、適切な権限がないと、実際のファイル有無と違う判定をする可能性がある。


結論として、ファイル関連の各種コマンドやシェルスクリプトの実行時には、パーミッション(特に親フォルダの)には要注意、という話でした。

終わり!閉廷!以上!解散!


■余談
 冒頭に書いた「ブイアールサンダー」の件ですが「半田市から…」と言ったら、めちゃくちゃ驚かれました。
豊橋の人からしたら、そんなに遠いですかね、半田~豊橋って。(今では一般道でも約1.5時間ですからね。昔はr41がなく、R247で行くしかなくて、2時間かかってましたから。早くなったものです。)