Jak mapować argumenty wiersza poleceń na wywołania funkcji?

0

Natrafiłem na problem, który zaczyna mnie denerwować – niby proste, a trudne.

Mój konsolowy program do generowania schematów blokowych w ASCII art (JavaScript, Node.js; pisałem już o nim w tym wątku) przyjmuje kilka argumentów. Jego wywołanie ma taki schemat:

node nazwa-pliku.js {nazwa-parametru} {lista-wartości} {nazwa-parametru} {lista-wartości} ... -- {lista-zawartości-bloków}

Argumenty {nazwa-parametru} mają zdefiniowane nazwy w kodzie programu jako typ String – na przykład "-w" czy "-b" – a argumenty {lista-wartości} następujące po poszczególnych parametrach odpowiadają tym parametrom. Czyli każdemu argumentowi-parametrowi może odpowiadać lista argumentów-wartości o dowolnej długości – 1, 2, 3… (w programie, dla wygody użytkownika, ustaliłem długości tych list na od 1 do 2). Przykładowo wygląda to tak:

paramsConfig = {
  "-w": {
    // Tu konfiguracja dla parametru "-w"
  },
  "-b": {
    // Tu konfiguracja dla parametru "-b"
  },
  ...
}

Z drugiej zaś strony użytkownik wywołując program spodziewa się schematu blokowego, który jest generowany przez funkcję constructSchema(schemaConfig). Żeby stworzyć schemat, muszę wcześniej stworzyć poszczególne jego bloki, wywołując funkcję constructBlock(blockConfig). Obie te funkcje przyjmują jako swoje argumenty swoją konfigurację.

Problem, z którym przychodzę w tym wątku, polega na tym, że muszę zmapować jeden zestaw wartości na drugi – konfigurację podaną w wierszu poleceń na konfiguracje w wywołaniach funkcji.

Obecne moje rozwiązanie wygląda tak, że wywołuję funkcje constructSchema oraz constructBlock i ręcznie mapuję jedne wartości na drugie. Czyli przykładowo:

const schema = constructSchema({
  {nazwa-właściwości-A}: parsedArgs["-w"],
  {nazwa-właściwości-B}: parsedArgs["-b"],
  {nazwa-właściwości-C}: parsedArgs["-w"]
  ...
});

gdzie parsedArgs jest obiektem zawierającym argumenty z wiersza poleceń zwalidowane i w odpowiednim formacie.

Jest to rozwiązanie o tyle brzydkie, że za każdą zmianą konfiguracji podawanej w wierszu poleceń:

  1. dla każdego zmienionego parametru muszę wyszukiwać miejsca w wywołaniach funkcji, w których go podałem;
  2. dla każdego zmienionego parametru, jeśli zmieniła się jego nazwa, to muszę zmieniać ją zarówno w wywołaniu funkcji, jak i w konfiguracji wiersza poleceń.

Że nie wspomnę już o tym, że jak obecnie próbuję przepisać to na coś lepiej zaprojektowanego, to nie mam pomysłu, w którym miejscu podawać wartości domyślne dla parametrów (niektóre parametry wiersza polecenia nie są wymagane, a inne są). To nie należy już to mojego problemu, z którym przychodzę, ale gdyby ktoś dodatkowo mógł coś zaproponować w tym zakresie, to byłbym wdzięczny.


UPDATE: Podsumowując, pisanie tego posta zajęło mi więcej czasu, niż powinienem przeznaczyć na projektowanie takiej funkcjonalności mapowania… a jednak nie potrafię. Czy ktoś więc miałby jakieś pomysły, jak wykonać takie mapowanie, o jakim piszę? Może w którymś miejscu opisuję rzecz bezsensowną z punktu widzenia takiego programu? Może w ogóle nie ma problemu, tylko ja go wymyśliłem, przeinżynierowywując wszystko? Chętnie przyjmę krytykę, byle tylko udało się to zaprojektować.

2
Silv napisał(a):

Jest to rozwiązanie o tyle brzydkie, że za każdą zmianą konfiguracji podawanej w wierszu poleceń:

  1. dla każdego zmienionego parametru muszę wyszukiwać miejsca w wywołaniach funkcji, w których go podałem;
  2. dla każdego zmienionego parametru, jeśli zmieniła się jego nazwa, to muszę zmieniać ją zarówno w wywołaniu funkcji, jak i w konfiguracji wiersza poleceń.

Trzymaj konfigurację w osobnym obiekcie. Następnie we wszystkich miejscach w kodzie korzystaj z tego obiektu. Dzięki temu jeżeli zmieni się nazwa parametru w lini poleceń, to zmienisz tylko kod odpowiedzialny za zasilenie tego obiektu, a sam obiekt się nie zmieni.

