4
\$\begingroup\$

I'm new to writing PowerShell scripts, and this would be my first relatively large undertaking in the language. Any feedback regarding style and readability would be greatly appreciated. Particularly, I was unsure of the best way of iterating through the various strings, and was wondering if there was a more elegant way of doing so.

The code is also available through this gist, where a sample CSV is provided to test the program.

function ShowHelp() {
    echo "Insufficient arguments passed."
    echo ""
    echo "Usage: "
    echo "    .\morse.ps1 ""words"" [path\to\table.csv]"
    echo ""
    echo "``morse`` takes a series of words as input through the first command line"
    echo "parameter and causes the Caps Lock key to blink accordingly."
    echo ""
    echo "    words                   the words to display"
    echo "    [path\to\table.csv]     uses the referenced file as the morse table."
    echo "                            default: $DefaultCsvPath"
    echo ""
    echo "Examples:"
    echo "    .\morse.ps1 ""SOS send help"""
    echo "    .\morse.ps1 ""wot is this???"" .\punct.csv"
}

# Definitions:
#  - A `symbol` is one of $LongBlinkSymbol or $ShortBlinkSymbol
#  - A `sequence` is a series of concatenated symbols
#  - A `letter` is any character
#  - A `letter` is valid if and only if it occurs in MorseTable
#  - A `word` is a series of concatenated letters
#  - A `sentence` is a series of `words` joined by whitespace characters

$TimeUnit = 200 # in milliseconds
$ShortBlinkDuration = 1 * $TimeUnit
$LongBlinkDuration = 3 * $TimeUnit

$BetweenSymbolsDuration = 1 * $TimeUnit
$BetweenLettersDuration = 3 * $TimeUnit
$BetweenWordsDuration = 7 * $TimeUnit

$LongBlinkSymbol = '1'
$ShortBlinkSymbol = '0'

$DefaultCsvPath = ".\morse.csv"

# strings which match this regex are valid symbol strings
$ValidSymbolRegex = "^($(
    [Regex]::Escape($LongBlinkSymbol)
)|$(
    [Regex]::Escape($ShortBlinkSymbol)
))*$"

# error types
$EmptyTableError = 1
$MalformedHeaderError = 2
$InvalidSequenceError = 3
$InvalidCharacterError = 4
$NoArgumentsPassedError = 5

# WScript interface for sending keys
$wshell = New-Object -ComObject wscript.shell
function SendKeys([string] $keys) {
    $wshell.SendKeys($keys)
}

function CapsLock() {
    SendKeys("{CAPSLOCK}")
}

function IsValidSequence([string] $code) {
    $code -match $ValidSymbolRegex
}

# NOTE: MorseTable is indexed by characters, **not** singleton strings
$MorseTable = @{}

function LoadMorseTable([string] $pathToCsv) {
    $csv = Import-Csv "$pathToCsv"

    # ensure non-empty csv
    if($csv.length -eq 0) {
        Write-Error "Table read was empty"
        exit $EmptyTableError
    }

    # ensure proper column naming
    $head = $csv[0] # sample csv; we know the first element must exist
    if($head.Symbol -eq $null -or $head.Sequence -eq $null) {
        Write-Error "Malformed table header"
        exit $MalformedHeaderError
    }

    # update $MorseTable
    $csv | foreach {
        # ensure proper formatting of Sequence
        if(-not (IsValidSequence($_.Sequence))) {
            Write-Error "Entry '$($_.Symbol)' has an invalid sequence '$($_.Sequence)'"
            exit $InvalidSequenceError
        }

        $MorseTable[[char] $_.Symbol] = $_.Sequence
    }
}

# given sequence of $LongBlinkSymbol and $ShortBlinkSymbol, blinks the light
# for the appropriate duration
function BlinkSequence([string] $code) {
    for($i = 0; $i -lt $code.length; $i++) {
        $char = $code[$i]
        CapsLock
        if($char -eq $LongBlinkSymbol) {
            Sleep -m $LongBlinkDuration
        }
        else {
            Sleep -m $ShortBlinkDuration
        }
        CapsLock

        # do not wait after the last symbol
        if($i + 1 -ne $code.length) {
            Sleep -m $BetweenSymbolsDuration
        }
    }
}

function GetSequence([char] $char) {
    return $MorseTable[$char]
}

function BlinkWord([string] $word) {
    for($i = 0; $i -lt $word.length; $i++) {
        $char = $word[$i]
        $code = GetSequence($char)
        BlinkSequence($code)

        # do not wait after the last letter
        if($i + 1 -ne $word.length) {
            Sleep -m $BetweenLettersDuration
        }
    }
}

function BlinkSentence([string] $sentence) {
    $words = $sentence.ToUpper().split()

    # verify that no invalid symbols occur
    $letterset = $words | % { $_.ToCharArray() } | Sort-Object | Get-Unique

    $letterset | foreach {
        if($MorseTable[$_] -eq $null) {
            Write-Error "Input contains the invalid character '$_'"
            exit $InvalidCharacterError
        }
    }

    # all symbols must be valid

    for($i = 0; $i -lt $words.length; $i++) {
        $word = $words[$i]
        BlinkWord($word)

        # do not wait after the last word
        if($i + 1 -ne $words.length) {
            Sleep -m $BetweenWordsDuration
        }
    }
}

if($args.length -eq 0) {
    ShowHelp
    exit $NoArgumentsPassedError
}

$input = $args[0]
$pathToCsv = $DefaultCsvPath
if($args.length -ge 2) {
    $pathToCsv = $args[1]
}

LoadMorseTable $pathToCsv
BlinkSentence $input
\$\endgroup\$
1

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.