0

Właśnie wczoraj sobie pomyślałem, po stworzeniu wątku, że nazwy parametrów wiersza poleceń ostatecznie mogę wyabstrahować z kodu do jakiegoś zewnętrznego obiektu. Może będzie to więcej abstrakcji… ale może bardziej czytelne… Zobaczę jeszcze, jak to może wyglądać dokładnie.

Nadal jednak zostanie problem, że nie podoba mi się ręczne mapowanie parametrów wiersza poleceń na właściwości obiektów w wywołaniach funkcji. Główna rzecz w tym to to, że te pierwsze nie mapują się jeden do jednego na te drugie. Mam, przykładowo, parametr o nazwie -h, który należy podać w wywołaniu w ten sposób:

node nazwa-pliku.js -h {liczba-całkowita >= 0} {liczba-całkowita >= 0}

Pierwsza liczba całkowita odpowiada minimalnej, a druga maksymalnej wysokości, jaką może przyjmować jeden blok. I tak, w wywołaniu funkcji podaję te dwie liczby oddzielnie:

constructBlock({
  ...
  minDeclaredBlockHeight = parsedArgs["-h"][0],
  maxDeclaredBlockHeight = parsedArgs["-h"][1],
  ...
});

Albo więc zmieniłbym konfigurację wiersza poleceń, by każdej właściwości odpowiadał jeden parametr, albo zmieniłbym wywołania funkcji, by każdemu parametrowi odpowiadała jedna właściwość. Obie opcje mi się nie podobają, bo z jednej strony chciałbym uprościć wywołanie programu (nie dając użytkownikowi zbyt wielu parametrów do wyboru / wymaganych), a z drugiej strony chciałbym, by wywołania funkcji były możliwie jak najbardziej logiczne w sensie domeny programu.

1

Może zainteresuj się też modułem yargs, który znacznie ułatwia pracę z argumentami wywołania aplikacji.

0

Dzięki; na razie sam spróbuję pokombinować; mam nadzieję, że coś dziś wymyślę; napiszę.

2

Na pewnym podstawowy poziomie ifologie, mapowanie, czy hardkodowanie jest nie do uniknięcia i to chyba taki przypadek, wiec cudów nie będzie.

Wydaje mi się ze nie potrzebnie masz płaską structure w tej konfiguracji. Gdyby DeclaredBlockHeigh był obiektem z propertisami min, max(albo min, max były by obiektami z property DeclaredBlockHeigh :P ) używał byś ich prawie tak samo jak teraz, za to dało by rade to zmapować półautomatycznie,.
coś w stylu.

mapConfig(parsedArgs, '-h', 'DeclaredBlockHeigh', ['min','max'])
mapConfig(parsedArgs, '-u', 'UserName')
/*jeśli udało by się wcisnąć parsedArgs pod spodem przez jakiś closure,
 wyszło by coś takiego, czyli dość blisko minimalnej ilości pracy jaką trzeba wykonać*/
mapConfig('-h', 'DeclaredBlockHeigh', ['min','max'])
mapConfig('-u', 'UserName')

console.log(config.DeclaredBlockHeigh.min)
console.log(config.UserName)

ED. tak mnie naszło ze przez nature js można zachować taka samą płaską strukturę konkatenując string, i ustawiać na ich podstawie properties. Wynik byłby taki.

mapConfig('-h', ['Min','Max'], 'DeclaredBlockHeigh',)
mapConfig('-u', null, 'UserName')

console.log(config.MinDeclaredBlockHeigh)
console.log(config.UserName)
0

Trochę zmieniłem. Załączam całą konfigurację wraz z domyślnymi wartościami – na razie zmapowaną tylko w komentarzach:

const contentBlockConfig = {
            innerWidth: 0, // -w[0]
            innerHeights: {
                min: 0, // -h[0]
                max: 0 // -h[1]
            },
            borders: {
                top: {
                    width: 0, // -b[0]
                    char: "" // -b[1]
                },
                right: {
                    width: 0, // -b[0]
                    char: "" // -b[1]
                },
                bottom: {
                    width: 0, // -b[0]
                    char: "" // -b[1]
                },
                left: {
                    width: 0, // -b[0]
                    char: "" // -b[1]
                }
            },
            paddings: {
                top: {
                    width: 0 // -p[0]
                },
                right: {
                    width: 0 // -p[0]
                },
                bottom: {
                    width: 0 // -p[0]
                },
                left: {
                    width: 0 // -p[0]
                }
            },
            widerThanHigher: false, // (currently no CLI parameter)
            overflowIndicator: "...", // (currently no CLI parameter)
            fillChar: "" // -f[0]
        };

const separatorBlockConfig = {
            width: 0 // -s[0]
        };

const schemaConfig = {
            colsNumber: undefined, // -c[0]
            link: {
                pointer: {
                    width: 0, // (currently no CLI parameter)
                    char: "" // (currently no CLI parameter)
                },
                line: {
                    width: 0, // (currently no CLI parameter)
                    char: "" // (currently no CLI parameter)
                }
            },
            fillChar: " " // (currently no CLI parameter)
        };

const contents: []; // --[0]

PS. Tablicę contents będę wyodrębniać z listy parametrów przed jakimkolwiek mapowaniem – tak wyszło mi, że będzie łatwiej – więc nie liczę jej jako parametr wiersza poleceń (podaję ją wyżej tylko dla pokazania, jak to u mnie wygląda).


PS2. @_flamingAccount: nie bardzo rozumiem, jak ma wyglądać funkcja mapConfig.


UPDATE: Przygotowałem sobie na razie coś takiego, co poniżej zamieszczam – ale tak się chyba nie da. W każdym razie nie wiem, co dalej.

const paramsPropertiesMapping = {
    "-w": [
        {
            // Here map property contentBlockConfig.innerWidth
        }
    ],
    "-h": [
        {
            // Here map property contentBlockConfig.innerHeights.min
        },
        {
            // Here map property contentBlockConfig.innerHeights.max
        }
    ],
    "-b": [
        {
            // Here map properties contentBlockConfig.borders.top.width, contentBlockConfig.borders.right.width, contentBlockConfig.borders.bottom.width and contentBlockConfig.borders.left.width
        },
        {
            // Here map properties contentBlockConfig.borders.top.char, contentBlockConfig.borders.right.char, contentBlockConfig.borders.bottom.char and contentBlockConfig.borders.left.char
        }
    ],
    "-p": [
        {
            // Here map properties contentBlockConfig.paddings.top.width, contentBlockConfig.paddings.right.width, contentBlockConfig.paddings.bottom.width and contentBlockConfigpaddings.left.width
        }
    ],
    "-f": [
        {
            // Here map property contentBlockConfig.fillChar
        }
    ],
    "-s": [
        {
            // Here map property separatorBlockConfig.width
        }
    ],
    "-c": [
        {
            // Here map property schemaConfig.colsNumber
        }
    ]
};
1

Brakuje abslugi pojedyczego parametru ale poza tym działa :)

	parsedArgs = {
    '-h': [12,123],
	'-b': [10,'abc']
}
let contentBlockConfig = {};

function wrap(config,args){
   let that = {}
   function moveTo(arg, path){
         return {set:function(properties,valuesNames){
			properties.forEach( prop => {
				mapConfig(arg, path + "." + prop,valuesNames)
			});}
		 }
   }   
   function mapConfig(arg, path, valuesNames){
		 let values =args[arg];
		 let parts = path.split('.');
		 parts = parts.filter(Boolean)
		 for(let i =0; i < valuesNames.length; i++){
			setValue(parts,valuesNames[i],values[i])
		 }
	}
	
	function setValue(parts,valueName, value){
		let current = config;
		parts.forEach(part =>{
			if(current[part] === undefined)  {
				current[part] = {};
			}
			current = current[part];
		})
		current[valueName] = value;
	}
	that.set = mapConfig;
	that.moveTo = moveTo;
	return that;
}

let mapping = wrap(contentBlockConfig,parsedArgs);

mapping.set('-h','innerHeights',['min', 'max'])
mapping.set('-h','some.other.Heights',['min', 'max'])
mapping.moveTo('-b','borders').set(['top','botton','left','right'],['size','char'])
//przerobienie na coś takiego fajnie by wylgądało :D
mapping.moveTo('-b','borders').set(['top','botton','left','right']).values(['size','char'])

Nie wiem czy nie popsuje to intelisensu, wrazie gdyby tak mozesz startowo zaicjalizować config domyślnymi wartościami, a dopiero potem zmapować konsole
ed. nie odpalało sie bo zamieniłem podczas edycji posta -p na -b ale nie wszedzie :)

0

@_flamingAccount: co to jest parts.filter(Boolean);?


UPDATE:

Czy nie prościej zrobić, jak poniżej:

zamiast

let that = {};
...
that.set = mapConfig;
that.moveTo = moveTo;
return that;

dać (ECMAScript 6, jak mi się zdaje)

return {
  set: mapConfig,
  moveTo
};

PS2. A może to powyższe moje rozwiązanie zadziała inaczej niż Twoje (niezgodnie z oczekiwaniem)?


UPDATE:

@_flamingAccount: w oczekiwaniu, aż mi wytłumaczysz, po swojemu doszedłem na razie do czegoś takiego:

const paramsMapping = {
    contentBlock: {
        innerWidth: {
            shortName: "-w",
            defaultValues: [0]
        },
        innerHeights: {
            shortName: "-h",
            defaultValues: [0, 0]
        },
        borders: {
            shortName: "-b",
            defaultValues: [0, ""]
        },
        paddings: {
            shortName: "-p",
            defaultValues: [0]
        },
        fill: {
            shortName: "-f",
            defaultValues: [""]
        }
    },
    separatorBlock: {
        shortName: "-s",
        defaultValues: [0]
    },
    schema: {
        colsNumber: {
            shortName: "-c",
            defaultValues: [undefined]
        }
    },
    content: {
        defaultValues: [""]
    }
};

exports.parseArgs = function (argsList) {
    const contents = getContentsOrDefault(paramsMapping.content[0], argsList);
    return {
        contentBlockConfig: {
            innerWidth: getArgOrDefault(paramsMapping.contentBlock.innerWidth, argsList),
            innerHeights: {
                min: getArgOrDefault(paramsMapping.contentBlock.innerHeights[0], argsList),
                max: getArgOrDefault(paramsMapping.contentBlock.innerHeights[1], argsList)
            },
            borders: {
                top: {
                    width: getArgOrDefault(paramsMapping.contentBlock.borders[0], argsList),
                    char: getArgOrDefault(paramsMapping.contentBlock.borders[1], argsList)
                },
                right: {
                    width: getArgOrDefault(paramsMapping.contentBlock.borders[0], argsList),
                    char: getArgOrDefault(paramsMapping.contentBlock.borders[1], argsList)
                },
                bottom: {
                    width: getArgOrDefault(paramsMapping.contentBlock.borders[0], argsList),
                    char: getArgOrDefault(paramsMapping.contentBlock.borders[1], argsList)
                },
                left: {
                    width: getArgOrDefault(paramsMapping.contentBlock.borders[0], argsList),
                    char: getArgOrDefault(paramsMapping.contentBlock.borders[1], argsList)
                }
            },
            paddings: {
                top: {
                    width: getArgOrDefault(paramsMapping.contentBlock.paddings[0], argsList)
                },
                right: {
                    width: getArgOrDefault(paramsMapping.contentBlock.paddings[0], argsList)
                },
                bottom: {
                    width: getArgOrDefault(paramsMapping.contentBlock.paddings[0], argsList)
                },
                left: {
                    width: getArgOrDefault(paramsMapping.contentBlock.paddings[0], argsList)
                }
            },
            widerThanHigher: false, // (currently no)
            overflowIndicator: "...", // (currently no)
            fillChar: getArgOrDefault(paramsMapping.contentBlock.fill[0], argsList)
        },
        separatorBlockConfig: {
            width: getArgOrDefault(paramsMapping.separatorBlock[0], argsList)
        },
        schemaConfig: {
            colsNumber: getArgOrDefault(paramsMapping.schema.colsNumber[0], argsList),
            link: {
                pointer: {
                    width: 0, // (currently no)
                    char: "" // (currently no)
                },
                line: {
                    width: 0, // (currently no)
                    char: "" // (currently no)
                }
            },
            fillChar: " " // (currently no)
        },
        contents: contents
    };
}

const getContentsOrDefault = function (argsList, argConfig) {
    // TODO
};

const getArgOrDefault = function (argsList, argConfig) {
    // TODO
};

PS3. Elementy tablicy paramsMapping mogą mieć dowolny poziom zagnieżdżenia. Dany element na najniższym poziomie reprezentuje jeden i tylko jeden parametr wiersza poleceń. — Obecnie w konfiguracji parametrów umieściłem nazwę skróconą i wartości domyślne; można dodać jeszcze np. nazwę rozszerzoną (tak jak zazwyczaj mają programy dostępne z wiersza poleceń). Długość tablicy z wartościami domyślnymi reprezentuje liczbę argumentów, które zostaną pobrane dla danej właściwości w funkcji getArgOrDefault. Dla parametru contents wymyśliłem (stety niestety) dowolną liczbę argumentów, więc musiałem zrobić oddzielną funkcję i wywoływać ją przed parsowaniem – jak pisałem wcześniej (poprawiłem teraz kod).

1

Weź mi to wytłumacz linijka po linijce

Nasz nasz plan to zrobić zeby nie trzeba było pisać nudnej formułki milion razy,

config.property = args[consoleKey][0]

To można przerobić na

function set(consoleKey, property){config[property] = args[consoleKey][0]}

Jest set to zgrabniejsze

set('-u', 'userName')   vs config.username= args['-u'][0]

Nie działa to jeśli jest wiecej niż jedena wartość dla parametru z konsoli np MaxDeclaredBlockHeigh i MinDeclaredBlockHeigh
Można to poprawić dodając pętle

function setManyAtOnce(consoleKey, properties){
    for(let i; i < properties.lenght; i++){
         config[properties[i]] = args[consoleKey][i]
    }
}
//uzycie
setManyAtOnce('-h',['MaxDeclaredBlockHeigh ','MinDeclaredBlockHeigh '])

to już rozwiązuje problem ale prawie 50% tekstu w tej lini to DeclaredBlockHeigh Można to poprawić robiąc hierarchie - odrobinę - miej płaską. O to mialem na mysli w poście bez implemenacji

function setManyAtOnce(consoleKey, property, valuesNames){
    for(let i; i < properties.lenght; i++){
         config[property][valuesNames[i]] = args[consoleKey][i]
    }
}
//uzycie
setManyAtOnce('-h','DeclaredBlockHeigh '['min','max']);

Ten kod ma buga jeśli config.propety jest undefined, wiec trzeba to sprawdzić i stworzyć pusty obiekt w razie co.

if(config[property] === undenined)
    config[property]  = {};

Kod zrobił się złożony z jak mieniłeś hierarchie i chciałem zaimplementować to niżej dla jednego argumentu.

// Here map properties contentBlockConfig.paddings.top.width, contentBlockConfig.paddings.right.width, contentBlockConfig.paddings.bottom.width and contentBlockConfigpaddings.left.width

To jest strasznie dużo rzeczy naraz a podajesz tylko jeden parametr wejściowy, wiec założyłem ze wszystkie 4 maja te samą wartość.

Pierwszym problemem jest dowolnie głeboka hierarchia. najprostszym ulepszeniem poprzedniego kodu jest zrobienie ze property przyjmuje wartości po kropce, tak jak w kodzie.

         let parts = path.split('.');
         parts = parts.filter(Boolean)//usuwa puste stringi po splicie np jak kropka jest na poczatku

Przez to ze trzeba każdy obiek po kropce znacjonalizować przynajmniej pusty obiektem wydzieliłem set value jako funkcje.

Drugim problemem jest ustawienie kilku wartości naraz, kolejna pętla. Ja napisałem moveTo bo pomyslałem ze fajnie będzie symulować cd w konsoli, i drugi set bo ładnie tak wygląda :P. Jedynym technicznym argumentem jest to zeby trzymać niską liczbę parametrów w metodach :P

btw. temten kod wyzej działa tylko miałem pomieszane -p i -b w argumentach :P

0

Postanowiłem troszkę zmienić dwie ostatnie funkcje, które podałem dwa posty wyżej, i zrobić takie coś, że dla każdej właściwości wywoływana jest funkcja o sygnaturze checkArg(argList, argConfig); zwraca ona obiekt z dwiema metodami o sygnaturach getArg() oraz getArgOrDefault(). Jeśli dla danej właściwości podanie wartości (=argumentu) jest wymagane, to wywołuję dla niej funkcję getArg, która w sytuacji braku rzuca wyjątkiem; jeśli nie, to wywołuję funkcję getArgOrDefault(), która w sytuacji braku bierze domyślną wartość.

Co do Twojego rozwiązania, to nadal nie bardzo rozumiem – ale nie wiem, czy jest sens próbować zrozumieć, skoro mam własne, które rozumiem.


PS. Pewnie jeszcze trochę muszę się JavaScriptu pouczyć, by zrozumieć.


UPDATE:

Po kilku próbach rozwiązania tego problemu w tę i w tamtą stronę wróciłem do rozwiązania z pierwszego postu (tego, które mi się nie podobało). Uznałem, że jest najbardziej czytelne. W skrócie: mam obiekt paramsConfigs oraz funkcję getConfig; ten pierwszy przechowuje główną konfigurację parametrów wiersza poleceń, a ta druga zwraca obiekt definiujący całą konfigurację aplikacji, w którym mapuję parametry wiersza poleceń na właściwości. Pełny kod dostępny jest już na GitHubie: https://github.com/silvuss/silvuss-paas

@_flamingAccount, wzmiankowałem Twoje rozwiązanie w sekcji Zasoby warte uwagi w README (https://github.com/silvuss/silvuss-paas#resources-worth-attention).

1 użytkowników online, w tym zalogowanych: 0, gości: 1